How to merge two menus in a MDI application - delphi

Anybody knows how to merge two menus with the same name in a MDI application.
More exactly, in the MDI main form I have a menu called 'File' which has a sub-menu called 'Load project'.
In the MDI child form, I have a menu called also 'File' which contains a sub-menu called 'Save project'.
How can I force my application to show both 'Load' and 'Save' sub-menus under the 'File' menu?
-
PS: setting the same GoupIndex value will not work.

To merge your menus use this procedure:
procedure MergeMenus(var SrcMenu, DstMenu: TMainMenu);
var
i, i2, i3: Integer;
Menu: TMenuItem;
begin
for i := 0 to SrcMenu.Items.Count - 1 do
begin
for i2 := 0 to DstMenu.Items.Count - 1 do
begin
if (SrcMenu.Items[i].Name = DstMenu.Items[i2].Name) and
(SrcMenu.Items[i].Count > 0) and (DstMenu.Items[i].Count > 0) then
begin
for i3 := 0 to SrcMenu.Items[i].Count - 1 do
begin
Menu := TMenuItem.Create(DstMenu.Owner);
// copy another properties if necessery
Menu.Name := SrcMenu.Items[i].Items[i3].Name;
Menu.Caption := SrcMenu.Items[i].Items[i3].Caption;
Menu.ShortCut := SrcMenu.Items[i].Items[i3].ShortCut;
Menu.OnClick := SrcMenu.Items[i].Items[i3].OnClick;
DstMenu.Items[i].Add(Menu);
end;
end;
end;
end;
end;
Call it in the OnCreate event of your MDIChildForm like this:
procedure TMDIChild.FormCreate(Sender: TObject);
begin
MergeMenus(YourMainForm.MainMenu1, Self.MainMenu1);
end;
It will work if two different MainMenus will have MenuItems with the same name. Also please note that there is a possible memory leek if your DstMenu does not have an owner (but I guess it have and it is your MDICHildForm).

Manual merging is a matter of calling FormMain.MainMenu.Merge(SubForm.MainMenu) and its counterpart FormMain.MainMenu.UnMerge(SubForm.MainMenu).
You shouldn't need it though, because if the FormStyles of your forms are properly set to fsMDIForm and fsMDIChild, then menu merging should be automatic.
Having said that, I am not sure that what you want is possible using the built in menu merging.
According to the GroupIndex help (and a couple of experiments), menu items from a child forms replace items on the main form with the same GroupIndex. Only when the GroupIndex of a menu item on the child form falls between GroupIndex values on the main form, will the menu be inserted. So, the File menu on your child form will always replace the File menu on the main form. Only if you give the File menu's different GroupIndex values will the File menu of the Main form remain, but then you have two File menu's...
So, I think the only solution would be to insert and remove the menu items of the subform manually, or to have them on the main menu all the time and enable/disable them according to the active MDIChild. Possibly even show/hide them.
Personally I would go for the option of having them around all the time and enabling/disabling them according to the active MDIChild, as I don't like menu items that "bounce around" (change position).

Related

Vcl Styles issue for menu items added during program load

