Lazarus

Programming => General => Topic started by: andyf97 on March 30, 2023, 01:48:35 am

Title: How to access and set properties of Labels dynamically
Post by: andyf97 on March 30, 2023, 01:48:35 am
Hey up chaps.

I have two forms

On the main Form1 I have 6 Labels

On Form2 I have a button.

What I want to do is have a random number 1-6 that sets one of the label captions to that number, exist many methods online to generate the number but formatting the code to set the label is quite mind boggling, at least for me.

All I can think of is something like:  Form1 Label[R].caption = R

Press the button on form2
Random Number is generated and written to the caption of that label on form1.

its a little bit like an electronic dice

What would be the best approach to do that?





   
Title: Re: How to access and set properties of Labels dynamically
Post by: jamie on March 30, 2023, 02:06:02 am
you can build the name..

Label+IntTostr(Your_Random_Integer_Number);

 and use TControl(FindComponent(Your_Random_Name)).Caption := IntTOStr(Your_Random_Number);

Something like that.

Title: Re: How to access and set properties of Labels dynamically
Post by: andyf97 on March 30, 2023, 02:52:09 am
That puzzles me, if it is known that the labels are on Form1 and we know the name of the one we want to change the caption for, why is it required to use the findcomponent?


you can build the name..

Label+IntTostr(Your_Random_Integer_Number);

 and use TControl(FindComponent(Your_Random_Name)).Caption := IntTOStr(Your_Random_Number);

Something like that.
Title: Re: How to access and set properties of Labels dynamically
Post by: KodeZwerg on March 30, 2023, 10:04:26 am
I would also use FindComponent but a little different than jamie has shown.
That puzzles me, if it is known that the labels are on Form1 and we know the name of the one we want to change the caption for, why is it required to use the findcomponent?
Because Form1 has "uses Unit2" (for Form2), but unit2 can not have "uses Unit1" (circulary references are forbid to have)

Attached a tested and working Demo.
Title: Re: How to access and set properties of Labels dynamically
Post by: wp on March 30, 2023, 11:40:38 am
Find my solution in the attachment. It introduces two ideas:
Code: Pascal  [Select][+][-]
  1. type
  2.   TForm1 = class(TForm)
  3.     Label1: TLabel;
  4.     Label2: TLabel;
  5.     Label3: TLabel;
  6.     Label4: TLabel;
  7.     Label5: TLabel;
  8.     Label6: TLabel;
  9.     procedure FormCreate(Sender: TObject);
  10.   private
  11.     FLabels: array[1..6] of TLabel;
  12.   end;
  13.  
  14. procedure TForm1.FormCreate(Sender: TObject);
  15. begin
  16.   FLabels[1] := Label1;
  17.   FLabels[2] := Label2;
  18.   FLabels[3] := Label3;
  19.   FLabels[4] := Label4;
  20.   FLabels[5] := Label5;
  21.   FLabels[6] := Label6;
  22.   ClearLabels;
  23. end;
Code: Pascal  [Select][+][-]
  1. // Form 2
  2. type
  3.   TRandomValueEvent = procedure(Sender: TObject; AValue: Integer) of object;
  4.  
  5.   TForm2 = class(TForm)
  6.     Button1: TButton;
  7.     procedure Button1Click(Sender: TObject);
  8.   private
  9.     FOnRandomValue: TRandomValueEvent;
  10.   public
  11.     property OnRandomValue: TRandomValueEvent read FOnRandomValue write FOnRandomValue;
  12.   end;
  13.  
  14. procedure TForm2.Button1Click(Sender: TObject);
  15. var
  16.   value: Integer;
  17. begin
  18.   if Assigned(OnRandomValue) then
  19.   begin
  20.     value := Random(6) + 1;    // 1 ... 6
  21.     OnRandomValue(Self, value);
  22.   end;
  23. end;
  24.  
  25. //-------------------------------------------------------
  26.  
  27. // Form1
  28.  
  29. uses
  30.   Unit2;
  31.  
  32. procedure TForm1.Button1Click(Sender: TObject);
  33. begin
  34.   if Form2 = nil then
  35.     Form2 := TForm2.Create(Application);
  36.   Form2.OnRandomValue := @RandomValueHandler; // Assign the event handler to Form2
  37.   Form2.Show;
  38. end;
  39.  
  40. procedure TForm1.RandomValueHandler(Sender: TObject; AValue: Integer);
  41. begin
  42.   GroupBox1.Caption := 'Received value: ' + IntToStr(AValue);
  43.   ClearLabels;
  44.   Labels[AValue].Caption := IntToStr(AValue);
  45. end;
             
