Create and then destroy TLabels at runtime in Firemonkey - delphi

I'm trying to generate TLabels at runtime and insert them into a VertScrollBox with this code;
var
i, f: integer;
RT_Label: TLabel;
begin
f:= 10;
for i := 0 to 20 do
begin
RT_Label := TLabel.Create(Self);
RT_Label.Name := 'Label' + i.ToString;
RT_Label.Text := 'SampleLabel' + i.ToString;
RT_Label.Position.Y := f;
RT_Label.Align := TAlignLayout.Top;
RT_Label.Parent := VertScrollBox1;
inc(f, 15);
end;
end;
Labels are displayed without any problem, but when I try to free the generated labels with this code:
var
i: integer;
LComponent: TComponent;
begin
for i := 0 to ComponentCount-1 do
begin
if( Components[i] is TLabel )then
if StartsText('Label', (Components[i] as TLabel).Name) then
begin
LComponent := (Components[i] as TLabel);
If Assigned(LComponent) then FreeAndNil(LComponent);
end;
end;
end;
Then I always get the error 'Argument out of range'.
How do I properly remove TLabels added to the VertScrollBox in runtime?

You start your loop with the following line
for i := 0 to ComponentCount-1 do
but when you free a component it removes itself from the Components list as part of it's clean-up code. So each component that gets freed reduces the size of the list by 1. The ComponentCount-1 expression is evaluated once when the for loop start and thus does not get updated to reflect the change.
Even if you could fix this your loop would be skipping items. I.e if your deleted item 3, item 4 would now become item 3, but your loop would advance to item 4.
The way around this is simple, though. Simply iterate the list backwards:
for i := ComponentCount-1 downto 0 do
It's worth mentioning that your code will only actually free items on Windows and OSX. On mobile the compiler uses ARC which only frees an object once all references it have been removed. The solution/work around/fudge[1] is to call DisposeOf instead of Free for components.
As an aside, the as operator already guarantees that the object is Assigned, so no need for the extra test. There's no need to FreeAndNil a local variable which will be either reassigned to or go straight out of scope, and there's no need to cast an object before freeing it. Since the Free (or DisposeOf) method is present in the common ancestor class the compiler will resolve links for any descendant classes.
Thus, your code can be simplified to:
var
i: integer;
begin
for i := ComponentCount-1 downto 0 do
begin
if Components[i] is TLabel then
if StartsText('Label', (Components[i] as TLabel).Name) then
Components[i].DisposeOf;
end;
end;
[1] - depending on who you talk to.

Related

How to find what form is calling a public procedure