I have a Delphi 10.4.2 program (32-bit) where menu items are added during program load (the Application.OnActivate event, coded to run only once). Without a vcl style the new items are displayed correctly, however when a style is applied (such as the very nice Iceberg Classico in the screenshot) the display is not correct. The menu options are there, and can be clicked on; but the text and the icon are not drawn.
Any workrounds? I'm assuming that it’s because those particular menu options are added after the style is applied. Is there a way to refresh the style?, or am I missing a setup property when creating the menu items?
Thanks.
Edit: Yes, the 'File' menu and sub menu items are displayed correctly. Code that creates the new menu and items (simplified) is:
procedure TDbHelper.CreateHelpMenu;
// Called by OnApplicationActivated event, and run just once
var
aMenu: TMainMenu;
mnHelp, mnItem: TMenuItem;
idx: Integer;
begin
aMenu := Application.MainForm.Menu;
// create new menu
mnHelp := aMenu.CreateMenuItem;
mnHelp.Name := 'WISHelp1';
mnHelp.Caption := 'WIS Help';
aMenu.Items.Add(mnHelp);
// now the submenu items
for idx := 0 to HelpLinks.Count - 1 do
begin
mnItem := TMenuItem.Create(mnHelp);
mnItem.Name := HelpLinks[idx].Key;
mnItem.Caption := HelpLinks[idx].Text;
mnItem.ImageIndex := HelpLinks[idx].ImageIndex;
mnItem.OnClick := WISHelpItemClick;
mnHelp.Add(mnItem);
end;
end;
Finally decided to switch off the vcl styles for the menus. I followed the advice of RRUZ on another question and added a line to the dpr source so that it became:
Application.Initialize;
TStyleManager.TrySetStyle('Iceberg Classico');
with TStyleManager do SystemHooks := SystemHooks - [shMenus];
Application.Title := 'blah, blah, etc'
The menu items have re-appeared, and they look fine:
Thank you to SilverWarior for their input and suggestions.
Tried to recreate this scenario in Delphi 10.3 and it works fine for me.
But then with some fiddling I managed to recreate your "end result". And in order to do so I had to pass empty strings for mnItem.Name and mnItem.caption.
So I believe that the problem you are facing is not by using VCL Styles but in fact by your HelpLinks[idx].Key and HelpLinks[idx].Text methods returning empty strings. So you end up with menu items with no name and no caption therefore it appears as they are rendered wrong.
If I'm correct disabling VCL styles will still have same result.

Dynamically create submenu

I have a TMainMenu with a menu item called mnuWindows. I wish to create submenu items dynamically. I thought this code might do it but it doesn't work:
var
mnuitm: TMenuItem;
mnuitm:=TMenuItem.Create(nil);
mnuitm.Text:='some text';
mnuWindows.AddObject(mnuitm);
When I click on mnuWIndows, nothing happens. Where am i going wrong?
EDIT:
The submenu was not displaying on clicking because each time I did so, the program had been freshly started and I didn't realize that under these circumstances, two clicks are necessary. The first click doesn't visibly do anything and the second click drops down the submenu. So, I concede the code snippet above works.
But I still have a difficulty. I need to create several submenu items so I tried the following loop inside the mnuWindows OnClick event handler:
for I := 0 to TabSet1.Tabs.Count - 1 do
begin
mnuitm := TMenuItem.Create(mnuWindows);
mnuitm.Text := TabSet1.Tabs[I].Text;
mnuitm.OnClick:=MenuItemClick;
if not mnuWindows.ContainsObject(mnuitm) then
mnuWindows.AddObject(mnuitm);
end;
The intent of the above code is that clicking the mnuWindows item displays a list of the tabs in a tabset. This code works up to a point. On first being clicked, it correctly lists the current tabs. But when I add a tab and click on mnuWindows again, the new tab is not shown in the list. The list is exactly as before. I wondered if the menu needed updating or refreshing somehow. I came across the following method
IFMXMenuService.UpdateMenuItem(IItemsContainer, TMenuItemChanges)
but it is poorly documented and I'm not sure how to use it or even whether it is relevant.
EDIT2:
I thought the two down votes on my post were harsh. I have searched the web extensively for an example of how to dynamically create submenus in Firemonkey and there is very little. I did find a solution from 2012, but syntax changes since then mean that it does not work in Tokyo 10.2.
Try something like this. As others have commented above, you need to provide an event that will happen when the menu item is clicked. Note also that my methods here require a bunch of parameters. It would have been cleaner if I had created a class and passed the details that way, but I wrote this a long time ago and now have many places in my code that use it in this form. Also, if I wrote this now, I would use a function to return the menu item created in case I needed to interact with it in particular cases (e.g., check it, assign a hot key, etc.)
procedure PopMenuAddItem(menu: TPopupMenu; sText: string; iID: integer;
clickEvent: TNotifyEvent; bEnabled: boolean = true);
var
NewMenuItem: TmenuItem;
begin
NewMenuItem := TmenuItem.create(menu);
with NewMenuItem do
begin
Caption := sText;
tag := iID;
Enabled := bEnabled;
OnClick := clickEvent;
end;
menu.Items.Add(NewMenuItem);
end;
procedure PopMenuAddSubItem(menuItem: TmenuItem; sText: string; iID: integer;
clickEvent: TNotifyEvent; bEnabled: boolean = true);
var
NewMenuItem: TmenuItem;
begin
NewMenuItem := TmenuItem.create(menuItem);
with NewMenuItem do begin
Caption := sText;
tag := iID;
Enabled := bEnabled;
OnClick := clickEvent;
end;
menuItem.Add(NewMenuItem);
end;
I have answered my own question.
As a reminder, what I wanted to do was to dynamically create a submenu under my top level menu item "Windows" (component name "mnuWindows"). In the submenu, I wished to list the names of the tabs in a tabset.
Attempting to create the submenu dynamically in the mnuWindows.OnClick event was a failure.
My eventual solution was to rebuild the submenu with the following method and to call this method immediately after creating a new tab, removing a tab, or renaming a tab:
procedure Form1.ReBuildWindowsMenu;
var
mnuitm: TMenuItem;
I: Integer;
begin
mnuWindows.Clear; // removes submenu items
for I := 0 to TabSet1.Tabs.Count - 1 do
begin
mnuitm := TMenuItem.Create(MainMenu1);
mnuitm.Caption:= TabSet1.Tabs[I].Text; // submenu item displays same text as associated tab
mnuitm.OnClick := MenuItemClick; // makes the associated tab active
mnuWindows.AddObject(mnuitm);
end;
end;
The OnClick handler contains the single statement
TabSet1.ActiveTabIndex:=(Sender as TMenuItem).Index;
This simple solution keeps my Windows list perfectly synched with the tabs in the tabset. I'm planning to use a similar approach to put a most recently used (MRU) file list into my File menu.

