喜大普奔:wxWidgets 修复了一个存在多年的输入法问题

在 Windows 上用中文输入法时,按 Escape 通常是为了取消还没上屏的拼音,或者关闭候选窗口。但在某些程序的对话框里,你可能会遇到一个很恼人的问题:输入法还没来得及取消输入,整个对话框就先关了。

wxWidgets 是一个跨平台 GUI 框架。除了 C++,它还有面向 Python 的 wxPython、面向 Rust 的 wxDragon 等语言绑定,开发者可以用不同的语言构建标准图形界面。我们熟知的 NVDA、Bookworm、Paperback 等都使用这套框架,所以这个问题的影响范围不小。

这个问题和 wxWidgets 在 Windows 上处理键盘消息的方式有关,相关报告可以追溯到十多年前。最近我把它重新整理后提交给了 wxWidgets,上游维护者随后合并了修复。2026 年 10 月 10 日发布的 wxWidgets 3.3.4 已经包含这项修复。

一个 Escape,为什么会关掉整个窗口?

假设你在一个模态对话框的文本框里用搜狗拼音输入中文,拼音还没确认,候选窗口也开着。这时按下 Escape,你只是想取消当前输入。

正常情况下,第一次按 Escape,输入法会取消组合输入,候选窗口消失,对话框仍然留在屏幕上。再按一次,才会关闭对话框。但在有问题的 wxWidgets 版本里,第一次按键就可能直接关闭对话框,没完成的操作也会一起被打断。

在 NVDA 的按键与手势对话框、查找对话框、插件商店等地方,都能观察到这个现象。它存在得太久,我几乎已经默默接受了。直到前段时间,一个沉寂多年的 NVDA 提案把它重新带了出来。我一路查下去,最终查到了上游的上游,也就是 wxWidgets,并用一个很小的 C++ demo 稳定复现了问题。完整的复现步骤和示例代码见 wxWidgets #27145。

问题出在输入法处理之前,程序就先处理了按键

在 Windows 上,wxWidgets 会通过 wxEVT_CHAR_HOOK 事件,让父窗口或对话框在子控件正常处理键盘输入之前先介入。这个机制本身是有用的,例如对话框可以借此响应 Escape。

问题是,Escape 不一定意味着用户想关闭窗口。输入法可能需要先用它取消尚未确认的组合输入,某些原生控件也需要先自己处理这个按键。

在原来的流程里,wxWidgets 过早地生成了 Escape 对应的 wxEVT_CHAR_HOOK。对话框先处理了这个事件,输入法却还没机会处理按键。于是,本该取消拼音的 Escape 变成了关闭对话框。

所以,关键不是禁止对话框响应 Escape,而是调整处理顺序:先让输入法和需要自行处理该按键的原生控件处理,再决定 wxWidgets 是否要生成相应的键盘钩子事件。

这个问题已经存在很多年

在 wxWidgets 的 issue tracker 里搜一下,就能找到不少类似的报告。

2009 年的 #11386 讨论过:用 Escape 取消输入法里尚未确认的字符时,对话框却被关闭了。2012 年的 #13945 涉及文本控件的自动补全下拉窗口:用户想关闭下拉窗口,结果整个模态对话框先关了。

2014 年,wxWidgets 针对自动补全窗口加入过特殊处理,让这类窗口显示时优先关闭它,而不是关闭对话框。但这没有覆盖所有情况,后来报告的 #16059 仍然涉及 Escape 被过早处理的问题。

遇到输入法就加一条判断,遇到自动补全窗口再加一条,这种局部修复很容易出现。但不同输入法、原生控件和键盘消息处理路径交织在一起,单靠补例外很难保证所有情况都正确。

最终合并的修复做了什么?

我最初提交的方案是:在 wxWidgets 生成 wxEVT_CHAR_HOOK 之前,先让 TSF 处理 Escape。如果 TSF 已经消费了这个按键,就不再继续处理。这个方案源于先前对相同问题的 IMM 处理思路,尽管能解决问题,但它又是一套特例,确实不够优雅。

维护者 Vadim Zeitlin(vadz)提出了另一种思路:不再为特定情况添加处理逻辑,而是直接调整 Escape 的处理顺序。

最终采用的是 wxWidgets PR #27148,于 2026 年 10 月 4 日合并到 wxWidgets 的主开发分支。它的核心是延后生成 Escape 对应的 wxEVT_CHAR_HOOK,让按键先进入后续的消息处理流程。这样,在输入法和原生控件还没处理这个按键时,wxWidgets 就不会提前把它交给对话框。

维护者在提交说明中也提到了此前的相关问题,包括输入法处理、原生控件对 Escape 的使用,以及自动补全窗口无法正确关闭等。这次修改撤销了部分旧的特殊处理,并关闭了包括 #27145 和 #16059 在内的相关问题。

我测试了这个方案:在微软拼音和搜狗拼音下,候选窗口显示时按 Escape,都能取消当前输入,而不会关闭对话框。

调整事件的生成时机也有代价。维护者指出,少数情况下可能不再生成 wxEVT_CHAR_HOOK,例如原生控件本身需要处理 Escape,或者某些插件 DLL 中的无模态对话框没有运行相应的事件循环。

上游已修复,下游还需更新

wxWidgets 3.3.4 已于 2026 年 10 月 10 日正式发布。这是 3.3 开发系列的版本,官方发布说明也提到了输入法处理方面的改进,详见 wxWidgets 3.3.4 发布公告。

不过,下游项目要用上这项修复,可能还要等一段时间。

wxPython 还要经过 Phoenix 项目。Phoenix 会把 wxWidgets 源码作为自身构建的一部分,所以需要先更新到包含修复的 wxWidgets 版本,再发布新的 wxPython。我为此提交了 Phoenix #2973,询问何时更新内置的 wxWidgets。维护者当时解释,他们通常采用 wxWidgets 的正式发布版本,并计划在 3.3.4 发布后更新依赖,同时发布新的 wxPython。

NVDA 也有类似的依赖关系。NVDA 主开发分支在 9 月已经更新到 wxPython 4.3.1,但这个版本仍然基于 wxWidgets 3.3.3。要让这项修复进入后续的 NVDA 构建,还要等 wxPython 更新,再由 NVDA 更新依赖。

对 NVDA 的中文用户来说,这项修复还关系到另一个场景。NVDA 中文输入法支持插件内置了以词定字功能,由于插件的实现方式,在对话框里使用这个功能也会触发同样的问题。这也是我希望这项修复早点进入 NVDA 的原因之一。

标签: 上游贡献, wxPython, 开发

添加新评论