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

切换键控制器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (23投票s)

2009年8月30日

CPOL

6分钟阅读

viewsIcon

52851

downloadIcon

1414

一个允许您控制和监控如大写锁定、数字锁定和滚动锁定等切换键的类。

screenshot.png

目录

概述

在我写这篇文章的前几天,一位成员询问如何控制大写锁定、数字锁定和滚动锁定键。我知道已经有一些实现,所以我搜索了一下,想为发帖人提供一个链接,但我找不到一个完整的。许多只读取这些键的状态,有些还允许你设置它们,但我找不到任何能监控它们,以便你的应用程序能相应更新的。所以,这篇文章就是结果。附带代码中的 `ToggleKeysController` 类将监控这些键,并在检测到更改时引发事件,并允许你获取或设置状态。

如果你只想看看如何在你的应用程序中使用该类,而不关心它是如何实现的,请点击这里

意外问题

这不像我最初想的那么简单!首先要做的就是获取按键的状态。我知道 `Console` 类有一个 `CapsLock` 属性,所以我认为这可能就是答案。不幸的是,它没有 `ScrollLock` 属性。

下一步是设置按键状态。`Console` 类中的那些是只读的,所以现在那个类肯定不适用了。我查阅了 `SendKeys`,但它不允许你永久更改状态,就像这里演示的那样。

最后,我想能够系统地监控这些键的状态,以便我提供的信息始终是最新的。我无法通过框架中的类找到实现这一点的方法,所以这不是一个好的开始。

所有这些问题的答案都在于一点点 PInvoke 到 *user32.dll*(和 *kernel32.dll*),它包含了我们所需的所有原生函数。

获取按键状态

获取按键状态需要调用`GetKeyState`函数。该函数只接受一个代表按键的值,并返回一个指示其状态的值。这个例子展示了使用它来获取大写锁定键状态的简单性。

bool isCapsLockOn = (GetKeyState(VK_CAPITAL) & 1) == 1;

函数定义是

[DllImport("user32.dll")]
static extern short GetKeyState(byte nVirtKey);

注意:为了简化其他地方的使用,我已经将每个键定义为字节。严格来说,上面的函数应该声明为 `int`。

设置按键状态

在尝试了多种方法后,我决定最简单的方法是使用`keybd_event`函数来模拟按键的抬起/按下周期,当需要改变状态时。下面的例子展示了如何调用这个函数来模拟大写锁定键的按下。

keybd_event(VK_CAPITAL, 0x45, KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYDOWN, 0);
keybd_event(VK_CAPITAL, 0x45, KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP, 0);

…以及函数定义

[DllImport("user32.dll")]
static extern void keybd_event(byte bVk, byte bScan, int dwFlags, int dwExtraInfo);

监控按键

这是所有这些中最具挑战性的部分。我能成功实现这一点的唯一方法是使用全局钩子。全局钩子字面上会挂钩到窗口以拦截键盘/鼠标事件。在我们挂钩系统之前,我们需要我们进程主模块的句柄。要做到这一点,我们获取当前进程,获取该进程的主模块,然后将模块名称传递给`GetModuleHandle`函数。一旦我们有了句柄,我们就可以使用`SetWindowsHookEx`函数安装钩子,指定我们需要哪种类型的钩子,在本例中是键盘。这听起来比实际要复杂!这是代码,后面是函数定义。

IntPtr hookHandle = IntPtr.Zero;
using (Process currentProcess = Process.GetCurrentProcess())
{
    using (ProcessModule currentModule = currentProcess.MainModule)
    {
        hookHandle = SetWindowsHookEx(WH_KEYBOARD_LL, hookProc, 
            GetModuleHandle(currentModule.ModuleName), 0);
    }
}

[DllImport("kernel32.dll")]
static extern IntPtr GetModuleHandle(string lpModuleName);

[DllImport("user32.dll")]
static extern IntPtr SetWindowsHookEx(int idHook, 
    LowLevelKeyboardProc lpfn, IntPtr hMod, int dwThreadId);

`SetWindowsHookEx` 函数将其第二个参数作为 `LowLevelKeyboardProc` 类型的委托。我传递了 `hookProc`,它是该委托的一个实例。每当发生按键事件时,它都会调用其关联的方法。文档说明,如果调用方法的第一个参数小于零,我们应该返回而不做进一步处理,所以我们只需要检查它是否为零,并确保它是 KeyDown,然后检查按键以确保是我们感兴趣的。

LowLevelKeyboardProc hookProc = KeyHookCallback;
IntPtr KeyHookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
    int wParamValue = wParam.ToInt32();
    if (nCode >= 0 && (wParamValue == WM_KEYDOWN || wParamValue == WM_KEYUP))
    {
        ToggleKey key = (ToggleKey)Marshal.ReadByte(lParam);
        if (key == ToggleKey.CapsLock || key == ToggleKey.NumLock || 
            key == ToggleKey.ScrollLock)
        {
            // Do our stuff here
        }
    }
    return CallNextHookEx(hookHandle, nCode, wParam, lParam);
}

delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);

[DllImport("user32.dll")]
static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

现在我们知道其中一个键被按下了。不幸的是,这并不像切换一个布尔字段/属性那么简单。首先,按住一个切换按钮会反复调用此方法,但切换状态仅在第一次发生时改变。显而易见的方法是读取按键的状态,但如果你尝试这样做,你会发现它还没有被更新,直到我们的线程不活跃时才会更新,这是第二个问题。在第一个版本中,我启动了一个工作线程,然后将消息发送回原始线程并检查状态。这对于准确性来说很好,但可能有点大材小用。在这个版本中,我在 keydown 时切换一个标志,在 keyup 时重置该标志。这样,只有第一次 keydown 才被认为是有效的。如果被检查的键在状态初始同步期间被按住,这可能会出现一个问题,因为收到的第一个 keydown 可能不是一个有效的。为了解决这个问题,同步后的第一个 keyup 会测试状态并根据需要纠正值。

// Do our stuff here
switch (key)
{
    case ToggleKey.CapsLock:
        if (wParamValue == WM_KEYUP)
        {
            keys[key].IsDown = false;
            if (!keys[key].Confirmed)
            {
                bool originalValue = keys[key].IsOn;
                keys[key].IsOn = (GetKeyState(VK_CAPITAL) & 1) == 1;
                if (keys[key].IsOn != originalValue)
                {
                    ToggleKeyChangedEventArgs e = 
                       new ToggleKeyChangedEventArgs(key, keys[key].IsOn);
                    OnCapsLockChanged(e);
                    OnToggleKeyChanged(e);
                }
                keys[key].Confirmed = true;
            }
        }
        else if (!keys[key].IsDown)
        {
            keys[key].IsDown = true;
            keys[key].IsOn = !keys[key].IsOn;
            ToggleKeyChangedEventArgs e = 
              new ToggleKeyChangedEventArgs(key, keys[key].IsOn);
            OnCapsLockChanged(e);
            OnToggleKeyChanged(e);
        }
        break;
// ...

现在,剩下的就是完成时取消挂钩。我将挂钩放在了构造函数中,为了保持整洁,我实现了标准的 Dispose 模式并在 `Dispose(bool disposing)` 中取消了挂钩。取消挂钩只是一个调用`UnhookWindowsHookEx`,传入我们从原始 `SetWindowsHookEx` 调用中获得的句柄。

UnhookWindowsHookEx(hookHandle);
hookHandle = IntPtr.Zero;

[DllImport("user32.dll")]
static extern bool UnhookWindowsHookEx(IntPtr hhk);

ToggleKeysController 类

最终的类将上述所有内容封装到一个易于使用的类中,该类只有三个 `public` 属性,三个额外的 `public` 方法(加上 `Dispose`),以及四个事件。所有这些都应该非常直观且易于理解,但所有代码都进行了文档化和注释,以防万一!这是(公共)类图。

classdiagram.png

属性用于获取或设置按键的状态。`Start` 和 `Stop` 用于安装/卸载钩子。这些可能不需要,因为 `Start` 由构造函数调用,而 `Stop` 在 `Dispose` 中调用。`Toggle` 只是切换你传递给它的按键的状态。最后,当按键状态发生变化时,会引发事件,`ToggleKeyChanged` 事件会在每次切换键状态变化时被引发。

演示

演示应用程序通过状态栏使用了 `ToggleKeysController` 类。按键的状态由标签的 `ForeColor` 指示。例如,这段代码由 `CapsLockStateChanged` 事件处理程序调用。

if (toggleKeysController.CapsLockOn)
    toolStripStatusLabelCAPS.ForeColor = OnForeColor;
else
    toolStripStatusLabelCAPS.ForeColor = OffForeColor;

点击标签会通过调用 `ToggleKey` 方法来切换相应按钮的状态。

void toolStripStatusLabelCAPS_Click(object sender, EventArgs e)
{
    toggleKeysController.ToggleKey(ToggleKeys.CapsLock);
}

可能存在的局限性?

我无法验证这一点,但有一些报告称,有些杀毒软件似乎不喜欢使用全局钩子的程序,因为它认为它们是键盘记录器。我在撰写本文时已在 Vista 和 XP 下使用最新版本的 Norton AV,以及在 XP 下使用 AVG Network Edition 进行过测试,均未发现问题。如果你的杀毒软件对此有异议,请告知我。

结论

没有太多要说的了;由于现在所有更复杂的东西都封装在一个类中,使用它非常简单。希望你觉得它有用!

历史

  • 2009年8月30日:初始版本。
  • 2009年9月3日:版本2。
© . All rights reserved.