How to Display Working Keyboard Shortcut for Menu Items?
Solution 1
I'll start with the warning. It can happen that you will need not only customizable hot keys but the menu itself. So think twice before using InputBindings
statically.
There is one more caution concerning InputBindings
: they imply that command is tied to the element in window's visual tree. Sometimes you need global hot keys not connected with any particular window.
The above said means that you can make it another way and implement your own application wide gestures processing with correct routing to corresponding commands (don't forget to use weak references to commands).
Nonetheless the idea of gesture aware commands is the same.
public class CommandWithHotkey : ICommand
{
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
MessageBox.Show("It Worked!");
}
public KeyGesture Gesture { get; set; }
public string GestureText
{
get { return Gesture.GetDisplayStringForCulture(CultureInfo.CurrentUICulture); }
}
public string Text { get; set; }
public event EventHandler CanExecuteChanged;
public CommandWithHotkey()
{
Text = "Execute Me";
Gesture = new KeyGesture(Key.K, ModifierKeys.Control);
}
}
Simple View Model:
public class ViewModel
{
public ICommand Command { get; set; }
public ViewModel()
{
Command = new CommandWithHotkey();
}
}
Window:
<Window x:Class="CommandsWithHotKeys.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:commandsWithHotKeys="clr-namespace:CommandsWithHotKeys"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<commandsWithHotKeys:ViewModel/>
</Window.DataContext>
<Window.InputBindings>
<KeyBinding Command="{Binding Command}" Key ="{Binding Command.Gesture.Key}" Modifiers="{Binding Command.Gesture.Modifiers}"></KeyBinding>
</Window.InputBindings>
<Grid>
<Menu HorizontalAlignment="Stretch" VerticalAlignment="Top" Height="Auto">
<MenuItem Header="Test">
<MenuItem InputGestureText="{Binding Command.GestureText}" Header="{Binding Command.Text}" Command="{Binding Command}">
</MenuItem>
</MenuItem>
</Menu>
</Grid>
</Window>
Sure, you should somehow load the gestures information from configuration and then init commands with the data.
The next step is keystokes like in VS: Ctrl+K,Ctrl+D, quick search gives this SO question.
Solution 2
If I haven't misunderstood your question try this:
<Window.InputBindings>
<KeyBinding Key="A" Modifiers="Control" Command="{Binding ClickCommand}"/>
</Window.InputBindings>
<Grid >
<Button Content="ok" x:Name="button">
<Button.ContextMenu>
<local:CustomContextMenu>
<MenuItem Header="Click" Command="{Binding ClickCommand}"/>
</local:CustomContextMenu>
</Button.ContextMenu>
</Button>
</Grid>
..with:
public class CustomContextMenu : ContextMenu
{
public CustomContextMenu()
{
this.Opened += CustomContextMenu_Opened;
}
void CustomContextMenu_Opened(object sender, RoutedEventArgs e)
{
DependencyObject obj = this.PlacementTarget;
while (true)
{
obj = LogicalTreeHelper.GetParent(obj);
if (obj == null || obj.GetType() == typeof(Window) || obj.GetType() == typeof(MainWindow))
break;
}
if (obj != null)
SetInputGestureText(((Window)obj).InputBindings);
//UnSubscribe once set
this.Opened -= CustomContextMenu_Opened;
}
void SetInputGestureText(InputBindingCollection bindings)
{
foreach (var item in this.Items)
{
var menuItem = item as MenuItem;
if (menuItem != null)
{
for (int i = 0; i < bindings.Count; i++)
{
var keyBinding = bindings[i] as KeyBinding;
//find one whose Command is same as that of menuItem
if (keyBinding!=null && keyBinding.Command == menuItem.Command)//ToDo : Apply check for None Modifier
menuItem.InputGestureText = keyBinding.Modifiers.ToString() + " + " + keyBinding.Key.ToString();
}
}
}
}
}
I hope this will give you an idea.
Solution 3
This is how it did it:
In the loaded-event of my window I match the Command bindings of the menu items with the Command bindings of all InputBindings, much like ethicallogics's answer, but for a menu bar and it actually compares the Command bindings and not just the value, because that didn't work for me. this code also recurses into submenus.
private void MainWindow_OnLoaded(object sender, RoutedEventArgs e)
{
// add InputGestures to menu items
SetInputGestureTextsRecursive(MenuBar.Items, InputBindings);
}
private void SetInputGestureTextsRecursive(ItemCollection items, InputBindingCollection inputBindings)
{
foreach (var item in items)
{
var menuItem = item as MenuItem;
if (menuItem != null)
{
if (menuItem.Command != null)
{
// try to find an InputBinding with the same command and take the Gesture from there
foreach (KeyBinding keyBinding in inputBindings.OfType<KeyBinding>())
{
// we cant just do keyBinding.Command == menuItem.Command here, because the Command Property getter creates a new RelayCommand every time
// so we compare the bindings from XAML if they have the same target
if (CheckCommandPropertyBindingEquality(keyBinding, menuItem))
{
// let a new Keygesture create the String
menuItem.InputGestureText = new KeyGesture(keyBinding.Key, keyBinding.Modifiers).GetDisplayStringForCulture(CultureInfo.CurrentCulture);
}
}
}
// recurse into submenus
if (menuItem.Items != null)
SetInputGestureTextsRecursive(menuItem.Items, inputBindings);
}
}
}
private static bool CheckCommandPropertyBindingEquality(KeyBinding keyBinding, MenuItem menuItem)
{
// get the binding for 'Command' property
var keyBindingCommandBinding = BindingOperations.GetBindingExpression(keyBinding, InputBinding.CommandProperty);
var menuItemCommandBinding = BindingOperations.GetBindingExpression(menuItem, MenuItem.CommandProperty);
if (keyBindingCommandBinding == null || menuItemCommandBinding == null)
return false;
// commands are the same if they're defined in the same class and have the same name
return keyBindingCommandBinding.ResolvedSource == menuItemCommandBinding.ResolvedSource
&& keyBindingCommandBinding.ResolvedSourcePropertyName == menuItemCommandBinding.ResolvedSourcePropertyName;
}
Do this one time in your Window's code-behind and every menu item has an InputGesture. Just the translation is missing
Related videos on Youtube
Comments
-
O. R. Mapper about 2 years
I am trying to create a localizable WPF menu bar with menu items that have keyboard shortcuts - not accelerator keys/mnemonics (usually shown as underlined characters that can be pressed to directly select a menu item when the menu is already open), but keyboard shortcuts (usually combinations of Ctrl + another key) that are displayed right-aligned next to the menu item header.
I am using the MVVM pattern for my application, meaning that I avoid placing any code in code-behind wherever possible and have my view-models (that I assign to the
DataContext
properties) provide implementations of theICommand
interface that are used by controls in my views.
As a base for reproducing the issue, here is some minimal source code for an application as described:
Window1.xaml
<Window x:Class="MenuShortcutTest.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MenuShortcutTest" Height="300" Width="300"> <Menu> <MenuItem Header="{Binding MenuHeader}"> <MenuItem Header="{Binding DoSomethingHeader}" Command="{Binding DoSomething}"/> </MenuItem> </Menu> </Window>
Window1.xaml.cs
using System; using System.Windows; namespace MenuShortcutTest { public partial class Window1 : Window { public Window1() { InitializeComponent(); this.DataContext = new MainViewModel(); } } }
MainViewModel.cs
using System; using System.Windows; using System.Windows.Input; namespace MenuShortcutTest { public class MainViewModel { public string MenuHeader { get { // in real code: load this string from localization return "Menu"; } } public string DoSomethingHeader { get { // in real code: load this string from localization return "Do Something"; } } private class DoSomethingCommand : ICommand { public DoSomethingCommand(MainViewModel owner) { if (owner == null) { throw new ArgumentNullException("owner"); } this.owner = owner; } private readonly MainViewModel owner; public event EventHandler CanExecuteChanged; public void Execute(object parameter) { // in real code: do something meaningful with the view-model MessageBox.Show(owner.GetType().FullName); } public bool CanExecute(object parameter) { return true; } } private ICommand doSomething; public ICommand DoSomething { get { if (doSomething == null) { doSomething = new DoSomethingCommand(this); } return doSomething; } } } }
The WPF
MenuItem
class has anInputGestureText
property, but as described in SO questions such as this, this, this and this, that is purely cosmetic and has no effect whatsoever on what shortcuts are actually processed by the application.SO questions like this and this point out that the command should be linked with a
KeyBinding
in theInputBindings
list of the window. While that enables the functionality, it does not automatically display the shortcut with the menu item. Window1.xaml changes as follows:<Window x:Class="MenuShortcutTest.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MenuShortcutTest" Height="300" Width="300"> <Window.InputBindings> <KeyBinding Key="D" Modifiers="Control" Command="{Binding DoSomething}"/> </Window.InputBindings> <Menu> <MenuItem Header="{Binding MenuHeader}"> <MenuItem Header="{Binding DoSomethingHeader}" Command="{Binding DoSomething}"/> </MenuItem> </Menu> </Window>
I have tried manually setting the
InputGestureText
property in addition, making Window1.xaml look like this:<Window x:Class="MenuShortcutTest.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MenuShortcutTest" Height="300" Width="300"> <Window.InputBindings> <KeyBinding Key="D" Modifiers="Control" Command="{Binding DoSomething}"/> </Window.InputBindings> <Menu> <MenuItem Header="{Binding MenuHeader}"> <MenuItem Header="{Binding DoSomethingHeader}" Command="{Binding DoSomething}" InputGestureText="Ctrl+D"/> </MenuItem> </Menu> </Window>
This does display the shortcut, but is not a viable solution for obvious reasons:
- It does not update when the actual shortcut binding changes, so even if the shortcuts are not configurable by users, this solution is a maintenance nightmare.
- The text needs to be localized (as e.g. the Ctrl key has different names in some languages), so if any of the shortcuts is ever changed, all translations would need to be updated individually.
I have looked into creating an
IValueConverter
to use for binding theInputGestureText
property to theInputBindings
list of the window (there might be more than oneKeyBinding
in theInputBindings
list, or none at all, so there is no specificKeyBinding
instance that I could bind to (ifKeyBinding
even lends itself to being a binding target)). This appears to me like the most desirable solution, because it is very flexible and at the same time very clean (it does not require a plethora of declarations in various places), but on the one hand,InputBindingCollection
does not implementINotifyCollectionChanged
, thus the binding would not be updated when shortcuts are replaced, and on the other hand, I did not manage to provide the converter with a reference to my view-model in a tidy manner (which it would need to access the localization data). What is more,InputBindings
is not a dependency property, so I cannot bind that to a common source (such as a list of input bindings located in the view-model) that theItemGestureText
property could be bound to, as well.Now, many resources (this question, that question, this thread, that question and that thread point out that
RoutedCommand
andRoutedUICommand
contain a built-inInputGestures
property and imply that key bindings from that property are automatically displayed in menu items.However, using either of those
ICommand
implementations seems to open a new can of worms, as theirExecute
andCanExecute
methods are not virtual and thus cannot be overridden in subclasses to fill in the desired functionality. The only way to provide that seems to be declaring aCommandBinding
in XAML (shown e.g. here or here) that connects a command with an event handler - however, that event handler would then be located in the code-behind, thus violating the MVVM architecture described above.
Trying nonetheless, this means turning most of the aforementioned structure inside-out (which also kind of implies that I need to make my mind up on how to eventually solve the issue in my current, comparably early stage of development):
Window1.xaml
<Window x:Class="MenuShortcutTest.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:MenuShortcutTest" Title="MenuShortcutTest" Height="300" Width="300"> <Window.CommandBindings> <CommandBinding Command="{x:Static local:DoSomethingCommand.Instance}" Executed="CommandBinding_Executed"/> </Window.CommandBindings> <Menu> <MenuItem Header="{Binding MenuHeader}"> <MenuItem Header="{Binding DoSomethingHeader}" Command="{x:Static local:DoSomethingCommand.Instance}"/> </MenuItem> </Menu> </Window>
Window1.xaml.cs
using System; using System.Windows; namespace MenuShortcutTest { public partial class Window1 : Window { public Window1() { InitializeComponent(); this.DataContext = new MainViewModel(); } void CommandBinding_Executed(object sender, System.Windows.Input.ExecutedRoutedEventArgs e) { ((MainViewModel)DataContext).DoSomething(); } } }
MainViewModel.cs
using System; using System.Windows; using System.Windows.Input; namespace MenuShortcutTest { public class MainViewModel { public string MenuHeader { get { // in real code: load this string from localization return "Menu"; } } public string DoSomethingHeader { get { // in real code: load this string from localization return "Do Something"; } } public void DoSomething() { // in real code: do something meaningful with the view-model MessageBox.Show(this.GetType().FullName); } } }
DoSomethingCommand.cs
using System; using System.Windows.Input; namespace MenuShortcutTest { public class DoSomethingCommand : RoutedCommand { public DoSomethingCommand() { this.InputGestures.Add(new KeyGesture(Key.D, ModifierKeys.Control)); } private static Lazy<DoSomethingCommand> instance = new Lazy<DoSomethingCommand>(); public static DoSomethingCommand Instance { get { return instance.Value; } } } }
For the same reason (
RoutedCommand.Execute
and such being non-virtual), I do not know how to subclassRoutedCommand
in a way to create aRelayCommand
like the one used in an answer to this question based onRoutedCommand
, so I do not have to make the detour over theInputBindings
of the window - while explicitly reimplementing the methods fromICommand
in aRoutedCommand
subclass feels like I might be breaking something.What is more, while the shortcut is automatically displayed with this method as configured in the
RoutedCommand
, it does not seem to get automatically localized. My understanding is that addingSystem.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("de-de"); System.Threading.Thread.CurrentThread.CurrentUICulture = System.Threading.Thread.CurrentThread.CurrentCulture;
to the
MainWindow
constructor should make sure that localizable strings supplied by the framework should be taken from the GermanCultureInfo
- however,Ctrl
does not change toStrg
, so unless I am mistaken about how to set theCultureInfo
for framework-supplied strings, this method is not viable anyway if I expect the displayed shortcut to be correctly localized.Now, I am aware that
KeyGesture
allows me to specify a custom display string for the keyboard shortcut, but not only is theRoutedCommand
-derivedDoSomethingCommand
class disjoint from all of my instances (from where I could get in touch with the loaded localization) due to the wayCommandBinding
has to be linked with a command in XAML, the respectiveDisplayString
property is read-only, so there would be no way to change it when another localization is loaded at runtime.This leaves me with the option to manually dig through the menu tree (EDIT: for the sake of clarification, no code here because I am not asking for this and I know how to do this) and the
InputBindings
list of the window to check which commands have anyKeyBinding
instances associated with them, and which menu items are linked to any of those commands, so that I can manually set theInputGestureText
of each of the respective menu items to reflect the first (or preferred, by whichever metric I want to use here) keyboard shortcut. And this procedure would have to be repeated every time I think the key bindings may have changed. However, this seems like an extremely tedious workaround for something that is essentially a basic feature of a menu bar GUI, so I'm convinced it cannot be the "correct" way to do this.What is the right way to automatically display a keyboard shortcut that is configured to work for WPF
MenuItem
instances?EDIT: All of the other questions I found dealt with how a
KeyBinding
/KeyGesture
could be used to actually enable the functionality visually implied byInputGestureText
, without explaining how to automatically link the two aspects in the described situation. The only somewhat promising question that I found was this, but it has not received any answers in over two years.-
Sheridan over 10 yearsWhat's wrong with
ToolTip="{Binding ToolTip}"
whereToolTip
equals something like"Ctrl+C"
? -
O. R. Mapper over 10 years@Sheridan: You mean
InputGestureText="{Binding ...}"? What should it be bound to; where should the
"Ctrl+C"` text come from (in a manner that is somehow inherently linked with theKeyGesture
defined for the command/menu item)? -
Sheridan over 10 yearsWell, if you're binding to something on the
MenuItem
already, such as theHeader
property, then you could just add another property to the data bound object that contains the relevant text for theInputGestureText
property. -
O. R. Mapper over 10 years@Sheridan: How does that other property automatically get synchronized with the defined
KeyGesture
instances in theInputBindings
list of the window, if any? -
Anatoliy Nikolaev over 10 yearsCan you provide the minimum code that would show the problem, and then it should be. So it would be easier to understand by looking at the code that needs to be done.
-
O. R. Mapper over 10 years@AnatoliyNikolaev: Thanks for your response. I have added some sample code representing both the initial and various intermediate stages of the source code after my various attempts to find a good solution. Let me know if you need any further information, please.
-
Pavel Voronin over 10 years@O.R.Mapper The information about hotkeys should be inside your implementation of ICommand. KeyBinding.Key and Modifiers are dependency properties, hence you can bind them to some properties of the command. Knowing the key and modifiers you can provide localized string to bind it to InputGester.
-
O. R. Mapper over 10 years@voroninp: Binding from the
KeyBinding
to the command sounds promising; I'll have to try that. An existing, but maybe minor (considering the alternatives) gripe with this solution would probably be that I'd be limited to one shortcut per menu item (at least using this technique) and (more importantly) that there would essentially need to be oneKeyBinding
for every existing menu item, just in case a shortcut is or gets assigned to the menu item. -
O. R. Mapper over 10 years@voroninp: Your solution works very well. If you add that in an answer (with a somewhat more elaborate description for future readers), I will mark it as accepted.
-
Pavel Voronin over 10 years@O.R. Mapper Ok. a bit later.
-
Adi Lester over 10 yearsThis is a good answer, only it needs to be adjusted for menus rather than context menus, and it should also take into account nested menu items, as currently it only handles the first level.
-
O. R. Mapper over 10 years+1 for the effort; I had already described that procedure myself toward the end of my question as a last-resort workaround. Also, I am not sure whether you really need the
LogicalTreeHelper
; if the code is already within aContextMenu
, couldn't you just iterate over the inheritedItems
property? -
O. R. Mapper over 10 yearsThis is indeed very similar to the solution I had tried after reading your comment. I did a few minor details differently; as I use a different localization framework (that supports non-framework-supported languages) I supplied my own localization for the key gesture, though
GetDisplayStringForCulture
seems helpful for situations where a culture can be used. Furthermore, I did not bind the key and the modifier directly to the command from the data context, but used a binding like{Binding Command.Gesture.Key, RelativeSource={RelativeSource Self}}
... -
O. R. Mapper over 10 years... so as to avoid some redundancy when setting the actual property name of the used command, as it just has to be indicated in the binding for the
Command
property like this. -
Pavel Voronin over 10 yearsI would clarify the directions of the binding. Dependency Property is a target so usually you bind TO the target. I bind command's data TO KeyBinding.
-
Pavel Voronin over 10 yearsAnd I cannot understand why RelativeSource? {RelativeSource Self} points to the object holding the bound target property i.e. KeyBinding in this case.
-
O. R. Mapper over 10 yearsAh, you're right. The target are the properties of the
KeyBinding
while the source are the properties of the command. As for the relative source - right, it points to theKeyBinding
, so via the binding path, I access theCommand
property of theKeyBinding
. Whatever command theKeyBinding
is set to, theKey
and theModifiers
properties will thus always automatically use the settings from that command. Think of defining 10 key bindings - you can copy and paste theKeyBinding
declaration and all you have to change is the source of theCommand
property binding, once per line. -
Pavel Voronin over 10 yearsNow I see. Yes, it's more generic way. You are right.
-
ergohack about 7 yearsYour answer makes me want to go the other direction,... i.e. instantiate the Window's
InputBindings
in code-behind from the display-onlyInputGestureText
and related attributes in the XAML. Or in other words, make effective theInputGestureText
in generalized windows initialization code behind. If I get around to doing this, I'll post the answer.