Keyboard shortcuts in WPF

Viewed 136136

I know about using _ instead of &, but I'm looking at all the Ctrl + type shortcuts.

Ctrl+Z for undo, Ctrl+S for save, etc.

Is there a 'standard' way for implementing these in WPF applications? Or is it a case of roll your own and wire them up to whatever command/control?

11 Answers

Although the top answers are correct, I personally like to work with attached properties to enable the solution to be applied to any UIElement, especially when the Window is not aware of the element that should be focused. In my experience I often see a composition of several view models and user controls, where the window is often nothing more that the root container.

Snippet

public sealed class AttachedProperties
{
    // Define the key gesture type converter
    [System.ComponentModel.TypeConverter(typeof(System.Windows.Input.KeyGestureConverter))]
    public static KeyGesture GetFocusShortcut(DependencyObject dependencyObject)
    {
        return (KeyGesture)dependencyObject?.GetValue(FocusShortcutProperty);
    }

    public static void SetFocusShortcut(DependencyObject dependencyObject, KeyGesture value)
    {
        dependencyObject?.SetValue(FocusShortcutProperty, value);
    }

    /// <summary>
    /// Enables window-wide focus shortcut for an <see cref="UIElement"/>.
    /// </summary>
    // Using a DependencyProperty as the backing store for FocusShortcut.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty FocusShortcutProperty =
        DependencyProperty.RegisterAttached("FocusShortcut", typeof(KeyGesture), typeof(AttachedProperties), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.None, new PropertyChangedCallback(OnFocusShortcutChanged)));

    private static void OnFocusShortcutChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (!(d is UIElement element) || e.NewValue == e.OldValue)
            return;

        var window = FindParentWindow(d);
        if (window == null)
            return;

        var gesture = GetFocusShortcut(d);
        if (gesture == null)
        {
            // Remove previous added input binding.
            for (int i = 0; i < window.InputBindings.Count; i++)
            {
                if (window.InputBindings[i].Gesture == e.OldValue && window.InputBindings[i].Command is FocusElementCommand)
                    window.InputBindings.RemoveAt(i--);
            }
        }
        else
        {
            // Add new input binding with the dedicated FocusElementCommand.
            // see: https://gist.github.com/shuebner20/349d044ed5236a7f2568cb17f3ed713d
            var command = new FocusElementCommand(element);
            window.InputBindings.Add(new InputBinding(command, gesture));
        }
    }
}

With this attached property you can define a focus shortcut for any UIElement. It will automatically register the input binding at the window containing the element.

Usage (XAML)

<TextBox x:Name="SearchTextBox"
         Text={Binding Path=SearchText}
         local:AttachedProperties.FocusShortcutKey="Ctrl+Q"/>

Source code

The full sample including the FocusElementCommand implementation is available as gist: https://gist.github.com/shuebner20/c6a5191be23da549d5004ee56bcc352d

Disclaimer: You may use this code everywhere and free of charge. Please keep in mind, that this is a sample that is not suitable for heavy usage. For example, there is no garbage collection of removed elements because the Command will hold a strong reference to the element.

I tried all kinds of approaches using XAML and nothing worked. I finally found a solution based on the answer proved by Shahid Neermunda

First, the menu bar:

<Menu x:Name="MainMenuBar" Grid.Row="0" HorizontalContentAlignment="Left">
    <MenuItem Header="_File" HorizontalContentAlignment="Left">
        <MenuItem x:Name="NewProjectMenuItem"
                    Header="New Project"
                    InputGestureText="Ctrl+N"
                    Click="NewProject_Click"/>
        <MenuItem x:Name="OpenProjectMenuItem"
                    Header="Open Project"
                    InputGestureText="Ctrl+O"
                    Click="OpenProject_Click"/>
        <MenuItem x:Name="CloseProjectMenuItem"
                    Header="Close Project"
                    Click="CloseProject_Click"/>
        <Separator/>
        <MenuItem x:Name="SaveProjectMenuItem"
                    Header="Save Project"
                    InputGestureText="Ctrl+S"
                    Click="SaveProject_Click"/>
        <MenuItem x:Name="SaveProjectAsMenuItem"
                    Header="Save Project As ..."
                    InputGestureText="Shift+Ctrl+S"
                    Click="SaveProjectAs_Click"/>
        <Separator/>
        <MenuItem x:Name="ExitMenuItem"
                    Header="Exit"
                    InputGestureText="Alt+F4"
                    Click="Exit_Click"/>
    </MenuItem>
</Menu>

Nothing fancy. Each menu item has an 'InputGestureText' property (except for the close)

I then modified the click event methods that were auto-generated by the Click="[tab]" command. I'm only showing two here--one where there's a shortcut key defined and another where it isn't (Close):

private void OpenProject_Executed(object sender, ExecutedRoutedEventArgs e) => OpenProject_Click(sender, e);
private void OpenProject_Click(object sender, RoutedEventArgs e)
{
    OpenProject();
}

private void CloseProject_Click(object sender, RoutedEventArgs e)
{
    CloseProject();
}

The XXX_Executed(...) method is called by the shortcut binding (which I'll get to next) and the XXX_Click method is called by the Click command.

I did the same for the New Project, Open Project, Save Project As, and Exit auto-generated XXX_Click methods.

I then created a new file with the bindings (I separated it out to make it easier to find when the time comes to add additional bindings):

partial class MainWindow
{
    private void BindShortcuts()
    {
        BindShortcut(Key.N, ModifierKeys.Control, NewProject_Executed);
        BindShortcut(Key.O, ModifierKeys.Control, OpenProject_Executed);
        BindShortcut(Key.S, ModifierKeys.Control, SaveProject_Executed);
        BindShortcut(Key.S, ModifierKeys.Control | ModifierKeys.Shift, SaveProjectAs_Executed);
        BindShortcut(Key.F4, ModifierKeys.Alt, Exit_Executed);
    }

    private void BindShortcut(Key key, ModifierKeys modifiers, ExecutedRoutedEventHandler executed)
    {
        RoutedCommand cmd = new();
        _ = cmd.InputGestures.Add(new KeyGesture(key, modifiers));
        _ = CommandBindings.Add(new CommandBinding(cmd, executed));
    }
}

This way, when I add new menu items with more shortcuts attached, I only need to add the appropriate <MenuItem .../> tag, define an XXX_Executed method to call into the auto-generated XXX_Click method, and update the BindShortcuts() function.

Finally, I added the following to my constructor for the MainWindow class:

public MainWindow()
{
    InitializeComponent();
    BindShortcuts();
}

Works like a charm.

Special case: your shortcut doesn't trigger if the focus is on an element that "isn't native". In my case for example, a focus on a WpfCurrencyTextbox won't trigger shortcuts defined in your XAML (defined like in oliwa's answer).

I fixed this issue by making my shortcut global with the NHotkey package.

In short, for XAML, all you need to do is to replace

<KeyBinding Gesture="Ctrl+Alt+Add" Command="{Binding IncrementCommand}" />

by

<KeyBinding Gesture="Ctrl+Alt+Add" Command="{Binding IncrementCommand}"
            HotkeyManager.RegisterGlobalHotkey="True" />

Answer has also been posted to: How can I register a global hot key to say CTRL+SHIFT+(LETTER) using WPF and .NET 3.5?

Related