Я пытаюсь создать локализованную панель меню WPF с элементами меню, в которых есть сочетания клавиш, а не клавиши ускорителя/мнемоники (обычно они отображаются как подчеркнутые символы, которые можно нажать, чтобы напрямую выбрать пункт меню, когда меню уже открыто), но сочетания клавиш (обычно комбинации Ctrl + another key), которые отображаются выравниванием по правому краю рядом с заголовком элемента меню.
Я использую шаблон MVVM для своего приложения, а это означает, что я избегаю поместить код в код по возможности и иметь свои модели просмотра (которые я присваиваю DataContext
свойства) предоставляют реализации 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
имеет InputGestureText
свойство, но, как описано в SO-вопросах, таких как this, this, this и , который является чисто косметическим и не влияет на то, какие ярлыки фактически обрабатываются приложением.
SO такие вопросы, как this и , укажите, что команда должна быть связана с KeyBinding
в окне InputBindings
окна. Хотя это позволяет использовать функциональные возможности, он не отображает ярлык с помощью пункта меню. 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 имеет разные имена на некоторых языках), поэтому, если какой-либо из ярлыков когда-либо изменен, все переводы нужно будет обновлять индивидуально.
Я рассмотрел создание IValueConverter
для использования для привязки свойства InputGestureText
к InputBindings
(может быть больше одного KeyBinding
в списке InputBindings
или вообще нет, поэтому нет специального KeyBinding
экземпляр, с которым я мог бы привязываться (если KeyBinding
даже поддается привязке)). Это кажется мне самым желанным решением, потому что оно очень гибкое и в то же время очень чистое (оно не требует множества объявлений в разных местах), но, с одной стороны, InputBindingCollection
не реализует INotifyCollectionChanged
, поэтому привязка не будет обновляться, если ярлыки заменяются, а с другой стороны, мне не удалось обеспечить конвертер ссылкой на мою модель представления в порядке, чтобы ему нужно было получить доступ к данным локализации. Более того, InputBindings
не является свойством зависимостей, поэтому я не могу привязать это к общему источнику (например, список привязок ввода расположенный в модели представления), что свойство ItemGestureText
также может быть привязано.
Теперь многие ресурсы (этот вопрос, этот вопрос, этот поток, этот вопрос и этот поток укажите, что RoutedCommand
и RoutedUICommand
содержат встроенное InputGestures
свойство и подразумевают, что привязки клавиш из этого свойства автоматически отображаются в пунктах меню.
Однако использование любой из этих реализаций ICommand
, похоже, открывает новую банку червей, так как их Execute
и CanExecute
методы не являются виртуальными и, следовательно, не могут быть переопределены в подклассах для заполнения требуемой функциональности. Единственный способ предоставить это, кажется, объявление CommandBinding
в XAML (показано, например здесь или здесь), который соединяет команду с обработчиком событий, однако этот обработчик событий затем будет расположен в коде, что нарушит архитектуру 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
, поэтому мне не нужно делать обход над InputBindings
окна - при явном переопределении методов из 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
для фреймворка, этот метод не является жизнеспособным в любом случае, если я ожидаю, что отображаемый ярлык будет правильно локализован.
Теперь я знаю, что KeyGesture
позволяет мне указать пользовательскую строку отображения для сочетания клавиш, но не только RoutedCommand
-derived DoSomethingCommand
класс, не связанный со всеми моими экземплярами (откуда я мог бы связаться с загруженной локализацией) из-за того, что CommandBinding
должен быть связан с командой в XAML, соответствующее свойство DisplayString
доступно только для чтения, поэтому не было бы способа изменить его, когда во время выполнения загружается другая локализация.
Это оставляет мне возможность вручную выкапывать дерево меню (EDIT: для пояснения, никакого кода здесь, потому что я не прошу об этом, и я знаю, как это сделать) и InputBindings
список в окне для проверки, какие команды имеют какие-либо связанные с ними экземпляры KeyBinding
, и какие элементы меню связаны с любой из этих команд, так что я могу вручную установить InputGestureText
каждого из соответствующих пунктов меню, чтобы отразить первый ( или предпочтительнее, в зависимости от того, какую метрику я хочу использовать здесь). И эта процедура должна повторяться каждый раз, когда я думаю, что привязки клавиш могут быть изменены. Однако это кажется очень утомительным обходным решением для чего-то, что по существу является основной функцией GUI меню, поэтому я убежден, что это не может быть "правильным" способом сделать это.
Каков правильный способ автоматического отображения сочетания клавиш, настроенного для работы с экземплярами WPF MenuItem
?
EDIT: все остальные вопросы, которые я нашел, касались того, как использовать KeyBinding
/KeyGesture
, чтобы фактически активировать функциональность, визуально подразумеваемую InputGestureText
, без объяснения того, как автоматически связывать два аспекта в описанном ситуация. Единственный многообещающий вопрос, который я нашел, был , но за два года он не получил никаких ответов.