Delphi firemonkey how to remove a menuitem programmatically? - delphi

How can I remove a menuitem I dynamically added to a menu?
I add dynamically menu items with for example:
m:=TMenuItem.Create(nil);
m.Text:='bla bla bla';
mnuMain.AddObject(m);
I could not find ANY function to remove the entry again.
I tried delete, free, removeobject etc. and the item is still nor removed and still visible. What is the trick?

If you are adding a item like so :
var
M : TMenuItem;
begin
M := TMenuItem.Create(nil);
M.Text := 'Bla Bla Bla';
MenuBar1.AddObject(M);
Then you are just giving it a parent, just set the parent of the menu items to nil ARC will then swoop in and do the rest seeing as there are no more references to the object
Just write a loop to go through and set all the parents to nil/ Or if you are targeting Windows as well (Or only Windows) in your code, make use of DisposeOf, or make use of compiler directives
var
I: Integer;
begin
for I := MenuBar1.ItemsCount-1 downto 0 do
begin
{$IFNDEF AUTOREFCOUNT}
MenuBar1.Items[I].disposeOf;
{$ELSE}
MenuBar1.Items[I].parent := nil;
{$ENDIF}
end;
end;

I faced the same issue: TMenuItem.RemoveObject is not working and the private member FContent of TMenuView is not accessable without tricks over RTTI.
That is why I build my own workaround by using a stringlist that stores the remaining menu child items before I call TMenuItem.Clear:
function RemoveFromMenu(mnuMain: TMenuItem; const MenuText: string);
var
list: TStringList;
c: integer;
Menu: TMenuItem;
begin
list := TStringList.Create;
try
for c := 0 to mnuMain.ItemsCount - 1 do
if mnuMain.Items[c].Text <> MenuText then
list.Add(mnuMain.Items[c].Text);
mnuMain.Clear;
for c := 0 to list.Count - 1 do
begin
Menu := TMenuItem.Create(self);
Menu.Text := list[c];
Menu.OnClick := mnuMainSubMenuClick; // The menu event handler
mnuMain.InsertObject(0, Menu);
end;
finally
list.Free;
end;
end;
In case your sub menu have for each menu item an own menu handler than you have also store this event handler. In such situations a generic list of TMenuItem (TList< TMenuItem >) would be a better approach than using the string list.

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.

TMainMenu build menu item dynamic

On my system , the main menu is fully dynamic, is builded from data in the database.
I have a specific situation where some menu items need to be assembled before being displayed.
Let's assume that my menu has the following main items:
Files - Customer - Reports - About
When I click in Reports menu item must assemble the items before they are displayed.
I did an analysis of the code in TMainMenu, TMenu and TMenuItem class. Unfortunately I have not found a simple solution to the problem.
Exist a way to create these items before being displayed?
There is a trick you can use. You need to add one dummy TMenuItem under Reports and set its Visible property to False. Then add OnClick event to Reports item and do your populating logic there. Before you add new items you have to delete existing ones, but you should leave your dummy item intact.
Something like:
procedure TForm1.ReportItemClick(Sender: TObject);
var
Item: TMenuItem;
I: Integer;
begin
// Delete all items but first - dummy - one
for I := ReportItem.Count - 1 downto 1 do
ReportItem.Items[I].Free;
Item := TMenuItem.Create(ReportItem);
Item.Caption := 'abc';
// Item.OnClick := ...
// or
// Item.Action := ...
ReportItem.Add(Item);
Item := TMenuItem.Create(ReportItem);
Item.Caption := '123';
// Item.OnClick := ...
// or
// Item.Action := ...
ReportItem.Add(Item);
end;
Lets Suppose that you have an DS (TDataSet) how load all your data reports. And in the moment that you drop Reports Menu you can call UpdateMenuReport.
type
TMenuArray = array of TMenuItem;
var MyMenu: TMenuArray;
procedure TMain.MyClickPopupMenu(Sender: TObject);
begin
case TMenuItem(TMenuItem(Sender).Parent).Tag of
// do Something
end;
end;
procedure TMain.UpdateMenuReport;
procedure InitMyMenu(var AMenu: TMenuArray);
var i: Integer;
begin
if Length(AMenu)>0 then for i:= 0 to Length(AMenu)-1 do AMenu[i].Free;
SetLength(AMenu, 0);
end;
var j, i: integer;
begin
InitMyMenu(MyMenu);
Reports.Clear;
if DS.Active and (DS.RecordCount > 0) then
begin
SetLength(MyMenu, DS.RecordCount);
for i:= 0 to DS.RecordCount-1 do
begin
MyMenu[i] := TMenuItem.Create(Self);
MyMenu[i].Name := 'Reports_'+ IntToStr(i);
MyMenu[i].Caption := DS.FieldByname('NOM_REPORT').AsString;
MyMenu[i].Tag := DS.FieldByname('ID').AsInteger;
MyMenu[i].OnClick := MyClickPopupMenu;
end;
end;
end;

