1

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:

enter image description here

("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)  ';

enter image description here

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

enter image description here

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

enter image description here

user1580348
  • 5,721
  • 4
  • 43
  • 105
  • Would it be OK to use a `TActionList` and connect the menu items to it? Or at least implement the shortcut by some other means than the `TMenuItem.ShortCut` property? Because in any case, you can do `mRasterizedResetToOriginalSVGSize.Caption := 'Original SVG size'#9'Numpad 0'`. – Andreas Rejbrand Mar 10 '21 at 11:05
  • (The reason the `TActionList` would help is that you then can use the secondary short cut facility.) – Andreas Rejbrand Mar 10 '21 at 11:10
  • Unfortunately, `actNumpad0Dummy.Shortcut := VK_NUMPAD0;` (of course have set the menu item's Ation property to `actNumpad0Dummy`) does not work. It shows the same result as previously. – user1580348 Mar 10 '21 at 11:40
  • My idea is that you use a `TActionList` with a `TAction` named `aResetZoom` with `Caption = 'Original SVG size'#9'Numpad 0'` and NO `Shortcut`. This one you place on the main menu. Then to get the actual keyboard short cut to work, you can either use `aResetZoom.SecondaryShortcuts` or create a dummy action with `Shortcut = VK_NUMPAD0` and the same `OnExecute` (which you don't put on the main menu). – Andreas Rejbrand Mar 10 '21 at 11:43
  • Please look at my question EDIT: Isn't there a function that translates the word "NUMPAD" into the current system language at-run-time? – user1580348 Mar 10 '21 at 12:18
  • It works well at design time, too. I often use the Object Inspector to add this kind of special captions with tabs. I just have to paste them into the OI or use the form's text view. See https://privat.rejbrand.se/menutab.mp4 – Andreas Rejbrand Mar 10 '21 at 13:32
  • "*EDIT4: I have found the solution*" - that solution should be posted as an answer, not as an edit. – Remy Lebeau Mar 10 '21 at 18:01
  • @RemyLebeau: Well, I still don't see how that EDIT4 differs from the bottom part of my A! – Andreas Rejbrand Mar 10 '21 at 18:12

1 Answers1

3

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:

Screenshot of main form with menu item View / Reset zoom shown. To the right of the menu item is the text "Numpad 0".

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

Message box with text "NUM 0"

on my system.

Andreas Rejbrand
  • 105,602
  • 8
  • 282
  • 384
  • Thanks for the nice example. However, when using `Caption = 'Reset zoom'#9'Numpad 0'` it works only in English Language. (I would have to make translations for other languages), while the `TMenuItem.Shortcut` feature automatically translates this to the current system language. Isn't there a function that translates the word "NUMPAD" into the current system language at-run-time? – user1580348 Mar 10 '21 at 12:50
  • Interesting idea. It has one drawback though: The shortcut in the menu will not be translated when running on Windows with a different locale. – dummzeuch Mar 10 '21 at 13:19
  • @dummzeuch: I don't know exactly how this works. If I look at the VCL source code, I can see that the VCL normally adds the short cut text in `Vcl.Menus.TMenuItem.AppendTo`: `Caption := Caption + #9 + ShortCutToText(FShortCut);` and that `ShortCutToText` uses the `SmkcCtrl = 'Ctrl+';` resourcestring in `Vcl.Const`. Hence, to me it seems like these shortcuts *aren't* automatically translated at all. If you have a German VCL app with German shortcut names and I run it on my Swedish system, will I not see `Strg+A` instead of `Ctrl+A`? – Andreas Rejbrand Mar 10 '21 at 13:25
  • Ah, only "special names" are localised. And they are localised using Win32 `GetKeyNameText`. – Andreas Rejbrand Mar 10 '21 at 13:41
  • @AndreasRejbrand Look at my **EDIT4** in the question. – user1580348 Mar 10 '21 at 14:19
  • @user1580348: Isn't that pretty much what I said [half an hour ago](https://stackoverflow.com/revisions/66564491/3)? :) – Andreas Rejbrand Mar 10 '21 at 14:22
  • @AndreasRejbrand The difference is that my **EDIT4** solution provides the correct system-language translation, while in your (still nice) solution you use a constant string ''Numpad 0''. So I get the desired result with `mRasterizedResetToOriginalSVGSize.Caption := mRasterizedResetToOriginalSVGSize.Caption + #9 + GetSpecialShortcutName(VK_NUMPAD0) + ' ';` – user1580348 Mar 10 '21 at 16:18
  • Look at the screenshot: https://i.imgur.com/ThhQ0j0.png – user1580348 Mar 10 '21 at 16:21
  • I have filed a Quality Report for this bug in Vcl.Menus: https://quality.embarcadero.com/browse/RSP-33296 Please vote for it! – user1580348 Mar 10 '21 at 17:11
  • @user1580348: Did you see the "You can probably do localisation ..." part at the bottom of my answer? :) – Andreas Rejbrand Mar 10 '21 at 17:13
  • @AndreasRejbrand What you describe as "probably" I get as "0 (ZEHNERTASTATUR)" with my **EDIT4** solution on my German Windows 10. However, I accept this answer because it gives a complete answer to my question. – user1580348 Mar 10 '21 at 17:22
  • @user1580348: Thank you. That's great. Probably it's just me not being very good at English, but to be clear, please notice that your EDIT4 actually does the exact same thing as the bottom part of my A: https://privat.rejbrand.se/edit4.png. Specifically, both my snippet and your EDIT 4 does `GetKeyNameText` of `MapVirtualKey` of `VK_NUMPAD0`, and both produce `0 (ZEHNERTASTATUR)` on a German system and `NUM 0` on an English system! :) – Andreas Rejbrand Mar 10 '21 at 17:34
  • @AndreasRejbrand When I use: `CodeSite.Send('TformMain.FormCreate: GetKeyNameText', GetKeyNameText(MapVirtualKey(VK_NUMPAD0, 0) shl 16, @name[0], Length(name)));` then I get as result: "18" !!! So it is clear that `GetKeyNameText` does not work in the intended way. – user1580348 Mar 10 '21 at 18:02
  • BTW, it is also strange that `var name: array[0..128] of Char;` cannot be declared as an INLINE VARIABLE! It must be declared in the `var` section. – user1580348 Mar 10 '21 at 18:04
  • @user1580348: You are using the function wrong. `GetKeyNameText` takes a pointer to string buffer and fills it. It returns the number of characters filled. 18 is the length of the string `0 (ZEHNERTASTATUR)`. So after the function `GetKeyNameText` has returned, *then* `string(name)` (yes, cast to a string) has the value `0 (ZEHNERTASTATUR)`. If you look at your EDIT 4, you can see that it too doesn't consider the returned value of `GetKeyNameText` to be the desired string, but instead looks at the value of its second parameter *afterwards*. – Andreas Rejbrand Mar 10 '21 at 18:08
  • You do know that `Winapi.Windows.GetKeyNameText` from your EDIT4 is exactly the same function as `GetKeyNameText` in my snippet? :) – Andreas Rejbrand Mar 10 '21 at 18:10
  • @user1580348: Regarding inline variables, that's because `array[..] of` is special. It's the same reason you cannot use such a construct as a returned value: `function Test: array[0..3] of Integer`. – Andreas Rejbrand Mar 10 '21 at 18:14
  • I REALLY didn't notice. My short-term memory must have deteriorated. – user1580348 Mar 10 '21 at 18:14
  • @user1580348: That happens to all of us! Just glad we are on the same page now! :) – Andreas Rejbrand Mar 10 '21 at 18:14
  • I always use the unit prefixes: `Winapi.Windows.GetKeyNameText`. With this habit, there cannot be misunderstandings. – user1580348 Mar 10 '21 at 18:19
  • @AndreasRejbrand Thank you for the update! I LOVE your explanations: They are so clear that even a fool like me can understand them! – user1580348 Mar 10 '21 at 18:28
  • Thank you for your kind words! :) I always try to be as clear as possible. – Andreas Rejbrand Mar 10 '21 at 18:29
  • @AndreasRejbrand And how to include a ShiftState like `CTROL`, `ALT`, `SHIFT` if I don't want to use a literal string? – user1580348 Mar 24 '21 at 11:02