Refer to an object instance and free it - delphi

If I create multiple TButton objects with this routine:
procedure CreateButton;
begin
Btn := TButton.Create(nil);
end;
Then, how can I refer to a specific object instance to free it using another method like:
procedure FreeButton;
begin
Btn[0].Free; //???
end;
Of course, this does not compile, but I think the question is clear: How do I declare Btn? And how do I free multiple instances?

It doesn't make much sense to create a TButton anywhere that isn't part of a form (which your code does).
With that being said, in order to refer to it later to free it, you need to store a reference to it somewhere.
Since you're referring to "multiple buttons" and using array code in your delete routine, I think you're probably wanting to track an array of buttons. Here's an example of doing just that:
type
TForm1 = class(TForm)
procedure FormCreate(Sender: TObject); // Add via Object Inspector Events tab
private
{ Private declarations }
// Add these yourself
BtnArray: array of TButton;
procedure CreateButtons(const NumBtns: Integer);
procedure DeleteBtn(BtnToDel: TButton);
procedure BtnClicked(Sender: TObject);
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.DeleteBtn(BtnToDel: TButton);
var
i: Integer;
begin
// Check each button in the array to see if it's BtnToDel. If so,
// remove it and set the array entry to nil so it can't be deleted
// again.
for i := Low(BtnArray) to High(BtnArray) do
begin
if BtnArray[i] = BtnToDel then
begin
FreeAndNil(BtnArray[i]);
Break;
end;
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
// Create 10 buttons on the form
CreateButtons(10);
end;
// Called when each button is clicked. Assigned in CreateButtons() below
procedure TForm1.BtnClicked(Sender: TObject);
begin
// Delete the button clicked
if (Sender is TButton) then
DeleteBtn(TButton(Sender));
end;
procedure TForm1.CreateButtons(const NumBtns: Integer);
var
i: Integer;
begin
// Allocate storage for the indicated number of buttons
SetLength(BtnArray, NumBtns);
// For each available array item
for i := Low(BtnArray) to High(BtnArray) do
begin
BtnArray[i] := TButton.Create(nil); // Create a button
BtnArray[i].Parent := Self; // Tell it where to display
BtnArray[i].Top := i * (BtnArray[i].Height + 2); // Set the top edge so they show
BtnArray[i].Name := Format('BtnArray%d', [i]); // Give it a name (not needed)
BtnArray[i].Caption := Format('Btn %d', [i]); // Set a caption for it
BtnArray[i].OnClick := BtnClicked; // Assign the OnClick event
end;
end;
If you put this code in a new blank VCL forms application and run it, you'll see 10 buttons ('Btn 0throughBtn 9`) on a form. Clicking on a button will remove it from the form (and the array).

Related

Update corresponding label depending on which combobox fired the event

I have a program with n ComboBoxes and n Labels and I want to update the corresponding Label depending on the selection from the adjacent ComboBox i.e ComboBox2 would update Label2.
I am using the same event handler for every ComboBox and currently checking if Combobox1 or Combobox2 has fired the event handler. Is there a way to use the ItemIndex of the ComboBox passed to the procedure, such as Sender.ItemIndex? This is not currently an option and gives the error 'TObject' does not contain a member named 'ItemIndex'.
procedure TForm2.ComboBoxChange(Sender: TObject);
begin
if Sender = ComboBox1 then
Label1.Caption := ComboBox1.Items.Strings[ComboBox1.ItemIndex]
else
Label2.Caption := ComboBox2.Items.Strings[ComboBox2.ItemIndex];
end;
This code has the desired behavior but is obviously not scale-able.
Every component has a Tag property inherited from TComponent, where the Tag is a pointer-sized integer. As such, you can store each TLabel pointer directly in the corresponding TComboBox.Tag, eg:
procedure TForm2.FormCreate(Sender: TObject);
begin
ComboBox1.Tag := NativeInt(Label1);
ComboBox2.Tag := NativeInt(Label2);
end;
This way, ComboBoxChange() can then directly access the TLabel of the changed TComboBox, eg:
procedure TForm2.ComboBoxChange(Sender: TObject);
var
CB: TComboBox;
begin
CB := TComboBox(Sender);
if CB.Tag <> 0 then
TLabel(CB.Tag).Caption := CB.Items.Strings[CB.ItemIndex];
end;
Option 1
This is the most robust one.
Let your form have private members
private
FControlPairs: TArray<TPair<TComboBox, TLabel>>;
procedure InitControlPairs;
and call InitControlPairs when the form is created (either in its constructor, or in its OnCreate handler):
procedure TForm1.InitControlPairs;
begin
FControlPairs :=
[
TPair<TComboBox, TLabel>.Create(ComboBox1, Label1),
TPair<TComboBox, TLabel>.Create(ComboBox2, Label2),
TPair<TComboBox, TLabel>.Create(ComboBox3, Label3)
]
end;
You need to add the controls to this array manually. That's the downside of this approach. But you only need to do this once, right here. Then everything else can be done automagically.
Now, this is where it gets really nice: Let all your comboboxes share this OnChange handler:
procedure TForm1.ComboBoxChanged(Sender: TObject);
var
i: Integer;
begin
for i := 0 to High(FControlPairs) do
if FControlPairs[i].Key = Sender then
FControlPairs[i].Value.Caption := FControlPairs[i].Key.Text;
end;
Option 2
Forget about any private fields. Now instead make sure that each pair has a unique Tag. So the first combo box and label both have Tag = 1, the second pair has Tag = 2, and so on. Then you can do simply
procedure TForm1.ComboBoxChanged(Sender: TObject);
var
TargetTag: Integer;
CB: TComboBox;
i: Integer;
begin
if Sender is TComboBox then
begin
CB := TComboBox(Sender);
TargetTag := CB.Tag;
for i := 0 to ControlCount - 1 do
if (Controls[i].Tag = TargetTag) and (Controls[i] is TLabel) then
begin
TLabel(Controls[i]).Caption := CB.Text;
Break;
end;
end;
end;
as the shared combo-box event handler. The downside here is that you must be sure that you control the Tag properties of all your controls on the form (at least with the same parent as your labels). Also, they must all have the same parent control.

Delphi - how to address a tool in after running the program

I want to know if there is a way to address a tool which I put in my form after the program is executed? For example:
Suppose there are 100 label components in a form and you put an edit box in your form and ask the user to enter a number in the edit. When the number is written in the edit, the label with the same number will change the font colour.
But you cannot code it before running the program and need sth like this:
Label[strtoint(edit1.text)].color:=clblue;
But as you know this code does not work. What should I write to do what I want?
Yes, you can do something like you demonstrate, you just need to store the form’s controls into some type of array or list.
Sorry, I currently do not have access to my Delphi IDE, but I think I can give you an overview to what you need to do. I will also provide a link that better demonstrate the concept.
Here are the steps:
First ensure your controls have a consistent naming format that includes an index number in the name.
Example: Label1, Label2, . . . .
Next you need to store the controls into some type of an array or TList.
Example:
Var
ControlList : TList
. . . .
ControlList := TList.Create;
. . . .
{ Load the controls into the list after they been created }
ControlList.Add (Label1)
ControlList.Add (Label2)
ControlList.Add (Label3)
Here an alternatives to adding the Labels to the list manually.
for I := 1 to 3 do
begin
ControlList.Add(TLabel(FindComponent('Label'+IntToStr(I)));
end;
Now designate some event handler where you will put the code to update the label. This handler routine will first convert the user inputted value to an integer. Them use that value as an index to the control array. Once you have the label designated to be updated, set whatever properties you like.
idx := StrToInt(InputBox.Text);
lbl := TLabel( ControlList[idx])
. . . .
lbl.Color := clBlue;
Check out this link Control Arrays in Delphi for a more detailed description.
-- Update --
Although my previous answer would work, Remy Lebeau comment give me an idea to a better approach. You do not need to store the controls in an array or list, just use the Findcomponent() command to locate the control. Below are two examples demonstrating this concept.
Example using an Edit box OnKeyPress event:
procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
var
LabelControl : TLabel;
begin
if ord(Key) = VK_RETURN then
begin
LabelControl := TLabel(FindComponent('Label'+Edit1.Text));
if (LabelControl <> nil) then
LabelControl.Color := clblue;
Key := #0; // prevent continue processing of the WM_CHAR message
end;
end;
Another example using a Button's OnClick event:
procedure TForm1.Button1Click(Sender: TObject);
var
LabelControl : TLabel;
begin
LabelControl := TLabel(FindComponent('Label'+Edit1.Text));
if (LabelControl <> nil) then
begin
LabelControl.Color := clBlue;
end;
end;
Things to note about the code:
In the first example, for the label to be updated, the user must press the enter key after inputting the desired label number.
In the second example, the user must press a button after entering
the number of the label to be updated.
In In both examples, invalid responses are ignored.
As I understand you right, all the Labels already contain a number in their caption.
Then, you could use the Controls array, that already exists in TForm, which contains all controls that belong to the form:
type
TForm1 = class(TForm)
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
// ...
Edit1: TEdit;
procedure Edit1Change(Sender: TObject);
private
public
end;
// ...
{ uses
System.RegularExpressions;
}
// ...
procedure TForm1.Edit1Change(Sender: TObject);
{
// if aLabel.Name instead of aLabel.Caption
// will work for Label1, Label2, Label3, Label4 ...
function TryNameToInt(AName: string; var ANumber: Integer): boolean;
var
aRegEx: TRegEx;
aMatch: TMatch;
aStr: string;
begin
aStr := '';
ANumber := -1;
aRegEx:= TRegEx.Create('[A-Za-z_]+([0-9]+)');
aMatch:= aRegEx.Match(AName);
if aMatch.Success then begin
aStr := aMatch.Groups.Item[1].Value;
end;
Result := TryStrToInt(aStr, ANumber);
end;}
var
aControl: TControl;
aLabel: TLabel;
aNumberEdit: Integer;
aNumberLabel: Integer;
aIdx: Integer;
begin
if TryStrToInt(Edit1.Text, aNumberEdit) then begin
for aIdx := 0 to ControlCount - 1 do begin // Controls is the list of all Controls in the form, ControlCount is the length of this list
aControl := Controls[aIdx];
if aControl is TLabel then begin // this gets only TLabel controls
aLabel := TLabel(aControl);
if TryStrToInt(aLabel.Caption, aNumberLabel)
{or TryNameToInt(aLabel.Name, aNumberLabel)} then begin
if aNumberLabel = aNumberEdit then begin
aLabel.Font.Color := clBlue;
end
else begin
aLabel.Font.Color := clWindowText; // if needed
end;
end;
end;
end;
end;
end;
You can use FindComponent function to do that:
Here I dropped a TButton and TEdit on form, you type the Label number you want to change the font color in Edit and then press the Button. Write this code in OnClick event for the Button:
Var
mColor: TColor;
mLabel: Tlabel;
begin
mColor := clGreen;
mLabel := FindComponent('Label' + Edit1.Text) as TLabel;
if mLabel <> nil then
mLabel.Font.Color := mColor;
end;
or if you don't want to press the Button and want it as you type in Edit, you have to write the code in OnChange event for Edit.

Creating a popup menu at runtime

I'm trying to simply create a popup menu (or context menu), add some items to it, and show it at the mouse location. All the examples I have found are doing this using the designer. I'm doing this from a DLL plugin, so there is no form/designer. The user will click a button from the main application which calls the execute procedure below. I just want something similar to a right click menu to appear.
My code obviously doesn't work, but I was hoping for an example of creating a popup menu during runtime instead of design time.
procedure TPlugIn.Execute(AParameters : WideString);
var
pnt: TPoint;
PopupMenu1: TPopupMenu;
PopupMenuItem : TMenuItem;
begin
GetCursorPos(pnt);
PopupMenuItem.Caption := 'MenuItem1';
PopupMenu1.Items.Add(PopupMenuItem);
PopupMenuItem.Caption := 'MenuItem2';
PopupMenu1.Items.Add(PopupMenuItem);
PopupMenu1.Popup(pnt.X, pnt.Y);
end;
You have to actually create instances of a class in Delphi before you can use them. The following code creates a popup menu, adds a few items to it (including an event handler for the click), and assigns it to the form. Note that you have to declare (and write) the HandlePopupItemClick event yourself like I've done).
In the interface section (add Menus to the uses clause):
type
TForm1 = class(TForm)
// Double-click the OnCreate in the Object Inspector Events tab.
// It will add this item.
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
// Add the next two lines yourself, then use Ctrl+C to
// generate the empty HandlePopupItem handler
FPopup: TPopupMenu;
procedure HandlePopupItem(Sender: TObject);
public
{ Public declarations }
end;
implementation
// The Object Inspector will generate the basic code for this; add the
// parts it doesn't add for you.
procedure TForm1.FormCreate(Sender: TObject);
var
Item: TMenuItem;
i: Integer;
begin
FPopup := TPopupMenu.Create(Self);
FPopup.AutoHotkeys := maManual;
for i := 0 to 5 do
begin
Item := TMenuItem.Create(FPopup);
Item.Caption := 'Item ' + IntToStr(i);
Item.OnClick := HandlePopupItem;
FPopup.Items.Add(Item);
end;
Self.PopupMenu := FPopup;
end;
// The Ctrl+C I described will generate the basic code for this;
// add the line between begin and end that it doesn't.
procedure TForm1.HandlePopupItem(Sender: TObject);
begin
ShowMessage(TMenuItem(Sender).Caption);
end;
Now I'll leave it to you to figure out how to do the rest (create and show it at a specific position).

creating checkbox

I am trying to create a few checkboxes, how many is decided by the recordcount of a query. Also i need to set the loction of the check box +38 from the previous location. Anyone give me some help with this? not sure how to create the checkboxes, The rest i should be able to do...anyhow he is what i have so far.
var
i, top,left : integer;
begin
......
left := 81;
top := 119;
while i < qry.RecordCount do
begin
// create check box
// set caption of checkbox to i
// set checkbox loction to left + 38, top
// left = left+38??
end;
After clarifying your needs, I would recommend you to use TObjectList as a container for your check boxes. This list can own the objects, what allows you to release them by a simple removing the item from the list either by Clear or by Delete. It also provides a simple access to each element by typecasting the obtained indexed item object to your known class type. More in the following untested pseudo-code:
uses
Contnrs;
type
TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
private
CheckList: TObjectList;
public
{ Public declarations }
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
CheckList := TObjectList.Create;
// setting OwnsObjects to True will ensure you, that the objects
// stored in a list will be freed when you delete them from list
CheckList.OwnsObjects := True;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
// this will also release all check boxes thanks to OwnsObjects
CheckList.Free;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
I: Integer;
CheckBox: TCheckBox;
begin
...
CheckList.Clear; // this will free all check boxes
for I := 0 to RecordCount - 1 do // iterate over your recordset
begin
CheckBox := TCheckBox.Create(nil); // be sure to use nil as an owner
CheckBox.Parent := Self; // where will be laying (Self = Form)
CheckBox.Caption := IntToStr(I); // caption by the iterator value
CheckBox.Top := 8; // fixed top position
CheckBox.Left := (I * 38) + 8; // iterator value * 38 shifted by 8
CheckBox.Width := 30; // fixed width
CheckList.Add(CheckBox); // add the check box to the list
end;
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
// this will check the first check box from the list (be careful to indexes)
TCheckBox(CheckList.Items[0]).Checked := True;
// this will delete 3rd check box from the list (using Clear will delete all)
CheckList.Delete(2);
end;
Your pseudo code translates almost literally into Delphi code although it's better to use a for loop here:
for I := 0 to qry.RecordCount-1 do
begin
CheckBox := TCheckBox.Create (Self); // the form owns the checkbox
CheckBox.Parent := Self; // checkbox is displayed on the form
CheckBox.Caption := IntToStr (I);
CheckBox.Top := Top;
CheckBox.Left := 81 + I*38;
end;
BTW, you don't have to free the created checkbox thanks to the ownership mechanism built into the VCL.

Delphi Dyncamically created Popup Menu Items - Invalid Pointer Operation/Access Violation

I have a problem with error message "Invalid Floating Point operation." The popup menu is a design time control and it is named NavPop. It has no menu items assigned. It is assigned as popupmenu for Panel1.
I then create the menu items dynamically from a listbox, and assign the caption and on click events. Everything works 100% in terms of what I am trying to accomplish. Ie it works.
Only when i close the program, do I get
Invalid floating point operation
or otherwise:
Access Violation Address 000007355. Read of Addrss 0000007355.
Please note that everything works perfectly, except that the error when I close the program. I would appreciate any help.
// I declare the Array of TMenuItems
private
{ Private declarations }
ItemArray : array of TMenuItem;
...
procedure TMainForm.Button1Click(Sender: TObject);
begin
CreateNavPop;
end;
// Create the menu items from listbox(Navlist) items and Link them
// to events on a navigation bar.
procedure TMainForm.CreateNavPop;
var
I: Integer;
NavIndex: Integer;
begin
SetLength(ItemArray, NavList.Items.Count);
NavIndex:=0;
For I:=0 to NavList.Items.Count-1 do
begin
NavIndex:=NavBar1.Items.ItemByCaption(NavList.Items.Strings[i]).Index;
ItemArray[i]:=TMenuItem.create(Nil);
ItemArray[i].Caption:=NavList.Items.Strings[i];
ItemArray[i].OnClick:=NavBar1.Items.Items[Navindex].OnClick;
NavPop.Items.Add(ItemArray[i]);
end;
end;
// Call the Items free on program close
procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
FreeItems(ItemArray);
end;
// Free Dynamically created Menu Items on Form Close
procedure TMainForm.FreeItems(MItems : array of TMenuItem);
var
cnt : integer;
begin
for cnt := High(MItems) downto Low(MItems) do
begin
MItems[cnt].Free;
MItems[cnt] := nil;
end;
end;
This happens because the TPopupMenu already free the items, and you are freeing it again.
This code causes an "Invalid pointer operation":
procedure TForm1.FormCreate(Sender: TObject);
var
I: Integer;
begin
for I := 0 to 3 do
PopupMenu1.Items.Add(TMenuItem.Create(nil));
end;
destructor TForm1.Destroy;
var
I: Integer;
begin
for I := 3 downto 0 do
PopupMenu1.Items.Free;
inherited;
end;
The Items property is a TMenuItem instance, and if you look at it's destructor, it already free all the items you added.
destructor TMenuItem.Destroy;
begin
...
while Count > 0 do Items[0].Free;
...
Keeping it short, you don't need to do it again in the FreeItems method.
I tested with ReportMemoryLeaksOnShutdown := True and no memory leaks occur.

Resources