Подтвердить что ты не робот

Как отобразить ярлык рабочей клавиатуры для элементов меню?

Я пытаюсь создать локализованную панель меню 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, без объяснения того, как автоматически связывать два аспекта в описанном ситуация. Единственный многообещающий вопрос, который я нашел, был , но за два года он не получил никаких ответов.

4b9b3361

Ответ 1

Я начну с предупреждения. Может случиться так, что вам понадобятся не только настраиваемые горячие клавиши, но и меню. Поэтому подумайте дважды, прежде чем использовать 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:

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

Конечно, вы должны как-то загружать информацию о жестов из конфигурации, а затем вводить команды с данными.

Следующий шаг - это keystokes, как в VS: Ctrl + K, Ctrl + D, быстрый поиск дает этот вопрос SO.

Ответ 2

Если я не неправильно понял ваш вопрос, попробуйте это:

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

Надеюсь, это даст вам представление.

Ответ 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;
    }

Сделайте это один раз в вашем коде окна, и каждый пункт меню имеет InputGesture. Только перевод отсутствует

Ответ 4

На основании ответа Павла Воронина я создал следующее. На самом деле я просто создал два новых UserControls, которые автоматически устанавливают Gesture в команде и читают ее.

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>