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

Posted

技术标签:

【中文标题】如何显示菜单项的工作键盘快捷键?【英文标题】:How to Display Working Keyboard Shortcut for Menu Items? 【发布时间】:2014-03-25 04:47:27 【问题描述】:

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

我在我的应用程序中使用 MVVM 模式,这意味着我尽可能避免将任何代码放在代码隐藏中,并让我的视图模型(我分配给 DataContext properties)提供 ICommand interface 的实现在我的视图中被控件使用。


作为重现问题的基础,这里是描述的应用程序的一些最小源代码:

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;
            
        
    


WPF MenuItem class 有一个InputGestureText property,但正如this、this、this 和 this 等 SO 问题中所述,这纯粹是装饰性的,对快捷方式的含义没有任何影响实际由应用程序处理。

this 和this 等问题指出该命令应与窗口的InputBindings 列表中的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 键在某些语言中具有不同的名称),因此如果任何快捷方式被更改,all 翻译将需要单独更新。

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

现在,许多资源(this question、that question、this thread、that question 和 that thread 指出 RoutedCommandRoutedUICommand 包含内置的 InputGestures property 并暗示键绑定来自该属性的自动显示在菜单项中。

但是,使用其中任何一个ICommand 实现似乎都会打开一个新的蠕虫罐,因为它们的ExecuteCanExecute 方法不是虚拟的,因此不能在子类中覆盖以填充所需的功能。提供它的唯一方法似乎是在 XAML 中声明一个 CommandBinding(例如 here 或 here),它将命令与事件处理程序连接起来 - 但是,该事件处理程序将位于代码隐藏中,因此违反了上述 MVVM 架构。


尽管如此,这意味着将上述大部分结构从内到外(这也意味着我需要下定决心在我目前相对早期的开发阶段最终解决该问题):

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 的 in an answer to this question ,所以我不必绕着窗口的InputBindings 绕道——同时在RoutedCommand 子类中显式地重新实现ICommand 中的方法感觉就像我可能会破坏某些东西。

更重要的是,虽然使用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,如果我希望显示的快捷方式正确本地化,这种方法无论如何都不可行。

现在,我知道 KeyGesture 允许我为键盘快捷键指定自定义显示字符串,但不仅是 RoutedCommand 派生的 DoSomethingCommand 类与我的所有实例都不相交(从那里我可以与加载的本地化取得联系)由于CommandBinding 必须与 XAML 中的命令链接的方式,respective DisplayString property 是只读的,因此在运行时加载另一个本地化时将无法更改它.

这让我可以选择手动挖掘菜单树(编辑:为了清楚起见,这里没有代码,因为我不要求这样做,我知道如何做到这一点)和 InputBindings 列表检查哪些命令具有与之关联的任何KeyBinding 实例的窗口,以及哪些菜单项链接到任何这些命令,以便我可以手动设置每个相应菜单项的InputGestureText 以反映第一个(或首选,无论我想在这里使用哪个指标)键盘快捷键。每次我认为键绑定可能已更改时,都必须重复此过程。但是,对于本质上是菜单栏 GUI 的基本功能的东西来说,这似乎是一个极其乏味的解决方法,所以我确信它不是执行此操作的“正确”方法。

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

编辑:我发现的所有其他问题都涉及如何使用 KeyBinding/KeyGesture 来实际启用 InputGestureText 在视觉上暗示的功能,而没有解释如何自动链接所描述的两个方面情况。我发现的唯一有希望的问题是this,但两年多来没有收到任何答案。

【问题讨论】:

ToolTip="Binding ToolTip" 有什么问题,其中ToolTip 等于"Ctrl+C" @Sheridan:您的意思是InputGestureText="Binding ..."? What should it be bound to; where should the "Ctrl+C"` 文本来自(以某种方式与为命令/菜单项定义的KeyGesture 固有地相关联)?跨度> @AnatoliyNikolaev:感谢您的回复。在我多次尝试找到一个好的解决方案之后,我添加了一些示例代码,代表源代码的初始阶段和各个中间阶段。如果您需要任何进一步的信息,请告诉我。 @O.R.Mapper 有关热键的信息应该在您的 ICommand 实现中。 KeyBinding.Key 和 Modifiers 是依赖属性,因此您可以将它们绑定到命令的某些属性。知道键和修饰符后,您可以提供本地化字符串以将其绑定到 InputGester。 @O.R.映射器好的。稍后。 【参考方案1】:

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

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

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

【讨论】:

这是一个很好的答案,只是它需要针对菜单而不是上下文菜单进行调整,并且它还应该考虑嵌套菜单项,因为目前它只处理第一级。 努力+1;在我的问题结束时,我已经将这个程序描述为最后的解决方法。另外,我不确定您是否真的需要LogicalTreeHelper;如果代码已经在ContextMenu 中,你就不能遍历继承的Items property 吗?【参考方案2】:

我将从警告开始。您可能不仅需要可自定义的热键,还需要菜单本身。所以在静态使用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>

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

下一步是类似 VS 中的按键操作:Ctrl+K,Ctrl+D,快速搜索会给出 SO question。

【讨论】:

这确实与我在阅读您的评论后尝试的解决方案非常相似。我做了一些不同的小细节;当我使用不同的本地化框架(支持非框架支持的语言)时,我为关键手势提供了自己的本地化,尽管GetDisplayStringForCulture 似乎对可以使用文化的情况很有帮助。此外,我没有将键和修饰符直接绑定到数据上下文中的命令,而是使用了像 Binding Command.Gesture.Key, RelativeSource=RelativeSource Self 这样的绑定 ... ... 以避免在设置所用命令的实际属性名称时出现一些冗余,因为它只需要在 Command 属性的绑定中像这样指示。 我会澄清绑定的方向。依赖属性是一个目标,所以通常你绑定到目标。我将命令的数据绑定到 KeyBinding。 啊,你是对的。目标是KeyBinding 的属性,而源是命令的属性。至于相对源——对,它指向KeyBinding,所以通过绑定路径,我访问了KeyBindingCommand属性。无论KeyBinding 设置为什么命令,KeyModifiers 属性将始终自动使用该命令的设置。考虑定义 10 个键绑定 - 您可以复制和粘贴 KeyBinding 声明,您只需更改 Command 属性绑定的来源,每行 一次 现在我明白了。是的,这是更通用的方式。你是对的。【参考方案3】:

它是这样做的:

在我的窗口的加载事件中,我将菜单项的命令绑定与所有 InputBindings 的命令绑定进行匹配,就像道德逻辑的答案一样,但是对于菜单栏,它实际上比较了命令绑定 而不仅仅是价值,因为那对我不起作用。此代码还递归到子菜单

    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;
    

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

【讨论】:

你的回答让我想去另一个方向,...即在 XAML 中的仅显示 InputGestureText 和相关属性的代码隐藏中实例化 Window 的 InputBindings。或者换句话说,使后面的通用windows初始化代码中的InputGestureText生效。如果我有时间这样做,我会发布答案。【参考方案4】:

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

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; 

Command 几乎相同,只是实现了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>

【讨论】:

以上是关于如何显示菜单项的工作键盘快捷键?的主要内容,如果未能解决你的问题,请参考以下文章

Visual Studio Code 中弹出快速修复菜单的键盘快捷键是啥?

如何重新定义内置键盘快捷键的行为?

MAC按键以及快捷键

Sketch如何设置键盘快捷键?

Qt 向 QMainWindow 添加非菜单栏键盘快捷键

如何在 Java 应用程序中为我的菜单栏设置键盘快捷键?