|
1 | | -# 实时探测当前前台窗口的 IME 转换模式(IME_CMODE_NATIVE 位 = 中文) |
2 | | -# 用法: pwsh scripts/probe_ime_mode.ps1 |
3 | | -# 然后用鼠标点击想观察的应用(记事本、浏览器、WPS 等)让其获得焦点, |
4 | | -# 再用快捷键切中英文,看本脚本输出的 NATIVE 标志是否实时翻转。 |
| 1 | +# 实时探测当前前台窗口的 IME 中英文模式(外部 / IMM32 视角)。 |
5 | 2 | # |
6 | | -# 与 KBLSwitch 同套路:用 ImmGetDefaultIMEWnd + WM_IME_CONTROL/IMC_GETCONVERSIONMODE |
7 | | -# 跨线程查询,能在 Win11 新版 Notepad / TSF-only 应用里也读出来。 |
| 3 | +# 目的:模拟 KBLSwitch 这类第三方工具的探测路径,验证我们 IME 的中英文状态 |
| 4 | +# 是否能被外部进程读到。 |
| 5 | +# |
| 6 | +# 用法: |
| 7 | +# pwsh scripts/probe_ime_mode.ps1 # 默认 200ms 轮询 |
| 8 | +# pwsh scripts/probe_ime_mode.ps1 -IntervalMs 100 |
| 9 | +# |
| 10 | +# 原理: |
| 11 | +# ImmGetDefaultIMEWnd(前台窗口) -> 拿到 IMM32 default IME 窗口句柄 |
| 12 | +# SendMessageTimeout(WM_IME_CONTROL, IMC_GETCONVERSIONMODE) 跨线程查询 |
| 13 | +# IME_CMODE_NATIVE 位 = 1 即中文模式 |
| 14 | +# |
| 15 | +# 输出列含义: |
| 16 | +# Mode CN/EN/OFF/NO-IMEWND(NO-IMEWND 表示 IMM32 桥不可用,几乎可断定 |
| 17 | +# 前台是 TSF-only 客户端,如 Win11 新版记事本 / Edge / 部分 UWP) |
| 18 | +# open IMC_GETOPENSTATUS 返回值(IME 是否打开) |
| 19 | +# conv IMC_GETCONVERSIONMODE 返回值(含 NATIVE/FULLSHAPE/SYMBOL 等位) |
| 20 | +# imeWnd IMM32 default IME window 句柄,0 = CUAS 没建窗口(TSF-only) |
| 21 | +# pid 前台窗口所在进程 ID |
| 22 | +# proc 前台进程名 |
| 23 | +# win 窗口标题(截断 40 字符) |
| 24 | +# |
| 25 | +# 说明: |
| 26 | +# 1. 输出仅在状态变化时刷新,避免刷屏。 |
| 27 | +# 2. TSF compartment 是进程内状态,**任何**外部 probe 都无法跨进程直接读取; |
| 28 | +# 对 TSF-only 应用,外部观察者只能依赖 CUAS 把 TSF 状态镜像到 IMM HIMC, |
| 29 | +# 若 CUAS 没建 HIMC(imeWnd=0),就是物理上不可读。这种情况下 KBLSwitch |
| 30 | +# 之类的工具也读不到,验证只能靠功能行为(实际锁定是否生效)。 |
| 31 | + |
| 32 | +[CmdletBinding()] |
| 33 | +param( |
| 34 | + [int]$IntervalMs = 200 |
| 35 | +) |
| 36 | + |
8 | 37 | Add-Type -Namespace W -Name Ime -MemberDefinition @' |
9 | 38 | [System.Runtime.InteropServices.DllImport("user32.dll")] |
10 | 39 | public static extern System.IntPtr GetForegroundWindow(); |
| 40 | +[System.Runtime.InteropServices.DllImport("user32.dll")] |
| 41 | +public static extern uint GetWindowThreadProcessId(System.IntPtr hWnd, out uint lpdwProcessId); |
11 | 42 | [System.Runtime.InteropServices.DllImport("imm32.dll")] |
12 | 43 | public static extern System.IntPtr ImmGetDefaultIMEWnd(System.IntPtr hWnd); |
13 | 44 | [System.Runtime.InteropServices.DllImport("user32.dll", CharSet=System.Runtime.InteropServices.CharSet.Auto)] |
14 | 45 | public static extern int GetWindowText(System.IntPtr hWnd, System.Text.StringBuilder s, int n); |
15 | 46 | [System.Runtime.InteropServices.DllImport("user32.dll", CharSet=System.Runtime.InteropServices.CharSet.Auto)] |
16 | | -public static extern System.IntPtr SendMessage(System.IntPtr hWnd, uint msg, System.IntPtr wp, System.IntPtr lp); |
| 47 | +public static extern System.IntPtr SendMessageTimeout(System.IntPtr hWnd, uint msg, System.IntPtr wp, System.IntPtr lp, uint flags, uint timeoutMs, out System.IntPtr result); |
17 | 48 | '@ |
18 | 49 |
|
19 | | -$WM_IME_CONTROL = 0x0283 |
| 50 | +$WM_IME_CONTROL = 0x0283 |
20 | 51 | $IMC_GETCONVERSIONMODE = 0x0001 |
21 | 52 | $IMC_GETOPENSTATUS = 0x0005 |
22 | 53 | $IME_CMODE_NATIVE = 0x0001 |
23 | | -$IME_CMODE_FULLSHAPE = 0x0008 |
24 | | -$IME_CMODE_SYMBOL = 0x0400 |
| 54 | +$SMTO_ABORTIFHUNG = 0x0002 |
25 | 55 |
|
26 | | -while ($true) { |
| 56 | +function Get-ImmView { |
27 | 57 | $hwnd = [W.Ime]::GetForegroundWindow() |
| 58 | + if ($hwnd -eq [IntPtr]::Zero) { |
| 59 | + return [pscustomobject]@{ Mode='?'; Open=0; Conv=0; ImeWnd=[IntPtr]::Zero; Pid=0; Proc=''; Title='' } |
| 60 | + } |
| 61 | + |
28 | 62 | $sb = New-Object System.Text.StringBuilder 256 |
29 | 63 | [W.Ime]::GetWindowText($hwnd, $sb, 256) | Out-Null |
| 64 | + $title = $sb.ToString() |
| 65 | + if ($title.Length -gt 40) { $title = $title.Substring(0, 40) } |
| 66 | + |
| 67 | + $procId = 0 |
| 68 | + [W.Ime]::GetWindowThreadProcessId($hwnd, [ref]$procId) | Out-Null |
| 69 | + $procName = '' |
| 70 | + try { |
| 71 | + $procName = (Get-Process -Id $procId -ErrorAction Stop).ProcessName |
| 72 | + } catch {} |
30 | 73 |
|
31 | 74 | $imeWnd = [W.Ime]::ImmGetDefaultIMEWnd($hwnd) |
32 | 75 | $open = 0; $conv = 0 |
33 | 76 | if ($imeWnd -ne [IntPtr]::Zero) { |
34 | | - $open = [W.Ime]::SendMessage($imeWnd, $WM_IME_CONTROL, [IntPtr]$IMC_GETOPENSTATUS, [IntPtr]::Zero).ToInt32() |
35 | | - $conv = [W.Ime]::SendMessage($imeWnd, $WM_IME_CONTROL, [IntPtr]$IMC_GETCONVERSIONMODE, [IntPtr]::Zero).ToInt32() |
| 77 | + # SendMessageTimeout 避免目标线程阻塞时主循环卡死。 |
| 78 | + $result = [IntPtr]::Zero |
| 79 | + if ([W.Ime]::SendMessageTimeout($imeWnd, $WM_IME_CONTROL, [IntPtr]$IMC_GETOPENSTATUS, [IntPtr]::Zero, $SMTO_ABORTIFHUNG, 200, [ref]$result) -ne [IntPtr]::Zero) { |
| 80 | + $open = $result.ToInt32() |
| 81 | + } |
| 82 | + if ([W.Ime]::SendMessageTimeout($imeWnd, $WM_IME_CONTROL, [IntPtr]$IMC_GETCONVERSIONMODE, [IntPtr]::Zero, $SMTO_ABORTIFHUNG, 200, [ref]$result) -ne [IntPtr]::Zero) { |
| 83 | + $conv = $result.ToInt32() |
| 84 | + } |
36 | 85 | } |
37 | 86 |
|
38 | | - $mode = if (-not $open) { |
| 87 | + $mode = if ($imeWnd -eq [IntPtr]::Zero) { |
| 88 | + 'NO-IMEWND' |
| 89 | + } elseif (-not $open) { |
39 | 90 | 'OFF' |
40 | 91 | } elseif ($conv -band $IME_CMODE_NATIVE) { |
41 | 92 | 'CN' |
42 | 93 | } else { |
43 | 94 | 'EN' |
44 | 95 | } |
| 96 | + |
| 97 | + return [pscustomobject]@{ |
| 98 | + Mode = $mode; Open = $open; Conv = $conv; ImeWnd = $imeWnd |
| 99 | + Pid = $procId; Proc = $procName; Title = $title |
| 100 | + } |
| 101 | +} |
| 102 | + |
| 103 | +Write-Host ("Probing IME mode via IMM32 path (interval={0}ms)" -f $IntervalMs) -ForegroundColor Cyan |
| 104 | +Write-Host "Ctrl+C 退出。状态变化时才输出,不刷屏。" -ForegroundColor Cyan |
| 105 | +Write-Host "" |
| 106 | + |
| 107 | +$lastSignature = $null |
| 108 | +while ($true) { |
45 | 109 | $stamp = (Get-Date).ToString('HH:mm:ss.fff') |
46 | | - $title = $sb.ToString() |
47 | | - if ($title.Length -gt 40) { $title = $title.Substring(0, 40) } |
48 | | - "$stamp $mode open=$open conv=0x{0:X4} imeWnd=0x{1:X} win=[{2}]" -f $conv, $imeWnd.ToInt64(), $title |
49 | | - Start-Sleep -Milliseconds 200 |
| 110 | + $v = Get-ImmView |
| 111 | + |
| 112 | + $signature = "{0}|{1}|{2}|{3}|{4}" -f $v.Mode, $v.Conv, $v.Open, $v.Pid, $v.Title |
| 113 | + if ($signature -ne $lastSignature) { |
| 114 | + $color = switch ($v.Mode) { |
| 115 | + 'CN' { 'Green' } |
| 116 | + 'EN' { 'Yellow' } |
| 117 | + 'OFF' { 'DarkGray' } |
| 118 | + 'NO-IMEWND' { 'Magenta' } |
| 119 | + default { 'White' } |
| 120 | + } |
| 121 | + $line = "{0} {1,-9} open={2} conv=0x{3:X4} imeWnd=0x{4:X} pid={5,-6} proc={6,-20} win=[{7}]" -f ` |
| 122 | + $stamp, $v.Mode, $v.Open, $v.Conv, $v.ImeWnd.ToInt64(), $v.Pid, $v.Proc, $v.Title |
| 123 | + Write-Host $line -ForegroundColor $color |
| 124 | + $lastSignature = $signature |
| 125 | + } |
| 126 | + |
| 127 | + Start-Sleep -Milliseconds $IntervalMs |
50 | 128 | } |
0 commit comments