web-dev-qa-db-ja.com

メニュー項目の作業用キーボードショートカットを表示する方法は?

キーボードショートカットのあるメニュー項目を使用して、ローカライズ可能なWPFメニューバーを作成しようとしています-notアクセラレータキー/ニーモニック(通常、メニューを直接選択するために押すことができる下線付きの文字として表示されますメニューがすでに開いている場合の項目)、ただしキーボードショートカット(通常は Ctrlanother 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;
            }
        }
    }
}

WPFMenuItemクラス には InputGestureTextプロパティ がありますが、SOなどの質問 this 、- thisthis および this 、これは純粋に表面的なものであり、アプリケーションによって実際に処理されるショートカットにはまったく影響しません。

thisthis のような質問は、コマンドを 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がバインドターゲットになるのに役立つ場合))。これは非常に柔軟性があり、同時に非常にクリーンであるため(さまざまな場所で多数の宣言を必要としないため)、最も望ましい解決策のように思えますが、一方では InputBindingCollectionINotifyCollectionChanged を実装していないため、ショートカットが置き換えられてもバインディングは更新されません。一方、コンバーターにビューモデルへの参照をきちんと提供することはできませんでした。 (ローカリゼーションデータにアクセスする必要があります)。さらに、 InputBindings は依存関係プロパティではないため、ItemGestureTextプロパティをバインドできる共通のソース(ビューモデルにある入力バインディングのリストなど)にバインドできません。 、 同じように。

さて、多くのリソース( この質問その質問このスレッドその質問 および そのthreadRoutedCommand および 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に基づいているので、私はしませんウィンドウの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から取得されるようにする必要があります-ただし、CtrlStrgに変更されないため、フレームワークが提供する文字列にCultureInfoを設定する方法を間違えない限り、このメソッドは表示されたショートカットが正しくローカライズされることを期待している場合は、とにかく実行可能ではありません。

KeyGesture を使用すると、キーボードショートカットのカスタム表示文字列を指定できますが、RoutedCommandから派生したDoSomethingCommandクラスは、すべてのインスタンスから切り離されているだけではありません(そこから取得できます)。 CommandBindingをXAMLのコマンドとリンクする必要があるため、ロードされたローカリゼーションに触れてください) それぞれのDisplayStringプロパティ は読み取り専用であるため、別のローカリゼーションの場合に変更する方法はありません。実行時にロードされます。

これにより、メニューツリーを手動で掘り下げるオプション(編集:明確にするために、これを求めていないのでここにコードはありません、これを行う方法を知っています)とウィンドウのInputBindingsリストを残してどれをチェックするかコマンドにはKeyBindingインスタンスが関連付けられており、どのメニュー項目がそれらのコマンドのいずれかにリンクされているので、それぞれのメニュー項目のそれぞれのInputGestureTextを手動で設定して、最初の(または優先される、必要なメトリックを反映する)ことができます。ここで使用)キーボードショートカット。そして、この手順は、キーバインディングが変更された可能性があると思うたびに繰り返す必要があります。ただし、これは基本的にメニューバーGUIの基本機能であるものに対しては非常に面倒な回避策のように思われるため、これを行うための「正しい」方法ではないと確信しています。

WPF MenuItemインスタンスで機能するように構成されたキーボードショートカットを自動的に表示する正しい方法は何ですか?

編集:私が見つけた他のすべての質問は、説明された状況で2つの側面を自動的にリンクする方法を説明せずに、KeyBinding/KeyGestureを使用してInputGestureTextによって視覚的に暗示される機能を実際に有効にする方法を扱っていました。私が見つけた唯一のやや有望な質問は this でしたが、2年以上も回答がありません。

19
O. R. Mapper

警告から始めましょう。カスタマイズ可能なホットキーだけでなく、メニュー自体も必要になる場合があります。したがって、InputBindingsを静的に使用する前によく考えてください。
InputBindingsに関してもう1つ注意があります。これらは、コマンドがウィンドウのビジュアルツリーの要素に関連付けられていることを意味します。特定のウィンドウに接続されていないグローバルホットキーが必要になる場合があります。

上記は、別の方法で、対応するコマンドへの正しいルーティングを使用して、独自のアプリケーション全体のジェスチャ処理を実装できることを意味します(コマンドへの弱参照を使用することを忘れないでください)。

それでも、ジェスチャ対応コマンドの考え方は同じです。

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

14
Pavel Voronin

私があなたの質問を誤解していない場合は、これを試してください:

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

..with:

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

これがあなたにアイデアを与えることを願っています。

4
yo chauhan

これはそれがそれをした方法です:

ウィンドウのloaded-eventで、メニュー項目のコマンドバインディングをすべてのInputBindingのコマンドバインディングと一致させます。これは、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;
    }

ウィンドウのコードビハインドでこれを1回実行すると、すべてのメニュー項目にInputGestureがあります。翻訳だけが欠落しています

1
JCH2k

Pavel Voroninの答えに基づいて、私は以下を作成しました。実際、コマンドにジェスチャを自動的に設定して読み取る2つの新しいUserControlを作成しました。

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