QuickAccent - 用于口音和符号的工具
使用 QuickAccent 快速将重音符号和符号复制到剪贴板。还可以阅读文章,了解编写系统托盘应用程序的要点。
引言
我住在比利时,所以经常需要用法语发送电子邮件。我经常会因为记不住重音符号或欧元符号的 Alt 键代码而放慢速度。我最终会谷歌搜索它们,然后将它们复制粘贴到电子邮件中。
然后我想到,拥有一个在系统托盘中运行的程序会非常有用,它可以让我将最常用的符号复制到剪贴板,最好只需按几下快捷键。
本文介绍了“QuickAccent”——一个实现此功能的工具。我将展示应用程序的功能,然后描述一些可以应用于其他项目的有用的源代码片段。
工作原理
安装 QuickAccent,然后右键单击托盘中的 QuickAccent 图标(一个带有重音符的白色大写字母“A”的小圆圈)。您也可以使用 Windows + Q 热键。这将显示 QuickAccent 菜单。
有用提示:打开菜单时按住 Shift 键,就会显示所有有大写版本的符号。
现在只需单击一个重音符号 - 它将被复制到您的剪贴板。就是这么简单!
自定义重音符号
打开菜单并选择“设置”将打开设置窗口,允许您自定义重音符号。
您可以删除重音符号、编辑它们或添加新的。
有趣的代码:纯托盘应用程序
通常,在编写 Windows Forms 应用程序时,您会从一个主窗体开始,然后添加菜单、系统托盘图标等组件。在此类应用程序中,我们希望程序仅作为托盘中的图标运行,并且仅在用户选择设置时显示窗口。
这意味着我们需要在程序启动时创建托盘图标,而不是在准备好之前显示主窗口。即使我们很聪明地尝试将窗口设置为默认隐藏,用户在启动应用程序时仍然会遇到其闪烁显示的问题。
那么我们实际上是如何做到的呢?这并不难——我们必须放弃系统托盘图标菜单的设计者,但这损失不大。让我们看看程序启动代码。
/// <summary> /// The main entry point for the application. /// </summary> [STAThread] static void Main() { // Enable visual styles and set the text rendering modes. Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); // Load the settings. ApplicationContext.Instance.LoadSettings(); // Create the context menu. CreateContextMenu(); // Create the tray icon. CreateTrayIcon(); // Create the menu items. CreateAccentMenuItems(); // Set the hotkey. UpdateHotkeyState(ApplicationContext.Instance.Settings.UseWindowsHotkey); // Run the main window. Application.Run(); }
这段代码很好地展示了这个过程。这是逐一的解释:
- 执行 WinForms 应用程序的常规启动操作(视觉样式和设置文本渲染选项)。
- 加载应用程序设置。这些设置位于 XML 文件中——它们由 ApplicationContext 类管理,该类是一个单例。此类的实例在应用程序的生命周期内存在,并处理应用程序的状态。
- 现在我们完全以编程方式创建一个上下文菜单。
- 然后,我们再次以编程方式创建一个系统托盘图标。我们将上下文菜单与其关联。
- 具体到此应用程序的要求,然后我们将设置文件中定义的重音符号添加到我们的上下文菜单中。
- 同样,具体到此应用程序,我们注册了一个 Windows 热键来打开菜单,
- 最后,我们使用“运行”来启动主消息循环并启动应用程序。否则,应用程序将在上一行后终止。
那么上下文菜单是如何创建的?
/// <summary> /// Creates the context menu. /// </summary> private static void CreateContextMenu() { contextMenu = new HotkeyEnabledContextMenuStrip(); contextMenu.Name = "Context Menu"; contextMenu.HotkeyModifier = ModifierKeys.Win; contextMenu.Hotkey = Keys.Q; // Add the separator. contextMenu.Items.Add(new ToolStripSeparator()); // Add 'Settings'. var settingsItem = new ToolStripMenuItem("&Settings"); settingsItem.Click += settingsItem_Click; contextMenu.Items.Add(settingsItem); // Add 'Exit'. var exitItem = new ToolStripMenuItem("E&xit"); exitItem.Click += exitItem_Click; contextMenu.Items.Add(exitItem); // The context menu will also have to wait for keyup/keydown to handle shift functionality. contextMenu.KeyDown += contextMenu_KeyDown; contextMenu.KeyUp += contextMenu_KeyUp; contextMenu.Closed += contextMenu_Closed; }
同样——这很简单。我们以编程方式创建上下文菜单并添加一些项。我们监听按键抬起/按下事件,以便在用户按下 Shift 键时更改菜单项的文本。我们还处理某些菜单项的单击事件——请留意“settingsItem_Click”处理程序——我们稍后会详细介绍它。
请注意,上下文菜单实际上是一个 HotkeyEnabledContextMenu。这是一个从上下文菜单派生的类,它允许用户随时使用 Windows 热键打开菜单。
接下来,我们可以看到托盘图标是如何创建的。
/// <summary> /// Creates the tray icon. /// </summary> private static void CreateTrayIcon() { trayIcon = new NotifyIcon(); trayIcon.ContextMenuStrip = contextMenu; trayIcon.Icon = Properties.Resources.QuickAccentIconSmall; trayIcon.Visible = true; trayIcon.Text = "QuickAccent - Quickly copy accents to your clipboard"; trayIcon.MouseDoubleClick += trayIcon_MouseDoubleClick; }
这里没有发生不寻常的事情——但是如果您习惯在设计器中创建托盘图标,那么这段代码可能对您来说是陌生的。我们所做的与您在设计器中一样——只是设置了一些基本属性。
还记得我们为“设置”菜单项的“Clicked”事件注册了一个名为“settingsItem_Click”的函数吗?这就是它将调用的函数。
/// <summary> /// Shows the settings window. /// </summary> private static void ShowSettingsWindow() { // Is the settings form open? If so, activate it. if (settingsForm != null && settingsForm.IsHandleCreated) { settingsForm.Activate(); return; } // Create the settings form. settingsForm = new FormQuickAccent(); // Show the settings form. settingsForm.Show(); }
如果设置窗口已打开,我们激活它(将其带到前景)。否则,我们创建窗口。
基本上,这就是创建一个标准的基于系统托盘的应用程序所需的一切。我们看到提示的其他所有细节都特定于 QuickAccent 的逻辑。
有趣的代码:Windows 热键
当使用 C 或 C++ 中的 Win32 API 时,注册 Windows 热键非常简单,但 .NET Framework 没有封装此功能。幸运的是,我们可以通过 P/Invoke 来调用所需的功能。
关键函数是:
RegisterHotKey
UnregisterHotKey
还值得查看 WM_HOTKEY 的文档。
链接指向 MSDN 文档。在编写包装器之前,我们必须注意一件事——热键通知以 Windows 消息的形式出现。同样,对于具有 Win32 背景的人来说,这会很熟悉和预料之中,但如果您是一个不熟悉 Win32 的 .NET 开发人员,那么您需要注意这一点。
系统会通过向窗口发送 Windows 消息来告知您何时按下了热键。这意味着仅仅调用函数来注册热键是不够的,我们还需要运行一个消息循环来监听消息。一旦收到消息,我们就可以将其传递给我们程序中的任何元素。
因此,由于我们需要一个消息循环来检查消息,最简单的做法是将其与某种窗口关联(记住在 Windows 中,许多东西都是窗口——按钮、控件以及几乎所有东西)。由于我们希望在按下热键时显示上下文菜单,我们可以实际使用上下文菜单类本身——我们可以从它派生来创建一个注册热键并在触发时打开自己的上下文菜单。
类将如下所示:
/// <summary> /// A Hotkey Enabled Context Menu Strip is a Context Menu Strip /// that can be opened via a Windows Hotkey. /// </summary> public class HotkeyEnabledContextMenuStrip : ContextMenuStrip { /// <summary> /// Registers the hot key. /// </summary> /// <param name="hWnd">The h WND.</param> /// <param name="id">The id.</param> /// <param name="fsModifiers">The fs modifiers.</param> /// <param name="vk">The vk.</param> /// <returns></returns> [DllImport("user32.dll")] private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk); /// <summary> /// Unregisters the hot key. /// </summary> /// <param name="hWnd">The h WND.</param> /// <param name="id">The id.</param> /// <returns></returns> [DllImport("user32.dll")] private static extern bool UnregisterHotKey(IntPtr hWnd, int id);
到目前为止一切顺利——我们有了类定义,并且立即导入了两个我们将需要的 Win32 函数。
/// <summary> /// An id for our application's hotkey. /// </summary> private int hotkeyUniqueId = 123; /// <summary> /// Registers a hot key in the system. /// </summary> /// <param name="modifier">The modifiers that are associated with the hot key.</param> /// <param name="key">The key itself that is associated with the hot key.</param> public void RegisterHotKey(ModifierKeys modifier, Keys key) { // Register the hot key. RegisterHotKey(Handle, hotkeyUniqueId, (uint)modifier, (uint)key); } /// <summary> /// Unregisters the hotkey. /// </summary> public void UnregisterHotkey() { // Unregister the hotkey. UnregisterHotKey(Handle, hotkeyUniqueId); }
现在我们向类的使用者提供两个函数——一个用于注册,一个用于注销。我们如何监听事件?
/// <summary> /// The WindowProc. /// </summary> /// <param name="m">The Windows <see cref="T:System.Windows.Forms.Message"/> to process.</param> protected override void WndProc(ref Message m) { // Call the base. base.WndProc(ref m); // Have we hit the hotkey? if (m.Msg == WM_HOTKEY && HotkeyEnabled) { // Open the menu. Show(); Focus(); Items.OfType<ToolStripMenuItem>().First().Select(); } }
这是对 Window Proc 的重写。Window Proc 只是一个接收 Windows 消息并对其进行处理的函数。我们总是先调用基类,以确保所有正常消息都能正确处理。然后,我们检查是否收到了 WM_HOTKEY 消息(只有在设置了类中的“HotkeyEnabled”标志时,我们才会检查)。
如果热键已被激活,我们可以打开菜单,聚焦它,然后将焦点移动到第一个菜单项,从而允许用户使用键盘进行选择。
这就是 Windows 热键的全部内容——在许多情况下,您会想将它们与菜单以外的东西关联——也许是主窗口或其他东西。在这种情况下,只需找到合适的窗口并劫持其消息循环即可。
有趣的代码:应用程序上下文
我认为最后一件有趣的也是代码的部分是 ApplicationContext 类。这是一个单例——只有一个实例,并且该实例是全局可访问的。我们为什么要有这个类?
使用这个类是因为我们应用程序的不同部分需要共享一些中心信息,例如应用程序设置、重音符号列表等。通过创建一个全局可访问的单例,我们可以共享这些核心信息并提供核心功能,例如“保存设置”和“加载设置”。
这是类的一部分:
/// <summary> /// The <see cref="ApplicationContext"/> Singleton class. /// </summary> public sealed class ApplicationContext { /// <summary> /// The Singleton instace. Declared 'static readonly' to enforce /// a single instance only and lazy initialisation. /// </summary> private static readonly ApplicationContext instance = new ApplicationContext(); /// <summary> /// Initializes a new instance of the <see cref="ApplicationContext"/> class. /// Declared private to enforce a single instance only. /// </summary> private ApplicationContext() { } /// <summary> /// Gets the ApplicationContext Singleton Instance. /// </summary> public static ApplicationContext Instance { get { return instance; } }
这是单线程 C# 单例的标准样板代码。这里有一个很好的 MSDN 文章关于单例:http://msdn.microsoft.com/en-us/library/ff650316.aspx,我强烈建议阅读它。
我使用我的 Apex 库创建单例——因为它允许我这样做:
从“添加新项”窗口插入单例。如果您需要此功能,只需转到 apex.codeplex.com 并下载并安装最新版本的 SDK。
现在我们有了单例,我们可以将核心函数放入其中,例如下面的函数:
/// <summary> /// Loads the settings. /// </summary> public void LoadSettings() { // Do we have a settings file? if (File.Exists(GetSettingsPath())) { // Try and load it. try { using (var stream = new FileStream(GetSettingsPath(), FileMode.Open)) { // Create a serializer. var serializer = new XmlSerializer(typeof(Settings)); // Read the settings. Settings = (Settings)serializer.Deserialize(stream); } } catch (Exception exception) { // Trace the exception. System.Diagnostics.Trace.WriteLine("Exception loading settings file: " + exception); // Warn the user. MessageBox.Show("Failed to load the settings file.", "Error"); } } else { // We have no settings file - create the default settings. CreateDefaultSettings(); SaveSettings(); } }
实际上,设置的加载不应该由不相关的对象控制——但您可能会发现,像“ApplicationContext”这样的类在应用程序设计正在变化时,是放置核心函数的有用位置。例如,如果这个函数在主窗口中,当主窗口不再在启动时加载时,我们会怎么做?我们必须将其移到 Program 类中。但这可能也不对——因此,将这些应用程序状态和生命周期函数放在一起可能非常有用——尤其是在项目的早期阶段。
最终想法
我希望有些人会发现这个工具很有用,或者描述的有趣代码片段。如果有人对改进有建议,请告诉我。