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





5.00/5 (10投票s)
一个允许自定义菜单快捷键的组件。这对于无障碍应用程序很有用。
引言
本文描述了一个可以在 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 日:初始发布