切换键控制器
一个允许您控制和监控如大写锁定、数字锁定和滚动锁定等切换键的类。
目录
概述
在我写这篇文章的前几天,一位成员询问如何控制大写锁定、数字锁定和滚动锁定键。我知道已经有一些实现,所以我搜索了一下,想为发帖人提供一个链接,但我找不到一个完整的。许多只读取这些键的状态,有些还允许你设置它们,但我找不到任何能监控它们,以便你的应用程序能相应更新的。所以,这篇文章就是结果。附带代码中的 `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`),以及四个事件。所有这些都应该非常直观且易于理解,但所有代码都进行了文档化和注释,以防万一!这是(公共)类图。
属性用于获取或设置按键的状态。`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。