Dynamically create submenu - delphi

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.

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.

Accelerator keys for `TActionToolBar` not working

I cannot get the accelerator keys for a TActionToolBar to work.
This is what I am doing (reproducable in D2006, XE4):
Select New -> VCL Forms Application
Add ActionManager1 to the form
Add a new action Action1 in ActionManager1, set caption of action to &Test
Add ActionToolBar1 to the form
Add an item to ActionManager.ActionBars and set ActionManager.ActionBars[0].ActionBar to ActionToolBar1
Add an item to ActionManager.ActionBars[0].Items and set Action to Action1
Set the Action1.OnExecute event to show a message
Start program --> toolbar is displayed just fine and works via mouse
Press ALT+T --> nothing happens, but a Ding sound
What step am I missing?
As the existing answer points out, action toolbars do not support this functionality.
My personal opinion is that, this has been overlooked. Toolbar buttons often showing images instead of text might be one reason to do so (at least it was for me). However, evidently, toolbar buttons have the functionality when they show their captions, so could the action toolbar buttons.
#Silver points out in a comment that action bars have the capability to find accelerated items. In fact action menus use that functionality. Same functionality could easily be integrated into TCustomForm.IsShortCut for action toolbars, which already iterates action lists to find possible shortcut targets.
We can override the method and do it ourselves. Below example gives priority to default handling so assigned shortcuts will suppress keyboard accelerators with the same character, but this logic could easily be reversed.
function TForm1.IsShortCut(var Message: TWMKey): Boolean;
var
Item: TActionClientItem;
i: Integer;
begin
Result := inherited IsShortCut(Message);
if not Result and (KeyDataToShiftState(Message.KeyData) = [ssAlt]) then begin
for i := 0 to ActionManager1.ActionBars.Count - 1 do begin
if ActionManager1.ActionBars[i].ActionBar is TActionToolBar then begin
Item := TActionToolBar(ActionManager1.ActionBars[i].ActionBar)
.FindAccelItem(Message.CharCode);
if Assigned(Item) and Item.ShowCaption and Assigned(Item.Action)
and Item.Action.Execute then begin
Result := True;
Break;
end;
end;
end;
end;
end;
It seems that accelerator keys are not implemented for TActionToolBar - so no steps missing.
The following is not a real solution but a workaround that adds shortcuts by parsing the captions of the action (thanks to the suggestion of #KenWhite). A real solution for the question you will find in the accepted answer. I'll keep that answer for reference anyway:
uses System.Actions, System.UiTypes, Vcl.Menus, Vcl.ActnMan;
procedure AddShortCutsFromActionCaption(AActionMan: TActionManager);
var
Act: TContainedAction;
AccelKey: string;
I: Integer;
begin
for I := 0 to AActionMan.ActionCount - 1 do
begin
Act := AActionMan.Actions[I];
if Act.ShortCut = 0 then
begin
AccelKey := GetHotKey(Act.Caption);
if AccelKey <> '' then
Act.ShortCut := TextToShortCut('Alt+' + AccelKey);
end;
end;
end;
AddShortCutsFromActionCaption must be run once for ActionManager1 after the localization is run. That way the different accelerator keys for different languages remain functional.
If a shortcut already exists or if the caption of the action is modified, this workaround will not work - but for my purposes this is okay.

Extend the event OnClick of all MenuItems in the screen to execute another block of code

I want to set an event OnClick to all TMenuItems on the screen to do what the event currently does, and another few lines of code. I am currently using Delphi 5
For example, say that I have a TMenuItem with the code:
procedure TdesktopForm.MenuFoo1Click(Sender: TObject);
begin
ShowMessage(TComponent(Sender).Name)
end;
and I also have the following procedure:
procedure TdesktopForm.bar;
begin
ShowMessage('extra')
end;
And I want to everytime I click the TMenuItem the program show the TMenuItem's name and also the 'extra' message.
The example shown is just a demonstration of my problem, as in the real software I have over 300 menu items, I want to do this generically, so I won't have to add extra lines of code to all current menu clicks, nor add them when I add new menu items. The order of execution (between the menu click and the extra block of code) doesn't matter.
I tried using TActionList but I couldn't retrieve the object triggering the action, hence, I can't print it's name. I tried using ActiveControl but it always return the focused currently focused object, not the actual menu that I clicked. And also, the TAction execute event overwrites my TMainMenu.OnClick event
As long as all your event handlers are assigned at some point (either at design time or at run time) and don't change afterwards, you can do something like this:
Enumerate all menu items in the menu
For each create an object like the one described below
type
TEventInterceptor = class(TComponent)
private
FOrigEvent: TNotifyEvent;
FAdditionalEvent: TNotifyEvent;
procedure HandleOnClick(_Sender: TObject);
public
constructor Create(_MenuItem: TMenuItem; _AdditionalEvent: TNotifyEvent);
end;
constructor TEventInterceptor.Create(_MenuItem: TMenuItem; _AdditionalEvent: TNotifyEvent);
begin
inherited Create(_MenuItem);
FOrigEvent := _MenuItem.OnClick;
FAdditionalEvent := _AdditionalEvent;
_MenuItem.OnClick := HandleOnClick;
end;
procedure TEventInterceptor.HandleOnClick(_Sender: TObject);
begin
FOrigEvent(_Sender);
FAdditinalEvent(_Sender);
end;
Note that this code is completely untested and may not even compile.
I'm also not sure whether this works with Delphi 5. It does with Delphi 6 though, so chances are good.
Edit:
Some additional notes (thanks for the comments):
Inheriting this class from TComponent makes the form free it automatically when it is being destroyed.
HandleOnClick should possibly check if FOrigEvent is assigned before calling it.

Can a Component Editor be executed on multiple components?

Short Version
I am trying to implement my first ever Component Editor for a custom button I have made. With the help of some online articles I have successfully installed the editor and can see the menu item when I right click on my button in the Form Designer.
But this component editor menu is not showing when selecting more than one of my button controls.
Do Component Editors only work with single selected controls by default, or can they work with multiple selected controls and if so how?
Long Version
I was in the process of implementing a TPropertyEditor for one of my own components but have now decided that a TComponentEditor would be better served, or so I thought.
Basically I have a TCustomButton which I have ownerdrawn, this button component has several published properties for changing the appearance such as the border and fill color etc.
The Component Editor I am implementing displays in the context menu a new menu item to "Load settings from a File". When executed a simple TOpenDialog is shown to which you can select the appropriate file, for example an Ini File which I then read and set the values from the File accordingly.
Everything is working good from what I can see, but as I am still sort of new and getting to grips with the whole custom controls side of Delphi I noticed something that does not happen - I am not sure if this is the actual intended behavior or whether I can change it.
The problem is using the Component Editor menu on multiple selected instances of my button control. If just one button is selected and I right click in the Designer, my menu is shown at the top of the context menu, however multiple selected controls do not display the Component Editor menu.
Code Sample
type
TMyButtonEditor = class(TComponentEditor)
public
procedure ExecuteVerb(Index: Integer); override;
function GetVerb(Index: Integer): string; override;
function GetVerbCount: Integer; override;
end;
implementation
{ TMyButtonEditor }
procedure TMyButtonEditor.ExecuteVerb(Index: Integer);
var
OpenDialog: TOpenDialog;
begin
case Index of
0:
begin
OpenDialog := TOpenDialog.Create(nil);
try
OpenDialog.Filter := 'All Files (*.*)|*.*';
if OpenDialog.Execute then
begin
// handle opened file..
end;
finally
OpenDialog.Free;
end;
end;
end;
end;
function TMyButtonEditor.GetVerb(Index: Integer): string;
begin
case Index of
0:
begin
Result := 'Load settings from File...';
end;
end;
end;
function TMyButtonEditor.GetVerbCount: Integer;
begin
Result := 1;
end;
In register procedure unit:
RegisterComponentEditor(TMyButton, TMyButtonEditor);
From what I can see only single components can use a Component Editor at any given time, or am I wrong and they can be used on multiple controls?
I was hoping to select say maybe 3 or 4 of my buttons on the Form Designer and use the Component Editor to apply imported settings on those buttons all at once.
Component editors can only operate on a single component.
This is one very good reason to prefer making properties available through the Object Inspector rather than component editors, wherever possible. Because the Object Inspector can operate on multiple components at once.

OnNewItem event for TMainMenu

I have a TMainMenu on a form and I want to add an event when an TMenuItem is added to the TmainMenu.
TMainMenu.OnChange(Sender: TObject; Source: TMenuItem; Rebuild: Boolean) doesn't seem to work because there is no difference in params values when adding or removing or updateing items. And I need to react only to new items.
Any ideas?
Problem
Looking at the code for TMenuItem.Add() it's clearly obvious that the only event that's triggered is the OnChange. Because of that there's no easy and clean solution.
Clean solution - refactor your code
My first thought: surely you know when a menu item is added to the menu, it's your code that's adding it. The best option would be to simply re-factor the code so it doesn't directly add the menu item but calls a method of your choice. For example, if you're using code like this:
someMenu.Add(M); // where someMenu is an existing menu item and "M" is the new item
you could refactor it to something like this:
// procedure definition in private section of main form, or somewhere else relevant:
procedure AddSubMenu(const Where, What: TMenuItem);
// Refactor your code to do this:
AddSubMenu(someMenu, M);
// Then in the "AddSubMenu" you can do whatever you want to do for new items:
procedure TFormName.AddSubmenu(const Where, What: TMenuItem);
begin
// Do your thing.
// Then do the actual adding of the menu item
Where.Add(What);
end;
Alternative solution - track existing menu items
Use the OnChange item that you know gets called, recursively walk the list of existing TMenuItems and do something with them so you know you've seen them before. For example set the Tag to 1 - you'll know items with Tag = 0 are new. Or add all items into a Dictionary so you can easily test what items are new and what items are pre-existing.
Here's an example OnChange handler using the Tag property to track rather a menu item is New or Old. Make sure you handle the initial creating of the Menu properly; For example, I'd assign the OnChange at runtime, from the form's OnCreate, after the Menu has been initialized from the DFM and after setting the Tag for all the design-time menu items to 1:
procedure TForm1.MainMenu1Change(Sender: TObject; Source: TMenuItem; Rebuild: Boolean);
var i: Integer;
procedure VisitMenuItem(const M: TMenuItem);
begin
if M.Tag = 0 then
begin
// First time I see this TMenuItem!
// DO my thing
ShowMessage(M.Caption);
// Mark the item so I don't "Do my thing" again
M.Tag := 1;
end;
end;
procedure RecursivelyVisitAllMenuItems(const Root: TMenuItem);
var i:Integer;
begin
VisitMenuItem(Root);
for i:=0 to Root.Count-1 do
RecursivelyVisitAllMenuItems(Root.Items[i]);
end;
begin
for i:=0 to MainMenu1.Items.Count-1 do
RecursivelyVisitAllMenuItems(MainMenu1.Items[i]);
end;

Resources