I have too many Forms and I have a procedure that should be running on all form when created
procedure TDM.SetupForm(Max, DisableResize,
DisableMove: Boolean; FormWidth: Integer = 0; FormHeight: Integer = 0);
var
Form: TForm;
begin
Form := ??? // How to find the what form is running this procedure?
Form.AutoScroll := True;
if Max then
begin
Form.Width := Screen.WorkAreaWidth;
Form.Height := Screen.WorkAreaHeight;
Form.Top := 0;
Form.Left := 0;
end
else
begin
if FormWidth > 0 then
Form.Width := FormWidth;
if FormHeight > 0 then
Form.Height := FormHeight;
Form.Position := poScreenCenter;
Form.Align := alCustom;
end;
if DisableResize then
DeleteMenu(GetSystemMenu(Form.Handle, False), SC_SIZE, MF_BYCOMMAND);
if DisableMove then
DeleteMenu(GetSystemMenu(Form.Handle, False), SC_MOVE, MF_BYCOMMAND);
Form.BorderIcons := [biSystemMenu];
if Form.Height > Screen.WorkAreaHeight then
Form.Height := Screen.WorkAreaHeight;
if Form.Width > Screen.WorkAreaWidth then
Form.Width := Screen.WorkAreaWidth;
Form.ShowHint := True;
Form.OnClose := CloseFormAction;
end;
I call this Procedure on FormCreate event
How can I find what form is calling this procedure and use it inside same procedure without passing it as parameter?
I call this procedure on FormCreate event
It seems to me you don't actually need to know the "Last Created Form", but rather which form is currently being created, which you want to call this code for. If that is the case, simply add a TForm parameter to this procedure instead of declaring a variable and trying to obtain it from elsewhere...
procedure TDM.SetupForm(Form: TForm; Max, DisableResize,
DisableMove: Boolean; FormWidth: Integer = 0; FormHeight: Integer = 0);
begin
...Use the `Form` parameter...
Then you would pass Self into this whenever you call it from FormCreate...
DM.SetupForm(Self, ....
Ultimately, this sort of thing is best accomplished by creating a base form first, and then inheriting all the rest of your forms from this base. Such code would be implemented in the base form's constructor, and then you wouldn't have to explicitly call it from each and every form you wish to apply it to. However, it seems you already have many forms written and this would require modifying all of your existing code to consider the base form. Such design should be done from the beginning of development.
I must also note that putting UI code of such nature into a data module is not the right practice. A data module's purpose is to be disconnected from the UI. That's why it's not actually a visible form, but a non-visual-component-only solution. It's best to put such code in independent units for that purpose, such as MyApp.UICommon.pas.

How to free runtime ListBoxItems from memory in Delphi Seattle Firemonkey Multi Device App

I have a few forms where I have a TListBox component that I fill runtime.
My question is how can I best free the items that I add runtime?
By using the owner? Form or ListBox?
Or freeing them myself?
or a different way?
Below an example how I fill my listbox:
procedure TForm1.LoadList;
var
item: TListBoxItem;
begin
myList.Clear;
myList.BeginUpdate;
try
with myQuery do
begin
First;
while not eof do
begin
item := TListBoxItem.Create(nil);
try
item.Tag := FieldByName(myIDField).AsInteger;
item.Text := FieldByName(myDescriptionField).AsString;
myList.AddObject(item);
finally
Next;
end;
end;
end;
finally
myList.EndUpdate;
end;
end;
I do have noticed that creating the list may take a bit longer when I set the owner of the item.
Also when the I call ListBox.Clear and the list was filled with items with no owners the list still gets cleared correctly. So does this mean the owner of the items get set when I use AddObject to add them to the ListBox?
Also I Free the form with the close action:
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
Action := TCloseAction.CaFree;
end;
I don't know if this changes things how to free the items in my list?
When you do a .AddObject you are just giving that object a parent.
My suggestion would be to create the objects as you do now, only giving them a parent. Instead of doing a .AddObject you can also do :
item.parent := mylist
And as has been mentioned, ARC is used on mobile platforms but on desktop it is still business as usual in regards to working with objects.
To free your items (regardless of platform) you could do this :
var
I: Integer;
begin
for I := Mylist.Count-1 downto 0 do
begin
{$IFNDEF AUTOREFCOUNT}
Mylist.ItemByIndex(I).DisposeOf
{$ELSE}
Mylist.ItemByIndex(I).parent := nil;
{$ENDIF}
end;
end;
Because what you are doing then is setting the parent to nil for ARC enabled platforms, resulting in the object being freed once its reference count reaches 0 (given that there are no other references to the object like an owner etc).
And on non-ARC platforms the .DisposeOf will simply be a call to free.
You can however also use .DisposeOf in ARC platforms too, but this will leave your objects in a "Zombie" state and is not considered the "ideal" solution to memory management regarding objects.

How to call objects that were cloned

I clone a panel and its contents(A image and a checkbox) 20 times.
Sample of the panel being cloned:
This is the procedure used to clone a whole panel:
procedure TForm1.ClonePanel(pObjectName: Tpanel);
var apanel : Tpanel;
Ctrl, Ctrl_: TComponent;
i: integer;
begin
//handle the Control itself first
TComponent(apanel) := CloneComponent(pObjectName);
with apanel do
begin
Left := 24;
Top :=64;
end;
//now handle the childcontrols
for i:= 0 to pObjectName.ControlCount-1 do
begin
Ctrl := TComponent(pObjectName.Controls[i]);
Ctrl_ := CloneComponent(Ctrl);
TControl(Ctrl_).Parent := apanel;
TControl(Ctrl_).Left := TControl(Ctrl).Left;
TControl(Ctrl_).top := TControl(Ctrl).top;
end;
end;
The following is the the code that physically does the cloning(called above):
function TForm1.CloneComponent(AAncestor: TComponent): TComponent;
var
XMemoryStream: TMemoryStream;
XTempName: string;
begin
Result:=nil;
if not Assigned(AAncestor) then
exit;
XMemoryStream:=TMemoryStream.Create;
try
XTempName:= AAncestor.Name;
AAncestor.Name:='clone_' + XTempName + inttostr(panels);
inc(panels);
XMemoryStream.WriteComponent(AAncestor);
AAncestor.Name:=XTempName;
XMemoryStream.Position:=0;
Result:=TComponentClass(AAncestor.ClassType).Create(AAncestor.Owner);
if AAncestor is TControl then TControl(Result).Parent:=TControl(AAncestor).Parent;
XMemoryStream.ReadComponent(Result);
finally
XMemoryStream.Free;
end;
end;
So now I want to use the cloned objects but how do I call them in my code?
For example how can I call the checked function of one of the cloned check boxes?
Thanks for your help :)
Others are right and it is better to use frame but if we want just use your code we must fix it first. there is a problem in your code and that is the Inc(panles); position. you must put this line after loop of for i:= 0 to pObjectName.ControlCount-1 do in the ClonePanle procedure, not in the CloneComponent function.
If you fix that, then you can use FindComponent function to access the components that you want as Marko Paunovic said.
For example the name of the component that you put on the first Panel that you defined as the first instance which other cloned panels are cloned from that is TestCheckBox. If you cloned 20 times the Panel that we talked about; you can access the TCheckBox of the 16th Cloned obejct like this and changing it's caption to whatever you want:
(I suppose that the panels variable was 0, when the program started.)
TCheckBox(FindComponent('clone_TestCheckBox15')).Caption:='aaaaa';

