65.9K
CodeProject 正在变化。 阅读更多。
Home

自定义菜单快捷方式的组件

starIconstarIconstarIconstarIconstarIcon

5.00/5 (10投票s)

2008年10月21日

CPOL

3分钟阅读

viewsIcon

48397

downloadIcon

1196

一个允许自定义菜单快捷键的组件。这对于无障碍应用程序很有用。

Binaries

引言

本文描述了一个可以在 WinForms 应用程序中使用的组件,允许用户自定义菜单快捷键。

许多残疾人士在使用双手方面有困难,因此很难同时按下多个键。这里的解决方案是,应用程序可以自定义菜单快捷键。例如,用户(残疾人)可以被分配 F2 键,而不是按 Ctrl + O 来打开文件。

背景

基本思想是创建一个易于使用的组件。在 WinForms 应用程序中,此组件只需要两行用户代码即可使其工作。自定义的快捷键会保存在 user.config 中(即使应用程序本身没有)。

这个项目的目的是帮助制作无障碍软件!

使用组件

要使用该组件,请将其拖到您希望允许用户自定义菜单快捷键的窗体上。该组件的默认名称是 customizeMenuShortCuts1

该组件提供了几种语言选项:de, en, es, fr, it, pt, ru(大多数语言都是使用 Google 翻译的;如果有任何错误,请告诉我)。

要从 user.config 文件加载快捷键,请在 Form_Load 事件中使用此代码

private void Form1_Load(object sender, System.EventArgs e)
{
    customizeMenuShortCuts1.LoadCustomShortCuts(menuStrip1);
}

进行自定义也很简单

private void button1_Click(object sender, System.EventArgs e)
{
    customizeMenuShortCuts1.CustomizeShortCuts(menuStrip1);
}

实现

本文基于我之前关于此主题的文章(请参阅 自定义菜单快捷键)。因此,这里没有解释每个细节,因为它们已经在我的上一篇文章中涵盖了(代码也有很好的注释)。

用户界面

该组件的主要部分是一个用于自定义的用户界面窗体。在加载窗体时,TreeView 会填充菜单的结构。快捷键只能分配给 ToolStripMenuItem,因此会检查每个项目是否符合此条件。

private void FillTreeView(TreeNodeCollection nodes, ToolStripItemCollection items)
{
    foreach (ToolStripItem item in items)
    {
        ToolStripMenuItem menuItem = item as ToolStripMenuItem;

        // Check for ToolStripMenuItem
        if (menuItem != null)
        {
            TreeNode tNode = new TreeNode();
            tNode.Text = menuItem.Text.Replace("&", string.Empty);
            tNode.Tag = menuItem;
            tNode.ImageKey = "Sub";
            nodes.Add(tNode);

            // Add the shortcut to the list for checking if a 
            // shortcut already exists:
            if (menuItem.ShortcutKeys != Keys.None)
                _assignedShortCuts.Add(menuItem.ShortcutKeys);

            // Recursion needed?
            if (menuItem.HasDropDownItems)
            {
                tNode.ImageKey = "Main";
                FillTreeView(tNode.Nodes, menuItem.DropDownItems);
            }
        }
    }
}

在上面的代码片段中,每个快捷键都添加到通用的 Hashtable(.NET 3.5 中的一个新特性),稍后用于检查快捷键是否已分配。

当选择 TreeView 的一个节点(即菜单项)时,在窗体中设置实际已分配的快捷键

private void tvMenu_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e)
{
    // Get selected item:
    _menuItem = e.Node.Tag as ToolStripMenuItem;

    // Check if the selected item is a parent for other toolstrip-items.
    // If so no shortcut can be assigned:
    if (_menuItem.HasDropDownItems)
    {
        MessageBox.Show(
            MyRessource.ShortCutNotPossible,
            Application.ProductName,
            MessageBoxButtons.OK,
            MessageBoxIcon.Exclamation);

        return;
    }

    // Enable editing:
    groupBox1.Enabled = true;

    // Set the actually assigned shortcut. The shortcut is a
    // combination of different Keys so they're bit-masked:
    Keys shortCut = _menuItem.ShortcutKeys;
    chkCtrl.Checked = (shortCut & Keys.Control) == Keys.Control;
    chkAlt.Checked = (shortCut & Keys.Alt) == Keys.Alt;
    chkShift.Checked = (shortCut & Keys.Shift) == Keys.Shift;

    // To get the letter or the F-key we have to eliminate the 
    // modifyers. This is done by combining all set modifiers 
    // and then by removing them from the shortcut with XOR.
    // Example: shortCut    = 111000101
    //            Ctrl        = 111000000 XOR
    //            -----------------------
    //            Letter        = 000000101
    Keys modifiers = Keys.None;
    if (chkCtrl.Checked) modifiers |= Keys.Control;
    if (chkAlt.Checked) modifiers |= Keys.Alt;
    if (chkShift.Checked) modifiers |= Keys.Shift;
    Keys buchstabe = shortCut ^ modifiers;

    // Select the value in the combobox:
    cmbKeys.SelectedValue = buchstabe;
}