Delphi CheckListBox and MainMenu

I have some problem with iteration in delphi. First, I have 2 MenuItems. Menu 1 has 5 submenus, and Menu 2 has 3 submenus. The idea is, if I checked checkListbox, which has 8 items (submenu of Menu 1 and Menu 2), it will checked submenu of Menu 1 and Menu 2.
[MainMenu 1] = menu1, menu2, menu3, menu4, menu5; (submenu)
[MainMenu 2] = menu1, menu2, menu3; (submenu)
CheckListBox = menu1, menu2, menu3, menu4, menu5 (MainMenu item 1), menu6, menu7, menu8(MainMenu item 2);
I tried with some code like:
for x := 0 to checkListBox.Items.Count - 1 do
begin
if checkListBox.Checked[x] then
begin
Menu1.items[x].Checked := true;
end;
end;
This code run failed, because list bounds of MainMenu 1 is 1 to 5.
if I try this,
MainMenu.items[0][2].Checked:= true;
sure it can be spesific, but not as I think.
Perhaps something along these lines:
var
CheckIndex, MenuIndex: Integer;
Checked: Boolean;
Menu: TMenu;
....
for CheckIndex := 0 to checkListBox.Items.Count - 1 do begin
Checked := checkListBox.Checked[CheckIndex];
if CheckIndex < Menu1.Items.Count then begin
Menu := Menu1;
MenuIndex := CheckIndex;
end else begin
Menu := Menu2;
MenuIndex := CheckIndex - Menu1.Items.Count;
end;
Menu.Items[MenuIndex].Checked := Checked;
end;
I'm not sure I fully understand your question. I certainly can't see where the sub-menus come into it. However, I hope that the ideas expressed above are useful.
Have you consider using ActionList and actions?
You do know that you can "bind" each menu item to its own action and that while doing so menu items properties like Checked and Enabled directly reflect the Checked and Enabled properties of the Action that has been "binded" to that specific menu item.
Using Action list you are accesing specific actions by their index (which I belive what you are trying to do) and since the changes made to Actions are relfected in Menu items binded to that specific action you don't need to edit each specific meni item individually and therefore don't have to worry about menu structure.
Infact you can bind multiple menu items from multiple menues to one action and changing the Checked or Enabled property of this action will affect all the menu items that have been binded to this specific action.
This comes especially usefull if you are using combination of MainMenu (with or without submenues) and PopupMenu (again with or woithout submenues).

