Как отобразить рабочую комбинацию клавиш для пунктов меню?
я пытаюсь создать локализуемую строку меню WPF с элементами меню, которые имеют сочетания клавиш -не клавиши акселератора / мнемоника (обычно отображаются как подчеркнутые символы, которые можно нажать, чтобы напрямую выбрать пункт меню, когда меню уже открыто), но сочетания клавиш (обычно комбинации Ctrl + еще один ключ), которые отображаются справа рядом с заголовком пункта меню.
я использую шаблон MVVM для мое приложение, что означает, что я избегаю размещения любого кода в коде, где это возможно, и имею свои модели представления (которые я назначаю DataContext
свойства) обеспечить реализацию ICommand
интерфейс которые используются элементами управления в моих представлениях.
в качестве основы для воспроизведения проблемы, вот некоторый минимальный исходный код для приложения как описание:
файл 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
свойства, но как описано в таких вопросах, как этой, этой, этой и этой, это чисто косметический и не имеет никакого эффекта независимо от того, какие ярлыки фактически обрабатываются приложением.
Итак, вопросы типа этой и этой укажите, что команда должна быть связана с 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
, таким образом, привязка не будет обновляться при замене ярлыков, а с другой стороны, мне не удалось предоставить конвертеру ссылку на мой view-model в аккуратном порядке (что необходимо для доступа к данным локализации). Более того, InputBindings
не является свойством зависимости, поэтому я не могу привязать это к общему источнику (например, список входных Привязок, расположенных в view-model), что 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
-производное DoSomethingCommand
класс непересекается от всех моих экземпляров (откуда я мог связаться с загруженной локализацией) из-за способа CommandBinding
должен быть связан с командой в XAML,предписания DisplayString
свойства доступно только для чтения, поэтому не было бы возможности изменить его при загрузке другой локализации во время выполнения.
этот оставляет мне возможность вручную копаться в дереве меню (EDIT: для уточнения, здесь нет кода, потому что я не прошу об этом, и я знаю, как это сделать) и InputBindings
список окна, чтобы проверить, какие команды имеют любой KeyBinding
экземпляров, связанных с ними, и какие пункты меню связаны с любой из этих команд, так что я могу вручную установить InputGestureText
каждого из соответствующих пунктов меню, чтобы отразить первый (или предпочтительный, по какой метрике я хочу использовать здесь) сочетание клавиш. И эта процедура должна повторяться каждый раз, когда я думаю, что привязки ключей могут измениться. Тем не менее, это кажется чрезвычайно утомительным обходным путем для чего-то, что по существу является основной особенностью GUI строки меню, поэтому я убежден, что это не может быть "правильным" способом сделать это.
как правильно автоматически отображать сочетание клавиш, настроенное для работы с WPF MenuItem
экземпляров?
EDIT: все другие вопросы, которые я нашел, касались того, как KeyBinding
/KeyGesture
может использоваться для фактического включения функций, визуально подразумеваемых InputGestureText
, не объясняя, как автоматически связать эти два аспекта в описанной ситуации. Единственный многообещающий вопрос, который я нашел, был этой, но он не получил никаких ответов в течение двух лет.
4 ответов
начну с предупреждения. Может случиться так, что вам понадобятся не только настраиваемые горячие клавиши, но и само меню. Поэтому подумайте дважды, прежде чем использовать 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, быстрый поиск дает это поэтому вопрос.
Если я не неправильно понял ваш вопрос, попробуйте следующее:
<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();
}
}
}
}
}
Я надеюсь, это даст вам идею.
вот как он это сделал:
в loaded-событии моего окна я сопоставляю привязки команд пунктов меню с привязками команд всех InputBindings, как и ответ ethicallogics, но для строки меню и его фактически сравнивает привязки команд и не только значение, потому что это не сработало для меня. этот код также повторяется в подменю.
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;
}
сделайте это один раз в коде вашего окна и в каждом пункте меню имеет InputGesture. Просто перевод отсутствует
основываясь на ответе Павла Воронина, Я создал следующее. На самом деле я только что создал два новых 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>