如何显示菜单项的工作键盘快捷键?

时间:2014-02-23 18:08:45

标签: wpf keyboard-shortcuts menuitem key-bindings

我正在尝试使用具有键盘快捷键的菜单项创建可本地化的WPF菜单栏 - 加速键/助记符(通常显示为带下划线的字符,可以按下以直接选择菜单项菜单已打开),但键盘快捷键(通常是 Ctrl + 另一个键的组合)在菜单项标题旁边右对齐显示。

我正在为我的应用程序使用MVVM模式,这意味着我尽可能避免在代码隐藏中放置任何代码,并让我的视图模型(我分配给DataContext properties)提供{{{ 3}}我的视图中的控件使用。


作为重现问题的基础,下面是一些应用程序的最小源代码:

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

ICommand interfaceWPF MenuItem class,但正如InputGestureText propertythisthisthis等问题所述,纯粹是装饰性的,对应用程序实际处理的快捷方式没有任何影响。

诸如thisthis之类的问题指出该命令应该与窗口this列表中的KeyBinding相关联。虽然它启用了该功能,但它不会自动显示菜单项的快捷方式。 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">
    <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>

我还尝试手动设置InputGestureText属性,使 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">
    <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>

这确实显示了快捷方式,但由于显而易见的原因,它不是一个可行的解决方案:

  • 当实际快捷方式绑定发生更改时,它不会更新,因此即使用户无法配置快捷方式,此解决方案也是一个维护噩梦。
  • 文本需要本地化(例如 Ctrl 键在某些语言中有不同的名称),因此如果任何快捷方式发生变化,所有翻译将会需要单独更新。

我已经考虑创建一个InputBindings来用于将InputGestureText属性绑定到窗口的IValueConverter列表中(可能有多个KeyBinding InputBindings列表,或者根本没有,因此没有可以绑定的特定KeyBinding实例(如果KeyBinding甚至适合作为绑定目标))。这在我看来是最理想的解决方案,因为它非常灵活,同时非常干净(它不需要在不同的地方进行过多的声明),但一方面,InputBindings没有实现InputBindingCollection,因此更换快捷方式时不会更新绑定,另一方面,我无法以一种整洁的方式为转换器提供对我的视图模型的引用(它需要访问本地化数据)。更重要的是,INotifyCollectionChanged不是依赖属性,所以我无法将它绑定到ItemGestureText属性可以绑定的公共源(例如位于视图模型中的输入绑定列表)也是。

现在,许多资源(InputBindingsthis questionthat questionthis threadthat question指出that thread和{{3}包含一个内置的RoutedCommand,暗示该属性的键绑定会自动显示在菜单项中。

然而,使用这些ICommand实现中的任何一个似乎都会打开一个新的蠕虫,因为它们的RoutedUICommandInputGestures property方法不是虚拟的,因此无法在子类中重写以填充在所需的功能。提供该方法的唯一方法似乎是在XAML中声明Execute(如CanExecuteCommandBinding所示)将命令与事件处理程序连接 - 但是,该事件处理程序将是位于代码隐藏中,因此违反了上述MVVM架构。


尽管如此,这意味着将大部分上述结构从内到外转变(这也意味着我需要考虑如何在当前的,相对早期的开发阶段最终解决问题):< / p>

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

出于同样的原因(RoutedCommand.Execute并且非虚拟),我不知道如何创建RoutedCommand的方式来创建RelayCommand,就像使用RoutedCommand一样InputBindings 3}}基于ICommand,所以我不必绕过窗口的RoutedCommand - 同时明确地重新实现RoutedCommand子类中System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("de-de"); System.Threading.Thread.CurrentThread.CurrentUICulture = System.Threading.Thread.CurrentThread.CurrentCulture; 的方法感觉我可能会破坏某些东西。

此外,虽然使用MainWindow中配置的此方法自动显示快捷方式,但它似乎不会自动本地化。我的理解是添加

CultureInfo

Ctrl构造函数应该确保框架提供的可本地化字符串应该来自德语Strg - 但是,CultureInfo不会更改为RoutedCommand,所以,除非我错误地知道如何为框架提供的字符串设置DoSomethingCommand,否则如果我希望显示的快捷方式被正确本地化,这种方法是不可行的。

现在,我知道here允许我为键盘快捷键指定自定义显示字符串,但不仅CommandBinding派生的InputBindings类与我的所有类都不相交由于KeyBinding必须与XAML中的命令链接,here是只读的,因此无法通过实例(我可以从中接收加载的本地化)在运行时加载另一个本地化时更改它。

这让我可以选择手动挖掘菜单树(编辑:为了澄清,这里没有代码,因为我没有要求这个,我知道如何做到这一点)和InputGestureText用于检查哪些命令具有与之关联的MenuItem个实例以及哪些菜单项链接到这些命令的窗口列表,以便我可以手动设置每个菜单的KeyBinding项目反映第一个(或首选,通过我想在这里使用的度量标准)键盘快捷键。每当我认为密钥绑定可能已经改变时,就必须重复这个过程。然而,对于一个基本上是菜单栏GUI的基本功能的东西来说,这似乎是一个非常繁琐的解决方法,因此我确信它不能成为&#34;正确的&#34;这样做的方法。

自动显示配置为适用于WPF KeyGesture实例的键盘快捷键的正确方法是什么?

编辑:我发现的所有其他问题都涉及InputGestureText / {{1}}如何用于实际启用{{1}}视觉隐含的功能,而不解释如何自动链接所述情况中的两个方面。我发现的唯一有点有希望的问题是in an answer to this question,但两年多来没有得到任何答案。

4 个答案:

答案 0 :(得分:14)

我将从警告开始。您可能不仅需要可自定义的热键,还需要菜单本身。因此,在静态使用InputBindings之前请三思而后行 还有一个关于InputBindings的警告:它们暗示命令与窗口可视树中的元素相关联。有时您需要没有与任何特定窗口连接的全局热键。

上述说明意味着您可以采用另一种方式并实现您自己的应用程序范围的手势处理,并正确路由到相应的命令(不要忘记使用对命令的弱引用)。

尽管如此,手势识别命令的想法是一样的。

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

简单视图模型:

public class ViewModel
{
    public ICommand Command { get; set; }

    public ViewModel()
    {
        Command = new CommandWithHotkey();
    }
}

窗口:

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

当然,您应该以某种方式从配置中加载手势信息,然后使用数据加载init命令。

下一步是像VS中的键盘:Ctrl + K,Ctrl + D,快速搜索给出SO question

答案 1 :(得分:4)

如果我没有误解你的问题,试试这个:

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

..用:

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

我希望这会给你一个想法。

答案 2 :(得分:1)

它就是这样做的:

在我窗口的loaded-event中,我将菜单项的Command绑定与所有InputBindings的Command绑定相匹配,很像ethicallogics的答案,但对于菜单栏而言实际上比较了Command绑定而不只是价值,因为这对我不起作用。此代码也递归到子菜单

   public static void main(String[] args) {
    Long l = Long.parseLong("BABEFACE", 16);
    String hexAsBin = Long.toBinaryString(l.longValue());
    System.out.println(hexAsBin);
    System.out.println("Number of 1: " + countChars(hexAsBin, "1"));
    System.out.println("Number of 0: " + countChars(hexAsBin, "0"));
    }

    private static int countChars(String hexAsBin, String x) {
    return hexAsBin.length() - hexAsBin.replace(x, "").length();
    }

在Window的代码隐藏中执行此操作一次,每个菜单项都有一个InputGesture。只是缺少翻译

答案 3 :(得分:0)

根据Pavel Voronin的回答,我创建了以下内容。实际上我刚刚创建了两个新的UserControls,它们自动在命令上设置Gesture并读取它。

class HotMenuItem : MenuItem
{
    public HotMenuItem()
    {
        SetBinding(InputGestureTextProperty, new Binding("Command.GestureText")
        {
            Source = this
        });
    }
}

class HotKeyBinding : KeyBinding
{

    protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
        base.OnPropertyChanged(e);
        if (e.Property.Name == "Command" || e.Property.Name == "Gesture")
        {
            if (Command is IHotkeyCommand hotkeyCommand)
                hotkeyCommand.Gesture = Gesture as KeyGesture;
        }
    }
}

使用过的界面

public interface IHotkeyCommand
{
    KeyGesture Gesture { get; set; }
}

命令几乎相同,只是实现了INotifyPropertyChanged

因此,我认为使用情况有点清晰:

<Window.InputBindings>
    <viewModels:HotKeyBinding Command="{Binding ExitCommand}" Gesture="Alt+F4" />
</Window.InputBindings>

<Menu>
    <MenuItem Header="File" >
        <viewModels:HotMenuItem Header="Exit"  Command="{Binding ExitCommand}" />
    </MenuItem>
</Menu>