【发布时间】: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 指出 RoutedCommand 和 RoutedUICommand 包含内置的 InputGestures property 并暗示键绑定来自该属性的自动显示在菜单项中。
但是,使用其中任何一个ICommand 实现似乎都会打开一个新的蠕虫罐,因为它们的Execute 和CanExecute 方法不是虚拟的,因此不能在子类中覆盖以填充所需的功能。提供它的唯一方法似乎是在 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.映射器好的。稍后。
标签: wpf keyboard-shortcuts menuitem key-bindings