Delphi: refer to a component from multiple frames - delphi

I want to refer to a component (using loop) which exists in multiple frames. The component is the same for all frames.
I have a ComboBox named "comboBoxAccount".
I also have multiple frames (which I instantiate at runtime).
The frames I create are named:
frm1, frm2, frm3
Each frame has a multple TabSheets created at runtime named:
page_1, page_2, page_3
So, I am currently using:
Tfrm1(FindComponent('page_'+i)).comboBoxAccount
Which means I use FindComponent with i in a loop to access all comboBoxAccount in all TabSheets.
The problem is that I have to use casting to make Delphi understand which component I want to access, so I've put Tfrm1( ) in front of FindComponent.
So, if I want to use casting with a loop what can I do?
I could just use:
Tfrm1(FindComponent('page_'+i)).comboBoxAccount
Tfrm2(FindComponent('page_'+i)).comboBoxAccount
Tfrm3(FindComponent('page_'+i)).comboBoxAccount
but is there a way to loop Tfrm without writing them one by one?

page_1, page_2, etc are TabSheets, not frames, but you are casting them to frames, which is wrong.
If each frame has multiple TabSheets on it, and each TabSheet has a ComboBox on it, you would not use FindComponent() at all. You would have to iterate through parent/child Controls[] lists instead, or use FindChildControl().
However, if the frame is the Owner of the ComboBox (which it would be if the ComboBox is added to the frame at design-time), then you would use FindComponent() on the frame itself. But you cannot have multiple child components with the same name, so each ComboBox on each TabSheet would need a unique name:
cb := TComboBox(frm1.FindComponent('comboBoxAccount_'+IntToStr(i)));
Repeating for each index of frm1, then all indexes of frm2, then frm3.
If your ComboBox really is named just comboBoxAccount then I wonder if you really have one parent container that has multiple TabSheets on it, and each TabSheet has a frame on it, and each frame has only one ComboBox named comboBoxAccount on it. If that is the case, you don't need to do any searching at all, just use your existing object pointers directly:
cb := frm1.comboBoxAccount;
cb := frm2.comboBoxAccount;
cb := frm3.comboBoxAccount;
But if you really want to use a loop, then put the frames into a list, eg:
private
Frames: TList;
...
Frames := TList.Create;
Frames.Add(frm1);
Frames.Add(frm2);
Frames.Add(frm3);
...
for I := 0 to Frames.Count-1 do
begin
// assuming TFrm1, TFrm2, etc do not have a common ancestor holding the ComboBox...
cb := TComboBox(TWinControl(Frames[i]).FindComponent('comboBoxAccount'));
...
end;
Or use an array:
private
// assuming TFrm1, TFrm2, etc do not have a common ancestor holding the ComboBox...
Frames: array[0..2] of TWinControl;
...
Frames[0] := frm1;
Frames[1] := frm2;
Frames[2] := frm3;
...
for I := Low(Frames) to High(Frames) do
begin
cb := TComboBox(Frames[i].FindComponent('comboBoxAccount'));
...
end;
Update: given new information you provided, you can find the ComboBox like this instead:
cb := Tfrm1(PageControl[i].Pages[j].FindChildControl('frame_'+IntToStr(i))).comboBoxAccount;
Your while loop is creating multiple tabsheets and frames that have the same Name, which is not valid. I would suggest not naming them at all.
while condition=true do
begin
TabSheet := TTabSheet.Create(PageControl[i]);
TabSheet.Caption := '';
//TabSheet.Name:='tabesheet_'+IntToStr(i);
TabSheet.PageControl := PageControl[i];
TabSheet.PageControl.ActivePageIndex:=0;
{I also create a frame for each TabSheet. In every frame there is the ComboBox that I want to access}
frame := Tfrm1.Create(TabSheet);
//frame.Name:='frame_'+IntToStr(i);
frame.Parent:=TabSheet;
end;
Assuming a frame is the only child of its TabSheet, you can then do this:
cb := Tfrm1(PageControl[i].Pages[j].Controls[0]).comboBoxAccount;

Related

Insert existing TLabels into a dynamically created array of TLabels

