太长不看版

如果你用传统的 Windows API 造了个窗体,开了条事件处理线程循环检查发过来的消息,但程序老是在开着输入法启动,或在程序里切出来输入法,按两下键后卡死,那么你需要检查你的消息循环,如果是这样写的:

while (GetMessage(&msg, hWnd, 0, 0) != 0)
{
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

在你的程序只有一个窗体时,你可能会写出这样的代码,而且你的程序又只有一条线程在跑这种消息循环,这种情况下要把 GetMessage 函数的第二个参数换成 NULL

while (GetMessage(&msg, NULL, 0, 0) != 0)
{
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

啰嗦版

这两天在看 FreeType 的 Demo 程序。在 Windows 上构建这堆玩意不可谓不困难。主要是有个依赖——librsvg 在Windows上的构建实在是太阴间了,它有一堆间接依赖,其中有一个 gdk-pixbuf 又带了更多的间接依赖(把什么 cairo、pangu 啥的一股脑全整出来了)。因此我最终还是放弃了带 librsvg 编译,整了组不带 SVG 的 Demo 程序。

但是在我检查其中的 ftview 和 ftgraph 工具时,我发现当我开着输入法打开这两个工具时,它们会直接卡死,不响应一切事件。同时如果我不带输入法打开,但在里面把输入法切出来再按几下键盘(不一定是会弹出输入法的键,方向键也一样),窗口也会卡死。这两个工具从命令行启动,但是是 GUI 工具,用的最原始的 Windows API,就是 CreateWindow,再去实现 WndProc、拉消息队列等等的做法,因此代码还算能懂,我直接挂上 VS 进行调试。

进去看到所有的 FreeType Demo 程序封装了自己的一套 UI 库,主要的和 Windows API 相关的代码都在 graph/win32/grwin32.c 里。在 DispatchMessage 上打不中断断点,加上往调试控制台打印信息的操作。

反复打开、让窗体卡死,发现很奇怪的现象:当卡死发生时,没有收到任何消息,事件线程还是阻塞在 GetMessage 函数调用上,而不是在代码其它位置发生了死循环啥的。遂挂上 Spy++ 监视窗体消息,发现消息还是有正常产生,但 GetMessage 函数没有返回。

这样找到了问题的一个方向,进行了一下 Google-fu,又带上 IME、Input Layout 相关的关键字,找到了这样的一篇 SO 回答。其中引用了 Raymond Chen 写的这篇 blog。两者都指出了,当你的程序只运行着一条消息处理线程时,你应该把 GetMessage 的第二个函数(按 HWND 筛选消息)设为 NULL。这是因为虽然只显式创建了一个窗体,输入法等还是会创建额外的窗体,如果这些窗体的事件得不到处理,把当前线程的消息队列卡了,新的事件就没法接收到了。

而 FreeType 的实现中,他们确实把当前窗体的 HWND 传了进去,作为筛选。改为 NULL 后,问题解决,带上断点查看其它窗体的 HWND,再根据 HWND 在 Spy++ 看窗体的信息,发现了有两个窗体:

  • 一个标题是 Default IME 的——输入法窗体,可以认为是“罪魁祸首”了
  • 一个没有标题,窗体类名叫 UserAdapterWindowClass 的——微软似乎没有这个的文档,不知道干啥用的

再讲两句

实际上在把玩这个程序的过程中,还遇到了另一个问题——上下左右不能被正常处理。

在 FreeType 这套 UI 库里,Windows 上从系统收消息、响应窗体事件的线程和主线程是分离的,因此他们把接收到的键盘事件里的扫描码先“翻译”为他们的格式,再通过 Windows 的 PostThreadMessage 发到主线程。重点来了,他们用这套方案时,发送键盘事件用的消息 ID 是 WM_CHAR。这是系统预定义的的消息之一,里面的参数是严格定义的。但他们的按键码格式可没有按照 WM_CHAR 的格式来。因此 Windows 表现了极奇怪的行为—— WM_CHAR 放字符的参数是个 wParam,也就是一个 WORD。但接收端接收到的这个函数的高一字节被截掉了。相当于整个参数被对 256 取了模,而方向键在 FreeType 自己定义的按键码里正好落在 256 之后的位置(262 左右),因此接收端接收的就是错误的值。

我不知道为什么 FreeType 的开发者没有注意到这点,可能是中文 Windows 相关的额外检查吧。但这个行为也从来没有在 MSDN 上有记录,因此我也不知道为什么 wParam 的高 8 位变成了 0。总之,重新在 WM_USER 之后的区域自己定义一个消息 ID,解决了这个问题。


不想被自己的惰性打败。