Title: Re: How to access and set properties of Labels dynamically
Post by: KodeZwerg on March 31, 2023, 01:55:40 am
I don't like to call FindComponent. Because I don't like to use the component's Name.
Okies, removed FindComponent and Name to get valid labels
KodeZwerg's solution is nice but works only when the label form is the MainForm of the application.
I was doing how I thought that the final should do but of course I challenge your statement with an updated approach.
To make it work, name your destination labels as "xyzTarget123" on any form or use Tag (exemplary 666), my new code needs of course the forms name to work properly.
Code: Pascal  [Select][+][-]
  1. procedure TForm2.Button1Click(Sender: TObject);
  2. // remember, the search is case sensitive, you can use LowerCase() or UpperCase() to make it more generic working
  3. // updated to work with Tag instead of Name for target labels
  4. type
  5.   TLabels = array of TLabel;
  6. var
  7.   x, y, z: Integer;
  8.   arr: TLabels;
  9. begin
  10.   // initialize empty array
  11.   arr := nil;
  12.   SetLength(arr, 0);
  13.   // iterate over root of application to find a specific form where we wanna change something
  14.   for x := 0 to Pred(Application.ComponentCount) do
  15.     // we searching for a form with a name of "Form1"
  16.     if (Pos('Form1', Application.Components[x].Name) > 0) then
  17.       // iterate over found form
  18.       for y := 0 to Pred((Application.Components[x] as TForm).ComponentCount) do
  19.         // exclude anything that is not a TLabel and find labels with a "Target" inside name or a matching Tag, just use one(!) way
  20.         if (((Application.Components[x] as TForm).Components[y] is TLabel) and
  21.             // compare against a value
  22.             (((Application.Components[x] as TForm).Components[y] as TLabel).Tag = 666)) then
  23.             // compare against a name
  24. //            (Pos('Target', (Application.Components[x] as TForm).Components[y].Name) > 0)) then
  25.           // we found something, add it to the internal array
  26.           begin
  27.             z := Length(arr);
  28.             SetLength(arr, Succ(z));
  29.             arr[z] := ((Application.Components[x] as TForm).Components[y] as TLabel);
  30.           end;
  31.   // when we found something
  32.   if (Length(arr) > 0) then
  33.     begin
  34.       // pick one random and rename the caption to an integer
  35.       x := Random(Length(arr));
  36.       arr[x].Caption := IntToStr(Succ(x));
  37.       // optional: we do not need the array anymore, kill it (it will get destroyed automagical when running out of scope)
  38.       SetLength(arr, 0);
  39.       arr := nil;
  40.     end;
  41. end;
Title: Re: How to access and set properties of Labels dynamically
Post by: tetrastes on March 31, 2023, 10:31:25 am
Oh, how many letters.  %)

I have quite a few such cases in my programs, and I always do something like

Code: Pascal  [Select][+][-]
  1. unit Unit2;
  2. . . .
  3.  
  4. implementation
  5.  
  6. uses Unit1;
  7. . . .
  8.  
  9. procedure TForm2.Button1Click(Sender: TObject);
  10. var
  11.     i: integer;
  12. begin
  13.     i := Random(5) + 1;
  14.     Form1.LabelsArray[i].Caption := IntToStr(i);
  15. end;

and have no problems.

Though I understand, what wp wrote, but to create an event every time... May end up with too many EventHandlers.
Title: Re: How to access and set properties of Labels dynamically
Post by: andyf97 on March 31, 2023, 12:18:27 pm
KodeZwerg, Thanks a lot for that and your time. 

I tried it but it only lists the components on Form2

So I broke the idea down a bit to try and figure out what is going on, with this little bit of code it is also only listing components from Form2.