在用户修改/自定义快捷键后,我们必须应用它们

private void btnApply_Click(object sender, EventArgs e)
{
    // Applying happens in a try-catch-block. When there's a faulty
    // input the shortcut is removed.
    // (Maybe there's a better approach - but in the moment I'm to
    // lazy for that;-)
    try
    {
        // When no letter selected in the combobox remove the shortcut:
        if (cmbKeys.SelectedIndex == -1)
            throw new Exception();

        // Create the shortcut:
        Keys shortCut = (Keys)cmbKeys.SelectedValue;
        if (chkCtrl.Checked) shortCut |= Keys.Control;
        if (chkAlt.Checked) shortCut |= Keys.Alt;
        if (chkShift.Checked) shortCut |= Keys.Shift;

        // Check if the shortcut exists:
        if (_assignedShortCuts.Contains(shortCut))
        {
            MessageBox.Show(
                MyRessource.ShortCutAlreadyAssigned,
                Application.ProductName,
                MessageBoxButtons.OK,
                MessageBoxIcon.Exclamation);

            return;
        }

        // Manage the list of assigned shortcuts:
        Keys oldShortCut = _menuItem.ShortcutKeys;
        if (shortCut != oldShortCut)
            _assignedShortCuts.Remove(oldShortCut);

        // Assign the new shortcut:
        _menuItem.ShortcutKeys = shortCut;
    }
    catch
    {
        _menuItem.ShortcutKeys = Keys.None;
    }
    finally
    {
        // Disable editing:
        groupBox1.Enabled = false;
    }
}

键列表

/// <summary>
/// Class providing a list with available keys
/// </summary>
internal sealed class MyKeys
{
    #region Properties
    public List<MyKey> MyKeyList { get; private set; }
    #endregion
    //---------------------------------------------------------------------
    #region Construtor
    /// <summary>
    /// The constructor initializes the list with keys (A-Z, F1-F11)
    /// </summary>
    public MyKeys()
    {
        this.MyKeyList = new List<MyKey>();

        // Letters - via ASCII-Table:
        for (byte b = 65; b <= 90; b++)
        {
            char c = (char)b;

            this.MyKeyList.Add(new MyKey { Name = c.ToString() });
        }

        // F-keys:
        for (byte b = 1; b <= 11; b++)
            this.MyKeyList.Add(new MyKey { Name = "F" + b.ToString() });
    }
    #endregion
}
//---------------------------------------------------------------------
/// <summary>
/// Class providing the item of the above list
/// </summary>
internal sealed class MyKey
{
    /// <summary>
    /// Name of the key
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// KeyCode
    /// </summary>
    public Keys Code
    {
        get
        {
            KeysConverter keyConverter = new KeysConverter();
            return (Keys)keyConverter.ConvertFrom(this.Name);
        }
    }
}

扩展设计器生成的设置

为了持久化快捷键,我使用 Visual Studio 的默认设计器生成的设置,并扩展它们以存储一个通用的 List

此列表的项定义如下。请注意,它必须具有 Serializable 属性;否则,它无法保存到 user.config

[Serializable()]
internal sealed class UserConfigEntry
{
    public string Text { get; set; }
    public Keys ShortCut { get; set; }
}

由于设计器不支持通用列表,我仅使用设计器生成用于访问配置的代码。为了存储列表,即序列化列表,请使用以下代码

