Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

Component for Customizing Menu Shortcuts

5.00/5 (11 votes)
21 Oct 2008CPOL3 min read 1   1.1K  
A component that allows the customization of menu shortcuts. This can be useful for barrier free applications.

Binaries

Introduction

This article describes a component that can be used in WinForms applications and allows the user to customize menu shortcuts.

Many people with disabilities have trouble using their hands, and thus have trouble pressing several keys at the same time. A solution here can be that an application can customize menu shortcuts. For instance, instead of pressing Ctrl + O to open a file, the (disabled) user could be assign the F2 key.

Background

The basic idea is to create a component that is easy to use. In an WinForms application, this component needs only two lines of user code to get it to work. The customized shortcuts are persisted in the user.config (even when the application itself doesn't have one).

The aim of this project is to help produce software that is barrier free!

Using the Component

To use the component, drag it onto a form where you want to allow the user to customize the menu shortcuts. The default name of the component is then customizeMenuShortCuts1.

The component provides several language options: de, en, es, fr, it, pt, ru (most languages were translated using Google-Translator; if there are any errors, please let me know).

To load the shortcuts from the user.config file, use this code in the Form_Load event:

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

Doing the customization is also quite simple:

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

Implementation

This article is based on my previous article about this topic (see Customizing Menu Shortcuts). Therefore, not every detail is explained here, because they're already covered on my previous article (the code is well commented too).

User Interface

The main part of the component is a form as the user interface for customization. On loading the form, a TreeView is filled with the structure of the menu. Shortcuts can only be assigned to ToolStripMenuItems, and therefore each item is checked for this.

C#
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);
            }
        }
    }
}

In the above snippet, every shortcut is added to a generic Hashtable (a new feature in .NET 3.5), which is later used to check if a shortcut is already assigned.

When selecting a node (i.e., a menu-item) of the TreeView, set the actually assigned shortcut in the form:

C#
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;
}

After the user has modified/customized the shortcut, we have to apply them:

C#
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;
    }
}

List of Keys

C#
/// <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);
        }
    }
}

Extending the Designer Generated Settings

To persist the shortcuts, I use the default designer generated settings of Visual Studio and extend them to store a generic List.

The items of this list are defined as follows. Note that it has to have the Serializable attribute; otherwise, it can't be saved to the user.config.

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

Because the designer doesn't support generic lists, I only use the designer for generating the code that is used to access the configuration. For storing the list, i.e., to serialize the list, use the following code:

C#
/// <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; }
    }
}

As you can see, the serialization is done by using the binary formatter, although serialization could be done by other methods.

The Component

Implementing a component means that a class has to be derived from the base class Component:

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

Loading the Shortcuts and Assigning Them

C#
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;
            }
    }
}

Therefore, a private method is used:

C#
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;
}

Method for Customization

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

Note: It is important to save the settings here (in this class library), because otherwise, the settings would not be persisted to user.config.

Points of Interest

  • Extending the designer generated settings
  • Storing a generic list in the user.config

Thanks

I thank Markus Lemcke for inspiring me to write this code and article. Especially for his thoughts about making software that is barrier free.

History

  • 21st October, 2008: Initial release

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)