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



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

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


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

窗口1.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>

窗口1.xaml.cs

using System;
using System.Windows;
namespace MenuShortcutTest
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
this.DataContext = new 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;
}
}
}
}

WPFMenuItem类具有InputGestureText属性,但如 SO 问题(如 this、this、this 和 this)中所述,这纯粹是装饰性的,对应用程序实际处理的快捷方式没有任何影响。

因此,像这样和这样的问题指出,该命令应与窗口InputBindings列表中的KeyBinding链接。虽然这将启用该功能,但它不会自动显示带有菜单项的快捷方式。窗口 1.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键在某些语言中具有不同的名称),因此如果更改了任何快捷方式,则需要单独更新所有翻译。

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

现在,许多资源(这个问题、那个问题、这个线程、那个问题和那个线程)指出RoutedCommandRoutedUICommand包含内置的InputGestures属性,并暗示该属性的键绑定会自动显示在菜单项中。

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


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

窗口1.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>

窗口1.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();
}
}
}

主视图模型.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);
}
}
}

做某事命令.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以创建类似于基于RoutedCommand回答此问题时使用的RelayCommand,所以我不必绕道窗口的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允许我为键盘快捷键指定自定义显示字符串,但由于CommandBinding必须与 XAML 中的命令链接的方式,RoutedCommand派生的DoSomethingCommand类不仅与我的所有实例(我可以从那里接触到加载的本地化)不相交, 相应的DisplayString属性是只读的,因此在运行时加载另一个本地化时无法更改它。

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

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

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

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

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

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

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

它是这样做到的:

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

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

在窗口的代码隐藏中执行此操作一次,每个菜单项都有一个输入手势。只是缺少翻译

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

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>

最新更新