I'm trying to create a dynamic array of TLabels, then insert already existing TLabels created in the IDE into it so I can then use the array in the code.
My purpose is to use this method for several similar processes.
I would like to make it by using loops.
I saw this thread and it was very helpful to understand about creating arrays of TLabels and populating them with TLabels for the desired purposes, but I couldnt find the particular solution for the case when the labels are already created.
Use variables for object name in Delphi
Basically, what I'm trying to automate is this:
var
LabelArray : array of TLabel;
SetLength(LabelArray, 17);
LabelArray[0] := M2;
LabelArray[1] := M3;
.
.
LabelArray[16] := M18
M2 to M18 are 17 labels that are the TLabels already created and positioned on the Form.
You can use the form's FindComponent method to add the labels to your array.
The code below depends on your having declared LabelArray as a private variable at the form level, so that it exists and is visible when the form is created and the code executes. It also hard-codes in 17 as the number of labels, so if you delete or rename a label you'll have an empty spot in the array - I did not include any error checking to make sure a label is found before putting it into the array and the element could end up nil if a label is missing.
procedure TForm1.FormCreate(Sender: TObject);
var
i: Integer;
begin
SetLength(LabelArray, 17);
{
I use Low() and High() here to avoid having multiple places where
you would have to change the code if you end up adding or removing
labels in the future.
Also note that the index into the array starts at 0, so the code that
calls Format() adjusts that index by 2 to start at M2 instead of M0.
}
for i := Low(LabelArray) to High(LabelArray) do
LabelArray[i] := TLabel(FindComponent(Format('M%d', [i + 2])));
end;
If your labels cannot be found using a common name scheme, but you simply want to get all labels on the same panel / group box etc. into the array, you can use the Controls[] property of the parent control:
SetLength(LabelArray, ParentCtrl.ControlCount);
cnt := 0;
for i := 0 to ParentCtrl.ControlCount - 1 do begin
if ParentCtrl.Controls[i] is TLabel then begin
LabelArray[cnt] := TLabel(ParentCtrl.Controls[i]);
Inc(cnt);
end;
end;
SetLength(LabelArray, cnt);
ParentCtrl is the panel / groupbox or even the form itself that is the common parent of all the labels you are interested in.
If you simply want all labels on a form, even if they are located on other controls, you can use the Components[] property of the form in the same way.

Delphi: TreeView elements vs. Form position changing

