小狼毫未能正确处理WM_IME_CONTROL相关消息
我在其他几个issues里看到有人在讨论,如何用代码控制输入法状态的问题。然后去做了一点功课,发现WM_IME_CONTROL消息可以实现该功能,但是小狼毫似乎没有正确处理该消息。
在Widnows平台上,可以用ImmGetDefaultIMEWnd获得输入法句柄,然后通过向该句柄发送WM_IME_CONTROL消息的方式,获得和控制输入法的状态(切换中英文输入状态,全角半角切换等)。
以下是用python写的完整示例:
import win32more.Windows.Win32.UI.WindowsAndMessaging as wm
import win32more.Windows.Win32.UI.Input.Ime as ime
IMC_GETCONVERSIONMODE = 1
IMC_SETCONVERSIONMODE = 2
IMC_GETOPENSTATUS = 5
hwnd = wm.GetForegroundWindow()
himc = ime.ImmGetDefaultIMEWnd(hwnd)
ret = wm.SendMessage(himc, wm.WM_IME_CONTROL, IMC_GETOPENSTATUS, 0)
print('open:', ret)
ret = wm.SendMessage(himc, wm.WM_IME_CONTROL, IMC_GETCONVERSIONMODE, 0)
print('conv:', ret)
print('\tNATIVE', (ret & ime.IME_CMODE_NATIVE))
print('\tKATAKANA', (ret & ime.IME_CMODE_KATAKANA))
print('\tLANGUAGE', (ret & ime.IME_CMODE_LANGUAGE))
print('\tFULLSHAPE', (ret & ime.IME_CMODE_FULLSHAPE))
print('\tROMAN', (ret & ime.IME_CMODE_ROMAN))
print('\tCHARCODE', (ret & ime.IME_CMODE_CHARCODE))
print('\tHANJACONVERT', (ret & ime.IME_CMODE_HANJACONVERT))
print('\tSOFTKBD', (ret & ime.IME_CMODE_SOFTKBD))
print('\tNOCONVERSION', (ret & ime.IME_CMODE_NOCONVERSION))
print('\tEUDC', (ret & ime.IME_CMODE_EUDC))
print('\tSYMBOL', (ret & ime.IME_CMODE_SYMBOL))
print('\tFIXED', (ret & ime.IME_CMODE_FIXED))
#ret = wm.SendMessage(himc, wm.WM_IME_CONTROL, IMC_SETCONVERSIONMODE, ime.IME_CMODE_ALPHANUMERIC)
#ret = wm.SendMessage(himc, wm.WM_IME_CONTROL, IMC_SETCONVERSIONMODE, ime.IME_CMODE_NATIVE)
下面这些定义Windows SDK里似乎没有,不过相关功能好像一直都可以用。
IMC_GETCONVERSIONMODE = 1
IMC_SETCONVERSIONMODE = 2
IMC_GETOPENSTATUS = 5
因为没有找到准确的文档,所以我就对比了一下微软拼音和小狼毫的返回值。
ret = wm.SendMessage(himc, wm.WM_IME_CONTROL, IMC_GETOPENSTATUS, 0)
print('open:', ret)
微软拼音中不管输入模式是中文还是英文,IMC_GETOPENSTATUS的返回值都是1。
小狼毫打开ascii_mode时返回1,没有问题。但是中文输入模式返回0,这里应该不太对。
open: 1
conv: 1033
NATIVE 1
KATAKANA 0
LANGUAGE 1
FULLSHAPE 8
ROMAN 0
CHARCODE 0
HANJACONVERT 0
SOFTKBD 0
NOCONVERSION 0
EUDC 0
SYMBOL 1024
FIXED 0
后面的部分微软拼音的返回值大概是上面这样,猜测各值的意义是:
- IME_CMODE_NATIVE:中文输入模式
- IME_CMODE_LANGUAGE: 未知
- IME_CMODE_FULLSHAPE: 全半角模式
- IME_CMODE_SYMBOL: 中英文标点
经过测试,可以通过组合上面各值的方式改变微软拼音的状态。
如果只是想切换中英文输入模式,可以直接单独发送IME_CMODE_NATIVE,可以成功切到中文输入状态,跟按Ctrl+Space的效果一样。
如果能提供WM_IME_CONTROL的相关支持,应该就能兼容市面上不少控制输入法状态的第三方工具了。只是不知道是不是还会出现某些方案没有“全角半角”和“中英文标点”的情况。
这是我对某个“文字输入”窗口 (hWndTarget)关闭“中文输入法”的代码(也就是切换到英文输入状态),对应的输入法应该是系统自带的微软拼音输入法,供参考~。~
HWND hCurWnd = ImmGetDefaultIMEWnd(hWndTarget);
if(SendMessage(hCurWnd, WM_IME_CONTROL, IMC_GETOPENSTATUS, 0))
{
SendMessage(hCurWnd, WM_IME_CONTROL, IMC_SETOPENSTATUS, 0);
// 已经切换到英文输入法。
}
mark
我也遇到这个问题了,
我开启了 global_ascii: true这个设置, 没开启前是没问题;
问题表现为,当我在autohotkey里 SendMessage WM_IME_CONTROL IMC_SETOPENSTATUS 0或1, 并不能设置小狼毫的中英文状态,
我觉得是 global_ascii: true 导致的
@fxliang 大佬麻烦看一下
补充: 并不总是出问题,出问题的场景大概是 窗口切换的同时还切换中英文和大小写状态, 不过稳定复现的步骤我还没找到
问题是什么? 为什么要受控制? 似乎上文描述的控制方式s都不确定是否可行作用原理(无文档)?
切换ascii,用WeaselServer.exe /ascii 和 WeaselServer.exe /nascii可以?
我后来又实验了一下,发现在小狼毫和微软拼音之间切换,ImmGetDefaultIMEWnd返回的句柄都是一样的。
所以WM_IME_CONTROL消息可能并不是直接发送给输入法,而是由操作系统接受到消息之后,调用了输入法的某个接口。
(不知道是不是也是GUID_开头的那些)
ImmGetOpenStatus 等函数是微软官方公开支持的,不过这些API似乎只能在进程内部使用。
IMC_GETCONVERSIONMODE 等似乎确实没有桌面Windows的官方支持,但是一直都能用。通过这些消息,可以用SendMessage从进程外部设置输入法的状态。
输入法如果正确实现了这些接口,应用程序就可以通过统一的API获得和设置输入法的状态。而不是微软拼音一套代码,小狼毫又要另外一套代码。
比如C#写GUI程序,TextBox之类的有一个IMEMode属性,可以控制该输入框获得焦点时输入法是开启还是关闭。(我测试过,当前版本的微软拼音仍然能正确处理IMEMode的设置,而小狼毫不行。)
再比如下面的这张游戏截图,游戏程序并不能获得小狼毫当前的输入法状态,游戏认为是英文输入状态,但其实不是。
状态获取我记得imtip似乎是没问题的,至于其他不能读到就是其他的问题了
@fxliang 你说的是这个么: https://imtip.aardio.com/
这个程序检测小狼毫的状态也是错误的,无论是否打开ASCII模式,显示的状态都是EN和半角。
我去看了一下他的代码,也是通过WM_IME_CONTROL消息获得的输入法状态:
(这部分代码在aardio本体,不在imtip里面。)
control = function(hwnd,command,data){
if(!hwnd) hwnd = ::User32.GetForegroundWindow();
var hIme = ::Imm32.ImmGetDefaultIMEWnd(hwnd);
if(hIme){
return ::User32.SendMessage(hIme,0x283/*_WM_IME_CONTROL*/,command,data)
}
}
getOpenStatus = function(hwnd){
if(!hwnd) hwnd = ::User32.GetForegroundWindow();
return !!control(hwnd,5/*_IMC_GETOPENSTATUS*/)
}
setOpenStatus = function(status,hwnd){
if(!hwnd) hwnd = ::User32.GetForegroundWindow();
return control(hwnd,6/*_IMC_SETOPENSTATUS*/,status)
}
getConversionMode = function(hwnd){
if(!hwnd) hwnd = ::User32.GetForegroundWindow();
return control(hwnd,1/*_IMC_GETCONVERSIONMODE*/)
}
setConversionMode = function(convMode,hwnd){
if(!hwnd) hwnd = ::User32.GetForegroundWindow();
return control(hwnd,2/*_IMC_SETCONVERSIONMODE*/,convMode)
}
该作者还写了一篇文档说到了这个问题: https://www.aardio.com/zh-cn/doc/library-guide/std/key/imeState.html
@fxliang 你说的是这个么: https://imtip.aardio.com/
这个程序检测小狼毫的状态也是错误的,无论是否打开ASCII模式,显示的状态都是EN和半角。
我去看了一下他的代码,也是通过WM_IME_CONTROL消息获得的输入法状态:
(这部分代码在aardio本体,不在imtip里面。)
control = function(hwnd,command,data){ if(!hwnd) hwnd = ::User32.GetForegroundWindow(); var hIme = ::Imm32.ImmGetDefaultIMEWnd(hwnd); if(hIme){ return ::User32.SendMessage(hIme,0x283/*_WM_IME_CONTROL*/,command,data) } } getOpenStatus = function(hwnd){ if(!hwnd) hwnd = ::User32.GetForegroundWindow(); return !!control(hwnd,5/*_IMC_GETOPENSTATUS*/) } setOpenStatus = function(status,hwnd){ if(!hwnd) hwnd = ::User32.GetForegroundWindow(); return control(hwnd,6/*_IMC_SETOPENSTATUS*/,status) } getConversionMode = function(hwnd){ if(!hwnd) hwnd = ::User32.GetForegroundWindow(); return control(hwnd,1/*_IMC_GETCONVERSIONMODE*/) } setConversionMode = function(convMode,hwnd){ if(!hwnd) hwnd = ::User32.GetForegroundWindow(); return control(hwnd,2/*_IMC_SETCONVERSIONMODE*/,convMode) }该作者还写了一篇文档说到了这个问题: https://www.aardio.com/zh-cn/doc/library-guide/std/key/imeState.html
@fxliang 我可能找到原因了。
我在方案的custom.yaml中写了如下内容:
patch:
"switches/@0/reset": 1
对应的应该是schema.yaml里的这个部分:
switches:
- name: ascii_mode
reset: 0
states: [ 中文, 西文 ]
也就是默认打开ascii_mode,似乎是这个设置导致了问题。
你可不可以帮忙测试一下,默认打开ascii_mode是否会遇到跟我一样的问题?
补充一下,小狼毫似乎也不是单纯的返回错误的值,更像imtip作者说的这种情况: 有时会返回正确的 opened 与 convMode, 但一会正确一会错误,错误的 convMode 会无规律地变化为各种奇怪的数值。
而且上面说的还只是获取输入法的状态,IMC_SETCONVERSIONMODE设置输入法状态似乎也不能用。
那我就不懂了。
只是会觉得,代码在那放着,这么多专业人士指出问题就是不贡献一下,还是说专业人士的判断也未必准所以改不好呢。让我们期待高手出现来pr吧
switches: - name: ascii_mode reset: 0 states: [ 中文, 西文 ]也就是默认打开ascii_mode,似乎是这个设置导致了问题。
你可不可以帮忙测试一下,默认打开ascii_mode是否会遇到跟我一样的问题?
我一直都是 reset 1, 默认英文输入,可以通过 windows 的 api 获得输入法中英文状态,也就是用你的 python 代码 print('\tNATIVE', (ret & ime.IME_CMODE_NATIVE)) 可以获得 0 或 1,ImTip 可以识别输入法状态,都没问题,我用的最新 0.16.3 的包。 但设置输入法状态没试成功,可能是我参数没给对。
总结,小狼毫的中英文输入状态可以通过系统 api 获取。很奇怪,结果与你不一样。
Win11 23H2,weasel 0.16.3.0
安装后用户目录内除了程序自动生成的文件外,只创建了luna_pinyin_simp.custom.yaml,里面只有:
patch:
"switches/@0/reset": 1
通过已上操作,我这里可以复现该问题。
另外根据作者的说法,第三方输入法返回的状态信息很多都不太规范,ImTip为了可用性额外增加了很多判断。
比如IMC_GETOPENSTATUS,只要输入法打开,不管是ascii_mode开没开,都应该是true才对。
我测试了 TSF 的 API ITfCompartment::SetValue,不知道与这个问题是否有关联。
测试代码流程大概如下:
- 创建
CLSID_TF_ThreadMgr对象,并得到ITfThreadMgr接口 - 调用
QueryInterface得到ITfCompartmentMgr接口 - 通过
ITfCompartmentMgr::GetCompartment得到GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION对应的ITfCompartment接口 - 调用
ITfCompartment::GetValue得到数值 - 反转最低位,即
TF_CONVERSIONMODE_ALPHANUMERIC或TF_CONVERSIONMODE_NATIVE - 调用
ITfCompartment::SetValue - 观察中英文状态是否变化
测试结果是不起作用。
@brglng https://github.com/rime/weasel/actions?page=2
如果你感兴趣的话,从这里往后翻,ime status相关的修改好像都跟这个问题有关。
我又测试了一下 Windows 10 自带的微软输入法,可以正确响应 WM_IME_CONTROL 消息。通过修改 IME_CMODE_ALPHANUMERIC/IME_CMODE_NATIVE 状态位可以切换中英模式。
TSF 的 API 必须要在有窗口,且是前台窗口的时候才起作用,Compartment API 对应 IMC_GETCONVERSIONMODE 中 IME_CMODE_ALPHANUMERIC/IME_CMODE_NATIVE 切换的则是 GUID_COMPARTMENT_KEYBOARD_OPENCLOSE,设置 GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION 的 TF_CONVERSIONMODE_ALPHANUMERIC/TF_CONVERSIONMODE_NATIVE 状态位对微软输入法同样不起作用。而 GUID_COMPARTMENT_KEYBOARD_OPENCLOSE 则对日文输入法同样有效。另外,这些状态的作用域也都是以 thread、document 和 context 来划分的,我目前没有找到类似 WM_IME_CONTROL 这样控制/获取另一个线程中的 GUID_COMPARTMENT_KEYBOARD_OPENCLOSE 状态的方法。
另外我还发现了下面这两个链接:
-
https://github.com/microsoft/terminal/issues/14407 (尤其是 https://github.com/microsoft/terminal/issues/14407#issuecomment-1953366065 这一层)
-
https://github.com/google/mozc/blob/master/docs/design_doc/input_scope.md
上面这两份资料似乎表示,现代输入法不响应 WM_IME_CONTROL 消息是被允许的(IMM 被微软认为是废弃的,虽然仍被支持),且应该支持 Input Scope,而这个需要应用侧同时支持,根据相关 context 告诉输入法如何切换状态,类似于浏览器中跳转到密码框时启动切换成英文的功能。
说实话,IMM 和 TSF 的 API 都挺黑暗的,文档都不清不楚的,也找不到什么资料。这也许也是第三方输入法都实现得不太好的一个原因。
从我个人角度,我其实不太喜欢 Conversion Mode 和 Open Status 这些状态。我认为 Windows 应该在所有语言的系统上都提供一个英文键盘,Shift 键的功能直接改成在英文和其他语言之间切换,为了保持向后兼容,操作系统可以把 Conversion Mode 中的 ALPNANUMERIC/NATIVE 和 Open Status 解释为这种英文和其他语言之间切换的方式。
我自己测试了 TSF 中的 ITfInputProcessorProfileMgr::ActivateProfile,是可以正确切换输入法的。这也是目前看来唯一可以在系统全局范围切换中英的方法,可以使用一个隐藏窗口来实现。
总结一下,我认为小狼毫的实现在原则上应该没有问题,在 TSF API 的支持上是与微软输入法一致的,但如果一定要支持 WM_IME_CONTROL 消息,可能需要针对这个消息单独处理一下(但我也不知道怎么单独处理,修改 WndProc?)。但如果不支持这种控制方式,用 TSF 的方式也是可以控制的,但只对当前窗口有效,且调用写起来比较麻烦。
另外,这里可以找到 ImTip 作者总结的资料:https://www.aardio.com/zh-cn/doc/library-guide/std/key/imeState.html
以及我找到的一些其他资料:https://r32.github.io/other/2022-10-10-win32-ime.html