Displaying Hints in a Delphi XE2 TActionMainMenubar - delphi

I am struggling greatly trying to get hints displayed in a TActionMainMenuBar.
Delphi XE2.
I am creating the menus at runtime. I add categories, subitems, clear the menu out, that all works great. Clicking on the menu item works properly (for now it just does a ShowMessage with the action item tag but that's fine).
This is the code that adds a new menu item :
function TActionF.NewAction(AParent: TActionClientItem; Caption: String; aTag : integer; ExecuteAction: TNotifyEvent):TActionClientItem;
var
newActionClient : TActionClientItem;
AnewAction : TAction;
begin
newActionClient := TActionClientItem(AParent.Items.insert(AParent.Items.Count));
newActionClient.Caption := Caption; //??
newActionClient.UsageCount := -1; // turn of menu priority stuff for now
AnewAction := TAction.Create(Self);
AnewAction.Tag := aTag;
AnewAction.ImageIndex := -1;
AnewAction.Caption := Caption;
AnewAction.Hint := Caption + 'Action Tag = ' + IntToStr(aTag);
AnewAction.OnHint := acnDoHint; // fixed, could be parameter, but onHint is never called !!??
AnewAction.OnExecute := ExecuteAction; // passed as parameter
newActionClient.Action := AnewAction;
Result := newActionClient;
end;
I am setting the Hint" of the action. I have also experimented with assigning the OnHint, but the OnHint is never called. I simply cannot get at that hint when browsing the menu.
I have ShowHint set True everywhere I can see a place to do it.
The problem is that I cannot get any menu hints displayed no matter what I try. If i could just get at it I could display it myself (if the program won't). The OnHint is never called.
I have posted the full source of my menu program (Delphi XE2) , a small example narrowed down as best I could, in my public DropBox if anyone wants to see the program.
https://dl.dropbox.com/u/58421925/Actions.zip

This does exactly what you want: www.delphi.about.com/od/vclusing/a/menuitemhints.htm
It handles the WM_MENUSELECT message and shows the hint in its own window ( TMenuItemHint = class(THintWindow) ).

Related

How to show "NUMERIC KEYPAD" for menu item shortcut assigned with `VK_NUMPAD0`?

In a Delphi 10.4.2 Win32 VCL Application, on Windows10 X64 (German language) I set the shortcuts for some menu items programmatically:
mRasterizedDoubleSize.Shortcut := VK_ADD;
mRasterizedHalveSize.Shortcut := VK_SUBTRACT;
mRasterizedResetToOriginalSVGSize.Shortcut := VK_NUMPAD0;
This results in the following menu at run-time:
("ZEHNERTASTATUR" is German for NUMERIC KEYPAD)
Why "Zehnertastatur" (numeric keypad) is not shown for the third menu item?
How can I show "ZEHNERTASTATUR" (NUMERIC KEYPAD) for the menu item shortcut assigned with VK_NUMPAD0?
I have filed a Quality Report for this bug in Vcl.Menus: https://quality.embarcadero.com/browse/RSP-33296
Please vote for it!
EDIT: I have tried Andreas' advice, but it does work only programmatically at run-time, not at design-time in the Object Inspector:
mRasterizedResetToOriginalSVGSize.Caption := mRasterizedResetToOriginalSVGSize.Caption + #9 + '0 (NUMPAD) ';
Isn't there a function that translates the word "NUMPAD" into the current system language at-run-time?
EDIT2: I have tried this to get the word for the VK_NUMPAD0 shortcut, but it only gives back the same "0" without the "NUMPAD" suffix:
var s: TShortCut;
s := ShortCut(VK_NUMPAD0, []);
CodeSite.Send('TformMain.FormCreate: ShortCutToText(s)', ShortCutToText(s));
EDIT3: I now have debugged Vcl.Menus: The bug seems to be in Vcl.Menus.ShortCutToText: While VK_ADD ($6B) is correctly translated by GetSpecialName(ShortCut), VK_NUMPAD0 ($60) is NOT being translated by GetSpecialName(ShortCut)!
EDIT4: I have found the solution:
function MyGetSpecialName(ShortCut: TShortCut): string;
var
ScanCode: Integer;
KeyName: array[0..255] of Char;
begin
Result := '';
ScanCode := Winapi.Windows.MapVirtualKey(LoByte(Word(ShortCut)), 0) shl 16;
if ScanCode <> 0 then
begin
if Winapi.Windows.GetKeyNameText(ScanCode, KeyName, Length(KeyName)) <> 0 then
Result := KeyName;
end;
end;
var s: System.Classes.TShortCut;
s := ShortCut(VK_NUMPAD0, []);
CodeSite.Send('ShortCutToText', MyGetSpecialName(s));
One approach is like this:
Use a TActionList. This is good practice in general. Define your actions within this list, and then simply map them to menu items, buttons, check boxes, etc. The action list facility is one of the very best parts of the VCL IMHO.
Now, create an action named aResetZoom with Caption = 'Reset zoom'#9'Numpad 0' and NO ShortCut. Put this on the menu bar.
Then, create a new action named aResetZoomShortcut with the same OnExecute (and possibly the same OnUpdate) and shortcut Num 0 (set at design time or programmatically at run time). Don't put this on the main menu.
The result:
and the action is triggered when I press numpad 0 (but not the alpha 0).
There are many variants to this approach. Maybe you can make it work with a single action with no ShortCut but with Num 0 in its SecondaryShortCuts list. Or you can use the form's KeyPreview and OnKeyPress properties instead of the "dummy" action.
Many options. Choose the one that is best suited for your particular scenario.
Bonus remarks
Please note it is perfectly possibly to set captions with tabs at design time using the Object Inspector. See example video.
You can probably do localisation using the Win32 GetKeyNameText function. The following code is adapted from the VCL:
var
name: array[0..128] of Char;
begin
FillChar(name, SizeOf(name), 0);
GetKeyNameText(MapVirtualKey(VK_NUMPAD0, 0) shl 16, #name[0], Length(name));
// string(name) now is 'NUM 0' on my system
That being said, personally I don't mind if shortcut descriptions are non-localized or manually localised -- like the rest of the application.
Update
A clarification on how to use the localisation code:
procedure TForm5.FormCreate(Sender: TObject);
var
name: array[0..128] of Char;
NameAsANormalString: string;
begin
FillChar(name, SizeOf(name), 0);
GetKeyNameText(MapVirtualKey(VK_NUMPAD0, 0) shl 16, #name[0], Length(name));
NameAsANormalString := name;
ShowMessage(name);
end;
produces
on my system.

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.

Shortcut triggers TAction on first created form instead of form with focus

I found (in Delphi 2010) that shortcuts always end up on first form (as owned by main form) that has that action, but not the currently focused form. My TMainFrm owns several TViewFrm. Each has a TActionManager with the same TActons.
I see some ways out, but wonder whats the best fix.. (and not a bad hack)
The forms are navigated using a tabset which calls their Hide() and Show(). I'd did not expect hidden forms to receive keypresses. Am i doing something wrong?
It seems that action shortcuts are always start at the main form, and using TCustomForm.IsShortCut() get distributed to owned forms. I see no logic there to respect hidden windows, should i override it and have it trigger the focused form first?
Disabling all TActions in TViewFrm.Hide() .. ?
Moving the TActionToolBar to TMainFrm but that is a pit of snakes and last resort.
I have found a workaround thats good enough for me; my main form now overrides TCustomForm.IsShortcut() and first checks visible windows from my list of editor tabs.
A list which i conveniently already have, so this might not work for everyone.
// Override TCustomForm and make it check the currently focused tab/window first.
function TFormMain.IsShortCut(var Message: TWMKey): Boolean;
function DispatchShortCut(const Owner: TComponent) : Boolean; // copied function unchanged
var
I: Integer;
Component: TComponent;
begin
Result := False;
{ Dispatch to all children }
for I := 0 to Owner.ComponentCount - 1 do
begin
Component := Owner.Components[I];
if Component is TCustomActionList then
begin
if TCustomActionList(Component).IsShortCut(Message) then
begin
Result := True;
Exit;
end
end
else
begin
Result := DispatchShortCut(Component);
if Result then
Break;
end
end;
end;
var
form : TForm;
begin
Result := False;
// Check my menu
Result := Result or (Menu <> nil) and (Menu.WindowHandle <> 0) and
Menu.IsShortCut(Message);
// Check currently focused form <------------------- (the fix)
for form in FEditorTabs do
if form.Visible then
begin
Result := DispatchShortCut(form);
if Result then Break;
end;
// ^ wont work using GetActiveWindow() because it always returns Self.
// Check all owned components/forms (the normal behaviour)
if not Result then
Result := inherited IsShortCut(Message);
end;
Another solution would be to change DispatchShortCut() to check for components being visible and/or enabled, but that might impact more than i'd like. I wonder whether the original code architects had a reason not to -- by design. Best would be have it called twice: first to give priority to visible+enabled components, and second call as fallback to normal behavior.

How to write and show something on Delphi IDE status bar

I want to know how can I write a module to show something like clock or other thing on Borland Delphi 7 IDE status bar, because I know it's possible but I couldn't find how!
To insert a text in a StatusBar, you have to insert a panel first.
Just select your statusbar, find the property "Panels" (or perform double click over the statusbar) and click in "Add new".
After that, you can write what you want inside the panel in the property "Text" (you can insert one or more panels).
To do it programmatically, you can do something like this:
procedure TForm1.Timer1Timer(Sender: TObject);
begin
StatusBar1.Panels[0].Text := 'Today is: ' + FormatDateTime('dd/mm/yyyy hh:nn:ss', Now);
end;
Since OP didn't replied with more details, I'm going to post a little demonstration how to reach a status bar of Delphi's edit window. I had no success with adding new distinct status panel w/o disturbing layout, so I'm just changing the text of INS/OVR indicator panel.
Disclaimer: I still do not have access to the machine with Delphi 7 installed, so I've done that in BDS ("Galileo") IDE. However, differences should be minor. I believe what main difference lies in the way how we locate edit window.
Key strings are: 'TEditWindow' for edit window class name and 'StatusBar' for TStatusBar control name owned by edit window. These strings are consistent across versions.
{ helper func, see below }
function FindForm(const ClassName: string): TForm;
var
I: Integer;
begin
Result := nil;
for I := 0 to Screen.FormCount - 1 do
begin
if Screen.Forms[I].ClassName = ClassName then
begin
Result := Screen.Forms[I];
Break;
end;
end;
end;
procedure Init;
var
EditWindow: TForm;
StatusBar: TStatusBar;
StatusPanel: TStatusPanel;
begin
EditWindow := FindForm('TEditWindow');
Assert(Assigned(EditWindow), 'no edit window');
StatusBar := EditWindow.FindComponent('StatusBar') as TStatusBar;
(BorlandIDEServices as IOTAMessageServices).AddTitleMessage(Format('StatusBar.Panels.Count = %d', [StatusBar.Panels.Count]));
//StatusPanel := StatusBar.Panels.Add;
StatusPanel := StatusBar.Panels[2];
StatusPanel.Text := 'HAI!';
end;
initialization
Init;
finalization
// nothing to clean up yet
Another note: As you see, I use Open Tools API to output debug messages only, to interact with IDE I do use Native VCL classes. Therefore, this code must be in package.
The code above is a relevant part of the unit which should be contained in package. Do not forget to add ToolsAPI to uses clause as well as other appropriate referenced units (up to you).
Package should require rtl, vcl and designide (important!).
Since I run the testcase directly from initialization section, installing the package is enough for testcase to run and produce some result.

Resources