Vcl Styles issue for menu items added during program load - delphi

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.

Related

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.

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.

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.

How to add menu items separators which work as expected on OSX?

On Windows platform, with the VCL, when we want to add a separator in a menu, we add a TMenuItem with a Caption := '-';
With FireMonkey, we add a TMenuItem with a Text := '-';
It works as expected on Windows platform, the item with the Text='-' is displayed as a separator.
But, when I run the same application on OSX, I have the minus sign visible...
I haven't found any property on the TMenuItem to specify it is a separator...
I have tried with a TMainMenu and a TMenuBar (UseOSMenu := True|False;) and I still have this issue.
Any idea to create a real separator? (otherwise, I will check the OS and remove it if OSX...)
This is a bug in FireMonkey. I am sure they will solve it. But meanwhile you can use the below code. Call the procedure FixSeparatorItemsForMac in the OnActivate event of your main form.
Dont forget mac specific files in the uses list.
uses
...
{$IFDEF MACOS}
,Macapi.ObjectiveC,MacApi.AppKit,MacApi.Foundation,FMX.Platform.Mac
{$ENDIF}
{$IFDEF MACOS}
Procedure FixSeparatorItemsForMenuItem(MenuItem:NSMenuItem);
var i:Integer;
subItem:NSMenuItem;
begin
if (MenuItem.hasSubmenu = false) then exit;
for i := 0 to MenuItem.submenu.itemArray.count -1 do
begin
subItem := MenuItem.submenu.itemAtIndex(i);
if (subItem.title.isEqualToString(NSSTR('-'))= true) then
begin
MenuItem.submenu.removeItemAtIndex(i);
MenuItem.submenu.insertItem(TNSMenuItem.Wrap(TNSMenuItem.OCClass.separatorItem),i);
end else begin
FixSeparatorItemsForMenuItem(subItem);
end;
end;
end;
Procedure FixSeparatorItemsForMac;
var NSApp:NSApplication;
MainMenu:NSMenu;
AppItem: NSMenuItem;
i: Integer;
begin
NSApp := TNSApplication.Wrap(TNSApplication.OCClass.sharedApplication);
MainMenu := NSApp.mainMenu;
if (MainMenu <> nil) then
begin
for i := 0 to MainMenu.itemArray.count -1 do
begin
AppItem := mainMenu.itemAtIndex(i);
FixSeparatorItemsForMenuItem(AppItem);
end;
end;
end;
{$ENDIF}
I never programmed for the Mac, and I don't eveb have a Mac but out of curiosity I found some Apple Documentation about it.
The Menu Separator item is a disabled blank menu item, maybe you can fake with that:
separatorItem
Returns a menu item that is used to separate logical groups of menu
commands.
+ (NSMenuItem *)separatorItem Return Value
A menu item that is used to separate logical groups of menu commands.
Discussion
This menu item is disabled. The default separator item is blank space.
(From: http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/ApplicationKit/Classes/NSMenuItem_Class/Reference/Reference.html#//apple_ref/doc/c_ref/NSMenuItem)
I don't have the facilities to test this, but it's worth a try.
By default, FireMonkey creates it's own fully styled menus, but set the TMenuBar.UseOSMenu property to true and it uses OS calls to create the menus.
You can then combine this with the advice for creating Cocoa menus already discussed.
From http://docwiki.embarcadero.com/RADStudio/en/FireMonkey_Application_Design#Menus :
"Setting the TMenuBar.UseOSMenu property to True causes FireMonkey to create the menu tree with OS calls, resulting in a native menu. On Windows, this menu is at the top of the parent form, and displayed using the current Appearance theme. On Mac OS X, the menu is displayed in the global menu bar on top of the main screen whenever the application has focus."

How to merge two menus in a MDI application

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).

Resources