I really do not understand the hierarchy, is it like the object inspector where there are branches. Also what is this Pred() I could not find it anywhere.



Code: Pascal  [Select][+][-]
  1. procedure TForm2.Button6Click(Sender: TObject);
  2.   var
  3.     i,x: Integer;
  4.   begin
  5.    for x := 0 to Pred(Application.ComponentCount) do
  6.    if (Pos('Form1', Application.Components[x].Name) > 0) then  // we searching for a form with a name of "Form1"
  7.    for i := 0 to ComponentCount-1 do
  8.    Memo2.Lines.Add(Components[i].Name);
  9. end;
  10.  
  11.  
  12.  
  13.  









Code: Pascal  [Select][+][-]
  1. procedure TForm2.Button1Click(Sender: TObject);
  2. // remember, the search is case sensitive, you can use LowerCase() or UpperCase() to make it more generic working
  3. // updated to work with Tag instead of Name for target labels
  4. type
  5.   TLabels = array of TLabel;
  6. var
  7.   x, y, z: Integer;
  8.   arr: TLabels;
  9. begin
  10.   // initialize empty array
  11.   arr := nil;
  12.   SetLength(arr, 0);
  13.   // iterate over root of application to find a specific form where we wanna change something
  14.   for x := 0 to Pred(Application.ComponentCount) do
  15.     // we searching for a form with a name of "Form1"
  16.     if (Pos('Form1', Application.Components[x].Name) > 0) then
  17.       // iterate over found form
  18.       for y := 0 to Pred((Application.Components[x] as TForm).ComponentCount) do
  19.         // exclude anything that is not a TLabel and find labels with a "Target" inside name or a matching Tag, just use one(!) way
  20.         if (((Application.Components[x] as TForm).Components[y] is TLabel) and
  21.             // compare against a value
  22.             (((Application.Components[x] as TForm).Components[y] as TLabel).Tag = 666)) then
  23.             // compare against a name
  24. //            (Pos('Target', (Application.Components[x] as TForm).Components[y].Name) > 0)) then
  25.           // we found something, add it to the internal array
  26.           begin
  27.             z := Length(arr);
  28.             SetLength(arr, Succ(z));
  29.             arr[z] := ((Application.Components[x] as TForm).Components[y] as TLabel);
  30.           end;
  31.   // when we found something
  32.   if (Length(arr) > 0) then
  33.     begin
  34.       // pick one random and rename the caption to an integer
  35.       x := Random(Length(arr));
  36.       arr[x].Caption := IntToStr(Succ(x));
  37.       // optional: we do not need the array anymore, kill it (it will get destroyed automagical when running out of scope)
  38.       SetLength(arr, 0);
  39.       arr := nil;
  40.     end;
  41. end;
Title: Re: How to access and set properties of Labels dynamically
Post by: KodeZwerg on March 31, 2023, 12:30:48 pm
and have no problems.
I do have a problem with your way.
Quote
Unit2.pas(8,9) Error: Circular unit reference between Unit2 and Unit1
Title: Re: How to access and set properties of Labels dynamically
Post by: KodeZwerg on March 31, 2023, 12:39:21 pm
I tried it but it only lists the components on Form2
I have no Idea about your projects source details so I attached a demo again that has on form2 a button to random change a "target/666" label from form1 or list everything that form1 has.
Title: Re: How to access and set properties of Labels dynamically
Post by: KodeZwerg on March 31, 2023, 12:44:37 pm
So I broke the idea down a bit to try and figure out what is going on, with this little bit of code it is also only listing components from Form2.
Code: Pascal  [Select][+][-]
  1.    for i := 0 to ComponentCount-1 do // <<<<< ERROR, you use componentcount for current form and not the one that you searched
  2.    Memo2.Lines.Add(Components[i].Name); // <<<<< ERROR, you use componentcount for current form and not the one that you searched
  3.  
