I whipped up this little sample application to demonstrate how to maintain a list of controls, specifically a number of dynamically created TPanel controls, each containing a number of sub-controls. Everything seems to work fine, except for one weird thing. Of course when I close my app, it goes through all the created controls and free's them. This works perfectly. But strangely, when I try to delete one of them, I get an access violation in the same code which works perfectly upon closing.
Just to explain the code below a little, there's a TStringList in the background which contains an object for each panel. I also maintain a "Last ID" which I assign to the tag of these panels, as well as the panel's child controls. The panels are dumped and aligned inside of a scroll box, so it's like a list control of panels with controls. Each panel can be referenced either by its Index or by its unique ID. The trouble started when implementing a 'Delete' button on each panel which is supposed to remove it. Clicking this delete button checks the ID in its tag property, and calls a procedure to delete that ID. In debug, I traced the ID and Index and it is what it's supposed to be, but it doesn't do what it's supposed to do...
unit uMain;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ExtCtrls, Buttons, XPMan;
type
TForm1 = class(TForm)
Panel1: TPanel;
Label1: TLabel;
BitBtn1: TBitBtn;
pMain: TScrollBox;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure BitBtn1Click(Sender: TObject);
private
FLastID: Integer;
FPanels: TStringList;
function GetPanel(Index: Integer): TPanel;
procedure DelPanClick(Sender: TObject);
function GetPanelID(ID: Integer): TPanel;
public
function GetID: Integer;
property Panels[Index: Integer]: TPanel read GetPanel;
property PanelByID[ID: Integer]: TPanel read GetPanelID;
function Add: TPanel;
procedure Delete(const Index: Integer);
procedure DeleteID(const ID: Integer);
function Count: Integer;
procedure Clear;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
function TForm1.Add: TPanel;
const
MARGINS = 8;
var
L1, L2: TLabel;
E1: TEdit;
C1: TComboBox;
B1: TBitBtn;
begin
Result:= TPanel.Create(nil);
Result.Parent:= pMain;
Result.Align:= alLeft;
Result.Width:= 150;
Result.ParentBackground:= True;
Result.ParentBackground:= False; //TPanel/XPMan color trick...
Result.Color:= clSilver;
Result.Tag:= GetID;
L1:= TLabel.Create(Result);
L1.Parent:= Result;
L1.Left:= MARGINS;
L1.Top:= MARGINS;
L1.Caption:= 'Some Text Box';
L1.Font.Style:= [fsBold];
L1.Tag:= Result.Tag;
E1:= TEdit.Create(Result);
E1.Parent:= Result;
E1.Left:= MARGINS;
E1.Top:= L1.Top + L1.Height + MARGINS;
E1.Width:= Result.ClientWidth - (MARGINS * 2);
E1.Anchors:= [akLeft,akTop,akRight];
E1.Text:= 'Some String Value';
E1.Tag:= Result.Tag;
L2:= TLabel.Create(Result);
L2.Parent:= Result;
L2.Left:= MARGINS;
L2.Top:= E1.Top + E1.Height + (MARGINS * 2);
L2.Caption:= 'Some Combo Box';
L2.Font.Style:= [fsBold];
L2.Tag:= Result.Tag;
C1:= TComboBox.Create(Result);
C1.Parent:= Result;
C1.Left:= MARGINS;
C1.Top:= L2.Top + L2.Height + MARGINS;
C1.Width:= Result.ClientWidth - (MARGINS * 2);
C1.Style:= csDropDownList;
C1.Items.Append('Some Selected Value');
C1.Items.Append('Some Other Value');
C1.ItemIndex:= 0;
C1.Tag:= Result.Tag;
B1:= TBitBtn.Create(Result);
B1.Parent:= Result;
B1.Width:= 60;
B1.Height:= 25;
B1.Left:= MARGINS;
B1.Top:= Result.ClientHeight - B1.Height - MARGINS;
B1.Anchors:= [akLeft,akBottom];
B1.Caption:= 'Delete';
B1.OnClick:= DelPanClick;
B1.Tag:= Result.Tag;
FPanels.AddObject(IntToStr(Result.Tag), Result);
end;
procedure TForm1.Clear;
begin
while Count > 0 do
Delete(0);
end;
function TForm1.Count: Integer;
begin
Result:= FPanels.Count;
end;
procedure TForm1.Delete(const Index: Integer);
var
P: TPanel;
begin
if (Index >= 0) and (Index < FPanels.Count) then begin
try
P:= TPanel(FPanels.Objects[Index]);
if assigned(P) then begin
P.Free; //<----- AV
end;
except
on e: exception do begin
raise Exception.Create('Failed to delete panel: '+e.Message);
end;
end;
FPanels.Delete(Index);
end else begin
raise Exception.Create('Panel index out of bounds ('+IntToStr(Index)+')');
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
FLastID:= 100;
pMain.Align:= alClient;
FPanels:= TStringList.Create;
Add;
Add;
Add;
Add;
Add;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
Clear;
FPanels.Free;
end;
function TForm1.GetPanel(Index: Integer): TPanel;
begin
Result:= TPanel(FPanels.Objects[Index]);
end;
procedure TForm1.BitBtn1Click(Sender: TObject);
begin
Add;
end;
procedure TForm1.DelPanClick(Sender: TObject);
begin
if Sender is TBitBtn then begin
DeleteID(TBitBtn(Sender).Tag);
end;
end;
function TForm1.GetID: Integer;
begin
Inc(FLastID);
Result:= FLastID;
end;
procedure TForm1.DeleteID(const ID: Integer);
var
X: Integer;
begin
X:= FPanels.IndexOf(IntToStr(ID));
if X >= 0 then begin
Delete(X);
end else begin
raise Exception.Create('Invalid ID ('+IntToStr(ID)+')');
end;
end;
function TForm1.GetPanelID(ID: Integer): TPanel;
var
X: Integer;
begin
X:= FPanels.IndexOf(IntToStr(ID));
if X >= 0 then begin
Result:= TPanel(FPanels.Objects[X]);
end else begin
raise Exception.Create('Invalid ID ('+IntToStr(ID)+')');
end;
end;
end.
And the DFM code:
object Form1: TForm1
Left = 385
Top = 556
Width = 540
Height = 247
Caption = 'Form1'
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'MS Sans Serif'
Font.Style = []
OldCreateOrder = False
OnCreate = FormCreate
OnDestroy = FormDestroy
PixelsPerInch = 96
TextHeight = 13
object Panel1: TPanel
Left = 0
Top = 0
Width = 524
Height = 33
Align = alTop
BevelWidth = 2
Color = clWhite
ParentBackground = False
TabOrder = 0
DesignSize = (
524
33)
object Label1: TLabel
Left = 8
Top = 6
Width = 218
Height = 20
Caption = 'Sample Dynamic Panel List'
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -16
Font.Name = 'MS Sans Serif'
Font.Style = [fsBold]
ParentFont = False
end
object BitBtn1: TBitBtn
Left = 450
Top = 8
Width = 57
Height = 17
Anchors = [akTop, akRight]
Caption = 'Add'
TabOrder = 0
OnClick = BitBtn1Click
end
end
object pMain: TScrollBox
Left = 0
Top = 33
Width = 475
Height = 176
Align = alLeft
Anchors = [akLeft, akTop, akRight, akBottom]
BorderStyle = bsNone
Color = clSkyBlue
ParentColor = False
TabOrder = 1
end
end
The panel eventually deletes, after these 3 access violations:

EDIT:
After making a few additions to my code, and adding David's fix, it did work, but now another A/V comes upon deleting the third of 5 panels, if deleting from left to right. But from deleting right to left, it all works fine. Here's my new code below, DFM is the same as above:
unit uMain;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ExtCtrls, Buttons, XPMan;
const
LABEL_1 = 0;
EDIT_1 = 1;
LABEL_2 = 2;
COMBO_1 = 3;
BUTTON_1 = 4;
type
TForm1 = class(TForm)
Panel1: TPanel;
Label1: TLabel;
BitBtn1: TBitBtn;
pMain: TScrollBox;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure BitBtn1Click(Sender: TObject);
private
FLastID: Integer;
FPanels: TStringList;
function GetPanel(Index: Integer): TPanel;
procedure DelPanClick(Sender: TObject);
function GetPanelID(ID: Integer): TPanel;
function GetBtn1(Index: Integer): TBitBtn;
function GetCbo1(Index: Integer): TComboBox;
function GetEdt1(Index: Integer): TEdit;
function GetLbl1(Index: Integer): TLabel;
function GetLbl2(Index: Integer): TLabel;
function GetBtn1ID(ID: Integer): TBitBtn;
function GetCbo1ID(ID: Integer): TComboBox;
function GetEdt1ID(ID: Integer): TEdit;
function GetLbl1ID(ID: Integer): TLabel;
function GetLbl2ID(ID: Integer): TLabel;
public
function GetID: Integer;
property Panels[Index: Integer]: TPanel read GetPanel;
property Lbl1[Index: Integer]: TLabel read GetLbl1;
property Lbl2[Index: Integer]: TLabel read GetLbl2;
property Edt1[Index: Integer]: TEdit read GetEdt1;
property Cbo1[Index: Integer]: TComboBox read GetCbo1;
property Btn1[Index: Integer]: TBitBtn read GetBtn1;
property PanelByID[ID: Integer]: TPanel read GetPanelID;
property Lbl1ByID[Index: Integer]: TLabel read GetLbl1ID;
property Lbl2ByID[Index: Integer]: TLabel read GetLbl2ID;
property Edt1ByID[Index: Integer]: TEdit read GetEdt1ID;
property Cbo1ByID[Index: Integer]: TComboBox read GetCbo1ID;
property Btn1ByID[Index: Integer]: TBitBtn read GetBtn1ID;
function Add: TPanel;
procedure Delete(const Index: Integer);
procedure DeleteID(const ID: Integer);
function Count: Integer;
procedure Clear;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
function TForm1.Add: TPanel;
const
MARGINS = 8;
var
L1, L2: TLabel;
E1: TEdit;
C1: TComboBox;
B1: TBitBtn;
begin
Result:= TPanel.Create(nil);
Result.Parent:= pMain;
Result.Align:= alLeft;
Result.Width:= 150;
Result.ParentBackground:= True;
Result.ParentBackground:= False; //TPanel/XPMan color trick...
Result.Color:= clSilver;
Result.Tag:= GetID;
//LABEL_1 = 0;
//EDIT_1 = 1;
//LABEL_2 = 2;
//COMBO_1 = 3;
//BUTTON_1 = 4;
L1:= TLabel.Create(Result);
L1.Parent:= Result;
L1.Left:= MARGINS;
L1.Top:= MARGINS;
L1.Caption:= 'Some Text Box';
L1.Font.Style:= [fsBold];
L1.Tag:= Result.Tag;
E1:= TEdit.Create(Result);
E1.Parent:= Result;
E1.Left:= MARGINS;
E1.Top:= L1.Top + L1.Height + MARGINS;
E1.Width:= Result.ClientWidth - (MARGINS * 2);
E1.Anchors:= [akLeft,akTop,akRight];
E1.Text:= 'Some String Value';
E1.Tag:= Result.Tag;
L2:= TLabel.Create(Result);
L2.Parent:= Result;
L2.Left:= MARGINS;
L2.Top:= E1.Top + E1.Height + (MARGINS * 2);
L2.Caption:= 'Some Combo Box';
L2.Font.Style:= [fsBold];
L2.Tag:= Result.Tag;
C1:= TComboBox.Create(Result);
C1.Parent:= Result;
C1.Left:= MARGINS;
C1.Top:= L2.Top + L2.Height + MARGINS;
C1.Width:= Result.ClientWidth - (MARGINS * 2);
C1.Style:= csDropDownList;
C1.Items.Append('Some Selected Value');
C1.Items.Append('Some Other Value');
C1.ItemIndex:= 0;
C1.Tag:= Result.Tag;
B1:= TBitBtn.Create(Result);
B1.Parent:= Result;
B1.Width:= 60;
B1.Height:= 25;
B1.Left:= MARGINS;
B1.Top:= Result.ClientHeight - B1.Height - MARGINS;
B1.Anchors:= [akLeft,akBottom];
B1.Caption:= 'Delete';
B1.OnClick:= DelPanClick;
B1.Tag:= Result.Tag;
FPanels.AddObject(IntToStr(Result.Tag), Result);
end;
procedure TForm1.Clear;
begin
while Count > 0 do
Delete(0);
end;
function TForm1.Count: Integer;
begin
Result:= FPanels.Count;
end;
procedure TForm1.Delete(const Index: Integer);
var
P: TPanel;
begin
if (Index >= 0) and (Index < FPanels.Count) then begin
try
P:= Panels[Index];
while P.ControlCount > 0 do
P.Controls[0].Free;
P.Free;
except
on e: exception do begin
raise Exception.Create('Failed to delete panel: '+e.Message);
end;
end;
FPanels.Delete(Index);
end else begin
raise Exception.Create('Panel index out of bounds ('+IntToStr(Index)+')');
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
var
X: Integer;
begin
FLastID:= 100;
pMain.Align:= alClient;
FPanels:= TStringList.Create;
Add;
Add;
Add;
Add;
Add;
for X:= 0 to Count - 1 do begin
Edt1[X].Text:= IntToStr(X);
Lbl1[X].Caption:= IntToStr(X);
Lbl2[X].Caption:= IntToStr(X);
end;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
Clear;
FPanels.Free;
end;
function TForm1.GetPanel(Index: Integer): TPanel;
begin
Result:= TPanel(FPanels.Objects[Index]);
end;
procedure TForm1.BitBtn1Click(Sender: TObject);
begin
Add;
end;
procedure TForm1.DelPanClick(Sender: TObject);
begin
if Sender is TBitBtn then begin
DeleteID(TBitBtn(Sender).Tag);
end;
end;
function TForm1.GetID: Integer;
begin
Inc(FLastID);
Result:= FLastID;
end;
procedure TForm1.DeleteID(const ID: Integer);
var
X: Integer;
begin
X:= FPanels.IndexOf(IntToStr(ID));
if X >= 0 then begin
Delete(X);
end else begin
raise Exception.Create('Invalid ID ('+IntToStr(ID)+')');
end;
end;
function TForm1.GetPanelID(ID: Integer): TPanel;
var
X: Integer;
begin
X:= FPanels.IndexOf(IntToStr(ID));
if X >= 0 then begin
Result:= TPanel(FPanels.Objects[X]);
end else begin
raise Exception.Create('Invalid ID ('+IntToStr(ID)+')');
end;
end;
function TForm1.GetBtn1(Index: Integer): TBitBtn;
begin
if (Index >= 0) and (Index < FPanels.Count) then begin
Result:= TBitBtn(Panels[Index].Controls[BUTTON_1]);
end else begin
raise Exception.Create('Index out of bounds ('+IntToStr(Index)+')');
end;
end;
function TForm1.GetCbo1(Index: Integer): TComboBox;
begin
if (Index >= 0) and (Index < FPanels.Count) then begin
Result:= TComboBox(Panels[Index].Controls[COMBO_1]);
end else begin
raise Exception.Create('Index out of bounds ('+IntToStr(Index)+')');
end;
end;
function TForm1.GetEdt1(Index: Integer): TEdit;
begin
if (Index >= 0) and (Index < FPanels.Count) then begin
Result:= TEdit(Panels[Index].Controls[EDIT_1]);
end else begin
raise Exception.Create('Index out of bounds ('+IntToStr(Index)+')');
end;
end;
function TForm1.GetLbl1(Index: Integer): TLabel;
begin
if (Index >= 0) and (Index < FPanels.Count) then begin
Result:= TLabel(Panels[Index].Controls[LABEL_1]);
end else begin
raise Exception.Create('Index out of bounds ('+IntToStr(Index)+')');
end;
end;
function TForm1.GetLbl2(Index: Integer): TLabel;
begin
if (Index >= 0) and (Index < FPanels.Count) then begin
Result:= TLabel(Panels[Index].Controls[LABEL_2]);
end else begin
raise Exception.Create('Index out of bounds ('+IntToStr(Index)+')');
end;
end;
function TForm1.GetBtn1ID(ID: Integer): TBitBtn;
var
X: Integer;
begin
X:= FPanels.IndexOf(IntToStr(ID));
if X >= 0 then begin
Result:= TBitBtn(PanelByID[ID].Controls[BUTTON_1]);
end else begin
raise Exception.Create('Invalid Panel ID ('+IntToStr(ID)+')');
end;
end;
function TForm1.GetCbo1ID(ID: Integer): TComboBox;
var
X: Integer;
begin
X:= FPanels.IndexOf(IntToStr(ID));
if X >= 0 then begin
Result:= TComboBox(PanelByID[ID].Controls[COMBO_1]);
end else begin
raise Exception.Create('Invalid Panel ID ('+IntToStr(ID)+')');
end;
end;
function TForm1.GetEdt1ID(ID: Integer): TEdit;
var
X: Integer;
begin
X:= FPanels.IndexOf(IntToStr(ID));
if X >= 0 then begin
Result:= TEdit(PanelByID[ID].Controls[EDIT_1]);
end else begin
raise Exception.Create('Invalid Panel ID ('+IntToStr(ID)+')');
end;
end;
function TForm1.GetLbl1ID(ID: Integer): TLabel;
var
X: Integer;
begin
X:= FPanels.IndexOf(IntToStr(ID));
if X >= 0 then begin
Result:= TLabel(PanelByID[ID].Controls[LABEL_1]);
end else begin
raise Exception.Create('Invalid Panel ID ('+IntToStr(ID)+')');
end;
end;
function TForm1.GetLbl2ID(ID: Integer): TLabel;
var
X: Integer;
begin
X:= FPanels.IndexOf(IntToStr(ID));
if X >= 0 then begin
Result:= TLabel(PanelByID[ID].Controls[LABEL_2]);
end else begin
raise Exception.Create('Invalid Panel ID ('+IntToStr(ID)+')');
end;
end;
end.
Results in this access violation:

PS - I know other parts of the new code don't work right, but that's a matter of another question :P