Looping all components on a TabSheet

I have the following code which shoul loop all components on a given tab on my tabsheet.
I have tried many variants of the same code found on the net, but I simply can't get it to work.
First I check if it is the right tab - that works.
Then I check to see how many components - that doesn't work. It says 0 component even though I now that there are 2 panels with 9 checkboxes total.
procedure TfrmHsUsers.pagUsersClick(Sender: TObject);
var
i: integer;
Fieldname: string;
begin
if pagUsers.Properties.ActivePage.Name = 'tabProgram' then
begin
ShowMessage(IntToStr(pagUsers.Properties.ActivePage.ComponentCount));
for i := 0 to pagUsers.Properties.ActivePage.ComponentCount - 1 do
if (pagUsers.Properties.ActivePage.Components[i]) is TcxDbCheckBox then
begin
Fieldname := TcxDbCheckBox(pagUsers.Properties.ActivePage.Components[i]).DataBinding.DataField;
TcxDbCheckBox(pagUsers.Properties.ActivePage.Components[i]).Enabled := Settings.License.IsEnabled(Fieldname);
end;
end;
end;
Any hints to what might be wrong in my code?
What's wrong is that you are looping over the Components property. That lists the components that are owned by the tab sheet. For components created in the form designer, the form is the owner. So it is expected that pagUsers.Properties.ActivePage.ComponentCount is zero since the only thing on your form that owns anything is the form itself.
What you need to to is use ControlCount and Controls[] to iterate over the children of the tab sheet. Simply replace all use of ComponentCount with ControlCount, and likewise replace Components[] with Controls[].
Note that the ControlCount and Controls[] properties only give the immediate children. Since you have panels you most likely have the panels as children of the tab sheet, and the check boxes as children of the panels. So you need to iterate over the children of the panels.
My answer here shows one way to do that. If you use the code I presented there then your iteration over checkboxes can be written very simply indeed:
TabSheet := pagUsers.Properties.ActivePage;
for CheckBox in TControls.Enumerator<TcxDbCheckBox>(TabSheet) do
....

How do you add MRF list to a TRibbons dropdown button collection?

I can easly add a MRF list to A TRibbon recent items list but how do you add
the same list to a ribbon item set as a dropdownbutton? The dropdown
item is ActionBars[2].Items[1].
var
ARecentFilesList: TStringList;
ACI: TActionClientItem;
if FileExists( ARecentFilesFilename ) then
begin
ARecentFilesList.LoadFromFile( ARecentFilesFilename );
for i := 0 to ARecentFilesList.Count - 1 do
begin
// add filename to Ribbon Recent Items
Ribbon1.AddRecentItem( ARecentFilesList.Strings[ i ] );
//add the file name to dropdown button collection
//add MostRecentFiles to ActionBars[2].Items[1]
//ACI := TActionClientItem.Create( );
//ACI.Caption := ARecentFilesList.Strings[ i ];
end;
end;
Thanks,
Bill
As with much of the actionbar controls, it is not as intuitive as you'd like. The basic structure on the ribbon is like this:
Each ribbon has tabs.
Each tab has groups.
Each group has a series of controls.
Each control has a TActionClient associated with it.
Each TActionClient can have other TActionClient objects associated with it, either as ContextItems or Items. And the more you repeat this level, the deeper the nested menus.
So your strategy then, is to get your hands on the TActionClient that represents the button you'd like to add your items to. On my simple test app, I grabbed the first control on the first group - your logic may need to be more advanced.
var
ActionClient: TActionClient;
ChildItem: TActionClientItem;
begin
// Does the same as Ribbon1.AddRecentItem('C:\MyFile.txt');
ActionClient := RibbonGroup1.ActionControls[0].ActionClient;
ChildItem := ActionClient.Items.Add;
ChildItem.Action := ActionThanOpensAFile;
ChildItem.Caption := 'C:\MyFile.txt';
end;
Note that I assign the caption of my menu item after I assigned the action - this is because the action replaces the caption (and other properties too) of the client it is associated with.

Resources