ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

CEGUI 0.7.4中文输入法适配:IME消息处理与候选框定位

CEGUI 0.7.4中文输入法适配:IME消息处理与候选框定位 简介针对CEGUI 0.7.4 中文输入法接入场景这套资源包面向已具备CEGUI基础、希望在界面中实现中文输入的开发者只保留与输入法案例紧密相关的代码、脚本与配置项避免携带庞大的完整SDK。压缩包为RAR格式共996个文件以371个h头文件、336个cpp源文件为主体配合102个pkg、73个in、67个am等自动构建描述文件以及lua脚本、c源文件、scheme/manual/info配置整体约2.58MB目录结构清晰便于替换到已有SDK工程中。已有290人浏览学习。对使用者来说这份包内既包含可直接覆盖到CEGUI 0.7.4 SDK上的输入法样例工程也整理出与中文输入法交互相关的配置入口、构建辅助脚本和目录结构能节省重新组织字符映射、输入法事件处理等环节的时间减少自行集成时的排错成本使用前提是原SDK已能显示中文按常规编译流程跑起来即可观察输入法案例的实际效果。 先说结论CEGUI 0.7.4 这个版本想把中文输入法跑通不是接个库、装个输入法就能完事的事。它默认的输入链路压根就没给 IME 留位置你装好搜狗输入法或者 fcitx 后打开游戏界面文本框里照样空空如也。我前阵子把这个老古董项目的源码翻出来从输入法框架到候选框再从中文字体到资源打包重走了一遍踩了不少坑这篇就把整个适配过程和关键代码整理出来。CEGUI 0.7.x 在国内的老游戏项目里存量不小不少祖传工程到现在还在用它做登录框、设置面板和聊天窗口。它本身的滚动条、多行输入框、事件回调设计在当年算够用可一旦涉及中文输入问题就一个接一个拼音打进去了没反应、候选词在屏幕角落、输入法候选框不跟随光标、好不容易出中文了全是方块。这些问题不是因为输入法本身坏了而是 CEGUI 的输入事件模型和系统 IME 之间根本没有连接口。下面我会按照实际排查的顺序把整条通路拆开讲清楚。1. CEGUI 0.7.4 的输入链路上到底缺了什么1.1 默认输入路径回顾先看 CEGUI 0.7.4 默认怎么处理键盘输入。它在 Win32 平台下实质上是靠外部把系统消息翻译成 CEGUI 自己的事件再注入到CEGUI::System里。正常情况下是这样一条链// 消息循环里把按键消息转给 CEGUI case WM_KEYDOWN: CEGUI::System::getSingleton().injectKeyDown( CEGUI::System::getSingleton().getDefaultInputContext(), mapWin32KeyToCEGUIKey(wParam)); break; case WM_CHAR: CEGUI::System::getSingleton().injectChar( CEGUI::System::getSingleton().getDefaultInputContext(), (CEGUI::utf32)wParam); break;injectChar接收的是一个utf32字符。英文、数字、标点走WM_CHAR都没问题因为系统已经帮你把按键组合转换成了字符码。但中文输入就不一样了。中文输入时系统消息不会直接变成WM_CHAR发给你而是走一套独立的 IME 消息序列。你按下拼音字母系统先把字母交给输入法框架输入法根据字典拼出候选词等用户确认后再把最终汉字提交到应用程序。这个过程里程序接收到的是WM_IME_STARTCOMPOSITION、WM_IME_COMPOSITION、WM_IME_ENDCOMPOSITION这类消息而不是普通的字符消息。如果你只在WM_CHAR里接字符那拼音过程你什么都看不见等到词组上屏的一刻可能会有一个WM_CHAR过来也可能没有完全取决于输入法实现和窗口消息处理顺序。这就是文本框里打不出中文的第一层原因消息入口就接错了。1.2 接了消息也不够编码和字体都是坎就算你在WM_CHAR里拿到了汉字后面还有两个坎。第一个坎是编码。CEGUI 0.7.4 内部统一用utf32存储字符串可 Windows 的WM_CHAR给的是当前代码页编码简体中文环境下就是 GBK。wParam里只有一个 16 位的值但 GBK 编码的汉字是两个字节如果系统把这个 16 位值当成两个 8 位字节拆开处理就会出现乱码。更麻烦的是有些输入法会直接发WM_IME_CHAR里面带的也是多字节编码。所以光做(CEGUI::utf32)wParam这种强转是不够的必须先做完整的编码转换。第二个坎是字体。CEGUI 用的字体文件要能覆盖中文字符范围。默认自带的那套字体往往只有 Latin-1 字符集连一个汉字都没有。就算你的 IME 文本成功进入了 CEGUI 编辑框渲染时找不到对应字形的 glyph显示出来就是方框。这两个问题不解决输入法折腾到天亮也白搭。我在做适配时把整条链分成四块来拆IME 消息拦截、文本编码与注入、候选框定位、字体与资源打包。接下来逐个讲。2. IME 消息拦截与文本注入从系统到 CEGUI 的一条通路2.1 拦截点选在哪一层接入 IME 不能像普通按键消息一样在MainWindowProc里随便写两行就完事最好是做一个独立的窗口子类化处理器把 IME 相关消息统一拦下来。这样做的好处是主窗口逻辑不用大规模改动游戏或应用的渲染循环照旧IME 处理只在这条旁路上完成。我用的是SetWindowSubclass传入自己的回调函数在这个回调里专门处理如下消息WM_IME_STARTCOMPOSITION拼音组合开始可以在这里通知 CEGUI 进入输入态。WM_IME_COMPOSITION拼音组合过程需要从 IME 上下文里取出当前未上屏的拼音串或候选词。WM_IME_ENDCOMPOSITION拼音组合结束可以在这里做一些清理。WM_IME_NOTIFY候选框打开、关闭、切换等通知事件主要用来做候选框跟随。WM_IME_SETCONTEXT输入法切换状态拦截后不能随便吃掉不然后续候选框配置会失败。子类化拦截只是第一步关键是第二步从系统 IME 上下文里把当前字符串取出来。2.2 提取组成字符串的代码逻辑Windows IME 的消息处理有一个规则lParam里是一组标志位你必须按标志位去ImmGetCompositionString取对应的数据。取拼音串和取结果串用的标志不一样很多人只会取GCS_RESULTSTR结果就是候选词上屏前的拼音永远看不见。我写了一个比较完整的提取函数std::wstring ExtractIMEString(HIMC hImc, LPARAM lParam, DWORD flag, HWND hwnd) { if (hImc NULL) { hImc ImmGetContext(hwnd); if (hImc NULL) return L; } DWORD dwSize ImmGetCompositionStringW(hImc, flag, NULL, 0); if (dwSize 0) { if (hImc ! NULL) ImmReleaseContext(hwnd, hImc); return L; } // 注意这里按 wchar_t 大小计算但 ImmGetCompositionStringW 返回的是字节数 std::wstring strResult; strResult.resize(dwSize / sizeof(wchar_t) 1); DWORD dwCopied ImmGetCompositionStringW(hImc, flag, (LPVOID)strResult.data(), dwSize sizeof(wchar_t)); if (dwCopied 0) { strResult.resize(dwCopied / sizeof(wchar_t)); } else { strResult.clear(); } if (hImc ! NULL) ImmReleaseContext(hwnd, hImc); return strResult; }在WM_IME_COMPOSITION里这么用case WM_IME_COMPOSITION: { HIMC hImc ImmGetContext(hwnd); // 候选词上屏后的最终结果串比如选定了“你好” if (lParam GCS_RESULTSTR) { std::wstring result ExtractIMEString(hImc, lParam, GCS_RESULTSTR, hwnd); InjectTextToCEGUI(hwnd, result); } // 组合过程中的拼音串比如输入了“nih”还没选定 if (lParam GCS_COMPSTR) { std::wstring comp ExtractIMEString(hImc, lParam, GCS_COMPSTR, hwnd); UpdateCompositionDisplay(hwnd, comp); } if (hImc ! NULL) ImmReleaseContext(hwnd, hImc); break; }GCS_RESULTSTR是最终上屏的字符串GCS_COMPSTR是还没上屏的拼音或临时候选词。如果你要在编辑框里实现拼音悬浮在光标前、还没上屏的效果就得处理GCS_COMPSTR。如果只想要最终结果那就只处理GCS_RESULTSTR就行简单很多。2.3 编码转换这一步千万不能省取出的是std::wstringUTF-16 编码。CEGUI 0.7.4 要的是utf32而且它的CEGUI::String构造会做内部转换。理论上来回切就行但如果你的工程字符集设置的是多字节MBCS并且没有显式调用setlocale(LC_CTYPE, )那一进一出就会乱。我稳定的做法是绕过 CEGUI 的隐式转换自己先把 UTF-16 转成 UTF-32再调用injectCharvoid InjectTextToCEGUI(HWND hwnd, const std::wstring text) { CEGUI::System sys CEGUI::System::getSingleton(); const CEGUI::InputContext ctx sys.getDefaultInputContext(); for (wchar_t wc : text) { // 宽字符直接转 UTF-32 在 Windows 上足够BCP 之外的特殊增补对除外 CEGUI::utf32 codepoint static_castCEGUI::utf32(wc); sys.injectChar(ctx, codepoint); } // 通知 CEGUI 我们做了一次完整的文本提交便于它更新事件 CEGUI::Window* target CEGUI::System::getSingleton().getKeyboardTarget(); if (target ! NULL) { target-notifyTextChanged(); } }notifyTextChanged是我在具体工程里加的补充唤醒因为某些版本的事件触发时机对程序式注入的字符不够敏感。如果你的 CEGUI 回落、Undo 状态没异常这行也可以不调但建议还是保留避免一些只监听文本变化事件的外部逻辑失效。3. 候选框跟踪与光标换算老 UI 做中文输入最容易被忽略的3.1 候选框为什么跑到屏幕角上IME 的候选框位置是由系统根据你传入的COMPOSITIONFORM结构决定的。程序可以告诉输入法候选框要显示在哪怎么跟光标对齐。CEGUI 0.7.4 没有现成的接口暴露当前文本框光标位置所以很多人就放任不管结果候选框永远出现在屏幕左下角甚至任务栏后面。解决办法是在WM_IME_NOTIFY消息里监听IMN_OPENCANDIDATE在候选框打开瞬间手动计算当前 CEGUI 编辑框的光标绝对坐标然后调用ImmSetCandidateWindow把候选框定位过去。3.2 坐标换算的实操公式候选框定位需要的是屏幕坐标而 CEGUI 里你拿到的是窗口内部的相对像素坐标必须换算两层第一层从 CEGUI 窗口坐标换到 CEGUI 根窗口的屏幕偏移量第二层把该偏移量加上 CEGUI 渲染窗口在桌面上的位置。我封装了一个函数核心是拿到编辑框中光标所在的绝对屏幕点bool GetCaretScreenPos(CEGUI::Window* editBox, POINT outPt) { if (editBox NULL) return false; // 拿到编辑框的绝对像素区域 CEGUI::Rect absRect editBox-getUnclippedPixelRect(); // 编辑框内部文本光标的相对偏移量 // 如果是 CEGUI 的 Editbox可以拿 caret 位置做乘算如果没有暴露默认取左侧 float caretX 0.0f; float caretY 0.0f; CEGUI::Editbox* eb dynamic_castCEGUI::Editbox*(editBox); if (eb ! NULL) { size_t carrierIndex eb-getCaretIndex(); // 实际项目中我用磅值累加计算出 X 偏移这里简化成按字符数比例估 caretX eb-getTextWidth(eb-getText()) * 0.0f; // 可自行改成逐字符计算 } // 窗口在桌面上的位置 HWND hwnd getHWND(); RECT winRect; GetWindowRect(hwnd, winRect); outPt.x winRect.left (int)(absRect.left caretX); outPt.y winRect.top (int)(absRect.top caretY) - 20; // 向上偏移一点更自然 return true; }这里有个细节Editbox::getCaretIndex()拿到的是字符索引不是你直接就能用的像素坐标。要在实践中精确定位需要逐字符扫描编辑框文本宽度我一般直接调用CEGUI::Font的getTextExtent()或getTextWidth()来累加默认字体宽度可以估但中文和 ASCII 宽度不同最好按实际字符算。等你拿到屏幕坐标后设置 COMPOSITIONFORMCOMPOSITIONFORM cf {0}; cf.dwStyle CFS_POINT; cf.ptCurrentPos.x caretPt.x; cf.ptCurrentPos.y caretPt.y; cf.rcArea.left caretPt.x; cf.rcArea.top caretPt.y; cf.rcArea.right caretPt.x 400; cf.rcArea.bottom caretPt.y 300; HIMC hImc ImmGetContext(hwnd); if (hImc) { ImmSetCandidateWindow(hImc, cf); ImmReleaseContext(hwnd, hImc); }CFS_POINT表示按点定位rcArea用来阻止候选框跑到屏幕外。这里我给的是一个 400x300 的矩形如果用户屏幕小System 会自动调整候选框方向。这个过程不需要每帧调用只要在候选框打开、切换光标位置时更新一次即可。3.3 无光标和小窗口模式下的兜底策略如果你的编辑框是自定义皮肤没有标准Editbox类或者窗口特别小那getCaretIndex和getTextWidth可能拿不到可靠结果。我遇到过一个全屏 UI 模式CEGUI 拿不到真实桌面的 monitor 信息候选框坐标算出来是负数直接导致候选词不显示。兜底做法是如果当前焦点不在任何 CEGUI 编辑框里就直接把候选框rcArea设成整个窗口右下角上方区域固定位置。虽然体验一般但至少保证候选框不消失用户能看到词。4. 中文字体与资源打包看不见的隐性工程量4.1 字体的选择与配置IME 给你的是字符串但能不能显示在界面上完全依赖字体。CEGUI 0.7.4 官方示例自带的一般是DejaVuSans这类字体覆盖不了中文范围。我建议直接准备一份覆盖 CJK 的 TTF比如文泉驿微米黑或者微软雅黑。配置时要注意 CEGUI 0.7.4 的 Font XML 格式它只认Size属性FontSize或PtSize都不对。一个典型配置长这样Font NameMinyin Filenamemsyh.ttf TypeFreeType Size16 NativeHorzRes1920 NativeVertRes1080 AutoScaledNo /关键点是要确保Filename指向真实存在的字体文件并且该文件在resources.cfg的[Fonts]目录下能被找到。如果加载失败CEGUI 通常不会立刻报错而是静默回退到默认字体表现出来就是中文全变方框但英文正常。4.2 字体缓存和字符集的问题还有一个坑CEGUI 0.7.4 加载字体时默认只缓存常用 Latin 字符集中文字符不会预建 glyph。如果你输入的是一个生僻字运行时不缓存渲染可能直接跳过或显示空白。这个版本的解决方案是在 Font 里配置Codepage或CharacterSet属性把 CJK 范围加进去。我没记错的话0.7.4 的 Font XML 里支持类似这样的写法Font NameMinyin Filenamemsyh.ttf TypeFreeType Size16 Mapping Codepoint RangeStart0x4E00 RangeEnd0x9FFF / /Mapping /Font这个写法在不同小版本里可能存在差异你最好检查一下CEGUIFont::loadFromFT的日志看它到底预缓存了多少个 glyph。日志里如果Cached Glyph Count很低基本可以判断中文字符集没加载完整。4.3 Scheme/Imageset 的资源引用问题0.7.4 的资源系统不像新版那样统一Scheme 里会引用.imageset文件imageset又引用具体的.png图片。输入法适配牵扯的编辑框皮肤往往也要用到 CEGUI 的Falagard皮肤。这里我踩过一个特别烦的坑Imageset里定义的图片文件名写的是相对路径但引用的皮肤图片放在了另一个子目录加载顺序不对就会报FileNotFound异常。而且不同 locale 下大小写敏感度不同Windows 不敏感Linux 下敏感你在 Windows 上测好的工程挪到 Linux 上跑候选框背景图直接消失。所以我把编辑框相关的所有图片、字体、Scheme 集中在同一个相对目录下并且资源引用统一用小写字母。这样至少能保证不同平台行为一致。4.4 资源和代码的资源释放顺序最后说一个看起来很小、但实际很坑的问题CEGUI 在退出时如果字体资源和 Imageset 资源的释放顺序不对会触发AlreadyDeleted异常。配套输入法工程时常常要动态加载 / 卸载输入法面板皮肤很容易把同一个Imageset重复载入。我的做法是所有输入法相关的资源全部放进一个单独的Scheme里通过CEGUI::SchemeManager按名加载和卸载并且加载之前先查一次isSchemePresent。这样哪怕来回切输入法界面一百次也不会崩。5. 最后分享一点我自己的体会输入法适配这个事单独看每一个环节都不复杂难的是它们串起来时变量太多窗口消息被引擎接管了、IME 上下文拿不到、字体加载又是静默失败、坐标计算又碰到 HiDPI 缩放……每一条都能让你排查一下午。我强烈建议你把整条链路先拆成消息 - 提取字符串 - 注入 CEGUI - 渲染中文 - 候选框定位五个小目标逐段加日志验证别一上来就调 UI 表现。还有一个容易被忽视的小技巧在 Windows 10 / 11 下如果你的程序没有调用ImmAssociateContextEx或保持默认上下文有时候输入法的中文模式会被 Tab 键或窗口切换打断。我在输入法适配工程里会主动在窗口失去焦点时保存当前 CEGUI 编辑框内容在重新获得焦点时做一次 IME 状态的重新初始化。这一步不做可能会导致用户切出去再切回来时输入法状态继续保持英文模式中文又打不出来了。这个项目做完我才真正理解一件事老 GUI 库不是不能接输入法而是它出生的时候根本没有考虑过非拉丁字母输入这件事。只要把 Win32 IME 消息翻译成 CEGUI 能理解的事件序列再补齐字体和候选框定位这两块短板它依然能稳定运行在生产环境里。这套思路不局限于 CEGUI换成其他老式自绘 UI 库一样能照搬过去用。本文还有配套的精品资源点击获取
返回列表