/// <summary>
/// Extends the designer generated Settings so that a 
/// custom list can be persisted.
/// </summary>
partial class Settings
{
    /// <summary>
    /// ShortCuts for the menus
    /// </summary>
    /// <remarks>
    /// Is serialized as base64-string
    /// </remarks>
    [UserScopedSetting()]
    [SettingsSerializeAs(SettingsSerializeAs.Binary)]
    [DefaultSettingValue("")]
    public List<UserConfigEntry> UserConfigEntries
    {
        get { return (List<UserConfigEntry>)this["UserConfigEntries"]; }
        set { this["UserConfigEntries"] = value; }
    }
}

如您所见,序列化是使用二进制格式化程序完成的,尽管序列化也可以通过其他方法完成。

组件

实现一个组件意味着必须从基类 Component 派生一个类

[ToolboxBitmap(typeof(CustomizeMenuShortCuts))]
[Description("Allows the user to customize menu shortcuts")]
public sealed class CustomizeMenuShortCuts : Component
{
...
}

加载快捷键并分配它们

public void LoadCustomShortCuts(MenuStrip menuStrip)
{
    // Get the list from the user.config:
    List<UserConfigEntry> userList =
        Properties.Settings.Default.UserConfigEntries;

    if (userList.Count == 0) return;

    // Create a list of the menu-items:
    List<ToolStripItem> menuList = MenuToList(menuStrip.Items);

    // Assign the shortcuts from the user.config to the menulist.
    // It can't be assumed that the list from the user.config has
    // the same length as the list from the menu-items, because
    // there might be changes in the menuStrip (due to updates, etc.)
    // Therefore the list from the menu-items gets iterated and
    // for each item the proper item in the list from the 
    // user.config is searched (the variant with the loops is the
    // fastest in comparison with LINQ and binary search).
    foreach (ToolStripItem menuEntry in menuList)
    {
        ToolStripMenuItem menuItem = menuEntry as ToolStripMenuItem;
        if (menuItem == null) break;
                
        // Search:
        foreach (UserConfigEntry userEntry in userList)
            if (userEntry.Text == menuItem.Text)
            {
                // Found -> assign shortcut:
                menuItem.ShortcutKeys = userEntry.ShortCut;

                break;
            }
    }
}

因此,使用一个 private 方法

private List<ToolStripItem> MenuToList(ToolStripItemCollection items)
{
    List<ToolStripItem> list = new List<ToolStripItem>();

    // Run through all items:
    foreach (ToolStripItem item in items)
    {
        ToolStripMenuItem menuItem = item as ToolStripMenuItem;

        // Check if it's a ToolStripMenuItem (i.e. no seperator):
        if (menuItem != null)
        {
            // Add to list:
            list.Add(menuItem);

            // Recursion needed?
            if (menuItem.HasDropDownItems)
                list.AddRange(MenuToList(menuItem.DropDownItems));
        }
    }

    return list;
}

自定义方法

public void CustomizeShortCuts(MenuStrip menuStrip)
{
    // Show form that allows for customization:
    frmMain frmMain = new frmMain(menuStrip);
    frmMain.ShowDialog();

    // No we will persist the shortcuts of the menuStrip:
    List<ToolStripItem> menuList = MenuToList(menuStrip.Items);
    Properties.Settings.Default.UserConfigEntries =
        new List<UserConfigEntry>(menuList.Count);

    // Iterate over all menu-items
    foreach (ToolStripItem item in menuList)
    {
        ToolStripMenuItem menuItem = item as ToolStripMenuItem;

        // Separators, for instance, won't be a ToolStripMenuItem
        // so check if it isn't null:
        if (menuItem == null) break;

        Properties.Settings.Default.UserConfigEntries.Add(
            new UserConfigEntry
            {
                Text = menuItem.Text,
                ShortCut = menuItem.ShortcutKeys
            });
    }

    // The settings from a classlibrary have to be saved here,
    // otherwise they will be lost:
    Properties.Settings.Default.Save();
}

注意:在这里(在这个类库中)保存设置非常重要,否则,设置将不会持久化到 user.config

关注点

  • 扩展设计器生成的设置
  • user.config 中存储一个通用列表

谢谢

我感谢 Markus Lemcke 激发我编写此代码和文章。尤其是他对制作无障碍软件的看法。

历史

  • 2008 年 10 月 21 日:初始发布
© . All rights reserved.