weasel icon indicating copy to clipboard operation
weasel copied to clipboard

小狼毫未能正确处理WM_IME_CONTROL相关消息

Open oTnTh opened this issue 1 year ago • 17 comments

我在其他几个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的相关支持,应该就能兼容市面上不少控制输入法状态的第三方工具了。只是不知道是不是还会出现某些方案没有“全角半角”和“中英文标点”的情况。

oTnTh avatar Sep 23 '24 13:09 oTnTh

这是我对某个“文字输入”窗口 (hWndTarget)关闭“中文输入法”的代码(也就是切换到英文输入状态),对应的输入法应该是系统自带的微软拼音输入法,供参考~。~

HWND hCurWnd = ImmGetDefaultIMEWnd(hWndTarget);
if(SendMessage(hCurWnd, WM_IME_CONTROL, IMC_GETOPENSTATUS, 0))
{
	SendMessage(hCurWnd, WM_IME_CONTROL, IMC_SETOPENSTATUS, 0);
	// 已经切换到英文输入法。
}

hoodlum1980 avatar Oct 11 '24 07:10 hoodlum1980

mark

Moirstral avatar Nov 03 '24 01:11 Moirstral

我也遇到这个问题了, 我开启了 global_ascii: true这个设置, 没开启前是没问题; 问题表现为,当我在autohotkey里 SendMessage WM_IME_CONTROL IMC_SETOPENSTATUS 0或1, 并不能设置小狼毫的中英文状态, 我觉得是 global_ascii: true 导致的 @fxliang 大佬麻烦看一下 补充: 并不总是出问题,出问题的场景大概是 窗口切换的同时还切换中英文和大小写状态, 不过稳定复现的步骤我还没找到

iamqiz avatar Nov 07 '24 16:11 iamqiz

问题是什么? 为什么要受控制? 似乎上文描述的控制方式s都不确定是否可行作用原理(无文档)?

切换ascii,用WeaselServer.exe /asciiWeaselServer.exe /nascii可以?

fxliang avatar Nov 07 '24 16:11 fxliang

我后来又实验了一下,发现在小狼毫和微软拼音之间切换,ImmGetDefaultIMEWnd返回的句柄都是一样的。

所以WM_IME_CONTROL消息可能并不是直接发送给输入法,而是由操作系统接受到消息之后,调用了输入法的某个接口。

(不知道是不是也是GUID_开头的那些)


ImmGetOpenStatus 等函数是微软官方公开支持的,不过这些API似乎只能在进程内部使用。

IMC_GETCONVERSIONMODE 等似乎确实没有桌面Windows的官方支持,但是一直都能用。通过这些消息,可以用SendMessage从进程外部设置输入法的状态。


输入法如果正确实现了这些接口,应用程序就可以通过统一的API获得和设置输入法的状态。而不是微软拼音一套代码,小狼毫又要另外一套代码。

比如C#写GUI程序,TextBox之类的有一个IMEMode属性,可以控制该输入框获得焦点时输入法是开启还是关闭。(我测试过,当前版本的微软拼音仍然能正确处理IMEMode的设置,而小狼毫不行。)

再比如下面的这张游戏截图,游戏程序并不能获得小狼毫当前的输入法状态,游戏认为是英文输入状态,但其实不是。

2024-11-08_proc

oTnTh avatar Nov 07 '24 17:11 oTnTh

状态获取我记得imtip似乎是没问题的,至于其他不能读到就是其他的问题了

fxliang avatar Nov 10 '24 12:11 fxliang

@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

oTnTh avatar Nov 11 '24 09:11 oTnTh

@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

imtips2

imtips1

fxliang avatar Nov 11 '24 11:11 fxliang

@fxliang 我可能找到原因了。

我在方案的custom.yaml中写了如下内容:

patch:
  "switches/@0/reset": 1

对应的应该是schema.yaml里的这个部分:

switches:
  - name: ascii_mode
    reset: 0
    states: [ 中文, 西文 ]

也就是默认打开ascii_mode,似乎是这个设置导致了问题。

你可不可以帮忙测试一下,默认打开ascii_mode是否会遇到跟我一样的问题?

oTnTh avatar Nov 12 '24 06:11 oTnTh

1

2

补充一下,小狼毫似乎也不是单纯的返回错误的值,更像imtip作者说的这种情况: 有时会返回正确的 opened 与 convMode, 但一会正确一会错误,错误的 convMode 会无规律地变化为各种奇怪的数值。

而且上面说的还只是获取输入法的状态,IMC_SETCONVERSIONMODE设置输入法状态似乎也不能用。

oTnTh avatar Nov 13 '24 14:11 oTnTh

那我就不懂了。

只是会觉得,代码在那放着,这么多专业人士指出问题就是不贡献一下,还是说专业人士的判断也未必准所以改不好呢。让我们期待高手出现来pr吧

fxliang avatar Nov 13 '24 15:11 fxliang

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 获取。很奇怪,结果与你不一样。

yjnu avatar Nov 27 '24 05:11 yjnu

Win11 23H2,weasel 0.16.3.0

安装后用户目录内除了程序自动生成的文件外,只创建了luna_pinyin_simp.custom.yaml,里面只有:

patch:
  "switches/@0/reset": 1

通过已上操作,我这里可以复现该问题。

另外根据作者的说法,第三方输入法返回的状态信息很多都不太规范,ImTip为了可用性额外增加了很多判断。

比如IMC_GETOPENSTATUS,只要输入法打开,不管是ascii_mode开没开,都应该是true才对。

oTnTh avatar Nov 27 '24 17:11 oTnTh

我测试了 TSF 的 API ITfCompartment::SetValue,不知道与这个问题是否有关联。

测试代码流程大概如下:

  1. 创建 CLSID_TF_ThreadMgr 对象,并得到 ITfThreadMgr 接口
  2. 调用 QueryInterface 得到 ITfCompartmentMgr 接口
  3. 通过 ITfCompartmentMgr::GetCompartment 得到 GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSION 对应的 ITfCompartment 接口
  4. 调用 ITfCompartment::GetValue 得到数值
  5. 反转最低位,即 TF_CONVERSIONMODE_ALPHANUMERICTF_CONVERSIONMODE_NATIVE
  6. 调用 ITfCompartment::SetValue
  7. 观察中英文状态是否变化

测试结果是不起作用。

brglng avatar Apr 29 '25 16:04 brglng

@brglng https://github.com/rime/weasel/actions?page=2

如果你感兴趣的话,从这里往后翻,ime status相关的修改好像都跟这个问题有关。

oTnTh avatar Apr 29 '25 21:04 oTnTh

我又测试了一下 Windows 10 自带的微软输入法,可以正确响应 WM_IME_CONTROL 消息。通过修改 IME_CMODE_ALPHANUMERIC/IME_CMODE_NATIVE 状态位可以切换中英模式。

TSF 的 API 必须要在有窗口,且是前台窗口的时候才起作用,Compartment API 对应 IMC_GETCONVERSIONMODEIME_CMODE_ALPHANUMERIC/IME_CMODE_NATIVE 切换的则是 GUID_COMPARTMENT_KEYBOARD_OPENCLOSE,设置 GUID_COMPARTMENT_KEYBOARD_INPUTMODE_CONVERSIONTF_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 的方式也是可以控制的,但只对当前窗口有效,且调用写起来比较麻烦。

brglng avatar Apr 30 '25 04:04 brglng

另外,这里可以找到 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

brglng avatar Apr 30 '25 05:04 brglng