如何在Windows 10中正确拦截(Hook)IME输入?
问题背景
开发自动化工具时,需要拦截Windows 10默认微软IME输入的中日文等Unicode字符串,现有实现仅对.NET编写的64位窗体等部分应用生效,无法在Firefox、Microsoft Edge等应用中获取输入内容。
现有实现代码
64位注入DLL(C++)
#include <windows.h> #include <fstream> #include <locale> // for output only static wchar_t* className(HWND hwnd) { static wchar_t className[128]; ::GetClassNameW(hwnd, className, 128); return className; } extern "C" __declspec(dllexport) LRESULT CALLBACK ImeCallback(int code, WPARAM wParam, LPARAM lParam) { std::wofstream out("test.log", std::ios::app); out.imbue(std::locale("zh_CN.UTF-8")); if (code >= 0) { PCWPSTRUCT msg = (PCWPSTRUCT)lParam; if (msg->message == WM_IME_COMPOSITION) { out << "composition: " << className(msg->hwnd) << ":"; if (msg->lParam & GCS_COMPATTR) { out << ":GCS_COMPATTR"; } if (msg->lParam & GCS_COMPCLAUSE) { out << ":GCS_COMPCLAUSE"; } if (msg->lParam & GCS_COMPREADSTR) { out << ":GCS_COMPREADSTR"; } if (msg->lParam & GCS_COMPREADATTR) { out << ":GCS_COMPREADATTR"; } if (msg->lParam & GCS_COMPREADCLAUSE) { out << ":GCS_COMPREADCLAUSE"; } if (msg->lParam & GCS_COMPSTR) { out << ":GCS_COMPSTR"; } if (msg->lParam & GCS_CURSORPOS) { out << ":GCS_CURSORPOS"; } if (msg->lParam & GCS_DELTASTART) { out << ":GCS_DELTASTART"; } if (msg->lParam & GCS_RESULTCLAUSE) { out << ":GCS_RESULTCLAUSE"; } if (msg->lParam & GCS_RESULTREADCLAUSE) { out << ":GCS_RESULTREADCLAUSE"; } if (msg->lParam & GCS_RESULTREADSTR) { out << ":GCS_RESULTREADSTR"; } if (msg->lParam & GCS_RESULTSTR) { out << ":GCS_RESULTSTR"; } out << std::endl; if (msg->lParam & GCS_RESULTSTR) { wchar_t data[128] = { 0 }; HIMC context = ImmGetContext(msg->hwnd); ImmGetCompositionStringW(context, GCS_RESULTSTR, data, 255); ImmReleaseContext(msg->hwnd, context); out << " result data: " << data << std::endl; } else if (msg->lParam & GCS_COMPSTR) { wchar_t data[128] = { 0 }; HIMC context = ImmGetContext(msg->hwnd); ImmGetCompositionStringW(context, GCS_COMPSTR, data, 255); ImmReleaseContext(msg->hwnd, context); out << " composition data: " << data << std::endl; } } else if (msg->message == WM_IME_STARTCOMPOSITION) { out << "start composition" << std::endl; } else if (msg->message == WM_IME_ENDCOMPOSITION) { out << "end composition" << std::endl; } else if (msg->message == WM_IME_SETCONTEXT) { out << "set context : " << className(msg->hwnd) << ":" << (wParam ? "TRUE" : "FALSE") << std::endl; switch (msg->lParam) { case ISC_SHOWUICOMPOSITIONWINDOW: out << " ISC_SHOWUICOMPOSITIONWINDOW" << std::endl; break; case ISC_SHOWUIGUIDELINE: out << " ISC_SHOWUIGUIDELINE" << std::endl; break; case ISC_SHOWUIALLCANDIDATEWINDOW: out << " ISC_SHOWUIALLCANDIDATEWINDOW" << std::endl; break; case ISC_SHOWUIALL: out << " ISC_SHOWUIALL" << std::endl; break; case ISC_SHOWUICANDIDATEWINDOW: out << " ISC_SHOWUICANDIDATEWINDOW" << std::endl; break; case ISC_SHOWUICANDIDATEWINDOW << 1: out << " ISC_SHOWUICANDIDATEWINDOW << 1" << std::endl; break; case ISC_SHOWUICANDIDATEWINDOW << 2: out << " ISC_SHOWUICANDIDATEWINDOW << 2" << std::endl; break; case ISC_SHOWUICANDIDATEWINDOW << 3: out << " ISC_SHOWUICANDIDATEWINDOW << 3" << std::endl; break; default: out << " default" << std::endl; } } else if (msg->message == WM_IME_NOTIFY) { HIMC context; wchar_t data[128] = { 0 }; out << "notify : " << className(msg->hwnd) << std::endl; switch (msg->wParam) { case IMN_CHANGECANDIDATE: out << " IMN_CHANGECANDIDATE" << std::endl; break; case IMN_CLOSECANDIDATE: out << " IMN_CLOSECANDIDATE" << std::endl; break; case IMN_CLOSESTATUSWINDOW: out << " IMN_CLOSESTATUSWINDOW" << std::endl; break; case IMN_GUIDELINE: out << " IMN_GUIDELINE" << std::endl; break; case IMN_OPENCANDIDATE: out << " IMN_OPENCANDIDATE" << std::endl; break; case IMN_OPENSTATUSWINDOW: out << " IMN_OPENSTATUSWINDOW" << std::endl; break; case IMN_SETCANDIDATEPOS: out << " IMN_SETCANDIDATEPOS" << std::endl; break; case IMN_SETCOMPOSITIONFONT: out << " IMN_SETCOMPOSITIONFONT" << std::endl; break; case IMN_SETCOMPOSITIONWINDOW: out << " IMN_SETCOMPOSITIONWINDOW" << std::endl; break; case IMN_SETCONVERSIONMODE: out << " IMN_SETCONVERSIONMODE" << std::endl; break; case IMN_SETOPENSTATUS: out << " IMN_SETOPENSTATUS" << std::endl; break; case IMN_SETSENTENCEMODE: out << " IMN_SETSENTENCEMODE" << std::endl; break; case IMN_SETSTATUSWINDOWPOS: out << " IMN_SETSTATUSWINDOWPOS" << std::endl; break; default: out << " default: " << msg->wParam << ":" << msg->lParam << std::endl; context = ImmGetContext(msg->hwnd); auto result = ImmGetCompositionStringW(context, GCS_RESULTSTR, data, 256); ImmReleaseContext(msg->hwnd, context); out << " data: " << data << std::endl; } } else if (msg->message == WM_IME_CHAR) { out << "char:" << msg->wParam << std::endl; } else if (msg->message == WM_IME_CONTROL) { out << "control" << std::endl; } else if (msg->message == WM_IME_SELECT) { out << "select : " << className(msg->hwnd) << " : " << (msg->wParam ? "TRUE" : "FALSE") << " : " << msg->lParam << std::endl; } else if (msg->message == WM_IME_KEYDOWN) { out << "key down" << std::endl; } else if (msg->message == WM_IME_KEYUP) { out << "key up" << std::endl; } else if (msg->message == WM_IME_REQUEST) { out << "request" << std::endl; } } out.close(); return ::CallNextHookEx(0, code, wParam, lParam); } BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: break; case DLL_THREAD_ATTACH: break; case DLL_THREAD_DETACH: break; case DLL_PROCESS_DETACH: break; } return TRUE; }
64位注入程序(.NET C#)
public delegate long HookProc(long code, long wParam, long lParam); [DllImport("kernel32.dll", EntryPoint = "LoadLibraryW", SetLastError = true)] public static extern IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPWStr)] string lpFileName); [DllImport("kernel32.dll", EntryPoint = "FreeLibrary", SetLastError = true)] public static extern IntPtr FreeLibrary([In] IntPtr hModule); [DllImport("kernel32.dll", BestFitMapping = false, CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true, ThrowOnUnmappableChar = true)] public static extern IntPtr GetProcAddress(IntPtr hModule, [MarshalAs(UnmanagedType.LPStr)] string procName); [DllImport("user32.dll")] public static extern int SetWindowsHookEx(int hookType, HookProc hookFn, IntPtr hMod, int threadId); [DllImport("user32.dll")] public static extern int UnhookWindowsHookEx(int hookId); public int hookId; private void button1_Click(object sender, EventArgs e) { IntPtr hMod = LoadLibrary("ImeHook.dll"); IntPtr callback = GetProcAddress(hMod, "ImeCallback"); // 4 represents WH_CALLWNDPROC // Try to inject into all other 64-bits applications hookId = SetWindowsHookEx(4, (HookProc)Marshal.GetDelegateForFunctionPointer(callback, typeof(HookProc)), hMod, 0); } private void button2_Click(object sender, EventArgs e) { UnhookWindowsHookEx(hookId); }
问题原因
- 你使用的传统WM_IME系列消息拦截方案,仅兼容使用旧式IME处理逻辑的应用。Edge、Chrome、Firefox等主流应用均已改用*TSF(Text Services Framework)*框架处理输入,不会产生和分发WM_IME_COMPOSITION这类旧式消息,因此钩子无法捕获。
- 部分应用的多进程架构(比如Chromium内核浏览器的多标签进程隔离)会导致全局钩子仅注入主进程,没有注入到实际处理输入的子渲染进程,也会导致消息捕获失败。
解决方案
- 改用TSF框架开发输入监控逻辑,通过注册TSF文本服务监听输入内容,该方案可以兼容所有支持TSF的应用,是目前Windows平台最通用的输入拦截方案。
- 如果仅需要获取最终输入的字符,可以补充拦截WM_CHAR消息,绝大多数应用在IME输入完成后都会将最终的Unicode字符以WM_CHAR消息分发,可直接从中获取输入内容。
- 针对多进程架构的应用,需要枚举目标程序的所有UI线程,单独为每个线程挂载钩子,确保DLL注入到实际处理输入的子进程中。
Windows 10下可以获取绝大多数普通64位应用的IME输入内容,仅系统高权限进程、开启了输入保护的UWP应用以及启用了安全防护的程序无法正常拦截,拦截这类进程需要更高的系统权限,甚至需要驱动层支持。
内容的提问来源于stack exchange,提问作者Chou Tan
相关产品推荐
相关产品推荐

