win32 输入法管理器
2022年10月10日 09:21GMT+8
应用程序与输入法通信, 本文档的目的是使用自定义的”组合”及”选词”界面代替输入法所提供的
tsf 方式 tsf 似乎只能控制 candidatelist 窗口的显示
imm 方式 控制 composition window
tsf
https://learn.microsoft.com/en-us/windows/win32/api/msctf/
https://www.cnblogs.com/drunkard87/articles/3476920.html
https://learn.microsoft.com/en-us/windows/win32/api/_tsf/
Thread Manager
tsf 的基础组件同时用于 application 和 text services(clients), 由 ITfThreadMgr 定义
application 通过 CoCreateInstance 创建 ITfThreadMgr, 大致如下:
ITfThreadMgrEx *tmgr = NULL;
CoCreateInstance(&CLSID_TF_ThreadMgr, NULL, CLSCTX_INPROC_SERVER, &IID_ITfThreadMgrEx, (void **)&tmgr);
TfClientId cid; // 这个值这里用不到, 它用于作为 text service 传递给 ITfTextInputProcessor::Activate
tmgr->lpVtbl->ActivateEx(tmgr, &cid, TF_TMAE_UIELEMENTENABLEDONLY);
ITfUIElementSink
要实现这个接口用于实现自绘 candidate, 在 BeginUIElement
函数中,
获取 ITfCandidateListUIElement
, 在 UpdateUIElement
标记刷新, 之后再下一帧或者 WM_PAINT
中读取 ITfCandidateListUIElement 中的数据
// ITfCandidateListUIElement::GetUpdatedFlags, 3a, 4
#define TF_CLUIE_DOCUMENTMGR 0x00000001
#define TF_CLUIE_COUNT 0x00000002
#define TF_CLUIE_SELECTION 0x00000004
#define TF_CLUIE_STRING 0x00000008
#define TF_CLUIE_PAGEINDEX 0x00000010
#define TF_CLUIE_CURRENTPAGE 0x00000020
Compartment
一些预定义的”组件”, 首先通过 ITfThreadMgr
查询获得 ITfCompartmentMgr
,
之后调用 ITfThreadMgr::GetCompartment
一些预订义的组件 : https://learn.microsoft.com/en-us/windows/win32/tsf/predefined-compartments
感觉对于一些简单的设置过于繁琐, 如果 imm 提供了相同的功能还是用 imm 好了, 比如设置获取 conversion
misc
ITfLangBarEventSink.OnThreadItemChange 属于 ctfutb.h
监控输入法变换
ITfInputProcessorProfiles.GetLanguageProfileDescription 获得输入法名称
catid : GUID_TFCAT_TIP_KEYBOARD
// 下边代码在 win10 中不起作用, 这里只是记录下
ITfLangBarMgr itflangmgr;
CoCreateInstance(&CLSID_TF_LangBarMgr, NULL, CLSCTX_INPROC_SERVER, &IID_ITfLangBarMgr, itflangmgr);
// 要自己实现 itflangbareventsink::OnThreadItemChange 以监听输入法的切换
itflangmgr->lpVtbl->AdviseEventSink(itflangmgr, &itflangbareventsink, hwnd, 0, &cookie);
//
static HRESULT STDMETHODCALLTYPE OnThreadItemChange(ITfLangBarEventSink *sink, DWORD dwThreadId)
{
ITfInputProcessorProfiles *profile = NULL;
itflangmgr->lpVtbl->GetInputProcessorProfiles(itflangmgr, dwThreadId, &profile, &dwThreadId);
LANGID langid = 0;
profile->lpVtbl->GetCurrentLanguage(profile, &langid);
CLSID clsid;
GUID guid;
profile->lpVtbl->GetDefaultLanguageProfile(profile, langid, &GUID_TFCAT_TIP_KEYBOARD, &clsid, &guid);
BSTR bstr = NULL;
profile->lpVtbl->GetLanguageProfileDescription(profile, &clsid, langid, &guid, &bstr);
trace(bstr); // 当前输入法名称
...
}
// 使用 NULL 切换到英文输入法, win10 不起作用, 要用 ImmSetConversionStatus 来切中英
profile->lpVtbl->ActivateLanguageProfile(profile, &CLSID_NULL, langid, &CLSID_NULL)
imm
imm 指的是所有输入法管理器, 并不是单独的某一输入法, 因此用
GetSystemMetrics(SM_IMMENABLED)
只能判断 imm 是否已启用, 而不是判断输入法是否可用
还有就是由于 imm 感知不到你是否切换了输入法, 因此很多检测状态的方法得到的结果可能是错误的.
每次调用了 ImmGetContext, 必须要调用 ImmReleaseContext 释放, 在释放之后一些 API 将无法再获取数据
一些 Imm 方法仅仅在特定的事件中才有效, 例如: ImmGetCompositionString
而另一些 Imm 方法则允许在事件后再次调用, 例如: ImmGetCandidateList
, 可以延迟到在 WM_PAINT
再获取数据
这是因为 WM_IME_NOTIFY + IMN_CHANGECANDIDATE
事件可能会在同一时间出现多个,
而转到 WM_PAINT
处理可以让系统帮忙过滤掉重复了的 IMN_CHANGECANDIDATE
imm 已经是过时的了, 因此输入法对 imm 的兼容都非常差
候选列表(Candidate Lists)
候选列表由一个 CANDIDATELIST 结构表示, 该结构包含一个字符串数组, 其指定了用户可选择的单个字符或词组,
IME 感知应用程序使用 ImmGetCandidateListCount
和 ImmGetCandidateList
获取”候选列表”信息
TODO: 为什么 ImmGetCandidateListCount 的返回值一直是 0 ?
组合字符串(Composition String)
组合字符串是合成窗口中的当前文本, 这是 IME 转换为最终字符的文本. 每个”组合字符串”由一个或多个”子句(clauses)”组成, 子句是 IME 可以转换为最终字符的最小字符组合.
为了获取状态信息和设置”组合字符串”,应用程序可以调用 ImmGetCompositionString
和 ImmSetCompositionString
函数,
函数的第二个参数用于查询相对应的状态信息,
注意: ImmGetCompositionString
仅在其相应的事件中调用才会有效
当用户在合成窗口中输入文本时,IME 将跟踪的”组合字符串”的状态信息如下:
-
属性信息 : 为 8位数组, 数组中每个字节表示单个字符(英文或汉字)的状态, 一个子句(clause)的所有字符必须具有相同的属性, 其 0-3 位的组合值如下:
#define ATTR_INPUT 0x00 // 用户正在输入的字符。 IME 尚未转换此字符 #define ATTR_TARGET_CONVERTED 0x01 // 字符由用户选择,然后由 IME 转换 #define ATTR_CONVERTED 0x02 // IME 已转换的字符 #define ATTR_TARGET_NOTCONVERTED 0x03 // 正在转换的字符。 用户已选择此字符,但 IME 尚未转换它 #define ATTR_INPUT_ERROR 0x04 // IME 无法转换的错误字符。 例如,IME 不能将一些辅音组合在一起 #define ATTR_FIXEDCONVERTED 0x05 // IME 将不再转换的字符
-
子句信息(clause) : 为 32位数组, 表示了字句在组合字符串中的位置以及一个额外的”组合字符串”的字符长度值
如果”组合字符串”有两个子句,则子句信息将有三个值:
- [0] 第一个字句的字符偏移值为 0
- [1] 第一个字句的字符偏移值
- [2] “组合字符串”的长度值
-
输入信息(typing??没有找到其相关的值) : 一个以 null 结尾的字符串, 表示用户在键盘上输入的字符
-
光标位置(GCS_CURSORPOS 0x0080) : 表示光标相对于组合字符串中的字符的位置, 该值是字符串偏移量(以字符为单位)
光标位置将直接由函数返回, 不需要提供某个 buff 变量去加载数据. 例如你按下方向左右键就可以看到变化.
例如: 键入 wodshij
IME 可能产生 wo'd'shi'j
和 “我是世界”,
其中: wo'd'shi'j
就是”输入信息”, 将出现在合成窗口, 合成窗口可能在输入光标的位置,
也可能在 IME 窗口上, 而且一些输入法不会有 '
来把拼音自动拆分
“我是世界”同样出现在”合成窗口”或者输出
最后”编辑控件”支持两条消息 EM_GETIMESTATUS
和 EM_SETIMESTATUS
用于更改 IME 对组合字符串的处理
实际上大多数中文输入法只支持一个字句, 就直接输出了 因为基于句子型的输入法并不好用, 对于单个字符或词组要按下两次空格, 而句子太长又会担心小心会导致整个句子都消失
例如当你键入 “nihao” 时, “nihao” 这几个拼音字母所显示的位置. 在视觉上它通常会带有下划线, (一些句子型的输入法甚至会包含有用户已经选择了的汉字, 一直直到用户再次按下空格)
状态窗口
指示 IME 处于打开状态, 并为用户提供设置转换模式的方法
快捷键(Hot Keys)
使用快捷键,用户可以快速更改输入法的输入模式或切换到另一个输入法,
尽管应用程序无法添加快捷键到系统,但可以用 ImmSimulateHotKey
启动与快捷键相同的操作
IME消息(Messages)
-
WM_IME_SETCONTEXT(0x281) : 当窗口获得输入焦点时, 此时修改 lparam 的值并传递给 DefWindowProc 可以改变默认的 IME 显示
调用
ImmAssociateContext
也会触发这个事件 - WM_IME_STARTCOMPOSITION(0x10D) : 合成字符串开始时
- WM_IME_ENDCOMPOSITION(0x10E) : 但当完成所有子句后, 或者按下 ESC
-
WM_IME_COMPOSITION(0x10F) : (重要)当合成字符串的更改后, 仅当有输入法时每次按键才会响应
-
WM_IME_NOTIFY(0x0282): (重要)获取 ime 所发送的通知
# 打开窗口首先就是: 无论当前有没有输入法 WM_IME_SETCONTEXT wparam(1), lparam(c000000f) WM_IME_NOTIFY wparam(IMN_OPENSTATUSWINDOW), lparam(0) # 即使没有打开输入法 # 第一次切换出输入法时 WM_IME_NOTIFY wparam(IMN_SETOPENSTATUS), lparam(0) # 只第一次, 因此通过 ImmGetOpenStatus 判断输入法是否打开是错误的 WM_IME_NOTIFY wparam(IMN_SETCONVERSIONMODE), lparam(0) WM_IME_NOTIFY wparam(IMN_SETSENTENCEMODE), lparam(0) # 后边再切换回英文, 以及再从英文切换回来都不会再触发 WM_IME_NOTIFY 事件了, 也就是说当仅切换输入法时, 是没有通知的 # 随便输入 WM_IME_NOTIFY wparam(IMN_OPENCANDIDATE), lparam(1) WM_IME_NOTIFY wparam(IMN_CHANGECANDIDATE), lparam(1) # 按下空格, 完成转换 (如果是句子型的输入法, 这时汉字表现为"组合字符串") WM_IME_NOTIFY wparam(IMN_CLOSECANDIDATE), lparam(1) # 按下 Shift 键时 WM_IME_NOTIFY wparam(IMN_SETCONVERSIONMODE), lparam(0) WM_IME_NOTIFY wparam(IMN_SETSENTENCEMODE), lparam(0) # 切换到其它窗口或最小化当前窗口后 WM_IME_SETCONTEXT wparam(0) WM_IME_NOTIFY wparam(IMN_CLOSESTATUSWINDOW), lparam(0) # 每次 IMN_OPENSTATUSWINDOW/IMN_CLOSESTATUSWINDOW 只会随着 WM_IME_SETCONTEXT 事件而触发, ## 但是发送了 (WM_IME_CONTROL, IMC_CLOSESTATUSWINDOW) 给 IME 窗口后, 就再也收不到了, ## 但什么是 IME_STATUS_WINDOW 了? 好像是由于我用的输入法是嵌入在任务栏上的, 所以没有 STATUS_WINDOW. # 如果要获取 ImmGetProperty 应该在 IMN_SETCONVERSIONMODE 时刻合适
-
WM_IME_CONTROL(0x0283) : 这个消息用于发送消息给 IME
先通过
ImmGetDefaultIMEWnd()
获得 IME 的 hwnd, 之后…(参考WM_IME_CONTROL
的在线文档) - WM_IME_COMPOSITIONFULL(0x0284) :
- WM_IME_SELECT(0x0285) :
- WM_IME_CHAR(0x0286) : 似乎没什么用, 需要把 WM_IME_COMPOSITION 发给 DefWindowProc 处理, 才得到此消息.
- WM_IME_REQUEST(0x0288) :
- WM_IME_KEYDOWN(0x0290) :
- WM_IME_KEYUP(0x0291) :
Input Context
它包含有关 IME 状态的信息,供 IME 窗口使用
由于默 Input Context 是共享资源, 因此对其所做的任何更改都应用于线程中的所有窗口 但可以创建自己的 Input Context 并将其与线程的一个或多个窗口相关联来替代此默认行为
设置输入法
-
设置输入法的开启通过
ImmSimulateHotKey()
, 但有的输入法根本就不理会这些, 或误解// Windows for Simplified Chinese Edition hot key ID from 0x10 - 0x2F #define IME_CHOTKEY_IME_NONIME_TOGGLE 0x10 #define IME_CHOTKEY_SHAPE_TOGGLE 0x11 #define IME_CHOTKEY_SYMBOL_TOGGLE 0x12 // 感觉每个输入法对 hotkey 的识别并不一致
https://learn.microsoft.com/en-us/windows/win32/intl/ime-hot-key-identifiers
-
获取输入法以及中英文的启用状态? 感觉没有方法可以做到这一点
只能通过 WM_IME_NOTIFY 获取 IME 的当前状态, IME 在切换时并不会通知你
-
设置输入法, 通过
ImmSetConversionStatus
其能力见下边(Conversion Mode Values)部分实际上好像就这一种方法兼容性比较好, 应该优先考虑
DWORD conversion; DWORD sentence; ImmGetConversionStatus(imc,&conversion,&sentence); // modifying conversion, sentence ImmSetConversionStatus(imc,conversion,sentence); // ImmGetConversionStatus 只能反应中文输入法下是否是英文的, 当从中文切换到英文后, 它可能还是之前旧值 // 也就是说它感知不到你切换了输入法.
-
设置输入法, 通过
ImmNotifyIME
-
设置输入法, 通过发 WM_IME_CONTROL 消息给 ime 窗口, 但觉这样太麻烦, 因为发消息能做的都能找到 ImmXXXX 相应的方法.
HWND ime = ImmGetDefaultIMEWnd(hwnd); SendMessage(ime, WM_IME_CONTROL, WPARAM, LPARAM);
ImmXXXXXX
当 WM_CREATE 时用 ImmGetProperty(GetKeyboardLayout(0), IGP_PROPERTY)
获得一些属性,
并且在 WM_INPUTLANGCHANGE 时刷新
当 WM_CHAR 时用 InvalidateRect 并且马上使用 UpdateWindow 使某一个区域无效
游戏内的 IME
-
在
WM_IME_SETCONTEXT
时将lparam
的值设置为 0, 防止 ime 的所有弹出窗口如何检测当前使用的输入法它不支持关闭 “候选窗口”了? 例如 win10 的输入法就无法这样关闭.
-
处理
WM_IME_COMPOSITION
事件后直接返回, 不要再调用DefWindowProc
, 以防止 ime “输入合成窗口” 跳出来WM_IME_STARTCOMPOSITION
,WM_IME_ENDCOMPOSITION
也一样
hex
不想每次都打开 vs 所以直接贴这里了
// parameter of ImmGetCompositionString, WM_IME_COMPOSITION(lparam)
#define GCS_COMPREADSTR 0x0001 // READ 字符串
#define GCS_COMPREADATTR 0x0002 // READ 属性信息
#define GCS_COMPREADCLAUSE 0x0004 // READ 子句信息
#define GCS_COMPSTR 0x0008 // 字符串
#define GCS_COMPATTR 0x0010 // 属性信息
#define GCS_COMPCLAUSE 0x0020 // 子句信息
#define GCS_CURSORPOS 0x0080 // 光标信息
#define GCS_DELTASTART 0x0100 // 字句光标开始位置
#define GCS_RESULTREADSTR 0x0200 // RESULT + READ 字符串
#define GCS_RESULTREADCLAUSE 0x0400 // RESULT + READ 子句信息
#define GCS_RESULTSTR 0x0800 // RESULT 字符串
#define GCS_RESULTCLAUSE 0x1000 // RESULT 子句信息
//
// <https://learn.microsoft.com/zh-cn/windows/win32/intl/ime-composition-string-values>
// (微软拼音 - 新体验 2010[即 office 所提供的拼音输入法])示例:
// 1d80 : 没有输入完成就切换输入法
// 1c00 : 但当完成所有子句后(对句子型的输入法来说就是按下2次空格后), 或按下 Shift 键
// 1b8 : 按下任意字符或方向键,或修改子句, 或按下退格都是这个
// 1bf : 当按下"退格"删除掉最后的一个字符后, 或连续按下 ESC 取消
// 个人感觉由于中文输入相对于日语来说较为简单, 只需要把字母转换成汉字即可, 因此不需要太多的属性
// 对于游戏内部的中文输入法来说, 只要获取 GCS_COMPSTR, GCS_RESULTSTR 和 GCS_CURSORPOS 就可以了
// dwAction for ImmNotifyIME
#define NI_OPENCANDIDATE 0x0010
#define NI_CLOSECANDIDATE 0x0011
#define NI_SELECTCANDIDATESTR 0x0012
#define NI_CHANGECANDIDATELIST 0x0013
#define NI_FINALIZECONVERSIONRESULT 0x0014
#define NI_COMPOSITIONSTR 0x0015
#define NI_SETCANDIDATE_PAGESTART 0x0016
#define NI_SETCANDIDATE_PAGESIZE 0x0017
#define NI_IMEMENUSELECTED 0x0018
// dwIndex for ImmNotifyIME/NI_COMPOSITIONSTR
#define CPS_COMPLETE 0x0001
#define CPS_CONVERT 0x0002
#define CPS_REVERT 0x0003
#define CPS_CANCEL 0x0004
// dwFlags for ImmAssociateContextEx
#define IACE_CHILDREN 0x0001
#define IACE_DEFAULT 0x0010
#define IACE_IGNORENOCONTEXT 0x0020
// wParam of report message WM_IME_NOTIFY
#define IMN_CLOSESTATUSWINDOW 0x0001
#define IMN_OPENSTATUSWINDOW 0x0002
#define IMN_CHANGECANDIDATE 0x0003
#define IMN_CLOSECANDIDATE 0x0004
#define IMN_OPENCANDIDATE 0x0005
#define IMN_SETCONVERSIONMODE 0x0006
#define IMN_SETSENTENCEMODE 0x0007
#define IMN_SETOPENSTATUS 0x0008
#define IMN_SETCANDIDATEPOS 0x0009
#define IMN_SETCOMPOSITIONFONT 0x000A
#define IMN_SETCOMPOSITIONWINDOW 0x000B
#define IMN_SETSTATUSWINDOWPOS 0x000C
#define IMN_GUIDELINE 0x000D
#define IMN_PRIVATE 0x000E
// wParam of report message WM_IME_REQUEST
#define IMR_COMPOSITIONWINDOW 0x0001
#define IMR_CANDIDATEWINDOW 0x0002
#define IMR_COMPOSITIONFONT 0x0003
#define IMR_RECONVERTSTRING 0x0004
#define IMR_CONFIRMRECONVERTSTRING 0x0005
#define IMR_QUERYCHARPOSITION 0x0006
#define IMR_DOCUMENTFEED 0x0007
// IME property bits/ImmGetProperty(GetKeyboardLayout(NULL), IGP_PROPERTY)
#define IME_PROP_AT_CARET 0x00010000
#define IME_PROP_SPECIAL_UI 0x00020000
#define IME_PROP_CANDLIST_START_FROM_1 0x00040000
#define IME_PROP_UNICODE 0x00080000
#define IME_PROP_COMPLETE_ON_UNSELECT 0x00100000
// IMN_SETSENTENCEMODE : 切换了某些模式后 , 使用 ImmGetConversionStatus(,&conversion, &sentence) 获取当前状态
// 如果 (conversion & 0x0001) 那么表示当前为汉字输入法
// Conversion Mode Values (转换模式, 不同的输入法对其中的值有不一样的解释)
#define IME_CMODE_ALPHANUMERIC 0x0000 // 英文字母数字
#define IME_CMODE_NATIVE 0x0001 // 中文输入法
#define IME_CMODE_CHINESE 0x0001 // 同上
#define IME_CMODE_HANGUL 0x0001 // ...
#define IME_CMODE_JAPANESE 0x0001 // ...
#define IME_CMODE_KATAKANA 0x0002 // 日语片假名模式 only effect under IME_CMODE_NATIVE
#define IME_CMODE_LANGUAGE 0x0003 //
#define IME_CMODE_FULLSHAPE 0x0008 // 全角符号及数字
#define IME_CMODE_ROMAN 0x0010 // 估计也是用于日语的
#define IME_CMODE_CHARCODE 0x0020 //
#define IME_CMODE_HANJACONVERT 0x0040 //
#define IME_CMODE_NATIVESYMBOL 0x0080 //
#define IME_CMODE_HANGEUL 0x0001 // ...
#define IME_CMODE_SOFTKBD 0x0080 // 软键盘
#define IME_CMODE_NOCONVERSION 0x0100 //
#define IME_CMODE_EUDC 0x0200 // 也是一种英语的输入模式
#define IME_CMODE_SYMBOL 0x0400 // 中文标点符号
#define IME_CMODE_FIXED 0x0800 //
#define IME_CMODE_RESERVED 0xF0000000 //
// Sentence Mode Values (句子模式, 以微软2010拼音为例, 句子型和非句子型得到的值都是 8, 似乎只是一些输入法的设置值)
#define IME_SMODE_NONE 0x0000 // 没有句子信息
#define IME_SMODE_PLAURALCLAUSE 0x0001 // IME 使用复数子句信息来执行转换处理
#define IME_SMODE_SINGLECONVERT 0x0002 // IME 在单字符模式下执行转换处理
#define IME_SMODE_AUTOMATIC 0x0004 // IME 在自动模式下执行转换处理
#define IME_SMODE_PHRASEPREDICT 0x0008 // IME 使用短语信息来预测下一个字符
#define IME_SMODE_CONVERSATION 0x0010 // 输入法使用对话模式。 这对于聊天应用程序很有用
#define IME_SMODE_RESERVED 0x0000F000 // 保留
// style of candidate
#define IME_CAND_UNKNOWN 0x0000
#define IME_CAND_READ 0x0001
#define IME_CAND_CODE 0x0002
#define IME_CAND_MEANING 0x0003
#define IME_CAND_RADICAL 0x0004
#define IME_CAND_STROKE 0x0005
// bit field for IMC_SETCOMPOSITIONWINDOW, IMC_SETCANDIDATEWINDOW
#define CFS_DEFAULT 0x0000
#define CFS_RECT 0x0001
#define CFS_POINT 0x0002
#define CFS_FORCE_POSITION 0x0020
#define CFS_CANDIDATEPOS 0x0040
#define CFS_EXCLUDE 0x0080