So if you copy and paste, please do it correct  :-*
Title: Re: How to access and set properties of Labels dynamically
Post by: wp on March 31, 2023, 12:45:00 pm
No, there is no circular unit reference when both units are listed under implementation:
Code: Pascal  [Select][+][-]
  1. // Unit 1
  2. unit Unit1;
  3. interface
  4. ...
  5. implementation
  6. uses
  7.   Unit2;
  8. procedure TForm1.Button1Click(Sender: TObject);
  9. begin
  10.   Form2.Show;
  11. end;
  12. end.
  13.  
  14. -------------------------------------------------------
  15.  
  16. // Unit 2
  17. unit Unit2;
  18. interface
  19. ...
  20. implementation
  21. uses
  22.   Unit1;
  23. ...
  24. procedure TForm2.Button1Click(Sender: TObject);
  25. var
  26.     i: integer;
  27. begin
  28.     i := Random(5) + 1;
  29.     Form1.LabelsArray[i].Caption := IntToStr(i);
  30. end;
  31. end.
  32.  

BUT: When the project gets bigger and bigger and more and more units are added the interdependence of the units becomes very complicated and there is a chance that such a solution will not compile any more. Also, you cannot test the project units independently - you always need Form1 if you want to test Form2 (and if Form1 uses other forms and units you will also need those - a huge mess!). Another issue is that unit2 compiles only when the instance of TForm1 is really named "Form1". Therefore, unless you have a very simple project you should always avoid such a situation.
Title: Re: How to access and set properties of Labels dynamically
Post by: KodeZwerg on March 31, 2023, 12:52:55 pm
Also what is this Pred() I could not find it anywhere.
Pred gives back the Value - 1.
Therefore, unless you have a very simple project you should always avoid such a situation.
I second that, to me it is very bad code design as it may lead to future bugs.
Title: Re: How to access and set properties of Labels dynamically
Post by: balazsszekely on March 31, 2023, 12:54:16 pm
@wp
Quote
No, there is no circular unit reference when both units are listed under implementation
IIRC one of the unit can be in the interface section if the other one is in the implementation section.

I would pass Form1 to Form2's constructor, then store the reference in a private variable. Also subscribing to an event(as in your demo) is a nice solution.
Title: Re: How to access and set properties of Labels dynamically
Post by: KodeZwerg on March 31, 2023, 02:26:01 pm
Another approach could be by using the Message system to have an event.
That way the "doing" is local.
Title: Re: How to access and set properties of Labels dynamically
Post by: andyf97 on March 31, 2023, 10:31:19 pm

It is really interesting KodeZwerg

Would you mind explaining a bit what is actually going on with the second line of this:

Code: Pascal  [Select][+][-]
  1.       if (Pos('Form1', Application.Components[x].Name) > 0) then  // This is searching the application for Form1, there are two forms.
  2.         for y := 0 to Pred((Application.Components[x] as TForm).ComponentCount) do  
  3.  

I really cannot understand the use of Pred or why it would be needed. Is is like a pointer that has past Form1 so its required to go backwards ie minus one in the results? Its confusing me because the first line is searching for one form so the results would only be one, or I am barking totally up the wrong tree?


The next one I don't follow is the relationship of the array and the label changes.

Code: Pascal  [Select][+][-]
  1.       // pick one random and rename the caption to an integer
  2.       x := Random(Length(arr));
  3.       arr[x].Caption := IntToStr(Succ(x));  
  4.  


I get the idea that the array will hold a list of labels and changing its caption in the array ( is the array holding the entire properties for the component or it is use more like a pointer to the real label), but how does that change the real component, surely it would require some form of update the component with the new caption.

Seeing new things with your code even this Succ I need now look it up :-)



Title: Re: How to access and set properties of Labels dynamically
Post by: Blaazen on March 31, 2023, 10:39:37 pm
Code: Pascal  [Select][+][-]
  1. for y := 0 to Pred((Application.Components[x] as TForm).ComponentCount) do

It is moreless the same as
Code: Pascal  [Select][+][-]
  1. for y := 0 to (Application.Components[x] as TForm).ComponentCount-1 do

https://www.freepascal.org/docs-html/rtl/system/pred.html (https://www.freepascal.org/docs-html/rtl/system/pred.html)

