キーボードショートカットのあるメニュー項目を使用して、ローカライズ可能なWPFメニューバーを作成しようとしています-notアクセラレータキー/ニーモニック(通常、メニューを直接選択するために押すことができる下線付きの文字として表示されますメニューがすでに開いている場合の項目)、ただしキーボードショートカット(通常は 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;
}
}
}
}
WPFMenuItem
クラス には InputGestureText
プロパティ がありますが、SOなどの質問 this 、- this 、 this および this 、これは純粋に表面的なものであり、アプリケーションによって実際に処理されるショートカットにはまったく影響しません。
this や 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>
これはショートカットを表示しますが、明らかな理由から実行可能な解決策ではありません。
IValueConverter
プロパティをウィンドウの InputGestureText
リストにバインドするために使用する InputBindings
の作成を検討しました(KeyBinding
リストに複数のInputBindings
がある場合とない場合がありますしたがって、バインドできる特定のKeyBinding
インスタンスはありません(KeyBinding
がバインドターゲットになるのに役立つ場合))。これは非常に柔軟性があり、同時に非常にクリーンであるため(さまざまな場所で多数の宣言を必要としないため)、最も望ましい解決策のように思えますが、一方では InputBindingCollection
INotifyCollectionChanged
を実装していないため、ショートカットが置き換えられてもバインディングは更新されません。一方、コンバーターにビューモデルへの参照をきちんと提供することはできませんでした。 (ローカリゼーションデータにアクセスする必要があります)。さらに、 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
から取得されるようにする必要があります-ただし、Ctrl
はStrg
に変更されないため、フレームワークが提供する文字列にCultureInfo
を設定する方法を間違えない限り、このメソッドは表示されたショートカットが正しくローカライズされることを期待している場合は、とにかく実行可能ではありません。
KeyGesture
を使用すると、キーボードショートカットのカスタム表示文字列を指定できますが、RoutedCommand
から派生したDoSomethingCommand
クラスは、すべてのインスタンスから切り離されているだけではありません(そこから取得できます)。 CommandBinding
をXAMLのコマンドとリンクする必要があるため、ロードされたローカリゼーションに触れてください) それぞれのDisplayString
プロパティ は読み取り専用であるため、別のローカリゼーションの場合に変更する方法はありません。実行時にロードされます。
これにより、メニューツリーを手動で掘り下げるオプション(編集:明確にするために、これを求めていないのでここにコードはありません、これを行う方法を知っています)とウィンドウのInputBindings
リストを残してどれをチェックするかコマンドにはKeyBinding
インスタンスが関連付けられており、どのメニュー項目がそれらのコマンドのいずれかにリンクされているので、それぞれのメニュー項目のそれぞれのInputGestureText
を手動で設定して、最初の(または優先される、必要なメトリックを反映する)ことができます。ここで使用)キーボードショートカット。そして、この手順は、キーバインディングが変更された可能性があると思うたびに繰り返す必要があります。ただし、これは基本的にメニューバーGUIの基本機能であるものに対しては非常に面倒な回避策のように思われるため、これを行うための「正しい」方法ではないと確信しています。
WPF MenuItem
インスタンスで機能するように構成されたキーボードショートカットを自動的に表示する正しい方法は何ですか?
編集:私が見つけた他のすべての質問は、説明された状況で2つの側面を自動的にリンクする方法を説明せずに、KeyBinding
/KeyGesture
を使用してInputGestureText
によって視覚的に暗示される機能を実際に有効にする方法を扱っていました。私が見つけた唯一のやや有望な質問は this でしたが、2年以上も回答がありません。
警告から始めましょう。カスタマイズ可能なホットキーだけでなく、メニュー自体も必要になる場合があります。したがって、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質問 。
私があなたの質問を誤解していない場合は、これを試してください:
<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();
}
}
}
}
}
これがあなたにアイデアを与えることを願っています。
これはそれがそれをした方法です:
ウィンドウの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があります。翻訳だけが欠落しています
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>