Check If TEdit's Text Property Is Null On A Frame

So here is my situation. I have a Form (MainMenu) and a Frame (TestFrame). TestFrame is displayed on a TPanel located on MainMenu. Using this code:
frTestFrame := TfrTestFrame.Create(nil);
frTestFrame.Parent := plMain;
frTestFrame.Align := alClient;
frTestFrame.Visible := true;
TestFrame displays fine with no error. TestFrame has a few TEdit boxes on it. A TButton on MainMenu calls a procedure located in TestFrame to check if the TEdit boxes text property is null.
procedure TfmMainMenu.tbCheckClick(Sender: TObject);
begin
frTestFrame.Check;
end;
This function on TestFrame is supposed to go through all the "TEdit" components and use the function GetErrorData that returns a string if the TEdit's text property is null. That string is added to a TStringList and displayed if any TEdit boxes are null.
function TfrTestFrame.Check: Boolean;
var
ErrorList: TStringList;
ErrorString: string;
I: Integer;
begin
ErrorList := TStringList.Create;
for I := 0 to (frTestFrame.ComponentCount - 1) do
begin
if (frTestFrame.Components[I] is TEdit) then
begin
ErrorString := GetErrorData(frTestFrame.Components[I]);
if (ErrorString <> '') then
begin
ErrorList.Add(ErrorString);
end;
end;
end;
if (ErrorList.Count > 0) then
begin
ShowMessage('Please Add The Following Information: ' + #13#10 + ErrorList.Text);
result := false;
end;
result := true;
end;
function TfrTestFrame.GetErrorData(Sender: TObject): string;
var
Editbox: TEdit;
ErrorString: string;
begin
if (Sender is TEdit) then
begin
Editbox := TEdit(Sender);
if (Editbox.Text <> '') then
begin
Editbox.Color := clWindow;
result := '';
end
else
begin
Editbox.Color := clRed;
ErrorString := Editbox.Hint;
result := ErrorString;
end;
end;
end;
The problem is that when it hits the line "for I := 0 to (frTestFrame.ComponentCount - 1) do
" It blows up and I get the error "Access violation at 0x00458... Read of address 0x000..."
I do not know why this error is happening. I can only assume that maybe the Frame is not getting creating. Any help would be great. Thanks in advance.
According to your question, the line
for I := 0 to (frTestFrame.ComponentCount - 1) do
leads to an access violation at address 0x000..... Now, for a start, why won't you tell us the precise error message with the full details? Hiding the address makes it harder!
Anyway, it looks like the address is going to be a value very close to zero. In any case the only explanation for an access violation there is that frTestFrame is invalid. Most likely it is nil.
I note that the code in question is inside a TfrTestFrame method. So why do you use frTestFrame to refer to the object? You are already inside an instance of the object. Do you have multiple global variables named frTestFrame? Perhaps one in the main form unit and one in the frame unit?
You should stop using global variables for your GUI objects. I know that the IDE leads you that way. Resist the temptation to program that way. Abuse of global variables leads to pain and suffering.
Since the code is inside a TfrTestFrame method you can use Self. In all your TfrTestFrame methods remove all references to frTestFrame. Your loop should be like this:
for I := 0 to ComponentCount - 1 do
and the rest of the methods in that class need similar treatment. Note that you don't need to explicitly write Self and it is idiomatic not to.
Finally, I urge you to learn how to use the debugger. It's a wonderful tool and if you would use it, it would have told you what the problem was. Don't be helpless, let the tools help you.

Delphi : Globally change ADO command timeout

We have a HUGE Delphi 2005 application with LOTS of ADO components (TADODataset, TADOStoredPRoc, TADOCommand...) spread on hundreads of forms. All of them are connected to a SINGLE TADOConnection.
Most of these components have their CommandTimeout property set to the default (30s) but a few have it set to 5 minutes (300s) and some are set to never timeout (0s).
I'd like to be able to globally change this setting for all ADO components application-wide. I'd prefer to do it programmatically at runtime so that I could tweak the timeouts on a per-installation basis if I need to.
I was hoping I could find a global event on the connection when an ADO component is created/attached, where I could tweak the commandtimeout, or hack my way into injecting my code in the components themselves, but came up blank.
I don't want to create decendants because I'll have to search/replace trought all the components, and if I ever forget to use the descendants instead of the regular ADO components my timeout wont follow the rest of the application.
Anybody has an idea how we could do this ?
If all of you ADO components are placed on a form, you can iterate over all forms using the Screen.Forms and Screen.FormCount properties. For each form iterate over its ComponentCount/Components property and check for TADOCommand, TADODataSet, TADOQuery, TADOStoredProc and TADOTable. Then you can set the timeout as you wish. Of course, if you create forms dynamically you have to take this into account separately.
The following code may guide you.
procedure SetADOTimeout(ATimeout: Integer);
var
cmp: TComponent;
frm: TForm;
I: Integer;
J: Integer;
begin
for I := 0 to Screen.FormCount - 1 do begin
frm := Screen.Forms[I];
for J := 0 to frm.ComponentCount - 1 do begin
cmp := frm.Components[J];
if cmp is TADOCommand then
TADOCommand(cmp).CommandTimeout := ATimeout
else if cmp is TADODataSet then
TADODataSet(cmp).CommandTimeout := ATimeout
else if cmp is TADOQuery then
TADOQuery(cmp).CommandTimeout := ATimeout
else if cmp is TADOStoredProc then
TADOStoredProc(cmp).CommandTimeout := ATimeout
else if cmp is TADOTable then
TADOTable(cmp).CommandTimeout := ATimeout;
end;
end;
end;
Greetings to all Argentinian solutions!
Just define the OnWillExecute event handler for the TADOConnection you have and write following code:
type
TCustomADODataSetAccess = class(TCustomADODataSet);
procedure TYourDataModule.ADOConnectionWillExecute(...);
var
i: Integer;
begin
for i := 0 to ADOConnection.DataSetCount - 1 do
TCustomADODataSetAccess(ADOConnection.DataSets[i]).CommandTimeout := Connection.CommandTimeout;
end;
This will set the command timeout for any query/table/stored procedure which uses your ADO connection.
According to the documentation, you can use CommandCount and Commands to locate all the open components attached to your TADOConnection.
Your problem is likely to be dynamically created forms. You'll need to find "something" to hook when a form is created and check for ADO components on that form.
If your forms descend from a custom form class, you could do this in the form's constructor or OnCreate event.
If not, you could look at TApplicationEvents and using the TApplication's OnIdle event.
Since CommandTimeout is introduced in the TCustomADODataset class, you can iterate for each form/datamodule, find a TCustomADODataset and it's descendants (ADODataset, ADOTable, ADOQuery) then set the Property.
procedure SetADOCommandTimeOut(aTimeOut: integer);
var
i, j: integer;
begin
for i:= 0 to Screen.FormCount-1 do
begin
for j:= 0 to Forms[i].ComponentCount-1 do
if Forms[i].Components[j] is TCustomADODataset then
TCustomADODataset1(Forms[i].Components[j]).CommandTimeOut:= aTimeOut;
end;
for i:= 0 to Screen.DataModuleCount-1 do
begin
for j:= 0 to Datamodules[i].ComponentCount-1 do
if Datamodules[i].Components[j] is TCustomADODataset then
TCustomADODataset1(Datamodules[i].Components[j]).CommandTimeOut:= aTimeOut;
end;
end;
Note: TCustomADODataset1 is exactly TCustomADODataset, only it has a published CommandTimeOut property :
TCustomADODataset1 = class(TCustomADODataset)
published
property CommandTimeOut;
end;
But it's only applied to forms/datamodules which are already created. If you create your forms/datamodules dynamically, then you must apply it whenever a new form/datamodule is created.
One way to do it is by override Notification in your Mainform, checking for a new a creation of form/datamodule, but this a little bit tricky since at the the creation time, all the components are not created yet. You trick it by delay it for a while using a timer (I don't know a more elegant way - just to show the idea)
Procedure TMainForm.Notification(AComponent: TComponent; Operation: TOperation);
begin
inherited;
if (Operation = opInsert) and (
((AComponent is TForm) and not (aComponent is TMainForm)) // exclude MainForm
or (AComponent is TDataModule)
) then
begin
Timer1.Interval:= 2000; // 2 seconds ?
Timer1.Enabled:= True;
end;
end;
Procedure TMainForm.Timer1Timer(Sender: TObject);
begin
Timer1.Enabled:= False;
SetADOCommandTimeOut(MyTimeOut);
end;
You create datamodule hierarchies? If so, you can use code like Uwe's answer on your patriarch form (which all other datamodules inherit).

Resources