We have found something is seems to be a bug(?), and cause bug in our code.
Delphi XE3, Win32. Two forms, the main have button:
procedure TForm4.Button1Click(Sender: TObject);
begin
with TForm1.Create(Application) do
begin
ShowModal;
Release;
end;
end;
The Form1 is doing this:
procedure TForm1.FormCreate(Sender: TObject);
var
i, j: integer;
mn: TTreeNode;
begin
for i := 1 to 10 do
begin
mn := TreeView1.Items.Add(nil, 'M' + IntToStr(i));
for j := 1 to 10 do
begin
TreeView1.Items.AddChild(mn, 'C' + IntToStr(j));
end;
end;
Position := poDesigned;
beep;
Caption := IntToStr(TreeView1.Items.Count);
end;
After this I get 0 elements in caption.
But when I have a button in that form with this code...
procedure TForm1.Button1Click(Sender: TObject);
begin
Caption := IntToStr(TreeView1.Items.Count);
end;
...
Then I can see the good number (110 elements).
If I write TreeView1.Handleneeded after Position changing, the count is also good.
The problem is based on RecreateWnd, which calls DestroyHandles.
But they will be repaired only in Show (in the Activate event I can see good result too).
TreeView is a special control, because the tree elements are children, and the count is calculated by on them, no matter it have real sub-object list.
The main problem that ReCreateWnd called often by another methods to, so it can cause problems in another sections too, and I cannot put HandleNeeded before all .Count calculation.
(We have special base form that correct the Position to poDesigned if it was poScreenCenter to it can be positionable later. This happens after FormCreate call, in an inner method. We found this problem only with these kind of forms, but later we could reproduce it in a simple code too)
So the question is - what is the global solution to this problem?
(Did you experience this in XE5 too?)
Thank you for all help, info, doc.
The Form's HWND is getting destroyed when you set the Position. That also destroys all child HWNDs. The TreeView's HWND has not been re-created yet when you read its Count, which is why it reports 0. Calling TreeView.HandleNeeded after setting the Position forces the TreeView to re-create its HWND immediately, which will re-load any TreeNodes that had beencached internally when the TreeView's HWND was destroyed (but only if the TreeView.CreateWndRestores property is True, which it is by default).
A TreeView stores its child nodes inside of its HWND. Reading the Items.Count merely asks the HWND how many nodes it has, so if there is no HWND then the Count will be 0. TTreeView does not keep its own list of TTreeNode objects, it merely assigns them as user-defined data in the physical nodes themselves. When nodes are removed from the tree, TTreeView frees the associated TTreeNode objects. In the case of HWND recreation, TTreeView caches the TTreeNode data and then re-assigns it back to new nodes when the HWND is re-created. But again, it does not keep track of the TTreeNode objects.
What TTreeView could have done is store the current number of nodes during HWND destruction, and then have Items.Count return that value if the HWND has not been re-created yet. But alas, TTreeView does not do that. But you could implement that manually by subclassing TTreeView to intercept the CreateWnd() and DestroyWnd() methods, and then write your own function that returns the actual Items.Count when HandleAllocated is true and returns your cached value if it is false.
If the Form is visible to the user then its HWND (and the HWND of its children) will be available, since a control is not visible without an HWND, so Items.Count will be available if the TTreeView is visible to the user. If its HWND is ever destroyed while visible, the VCL will re-create the HWND immediately so the user does not see a missing control. However, if the Form (or TreeView) is not visible to the user when the HWND is destroyed, the HWND will not be re-created until it is actually needed when the Form/TreeView is made visible again.
Since you are forcing the Position to always be poDesigned at the time of Form creation, why not just set the Position to poDesigned at design-time? Otherwise, you can simply set the Position before populating the TreeView instead of afterwards, at least:
procedure TForm1.FormCreate(Sender: TObject);
var
i, j: integer;
mn: TTreeNode;
begin
Position := poDesigned; // <-- here
for i := 1 to 10 do
begin
mn := TreeView1.Items.Add(nil, 'M' + IntToStr(i)); // <-- first call to Add() forces HWND recreation if needed
for j := 1 to 10 do
begin
TreeView1.Items.AddChild(mn, 'C' + IntToStr(j));
end;
end;
Beep;
Caption := IntToStr(TreeView1.Items.Count); // <-- correct value reported
end;
This happens in all versions of Delphi. This is simply how things work in the VCL.
On a side note, you should use Free() instead of Release(). Release() is meant to be used only when a Form needs to Free() itself, such as in an event handler, by delaying the Free() until the Form becomes idle. Since the form is already closed and idle by the time ShowModal() exits, it is safe to Free() the Form immediately.
I think you are over-reacting. You state:
The main problem that ReCreateWnd called often by another methods to, so it can cause problems in another sections too, and I cannot put HandleNeeded before all .Count calculation.
But then in comments you say that these other scenarios are:
CMCtlD3Changed CMSysColorChange BORDERSTYLE SetAxBorderStyle SetBorderIcons Dock SetPosition SetPopupMode set_PopupParent RecreateAsPopup ShowModal SetMainFormOnTaskBar
Indeed these methods will cause window recreation. But why does that matter for you? Do you routinely assign to MainFormOnTaskBar and then immediately request the count of items in your tree view? Do you change Ctl3D regularly? Are you changing BorderIcons dynamically? I very much doubt it.
So I think you need to tackle the immediate problem which relates to the timing of the setting of Position. I would deal with that by making sure that Position is set before you populate the tree view. Do that by either setting Position at design time, or simply before you populate the tree view.
Of course, there may well be other problem related to window creation. You'll need to treat them on a case by case basis. I suspect that you are hoping for some sort of magic switch that just makes this issue disappear. I don't believe that there is one. If you try to read the item count before the handle is created, then you will suffer from this problem. The solution is not to read the item count before the handle is created.

Typecast child component properties to ini file

