You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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内核浏览器的多标签进程隔离)会导致全局钩子仅注入主进程,没有注入到实际处理输入的子渲染进程,也会导致消息捕获失败。

解决方案

  1. 改用TSF框架开发输入监控逻辑,通过注册TSF文本服务监听输入内容,该方案可以兼容所有支持TSF的应用,是目前Windows平台最通用的输入拦截方案。
  2. 如果仅需要获取最终输入的字符,可以补充拦截WM_CHAR消息,绝大多数应用在IME输入完成后都会将最终的Unicode字符以WM_CHAR消息分发,可直接从中获取输入内容。
  3. 针对多进程架构的应用,需要枚举目标程序的所有UI线程,单独为每个线程挂载钩子,确保DLL注入到实际处理输入的子进程中。

Windows 10下可以获取绝大多数普通64位应用的IME输入内容,仅系统高权限进程、开启了输入保护的UWP应用以及启用了安全防护的程序无法正常拦截,拦截这类进程需要更高的系统权限,甚至需要驱动层支持。

内容的提问来源于stack exchange,提问作者Chou Tan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.01 10:09:02