Programatically enable a menu item (and all it's parents) - Delphi

I need to selectively enable certain menu items based upon the status of the user. I've managed to get code to enable the actual items I want but I can't see how to enable all the parent menu items above each one that I enable in a multi-level menu. Without enabling them as well the menu item still can't be used as the user cannot reach it.
eg if I have
EditTop
EditSub1
Editsub2
EditSubSub1
EditSub3
I can enable EditSubSub1 but I also therefore need to enable Editsub2 and EditTop as well or it can't be reached by the user. That's what I would appreciate help with.
The code I have at the moment is the following (Assume that other code has given me a TstringList containing the menu names I want enabled)
First some code to disable everything.
procedure DisableMenu(AMenu: TMenuItem);
//recurses through all the menu and disables eveything
var
i: integer;
begin
for i := 0 to AMenu.Count - 1 do
begin
AMenu[i].enabled := false;
DisableMenu(AMenu[i]);
end;
end;
Then code that searches for and returns a TmenuItem based upon its name
(This came from
http://www.delphipages.com/forum/showthread.php?t=45723)
function FindMnuItem(Menu: TMenu; MenuName: string): TMenuItem;
procedure FindSubItems(mnuItem: TMenuItem);
var i: integer;
begin
for i:=0 to mnuItem.Count- 1 do
if mnuItem.Items[i].Name= MenuName then
begin
Result:= mnuItem.Items[i];
break;
end
else
FindSubItems(mnuItem.Items[i]);
end;
var i: integer;
begin
Result:= nil;
for i:= 0 to Menu.Items.Count -1 do
begin
if Menu.Items[i].name = MenuName then
begin
Result:= Menu.Items[i];
break;
end
else
if Result<> nil then
break
else
FindSubItems(Menu.Items[i]);
end;
end;
Finally the code I would like some help with. This selectively enables each menu item based upon the names in the Stringlist AllowedMenus but only those ones, not the ones above each one in the tree. How do I do that?
//first disable all menu items
DisableMenu(MainMenu1.Items);
//now enable the ones we want enabled
for i := 0 to AllowedMenus.count-1 do
begin
MenuName := AllowedMenus[i];
FindMnuItem(MainMenu1, MenuName).Enabled := true; //enable an item
end
All you need to do is walk up the menu tree using the TMenuItem.Parent property.
var vMenuItem : TMenuItem;
[...]
//first disable all menu items
DisableMenu(MainMenu1.Items);
//now enable the ones we want enabled
for i := 0 to AllowedMenus.count-1 do
begin
MenuName := AllowedMenus[i];
vMenuItem := FindMnuItem(MainMenu1, MenuName);
while Assigned(vMenuItem) do
begin
vMenuItem.Enabled := true; //enable an item
vMenuItem := vMenuItem.Parent;
end;
end
i think that u can use this function;
(D21 is your Actual Items):
procedure UpdateMenuParent(MyItemMenu: TMenuItem);
begin
TMenuItem(MyItemMenu).Enabled := true;
if TMenuItem(MyItemMenu).Parent <> nil then
UpdateMenuParent(TMenuItem(MyItemMenu).Parent);
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
UpdateMenuParent(D21);
end;

Delphi - How to delete all child components at runtime?

At design time, I create a TScrollBox which will be the parent of TLayouts created at runtime.
The Layouts will also contain Tlabels and Tedits like this:
var
Layout1: TLayout;
Label1: TLabel;
Edit1: TEdit;
begin
Layout1 := TLayout.Create(self);
Layout1.Parent := ScrollBox1;
Label1 := TLabel.Create(self);
Label1.Parent := Layout1;
Label1.Text := 'abc';
end;
Now I want to delete everything out like this procedure has never been called.
I have tried the following, but the program would just crash.
var
i : integer;
Item : TControl;
begin
for i := 0 to Scrollbox1.ControlCount - 1 do
begin
Item := Scrollbox1.controls[i];
Item.Free;
end;
end;
Can anyone please give me a hint?
When you remove a control, the index of the ones behind it in the control list shifts down. I.e, you end up trying to access positions that do not exist.
You need to iterate the list downwards:
var
i : integer;
Item : TControl;
begin
for i := (Scrollbox1.ControlCount - 1) downto 0 do
begin
Item := Scrollbox1.controls[i];
Item.Free;
end;
end;
Another way is to stay always at index 0, free its control and check that you still have controls to free:
var
i : integer;
Item : TControl;
begin
while Scrollbox1.ControlCount > 0 do
begin
Item := Scrollbox1.controls[0];
Item.Free;
end;
end;
UPDATE
As #DavidHeffernan pointed out, there is nested parentage here. This means you should free your components from bottom up. One way to do it is by recursion.
Basically you would need a procedure to encapsulate the freeing of child controls. The code would be similar to following (please note this is just a small test I did and extra code may be required):
procedure freeChildControls(myControl : TControl; freeThisControl: boolean);
var
i : integer;
Item : TControl;
begin
if Assigned(myControl) then
begin
for i := (myControl.ControlsCount - 1) downto 0 do
begin
Item := myControl.controls[i];
if assigned(item) then
freeChildControls(item, childShouldBeRemoved(item));
end;
if freeThisControl then
FreeAndNil(myControl);
end;
end;
function childShouldBeRemoved(child: TControl): boolean;
begin
//consider whatever conditions you need
//in my test I just checked for the child's name to be layout1 or label1
Result := ...;
end;
In order to free the scrollbox1 child controls (but not itself) you would call it like this:
freeChildControls(scrollbox1, false);
Please note that I had to add the childShouldBeRemoved function in order to avoid this recursive function to free child controls of the label and layout that you should leave for their destructors to free.
One possible solution to implement this function would be to use an object list where you would add your created components, and then inside the function check if the passed child component has to be freed.
If you create components at runtime - use parent control as parameter of the constructor. Like Label1 := TLabel.Create(Layout1); - so that the parent is also the owner. When you destroy Layout1 the Label1 also will be destroyed.

How can I refer to a control whose name is determined at runtime?

As a kind of self-study exercise, I've made a form which contains six panels in a 2x3 rectangle and I want them to switch between visible and invisible one after another. I'm trying to do so by using a for loop of some kind. I could of course write something like:
Panel1.Visible := true;
Panel1.Visible := false;
Panel2.Visible := true;
Panel2.Visible := false;
Panel3.Visible := true;
etc. etc.
But this takes quite a lot of typing and is pretty inefficient when I decide I want it to wait for 100ms between each step. For example, I'd then have to edit all the six steps to wait. This is doable for six steps, but maybe another time I want to do it a hundred times! So I'm thinking there must also be a way to use a for loop for this, where a variable varies from 1 to 6 and is used in the object identifier. So it would something like this:
for variable := 1 to 6 do begin
Panel + variable.Visible := true;
Panel + variable.Visible := false;
end;
Now, this obviously doesn't work, but I hope somebody here can tell me if this is in fact possible and if yes, how. Maybe I can use a string as the identifier? My explanation is probably pretty bad because I don't know all the technical terms but I hope the code explains something.
You can loop through the panel's Owner's Components array.
var
i: Integer;
TmpPanel: TPanel;
begin
{ This example loops through all of the components on the form, and toggles the
Visible property of each panel to the value that is opposite of what it has (IOW,
if it's True it's switched to False, if it's False it's switched to True). }
for i := 0 to ComponentCount - 1 do
if Components[i] is TPanel then
begin
TmpPanel := TPanel(Components[i]);
TmpPanel.Visible := not TmpPanel.Visible; // Toggles between true and false
end;
end;
You can also use the FindComponent method, if you want a very specific type of component by name. For instance, if you have the 6 panels, and their names are Panel1, Panel2, and so forth:
var
i: Integer;
TmpPanel: TPanel;
begin
for i := 1 to 6 do
begin
TmpPanel := FindComponent('Panel' + IntToStr(i)) as TPanel;
if TmpPanel <> nil then // We found it
TmpPanel.Visible := not TmpPanel.Visible;
end;
end;
This is a situation where you want to create the controls dynamically at runtime rather than at designtime. Trying to grapple with 6 different variables is just going to be a world of pain. And when you need the grid to be 3x4 rather than 2x3, you'll regret that decision even more.
So, start with a completely blank form. And add, in the code, a two dimensional array of panels:
private
FPanels: array of array of TPanel;
Then, in the form's constructor, or an OnCreate event handler, you can initialise the array by calling a function like this:
procedure TMyForm.InitialisePanels(RowCount, ColCount: Integer);
var
Row, Col: Integer;
aLeft, aTop, aWidth, aHeight: Integer;
Panel: TPanel;
begin
SetLength(FPanels, RowCount, ColCount);
aTop := 0;
for Row := 0 to RowCount-1 do begin
aLeft := 0;
aHeight := (ClientHeight-aTop) div (RowCount-Row);
for Col := 0 to ColCount-1 do begin
Panel := TPanel.Create(Self);
FPanels[Row, Col] := Panel;
Panel.Parent := Self;
aWidth := (ClientWidth-aLeft) div (ColCount-Col);
Panel.SetBounds(aLeft, aTop, aWidth, aHeight);
inc(aLeft, aWidth);
end;
inc(aTop, aHeight);
end;
end;
And now you can refer to your panels using cartesian coordinates rather than a flat one dimensional array. Of course, you can easily enough declare a flat one dimensional array as well if you want.
The key idea is that when you are creating large numbers of control in a structured layout, you are best abandoning the designer and using code (loops and arrays).
Use FindComponent method of TComponent:
for variable := 1 to 6 do begin
pnl := FindComponent('Panel' + IntToStr(variable));
if pnl is TPanel then
begin
TPanel(pnl).Visible := true;
TPanel(pnl).Visible := false;
end;
end;
As others have answered, FindComponent is the way to go.
But if you just want to modify generic properties for the component, such as visible, position etc, it's not necessary to compare to the type.
This will work just as fine:
for i := 1 to 16 do
begin
(FindComponent( 'P' + inttostr(i) ) as TControl).Visible := false;
end;
(NOTE: this is for Delphi 6/ 7, modern versions probably do this in other ways)
Actually my answer
If you use a name convention to name your component like
"Mycomponent" + inttostr(global_int)
you can use it to find it very easily :
function getMyComponent(id:integer) : TComponent;
begin
result := {Owner.}FindConponent('MyComponent'+inttostr(id));
end;
You also can make your generated components to interact each other by using (sender as TComponent).name to know which other component are related to him.
Exemple
Following is an example of what you can do with this :
Imagine a pagecontrol where tabs are an interface you want to have multiple time
(for ex, to describe columns in a file with 1 tab = 1 col, and you want to dynamically add tabs).
For our example, we are naming button and edit this way :
Button : "C_(column_number)_btn"
Edit : "C_(column_number)_edi"
You can actually refer directly to the edit with a buttonclick, linked at runtime by calling findcomponent :
procedure TForm1.ColBtnClick(Sender:TObject);
var nr : string; Edit : TEdit;
begin
// Name of the TButton. C(col)_btn
nr := (Sender as TButton).Name;
// Name of the TEdit C_(column)_edi
nr := copy(nr,1,length(nr)-3)+'edi';
// Get the edit component.
edit := (Form1.Findcomponent(nr) as TEdit);
//play with it
Edit.Enabled := Not Edit.Enabled ;
showmessage(Edit.Text);
Edit.hint := 'this hint have been set by clicking on the button';
//...
end;
Of course, you link this procedure to every generated buttons.
If anyone wants to practice with it, you may want to know how to generate the tabsheet and components, here you go :
procedure Form1.addCol(idcol:integer, owner : TComponent); // Form1 is a great owner imo
var
pan : TPanel; // Will be align client with the new tabsheet
c: TComponent; //used to create components on the pannel
tab : TTabSheet;
begin
try
pan := TPanel.create(owner);
pan.name := format('Panel_%d',[idcol]);
pan.caption := '';
// dynamically create that button
c := TButton.create(Owner);
with c as TButton do
begin
Name := format('C%d_btn',[idcol]);
Parent := pan;
//Top := foo;
//Left := bar;
caption := 'press me';
OnClick := Form1.ColBtnClick; // <<<<<<< link procedure to event
end;
//create a Tedit the same way
c := TEdit.create(Owner);
with c as TEdit do
Name := format('C%d_edi',[idcol]);
Parent := pan;
// other properties
// create the tabsheet and put the panel in
finally
tab := TTabSheet.Create(Parent);
tab.caption := 'Column %d';
tab.PageControl := Pagecontrol1;
pan.Parent := tab;
pan.Align := alClient;
end;
end;
Generating names to get the component is actually a very good way to have a clean code.
Scrolling through parent - child components in order to find the one you want is actually inefficient and becomes hell if there is many component (in my example, if there is 3, 10 or unknown number of TEdit looping child (brother) components will be ugly.
Maybe this example is useless but It may helps someone, someday.

Resources