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 感知应用程序使用 ImmGetCandidateListCountImmGetCandidateList 获取”候选列表”信息

TODO: 为什么 ImmGetCandidateListCount 的返回值一直是 0 ?

组合字符串(Composition String)

组合字符串是合成窗口中的当前文本, 这是 IME 转换为最终字符的文本. 每个”组合字符串”由一个或多个”子句(clauses)”组成, 子句是 IME 可以转换为最终字符的最小字符组合.

为了获取状态信息和设置”组合字符串”,应用程序可以调用 ImmGetCompositionStringImmSetCompositionString 函数, 函数的第二个参数用于查询相对应的状态信息,

注意: 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_GETIMESTATUSEM_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