I'm currently having a major headache with typecasting of component properties.
On my form, i have a TPanel called "scene". Also on the form, i have a button that creates a TSelection, and within that TSelection creates a TImage and then load a picture into that TImage. The name for the TSelection is assigned via a TEdit known as "ImgObjName". It then writes this name to an inifile. The events for the TSelection are assigned to procedures elsewhere in the code. As you know, the TSelection component can be moved around (and resized) at runtime. The TImage has it's HitTest turned off while the TSelection has it turned on.
The above works as i want it to, but the next part is where i'm stuck. Essentially, on a timer, i want to write a select few of the properties of each child component to an TMemIniFile. There's 2 ways i'm willing to do this;
1) Write the properties of each child to seperate TMemInifiles.
2) Write the properties of each child to a single TMemIniFile, but make the section identify which component the values in that section relate to.
I've tried a few different methods, but all of them have caused me some major problems (usually "index out of bounds").
My current method is as such;
ChgPos is a global boolean variable which is TRUE when the the mousedown event on one of the TSelection objects is fire, and FALSE when the MouseUp event is fired. This boolean procedure works perfectly for these purposes so no changes are needed there.
TimerBar is a TTrackBar that's been created at designtime. It's value changes based on a timer.
AnimIni is the TMemIniFile which has been assigned earlier in the code. For this purpose, i've set it not to free up the file (so as there's no access violations).
var
i: Integer;
PosX, PosY: Integer;
begin
for i := 0 to Scene.ChildrenCount - 1 do
begin
if Scene.Components[i] is TSelection then
begin
PosX := AnimIni.ReadInteger(IntToStr(Round(TimerBar.Value)) + '_Object' +
IntToStr(i), 'PosX', PosX);
PosY := AnimIni.ReadInteger(IntToStr(Round(TimerBar.Value)) + '_Object' +
IntToStr(i), 'PosY', PosY);
end;
end;
if ChgPos = False then
begin
if Scene.Components[i] is TSelection then
begin
(Scene.Components[i] as TSelection).Position.X := PosX;
(Scene.Components[i] as TSelection).Position.Y := PosY;
end;
end
else if ChgPos = True then
begin
AnimIni.WriteInteger(IntToStr(Round(TimerBar.Value)) + '_Object' +
IntToStr(i), 'PosX', Round((Scene.Children[i] as TSelection).Position.X));
AnimIni.WriteInteger(IntToStr(Round(TimerBar.Value)) + '_Object' +
IntToStr(i), 'PosY', Round((Scene.Children[i] as TSelection).Position.Y));
end;
end;
I'm struggling to figure out where to go with this. I'm getting "Index out of range" errors with it. I'll also need to save the TImage component properties (particularly the parent and Bitmap location, but i feel it's important for me to get the code working with at least one component at the moment).
I'm somewhat new to typecasting (as all my previous projects worked without the need for it), but all my experience with it so far has been pleasently successful. It's just in this particular case it's proven to become more complicated than i can work out without some assistance.
I did try WriteComponent and ReadComponent and using multiple files to stream the data in realtime in relation to the value of TimerBar but it's much too slow for what i want to achieve (particularly on the write function). The inifile method does work from my earlier testing, but it's actually working with typecasting of multiple components that are created at runtime that i'm having issues with.
Can anyone shed some light on a potential solution or the direction i should head in?
You are mixing components and children. If you loop through all children of the panel, do not use that index on the Components property, but the Children property. (I assume your code compiled and XE2 has a Children property, otherwise I think you mean Controls and ControlCount).
Like LU RD already commented, you are using the for-loop variable i outside the for-loop. I am sure you want it inside. You are also warned by the compiler for this:
FOR-loop variable 'i' may be undefined after loop
Always make sure you have zero! compiler errors, warnings and hints.
I do not understand your routine logic for it loads the settings even if you actually want to write them. I think you only want to load the settings from the MemIniFile when ChgPos is false.
No guarantees given, but I think the routine should look like this (including a few syntax improvements):
var
i: Integer;
Selection: TSelection;
PosX, PosY: Integer;
begin
for i := 0 to Scene.ChildrenCount - 1 do
if Scene.Children[i] is TSelection then
begin
Selection := Scene.Children[i] as TSelection;
if ChgPos then
begin
AnimIni.WriteInteger(IntToStr(Round(TimerBar.Value)) + '_Object' +
IntToStr(i), 'PosX', Round(Selection.Position.X));
AnimIni.WriteInteger(IntToStr(Round(TimerBar.Value)) + '_Object' +
IntToStr(i), 'PosY', Round(Selection.Position.Y));
end
else
begin
PosX := AnimIni.ReadInteger(IntToStr(Round(TimerBar.Value)) +
'_Object' + IntToStr(i), 'PosX', PosX);
PosY := AnimIni.ReadInteger(IntToStr(Round(TimerBar.Value)) +
'_Object' + IntToStr(i), 'PosY', PosY);
Selection.Position.X := PosX;
Selection.Position.Y := PosY;
end;
end;
end;
Although I seriously doubt the default values for the AnimIni.ReadInteger function, PosX and PosY, which are unassigned. If no section in the ini file is found, then PosX and PosY will have arbitrary values. You should initialize them to whatever makes sense.
Your loop counter and indexed properties don't match. ComponentCount and Components[] go together. And ChildrenCount and Children[] go together. You want to work with the latter pair since you are interesting in the children of the controls. The ComponentCount and Components[] properties refer to ownership which is a different concept.
What's more your loop ends but you continue using the loop variable after the loop variable. That's clearly wrong. It looks like it needs to be inside the loop and inside the Scene.Children[i] is TSelection test.
As an aside ChildrenCount is grammatically incorrect alongside ComponentCount and ControlCount. This property should have been named ChildCount.