So the loop var y will not go out of bounds.
Title: Re: How to access and set properties of Labels dynamically
Post by: KodeZwerg on March 31, 2023, 11:08:02 pm
Would you mind explaining a bit what is actually going on...
I'd thought I've already comment all enough but I can try to do more ...
Code: Pascal  [Select][+][-]
  1. procedure TForm2.Button1Click(Sender: TObject);
  2. // here I do create a new type that I need to store found TLabel object in
  3. type
  4.   TLabels = array of TLabel;
  5. var
  6.   // x, y, z are needed as Counters (x = a Form, y = an Object, z = array entries)
  7.   x, y, z: Integer;
  8.   // this is the actual array that holds the found objects of type TLabel
  9.   arr: TLabels;
  10. begin
  11.   // initialize empty array
  12.   arr := nil;
  13.   SetLength(arr, 0);
  14.   // x is my counter for the root of everything, my Pred() can also be written like
  15.   //  for x := 0 to Application.ComponentCount -1 do       ...but I do hate to write -1 or +1
  16.   for x := 0 to Pred(Application.ComponentCount) do
  17.     // Application.Components[x] is the current object I am into, in that case I want to dig deeper into a TForm what has inside its name "Form1" written (case sensitive!)
  18.     if (Pos('Form1', Application.Components[x].Name) > 0) then
  19.       // y is now the root of the found form where I am digging and digging
  20.       // again using Pred to avoid writing "-1"
  21.       // translated this line means:
  22.       //  for y := 0 to Form1.ComponentCount -1 do
  23.       // but with this kind of technique we have to write it like that:
  24.       for y := 0 to Pred((Application.Components[x] as TForm).ComponentCount) do
  25.         // remember, x = Form1 and y = all objects on that
  26.         // next line eliminate everything that is not a TLabel
  27.         if (((Application.Components[x] as TForm).Components[y] is TLabel) and
  28.             // and only keep a TLabel when it's Tag value is "666"
  29.             (((Application.Components[x] as TForm).Components[y] as TLabel).Tag = 666)) then
  30.           begin
  31.             // z keeps track of my array, it represent current amount of entries
  32.             // so first get current number of entries:
  33.             z := Length(arr);
  34.             // increase the array by one
  35.             // you can also write:
  36.             // SetLength(arr, z + 1);
  37.             // but as told, I do hate write -1 or +1
  38.             SetLength(arr, Succ(z));
  39.             // here we add the found TLabel into my array
  40.             // remember, x = Form1, y = the object
  41.             arr[z] := ((Application.Components[x] as TForm).Components[y] as TLabel);
  42.           end;
  43.   // when my array is bigger than 0
  44.   if (Length(arr) > 0) then
  45.     begin
  46.       // generate a random number based on the number of possible entries
  47.       x := Random(Length(arr));
  48.       // patch/overwrite the caption for one of the found possible TLabels
  49.       arr[x].Caption := IntToStr(Succ(x));
  50.       // the following is not needed, but I do it anyway
  51.       SetLength(arr, 0);
  52.       arr := nil;
  53.     end;
  54. end;
  55.  
I hope I was able to explain everything deep enough?
Feel free to ask more!  :-*
Title: Re: How to access and set properties of Labels dynamically
Post by: tetrastes on April 01, 2023, 11:07:09 pm
BUT: When the project gets bigger and bigger and more and more units are added the interdependence of the units becomes very complicated and there is a chance that such a solution will not compile any more.

No, if you place uses UnitX only in implementation section.

Quote
Also, you cannot test the project units independently - you always need Form1 if you want to test Form2 (and if Form1 uses other forms and units you will also need those - a huge mess!). Another issue is that unit2 compiles only when the instance of TForm1 is really named "Form1".

And what? You also need Form2 to test Form1... etc. And your unit1 also compiles only when the instance of TForm2 is really named "Form2". What is the issue with that?

Quote
Therefore, unless you have a very simple project you should always avoid such a situation.

You didn't convince me. But, of course, I do not urge you to use my solution at all.



Title: Re: How to access and set properties of Labels dynamically
Post by: tetrastes on April 01, 2023, 11:17:46 pm
Therefore, unless you have a very simple project you should always avoid such a situation.
I second that, to me it is very bad code design as it may lead to future bugs.

At least it doesn't violate KISS principle. And what are "future bugs" it may lead to?

Of course, you may consider crawling through application components to be good design for this small task.
TinyPortal © 2005-2018