HotKee 宏工具——将简单脚本分配给热键或关键字






4.82/5 (11投票s)
2003年8月19日
8分钟阅读

67207

1279
利用 Windows 挂钩监控操作系统,捕获用户指定的快捷键或关键字,并通过 Windows 消息和映射内存进行进程间通信 (IPC)。
引言
我编写这个工具的目的是允许我在任何窗口中使用键盘宏。我在 Visual Studio 中有几个喜欢的宏,但无法在其他地方使用它们。其中一个例子是我的代码注释。当我修改旧代码(总是别人的代码,请注意)并修复 bug 时,我会这样注释代码:
// 08/19/03 AlexR: Should have been checking the return value here...
当我要进入我们的 bug 跟踪系统时,我会使用相同的格式化文本来加上我的注释。但是,我无法在那里使用 VS 宏,所以必须手动输入日期和我的用户名(噢,多么痛苦!)。这是以及其他原因促使我下载了一些共享软件宏程序。我发现了几个非常好的,其中大多数甚至比这个小工具做得更多。但通过自己编写,我学会了不少关于 Windows 挂钩和内存映射文件(memory mapped files)的知识,并且也温习了如何使用 Windows 消息作为进程间通信 (IPC) 的一种方式。
此应用程序允许用户定义一个简单的脚本(我称之为宏),并将其分配给一个或多个快捷键(我称之为 HotKees)和/或关键字(我称之为 KeeWords)。(我选择 HotKee 这个名字是因为它独特,如果不是说有创意的话。)例如,用户可以定义一个宏,该宏展开为文本“ABC Broadcasting Networks, LLC”,然后创建一个快捷键 LeftWin-A,并将该宏分配给该快捷键。这样,当用户在任何窗口中按下 LeftWin-A 时,就会插入上述文本。同样,他可以定义一个关键字,比如“abnl”,并将其链接到该宏,这样当他输入“abnl”时,该文本就会被宏的文本替换。听糊涂了?我也一样。 :)
这个工具采用了三种中等水平的关键技术:Windows 挂钩、内存映射文件和 Windows 消息(可以说也是中等水平)。
一、Windows 挂钩
到目前为止,这个项目对我来说最大的挑战是弄清楚挂钩是如何工作的。CodeProject 上有一些关于如何使用它们的精彩文章,MSDN 上也有一些很好的信息。最令人恍然大悟的是,我意识到另一个进程无法通过 LoadLibrary
调用我的可执行文件。
我的问题是如何让操作系统在收到消息时调用我的代码,以便我能判断该消息是我用户预定义的快捷键还是预定义的关键字的结束。所以我使用了这段代码
HHOOK hKeyboardHook = SetWindowsHookEx (WH_GETMESSAGE,
CMyApp::MessageProc, NULL, 0);
由于我将线程 ID 指定为 0,我期望系统中所有线程在收到消息时都会调用我的 MessageProc
。但这里有一个诀窍:使用 0 作为线程 ID 调用 SetWindowsHookEx()
会指示每个线程对指定的 DLL 执行 LoadLibrary
,以便它们可以调用 DLL 中的函数来预处理它们的消息。你不能将 EXE 的代码加载到你的线程空间中。所以,我把消息处理程序放在了一个 DLL 中,并使用了以下代码让系统中所有线程加载它
DWORD CHotKeeDlg::RegisterHook ()
{
DWORD rc = 0;
if (IsHookRegistered ()) UnregisterHook ();
HINSTANCE hinstDLL = LoadLibrary ((LPCTSTR) "HKMSGHND.DLL");
if (hinstDLL) {
// "CHotKeeMsgHandlerApp::MessageProc(int,unsigned int,long)"
HOOKPROC hkProc =
(HOOKPROC) GetProcAddress (hinstDLL, (LPCTSTR) 1);
ASSERT (hkProc);
int nHook = WH_GETMESSAGE;
DWORD dwThreadID = 0;
if (hkProc)
m_pHKData->m_hKeyboardHook =
SetWindowsHookEx (nHook, hkProc,
hinstDLL, dwThreadID);
ASSERT (m_pHKData->m_hKeyboardHook);
}
if (!IsHookRegistered ()) rc = GetLastError ();
return (rc);
}
二、内存映射文件
下一个问题:主应用程序允许用户配置所有宏、快捷键和关键字,但我如何告诉 DLL 的每个实例数据在哪里呢?我的第一个想法是将数据保存在文件中,然后将文件名发送给每个人让他们加载。然而,正如你所知,磁盘 I/O 是任何应用程序最大的性能瓶颈之一。在尝试优化应用程序时,尽量减少磁盘 I/O 通常是首要考虑的。再加上我们看到大约有两到五百个线程加载 DLL,所以这根本不可行。
我找到了一个解决方案,就是使用内存映射(mapped memory)。这是一项我以前从未用过的技术,所以我对学习它感到很兴奋。基本上它的工作原理是,我向操作系统请求一块公共可访问的内存,告诉它我想要的大小,并给它一个唯一的名称。操作系统会返回一个内存地址,然后我就可以简单地将我想要的所有数据写入这个地址,直到我指定的尺寸。这是代码
CMemFile mf; CArchive ar (&mf, CArchive::store); m_pHKData->Serialize (ar); ar.Close (); DWORD dwLength = mf.GetLength (); m_hMap = CreateFileMapping ((HANDLE) 0xFFFFFFFF, NULL, PAGE_READWRITE, 0x0, dwLength + 4, // Extra 4 bytes for the buffer size HK_SHARED_MEMORY_FILENAME); ASSERT (m_hMap != NULL); if (m_hMap) { m_pMapBase = (BYTE *) MapViewOfFile (m_hMap, FILE_MAP_WRITE, 0, 0, 0); ASSERT (m_pMapBase); if (m_pMapBase) { memcpy (m_pMapBase, &dwLength, sizeof (dwLength)); mf.SeekToBegin (); mf.Read (m_pMapBase + sizeof (dwLength), dwLength); } }
现在,剩下要做的就是告诉所有那些线程有内存可供读取。这就引出了我们的下一个部分……
三、Windows 消息
有许多 IPC(进程间通信)方式,既简单又高效。然而,在这种特定情况下,我有一个应用程序要与同一台机器上的数百个其他线程进行通信。我脑海中显而易见的解决方案是使用用户定义的(user-defined)消息。Windows API 有一个名为 UINT RegisterWindowMessage (LPCSTR sMsgName)
的好函数,它“定义了一个保证在整个系统中唯一的窗口消息”。这确保了没有其他进程会使用我们的消息编号调用 Send
/PostMessage
。如果指定的 sMsgName
没有对应的消息编号,操作系统就会注册一个新的并返回。如果已经存在,就会返回那个编号。这非常方便,因为我们需要 DLL 的所有实例和主应用程序发送和接收相同的消息编号。主应用程序在设置挂钩之前调用一次。一旦挂钩设置完毕,每个线程都会初始化它自己的 DLL 实例,这些实例会调用 RegisterWindowMessage
,获取与主应用程序相同的消息编号。现在我们都在说同一种语言了!
我不想让消息过多,所以只注册了一个消息。消息编号表明该消息是 HotKee 主应用程序和 DLL 实例之间的通信。我使用 WPARAM
来指定消息类型,这样接收代码就知道如何处理该消息。如果消息附带了任何数据,它可以放在 LPARAM
中。我主要在这里使用它是在主应用程序中,当指示所有实例从内存文件中读取新数据时。这是调用
::PostMessage (HWND_BROADCAST, m_nCommMessage, WPARAM_HK_RELOAD_DATA, (LPARAM) m_hWnd);
这里我告诉 DLL 实例从内存映射中重新加载它们的数据。我使用 HWND_BROADCAST
常量,这样 API 就会将消息发布到系统中每个消息队列。这确保了每个拥有带有消息循环的父窗口的 DLL 实例都会收到消息。(我们实际上不关心其他线程,因为总有一个窗口获得焦点来接收我们的 HotKees 或 KeeWords。)我还将主窗口的句柄传递给了 DLL 代码。这使得 DLL 实例只响应主窗口,而不是将响应广播到整个系统,从而导致系统充斥着数千条消息。
在 DLL 端,消息被发送到自身和它的父窗口,以完成它的大部分工作。为什么要发送消息给自己?因为当 DLL 为其父窗口过滤消息时,它不会阻止消息循环来执行宏,而是会在队列中发布另一个消息来执行工作。这允许在此时处理任何其他消息(如 WM_PAINT
等)。如果需要从队列中删除消息,这也很容易:只需将 hwnd
和消息编号设置为 0,然后将其传递下去。
当宏被解析为需要输入到窗口中的文本时,DLL 会向其父窗口发送消息。文本的每个字符都作为消息发送到窗口进行处理,就像正常情况一样。我在编写程序很久之后才发现 SendInput()
函数(事实上是今天才发现的),它可能是模拟发送到窗口的字符消息的更好的解决方案。如果有人有兴趣尝试,我很想知道结果如何!
安装和运行二进制文件
以下是 zip 文件中包含的文件列表
- HotKee.exe - 主应用程序。
- Hkmsghnd.dll - 包含消息过滤代码的 DLL。
- HotKee.hlp - 帮助文件(有限,但信息丰富)。
- HotKee.cnt - 帮助文件的目录...非必需。
只需将所有这些文件放在一个目录中并运行可执行文件即可。无需对您的系统进行任何其他修改。
结论
Windows API 是一个强大的工具,它允许您利用运行应用程序的操作系统的强大功能。我希望您发现这个工具既有用又有教育意义。它(像大多数免费工具一样)为扩展开放了空间!如果您进行了任何愿意分享的修改,请告诉我!
最重要的是,请记住:“用你的力量行善!”