如何显示菜单项的工作键盘快捷方式?

11 浏览
0 Comments

如何显示菜单项的工作键盘快捷方式?

我正在尝试创建一个可本地化的WPF菜单栏,其中菜单项具有键盘快捷键-不是加速键/助记符(通常显示为可以按下的下划线字符,以便在菜单已打开时直接选择菜单项),而是键盘快捷键(通常是 Ctrl + another key 的组合),它们显示在菜单项标题的右对齐位置。

我使用MVVM模式开发我的应用程序,意味着在尽可能的情况下避免在代码后台中放置任何代码,并且我的视图模型(我分配给DataContext属性)提供了ICommand接口的实现,这些实现由我的视图中的控件使用。


作为重现问题的基础,以下是一个简化的应用程序的一些源代码:

Window1.xaml


    
        
            
        
    

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 {
                // 在实际代码中:从本地化中加载此字符串
                return "Menu";
            }
        }
        public string DoSomethingHeader {
            get {
                // 在实际代码中:从本地化中加载此字符串
                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)
            {
                // 在实际代码中:对视图模型进行有意义的操作
                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属性,但正如在Stack Overflow的问题中所描述的,例如这个问题这个问题这个问题这个问题,那只是纯粹的装饰,对应用程序实际处理的快捷键没有任何影响。

这个问题这个问题指出,命令应该与窗口的InputBindings列表中的KeyBinding关联。虽然这可以实现功能,但它不会自动显示菜单项的快捷键。 Window1.xaml更改如下:


    
        
    
    
        
            
        
    

我尝试了手动设置InputGestureText属性,使Window1.xaml如下所示:


    
        
    
    
        
            
        
    

这确实显示了快捷键,但出于明显的原因,这不是一个可行的解决方案:

- 当实际快捷键绑定更改时,它不会更新,因此即使快捷键不能由用户配置,这种解决方案也是一个维护噩梦。

- 文本需要进行本地化(例如Ctrl键在某些语言中有不同的名称),因此如果任何快捷键发生更改,所有翻译都需要单独更新。

我尝试了创建一个IValueConverter,用于将InputGestureText属性绑定到窗口的InputBindings列表(可能有多个KeyBindingInputBindings列表中,或者根本没有,因此没有特定的KeyBinding实例可以绑定(如果KeyBinding可以作为绑定目标))。对我来说,这似乎是最理想的解决方案,因为它非常灵活而且非常干净(不需要在各个位置进行大量的声明),但一方面,InputBindingCollectio不实现INotifyCollectionChanged,因此绑定不会在快捷键被替换时更新,另一方面,我没有以整洁的方式向转换器提供视图模型的引用(它需要访问本地化数据)。此外,InputBindings不是依赖属性,因此我无法将其绑定到通用源(例如位于视图模型中的输入绑定列表),ItemGestureText属性也可以绑定到该源。

现在,许多资源(这个问题那个问题这个线程那个问题那个线程)指出RoutedCommandRoutedUICommand包含内置的InputGestures属性,并暗示从该属性中获取的键绑定会自动显示在菜单项中。

然而,使用这两个ICommand实现似乎会引发新的问题,因为它们的ExecuteCanExecute方法不是虚拟的,因此无法在子类中重写这些方法以填充所需的功能。提供功能的唯一方法似乎是在XAML中声明一个CommandBinding(如这里这里所示),该绑定将命令与事件处理程序连接起来-然而,那个事件处理程序将位于代码后台中,违反了上面描述的MVVM体系结构。


尽管如此,这意味着将大部分前述结构颠倒过来(这也暗示着在当前的开发阶段,我需要就如何解决问题最终做出决定):

Window1.xaml


    
        
    
    
        
            
        
    

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 {
                // 在实际代码中:从本地化中加载此字符串
                return "Menu";
            }
        }
        public string DoSomethingHeader {
            get {
                // 在实际代码中:从本地化中加载此字符串
                return "Do Something";
            }
        }
        public void DoSomething()
        {
            // 在实际代码中:对视图模型进行有意义的操作
            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 instance = new Lazy();
        public static DoSomethingCommand Instance {
            get {
                return instance.Value;
            }
        }
    }
}


出于同样的原因(RoutedCommand.Execute等非虚拟),我不知道如何以一种方式子类化RoutedCommand来创建基于RoutedCommandRelayCommand,以便我不必经过窗口的InputBindings进行绕行-尽管在RoutedCommand派生类中显式重新实现ICommand中的方法感觉像是可能会破坏某些东西。

此外,虽然通过此方法配置的快捷键会自动显示在菜单项中,但似乎不会自动本地化。我的理解是将以下代码添加到MainWindow构造函数中应确保从德语CultureInfo获取框架提供的可本地化字符串:

System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("de-de");
System.Threading.Thread.CurrentThread.CurrentUICulture = System.Threading.Thread.CurrentThread.CurrentCulture;

然而,Ctrl键没有更改为Strg,因此除非我对如何设置框架提供的字符串的CultureInfo有误解,否则无论如何这种方法都不可行,如果我希望显示的快捷键的本地化正确。

现在,我知道我可以手动查找菜单树(编辑:为了澄清,这里没有代码,因为我不需要它,而且我知道如何做)和窗口的InputBindings列表,以检查哪些命令与任何KeyBinding实例相关联,以及哪些菜单项与这些命令相关联,以便我可以手动设置各个菜单项的InputGestureText以反映第一个(或按照我想要使用的任何度量标准的首选)键盘快捷键。每当我认为快捷键可能已更改时,必须重复执行此过程。然而,这似乎是一个极其繁琐的解决方法,对于本质上是菜单栏GUI的基本功能来说,我相信这不可能是“正确”的方法。

有什么办法可以自动显示为WPF MenuItem实例配置的键盘快捷键吗?

编辑:我找到的所有其他问题都涉及如何使用KeyBinding/KeyGesture实际启用InputGestureText所暗示的可视化功能,而没有解释如何在所描述的情况下自动链接这两个方面。我找到的唯一有点希望的问题是这个问题,但它在两年多的时间里没有收到任何答案。

0
0 Comments

如何在菜单项中显示工作的键盘快捷键?

有时候,您不仅需要可自定义的热键,还需要菜单本身。因此,在使用静态的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();
    }
}

窗口:


    
        
    
    
        
    
    
        
            
                
            
        
    

当然,您应该以某种方式从配置中加载手势信息,然后使用该数据初始化命令。

下一步是像VS一样的按键操作:Ctrl+K,Ctrl+D。快速搜索给出了这个SO问题。

这确实与我在阅读您的评论后尝试的解决方案非常相似。我做了一些细节上的不同之处;由于我使用了一个不支持框架的语言的本地化框架,我提供了自己的键盘手势本地化,虽然GetDisplayStringForCulture在可以使用语言环境的情况下似乎很有帮助。此外,我没有直接将键和修饰符绑定到数据上下文中的命令,而是使用了类似`{Binding Command.Gesture.Key, RelativeSource={RelativeSource Self}}`的绑定...

...以避免在设置所使用的命令的实际属性名称时出现一些冗余,因为它只需在Command属性的绑定中指示一次。

我会澄清绑定的方向。依赖属性是一个目标,所以通常你将绑定到目标。我将命令的数据绑定到KeyBinding。

我无法理解为什么要使用RelativeSource?{RelativeSource Self}指向持有绑定目标属性的对象,即在这种情况下是KeyBinding。

啊,你是对的。目标是KeyBinding的属性,而源是命令的属性。至于相对源,是的,它指向KeyBinding,所以通过绑定路径,我访问KeyBinding的Command属性。无论将KeyBinding设置为哪个命令,Key和Modifiers属性都将自动使用该命令的设置。想象一下定义10个键绑定 - 您可以复制和粘贴KeyBinding的声明,唯一需要更改的是Command属性绑定的源,每行只需更改一次。

现在我明白了。是的,这是一种更通用的方式。你是对的。

0
0 Comments

问题的原因是在菜单项上显示工作的键盘快捷键。解决方法是在窗口的loaded事件中将菜单项的命令绑定与所有InputBindings的命令绑定进行匹配,并递归到子菜单中。然后使用KeyGesture创建一个新的字符串,并将其赋给菜单项的InputGestureText属性。这样,在窗口的code-behind中执行一次即可为每个菜单项添加输入手势。只是缺少翻译部分。

0
0 Comments

问题的原因是希望在菜单项上显示键盘快捷方式,但目前的代码只能处理上下文菜单,并且仅限于第一层菜单项。解决方法是修改代码以适应菜单而不是上下文菜单,并考虑到嵌套菜单项。此外,可以通过继承的Items属性迭代菜单项,而不需要使用LogicalTreeHelper。

以下是修改后的代码:


    


    
        
            
            
        
        
            
            
            
        
    

public class CustomMenu : Menu
{
    public CustomMenu()
    {
        this.Loaded += CustomMenu_Loaded;
    }
    void CustomMenu_Loaded(object sender, RoutedEventArgs e)
    {
        SetInputGestureText(((FrameworkElement)sender).InputBindings);
    }
    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();
                }
            }
        }
    }
}

希望这能给你一个思路。这是一个很好的答案,只需要调整以适应菜单而不是上下文菜单,并且还需要考虑到嵌套菜单项。

0