首页 > 解决方案 > 如何在 Windows 10 中正确拦截(挂钩)IME 输入?

问题描述

我尝试实现一个自动化工具,但在拦截 IME(Windows 10 的默认 Microsoft IME)输入 Unicode 字符串(如日文/中文)时遇到问题。

我编写了一个 64 位 dll 用于注入其他进程/窗口。dll如下,

#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 应用程序来注入这个 dll。

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);
}

该程序确实可以将 DLL 注入其他进程,因为我可以在日志文件中看到来自各个窗口的消息。

我什至可以截取 WM_IME_COMPOSITION 消息 (GCS_RESULTSTR) 中的 IME Unicode 字符串,但仅限于某些应用程序,例如 .NET 编写的 64 位表单。

对于其他一些应用程序,例如 Firefox 和 Microsoft Edge,我只能看到一些 WM_IME_NOTIFY 消息,并且这些窗口没有收到任何 WM_IME_COMPOSITION/WM_IME_STARTCOMPOSITION/WM_IME_ENDCOMPOSITION 消息。因此,我无法获得这些窗口的最终 Unicode 输入字符串。

我做错什么了吗?是否甚至可以在 Windows 10 中获取所有进程或窗口(例如 64 位 Edge/Chrome/Firefox)的此类信息?

标签: c.netwindowshook

解决方案


推荐阅读