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:
private void Form1_Load(object sender, System.EventArgs e)
{
customizeMenuShortCuts1.LoadCustomShortCuts(menuStrip1);
}
Doing the customization is also quite simple:
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 ToolStripMenuItem
s, and therefore each item is checked for this.
private void FillTreeView(TreeNodeCollection nodes, ToolStripItemCollection items)
{
foreach (ToolStripItem item in items)
{
ToolStripMenuItem menuItem = item as ToolStripMenuItem;
if (menuItem != null)
{
TreeNode tNode = new TreeNode();
tNode.Text = menuItem.Text.Replace("&", string.Empty);
tNode.Tag = menuItem;
tNode.ImageKey = "Sub";
nodes.Add(tNode);
if (menuItem.ShortcutKeys != Keys.None)
_assignedShortCuts.Add(menuItem.ShortcutKeys);
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:
private void tvMenu_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e)
{
_menuItem = e.Node.Tag as ToolStripMenuItem;
if (_menuItem.HasDropDownItems)
{
MessageBox.Show(
MyRessource.ShortCutNotPossible,
Application.ProductName,
MessageBoxButtons.OK,
MessageBoxIcon.Exclamation);
return;
}
groupBox1.Enabled = true;
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;
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;
cmbKeys.SelectedValue = buchstabe;
}
After the user has modified/customized the shortcut, we have to apply them:
private void btnApply_Click(object sender, EventArgs e)
{
try
{
if (cmbKeys.SelectedIndex == -1)
throw new Exception();
Keys shortCut = (Keys)cmbKeys.SelectedValue;
if (chkCtrl.Checked) shortCut |= Keys.Control;
if (chkAlt.Checked) shortCut |= Keys.Alt;
if (chkShift.Checked) shortCut |= Keys.Shift;
if (_assignedShortCuts.Contains(shortCut))
{
MessageBox.Show(
MyRessource.ShortCutAlreadyAssigned,
Application.ProductName,
MessageBoxButtons.OK,
MessageBoxIcon.Exclamation);
return;
}
Keys oldShortCut = _menuItem.ShortcutKeys;
if (shortCut != oldShortCut)
_assignedShortCuts.Remove(oldShortCut);
_menuItem.ShortcutKeys = shortCut;
}
catch
{
_menuItem.ShortcutKeys = Keys.None;
}
finally
{
groupBox1.Enabled = false;
}
}
List of Keys
internal sealed class MyKeys
{
#region Properties
public List<MyKey> MyKeyList { get; private set; }
#endregion
#region Construtor
public MyKeys()
{
this.MyKeyList = new List<MyKey>();
for (byte b = 65; b <= 90; b++)
{
char c = (char)b;
this.MyKeyList.Add(new MyKey { Name = c.ToString() });
}
for (byte b = 1; b <= 11; b++)
this.MyKeyList.Add(new MyKey { Name = "F" + b.ToString() });
}
#endregion
}
internal sealed class MyKey
{
public string Name { get; set; }
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.
[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:
partial class Settings
{
[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
:
[ToolboxBitmap(typeof(CustomizeMenuShortCuts))]
[Description("Allows the user to customize menu shortcuts")]
public sealed class CustomizeMenuShortCuts : Component
{
...
}
Loading the Shortcuts and Assigning Them
public void LoadCustomShortCuts(MenuStrip menuStrip)
{
List<UserConfigEntry> userList =
Properties.Settings.Default.UserConfigEntries;
if (userList.Count == 0) return;
List<ToolStripItem> menuList = MenuToList(menuStrip.Items);
foreach (ToolStripItem menuEntry in menuList)
{
ToolStripMenuItem menuItem = menuEntry as ToolStripMenuItem;
if (menuItem == null) break;
foreach (UserConfigEntry userEntry in userList)
if (userEntry.Text == menuItem.Text)
{
menuItem.ShortcutKeys = userEntry.ShortCut;
break;
}
}
}
Therefore, a private
method is used:
private List<ToolStripItem> MenuToList(ToolStripItemCollection items)
{
List<ToolStripItem> list = new List<ToolStripItem>();
foreach (ToolStripItem item in items)
{
ToolStripMenuItem menuItem = item as ToolStripMenuItem;
if (menuItem != null)
{
list.Add(menuItem);
if (menuItem.HasDropDownItems)
list.AddRange(MenuToList(menuItem.DropDownItems));
}
}
return list;
}
Method for Customization
public void CustomizeShortCuts(MenuStrip menuStrip)
{
frmMain frmMain = new frmMain(menuStrip);
frmMain.ShowDialog();
List<ToolStripItem> menuList = MenuToList(menuStrip.Items);
Properties.Settings.Default.UserConfigEntries =
new List<UserConfigEntry>(menuList.Count);
foreach (ToolStripItem item in menuList)
{
ToolStripMenuItem menuItem = item as ToolStripMenuItem;
if (menuItem == null) break;
Properties.Settings.Default.UserConfigEntries.Add(
new UserConfigEntry
{
Text = menuItem.Text,
ShortCut = menuItem.ShortcutKeys
});
}
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
Engineer in combustion engine development.
Programming languages: C#, FORTRAN 95, Matlab
FIS-overall worldcup winner in Speedski (Downhill) 2008/09 and 2009/10.