How to handle subcomponents and properties created in runtime vs designtime?

I can' t realize this thing. I have a component in DELPHI that includes 2 other components: a Firemonkey Layout and inside of that an dynamic array of TLayout which includes a TRectangle.
This is achieved through the property BarNumber.
I have lots of problems about Design Time vs. Runtime behaviour, this is due to the DFM (FMX in Firemonkey) that stores the subcomponents as part of the Object.
Now. This is the code of the On Create part.
constructor TFluffyTable.Create(Owner: TComponent);
var
i: integer;
begin
inherited Create(Owner);
Width:=300;
Height:= 160;
BarNumber:=100;
SetLength(Column, FBarNumber);
for i := 0 to (FBarNumber-1) do
begin
Column[i]:= TColumn.Create(Self);
Column[i].Name:= 'Column_' + IntToStr(i);
Column[i].Parent:= Self;
Column[i].Height:=Height;
Column[i].Width:=Width/FBarNumber;
Column[i].Align:= TAlignLayout.alMostLeft;
end;
end;
If I register the component and I use it in design time I get the correct number of bars displayed. But if I run the program with the component, I get twice the number of bars, since the EXE loads the values. I managed to solve this with
if not (csDesigning in ComponentState) then
just before the for loop.
But I can't see, obviously, the BARS in design mode. Well I can stand that if this is the only solution.
That's not over..!
For a strange reason, The only one place I can set my values for Width, Height and BarNumber is that part of code. If I set them in the object inspector they won't be considered and reset to default when I run the program.
(BarNumber is a variable which reads and writes on FBarNumber)
In short: I don't know how to handle and manage my component to make BarNumber and other properties to be set in design time, and to see the correct number of bars in Runtime.
Thank you so much.
I had the similar problem. I used stored property to avoid this problem.
Example:
constructor TMachine.Create(AOwner: TComponent);
begin
inherited;
self.Width := 50;
self.Height := 90;
// create machine rectangle and set default properties
FMachine := TRectangle.Create(self);
FMachine.Parent := self;
FMachine.Height := 50;
FMachine.Align := TAlignLayout.alBottom;
FMachine.Fill.Color := TAlphaColorRec.red;
FMachine.Stroke.Color := TAlphaColorRec.Black;
FMachine.Stroke.Thickness := 3;
FMachine.Stored := false;
end;
The problem is that the component you create at design time will be stored in the fmx files. When you run the application you have twice controls, to resolve the problem you need to set the stored property to false to the sub objects of your component like this:
Column[i].Stored := False;
You have to make sure that you are starting with 0 columns at runtime.
Just add something like:
for [i] = pred(length(column)) downto 0 do
begin
column[i].free
end;
before you start making the columns.

create Tframes on runtime:

is it posible to create runtime frame and add existing panels like setting the parent of panel to the frame? and when it added, dulplicate the frame and use it?
like:
f:= Tframe. create(..)
...
panel3.parent = f; //where panel3 has many controls.
then duplicate the f? was it posible? how? or any other suggerstion?
e
I don't think you would solve this by duplicating. What you need is a function like this:
function CreateFrameAndHostPanel(Owner: TComponent; Parent: TWinControl; Panel: TPanel): TFrame;
begin
Result := TFrame.Create(Owner);
Try
Result.Parent := Parent;
Panel.Parent := Result;
Except
FreeAndNil(Result);
raise;
End;
end;
You need to remember that all controls have a parent and an owner. Owners could be nil but then you need to free those controls through code, so most controls are owned by some other component.
Thus, if the owner gets destroyed, the panel would be destroyed too. And if the panel was created in design-time then it's owned by the form that it's on!
Destroying that form would destroy the panel!
But if you create the panels in runtime and set Application as owner instead of a form, they could be moved over multiple forms and frames.
But is it a good design pattern? I don't know what you're trying to do but it's likely a bad idea!
In general, it would be more practical to design the whole frame with panels in design-time. Then add some code that would allow the frame to be created by copying data from another panel or control. That would be a better design pattern...
You must create the new frame (FRAME2) with the same code that you have used to create the first (FRAME1); And later, you must create all the component included (created on runtime) inside FRAME1 on FRAME2.
For to this, use:
for i := 0 to (FRAME1.ComponentCount - 1) do
...
cmp := TComponent(FRAME1.Component[i]);
... create cmp on Frame2
You can try a second alternative; Save the FRAME1 using a TMemoryStream (SaveComponent) and later create the new Frame and retrieve the saved information on Stream (I don't have test this option).
Regards.

Resources