论使用HOOK实现命令行窗口自动切换英文输入
本帖最后由 LoveArx 于 2025-11-27 14:42 编辑 <br /><br /><font size="6"><b><font color="#2563eb">🔤 AutoCAD 输入法自动切换 - 技术总结</font></b></font><div align="center"><font color="#64748b">ObjectARX 开发技术文档</font></div>
<hr class="l">
<font size="5"><b><font color="#1e40af">一、功能概述</font></b></font>
在 AutoCAD 中,当焦点进入命令行或动态输入框时,<b>自动将输入法切换为英文模式</b>,避免用户手动切换输入法。
<b>目标区域:</b><div><b><br></b>
<table class="t_table"><tbody><tr><td><b>区域</b></td><td><b>说明</b></td></tr><tr><td>传统命令行</td><td>AutoCAD 2015 及以前版本的 MFC 命令行</td></tr><tr><td>WPF 命令行</td><td>AutoCAD 2016 及以后版本的 WPF 命令行</td></tr><tr><td>动态输入框</td><td>在绘图区光标旁边弹出的输入框</td></tr></tbody></table><br>
<hr class="l">
<font size="5"><b><font color="#1e40af"><br></font></b></font><div><font size="5"><b><font color="#1e40af">二、技术路线</font></b></font>
<b>整体架构:</b>
<div class="blockcode"><blockquote>
┌─────────────────────────────────────────────────────────────┐
│ 整体架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ CBT Hook │───→│ 焦点变化检测 │───→│ IME 状态切换 │ │
│ │ (WH_CBT) │ │ │ │ (IMM API) │ │
│ └──────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ HCBT_SETFOCUS IsTargetEditArea() ImmSetOpenStatus() │
│ │
└─────────────────────────────────────────────────────────────┘
</blockquote></div>
<b><div><b><br></b></div>核心流程:</b></div><div><b><br></b>
<div class="blockcode"><blockquote>
用户按键 L
│
▼
AutoCAD 激活 WPF 命令行
│
▼
CBT Hook 捕捉 HCBT_SETFOCUS
│
├─ hWndGainFocus = HwndWrapper (目标:是)
├─ hWndLoseFocus = 绘图区 (目标:否)
│
▼
IsTargetEditArea() 判断
│
├─ IsWpfCommandLine() → 类名含 "HwndWrapper" → true
│
▼
OnEnterEditArea()
│
├─ IsChineseMode() → 检查当前是否中文
├─ SetEnglishMode() → ImmSetOpenStatus(FALSE)
├─ m_bInEditBox = TRUE
│
▼
用户输入 L(此时已是英文)
│
▼
焦点切换到 Edit(动态输入框)
│
├─ 两者都是目标区域 → 不处理
│
▼
命令完成,焦点离开
│
▼
OnLeaveEditArea()
│
├─ m_bInEditBox = FALSE
└─ 不恢复 IME 状态(保持当前状态)
</blockquote></div>
<hr class="l">
<font size="5"><b><font color="#1e40af"><br></font></b></font></div><div><font size="5"><b><font color="#1e40af">三、核心组件</font></b></font> </div><div><br></div><div>
<font size="4"><b>1. Hook 机制</b></font> </div><div> 使用 Windows CBT Hook 监控焦点变化:</div><div>
// 安装 Hook
m_hHook = SetWindowsHookEx(
WH_CBT, // CBT Hook 类型
CBTHookProc, // 回调函数
NULL, // 本进程
GetCurrentThreadId() // 当前线程
);
// Hook 回调
LRESULT CALLBACK CBTHookProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode == HCBT_SETFOCUS)
{
HWND hWndGainFocus = (HWND)wParam;// 获得焦点的窗口
HWND hWndLoseFocus = (HWND)lParam;// 失去焦点的窗口
// 处理焦点变化...
}
return CallNextHookEx(m_hHook, nCode, wParam, lParam);
}
</div><div><br></div><div>
<font size="4"><b>2. 目标区域识别</b></font></div><div><font size="4"><b><br></b></font>
<table class="t_table"><tbody><tr><td><b>区域</b></td><td><b>识别方法</b></td></tr><tr><td>传统命令行</td><td>acedGetAcadDockCmdLine() / acedGetAcadTextCmdLine() 的子窗口</td></tr><tr><td>WPF 命令行</td><td>类名包含 HwndWrapper</td></tr><tr><td>动态输入框</td><td>父窗口链中有 CAcDynInputWndControl</td></tr></tbody></table><br>
// 判断是否为 WPF 命令行
bool IsWpfCommandLine(HWND hWnd) const
{
if (!hWnd) return false;
if (! IsAutoCADWindow(hWnd)) return false;
TCHAR szClassName = {0};
GetClassName(hWnd, szClassName, _countof(szClassName));
// 只要是 HwndWrapper 就认为是目标区域
if (_tcsstr(szClassName, _T("HwndWrapper")) != nullptr)
return true;
return false;
}
// 判断是否为动态输入窗口
bool IsDynamicInputWindow(HWND hWnd) const
{
if (! hWnd) return false;
if (!IsAutoCADWindow(hWnd)) return false;
TCHAR szClassName = {0};
GetClassName(hWnd, szClassName, _countof(szClassName));
// Edit 控件,检查父窗口链
if (_tcsicmp(szClassName, _T("Edit")) == 0)
{
HWND hParent = GetParent(hWnd);
while (hParent)
{
TCHAR szParentText = {0};
GetWindowText(hParent, szParentText, _countof(szParentText));
if (_tcsstr(szParentText, _T("CAcDynInputWndControl")) != nullptr)
return true;
hParent = GetParent(hParent);
}
}
return false;
}
</div><div><br></div><div>
<font size="4"><b>3. IME 操作</b></font> </div><div><br></div><div>
// 检查当前是否为中文模式
bool IsChineseMode() const
{
HWND hWnd = GetFocus();
HIMC hImc = ImmGetContext(hWnd);
if (!hImc) return false;
BOOL bOpen = ImmGetOpenStatus(hImc);
DWORD dwConv = 0, dwSent = 0;
ImmGetConversionStatus(hImc, &dwConv, &dwSent);
ImmReleaseContext(hWnd, hImc);
return bOpen && (dwConv & IME_CMODE_NATIVE);
}
// 设置为英文模式
void SetEnglishMode()
{
HWND hWnd = GetFocus();
HIMC hImc = ImmGetContext(hWnd);
if (!hImc) return;
ImmSetOpenStatus(hImc, FALSE);
ImmReleaseContext(hWnd, hImc);
}
</div><div><br>
<hr class="l">
<font size="5"><b><font color="#1e40af"><br></font></b></font></div><div><font size="5"><b><font color="#1e40af">四、关键注意点</font></b></font> </div><div><br></div><div>
<font size="4"><b>⚠️ 1. 不要在 Hook 回调中调用 AutoCAD API</b></font> </div><div><br></div><div> 在 Hook 回调中调用 AutoCAD API(如 acedGetAcadDockCmdLine())会触发命令行激活,导致首字母丢失。
<font color="#dc2626"><b>❌ 错误写法:</b></font>
LRESULT CALLBACK CBTHookProc(... ) {
acedGetAcadDockCmdLine();// 不要在这里调用!
}
<font color="#16a34a"><b>✅ 正确写法:</b></font>
bool CImeAutoSwitch::Enable() {
CWnd* p = acedGetAcadDockCmdLine();
s_hDockCmdLine = p ?p->m_hWnd : NULL;
}
</div><div><br></div><div>
<font size="4"><b>⚠️ 2. 使用 m_hWnd 而不是 GetSafeHwnd()</b></font>
根据 ObjectARX 文档,跨 MFC DLL 时直接使用 m_hWnd 更安全:
<font color="#16a34a"><b>✅ 推荐:</b></font>
CWnd* pCmdLine = acedGetAcadDockCmdLine();
HWND hWnd = pCmdLine ? pCmdLine->m_hWnd : NULL;
<font color="#f59e0b"><b>⚠️ 可能有问题:</b></font>
HWND hWnd = pCmdLine->GetSafeHwnd();
// 跨 MFC DLL 调用成员函数可能出问题
</div><div><br></div><div>
<font size="4"><b>⚠️ 3. 区分目标区域内部切换</b></font>
目标区域内部切换(如从 WPF 命令行切到动态输入框的 Edit)不应该触发"进入/离开"事件:
bool bGainInTarget = IsTargetEditArea(hWndGainFocus);
bool bLoseInTarget = IsTargetEditArea(hWndLoseFocus);
if (bGainInTarget && bLoseInTarget) {
// 目标区域内部切换(如 WPF→Edit),不处理
}
else if (bGainInTarget && !bLoseInTarget) {
// 进入目标区域,切换到英文
OnEnterEditArea(hWndGainFocus);
}
else if (!bGainInTarget && bLoseInTarget) {
// 离开目标区域
OnLeaveEditArea(hWndLoseFocus);
}
</div><div><br></div><div>
<font size="4"><b>⚠️ 4. 使用状态标志避免重复处理</b></font>
// 进入时
if (! m_bInEditBox) {
OnEnterEditArea(hWnd);
}
// 离开时
if (m_bInEditBox) {
OnLeaveEditArea(hWnd);
}
</div><div><br></div><div>
<font size="4"><b>⚠️ 5. 正确的启用时机</b></font>
<font color="#16a34a"><b>✅ 正确:kLoadDwgMsg</b></font>
case AcRx::kLoadDwgMsg:
CImeAutoSwitch::GetInstance().Enable();
break;
// 此时命令行已创建
<font color="#dc2626"><b>❌ 错误:kInitAppMsg</b></font>
case AcRx::kInitAppMsg:
// 太早了!命令行可能未创建
// 可能导致输入法冻结
</div><div><br></div><div>
<font size="4"><b>⚠️ 6. DockCmdLine 和 TextCmdLine 的区别</b></font></div><div><font size="4"><b><br></b></font>
<table class="t_table"><tbody><tr><td><b>函数</b></td><td><b>返回窗口</b></td></tr><tr><td>acedGetAcadDockCmdLine()</td><td>可停靠的命令行面板(外层容器)</td></tr><tr><td>acedGetAcadTextCmdLine()</td><td>文本命令行窗口(实际输入区域)</td></tr></tbody></table><br>
<div class="blockcode"><blockquote>
┌─────────────────────────────────────────────────┐
│AutoCAD 命令行面板 (DockCmdLine) │
│┌───────────────────────────────────────────┐│
││ 命令: LINE ││← TextCmdLine
││ 指定第一点: ││ (文本输入区)
││ _ ││
│└───────────────────────────────────────────┘│
│[最近输入] [历史记录] [其他按钮...] │
└─────────────────────────────────────────────────┘
↑
DockCmdLine (整个可停靠面板)
</blockquote></div>
<br><hr class="l">
<font size="5"><b><font color="#1e40af"><br></font></b></font></div><div><font size="5"><b><font color="#1e40af">五、代码结构</font></b></font></div><div><font color="#1e40af" size="5"><b><br></b></font>
<div class="blockcode"><blockquote>
CImeAutoSwitch (单例)
│
├── Enable() // 启用,获取命令行句柄,安装 Hook
├── Disable() // 禁用,卸载 Hook
│
├── CBTHookProc() // 静态回调,监控焦点变化
│ ├── IsTargetEditArea() // 判断是否在目标区域
│ │ ├── IsCommandLineWindow() // 传统命令行
│ │ ├── IsWpfCommandLine() // WPF 命令行
│ │ └── IsDynamicInputWindow()// 动态输入
│ │
│ ├── OnEnterEditArea() // 进入:切换到英文
│ └── OnLeaveEditArea() // 离开:不做操作
│
├── IsChineseMode() // 检查当前是否中文
├── SetEnglishMode() // 切换到英文
│
└── 静态成员
├── s_hDockCmdLine // 命令行面板句柄
└── s_hTextCmdLine // 文本命令行句柄
</blockquote></div>
<br><hr class="l">
<font size="5"><b><font color="#1e40af"><br></font></b></font></div><div><font size="5"><b><font color="#1e40af">六、兼容性</font></b></font></div><div><font color="#1e40af" size="5"><b><br></b></font>
<table class="t_table"><tbody><tr><td><b>AutoCAD 版本</b></td><td><b>命令行类型</b></td><td><b>识别方法</b></td></tr><tr><td>2015 及以前</td><td>传统 MFC</td><td>acedGetAcadDockCmdLine() 子窗口</td></tr><tr><td>2016 及以后</td><td>WPF</td><td>类名含 HwndWrapper</td></tr><tr><td>所有版本</td><td>动态输入</td><td>父窗口含 CAcDynInputWndControl</td></tr></tbody></table><br>
<hr class="l">
<font size="5"><b><font color="#1e40af">七、常见问题</font></b></font> </div><div><br></div><div>
<font size="4"><b>Q1: 首字母丢失</b></font> </div><div><b>原因:</b>在 Hook 回调中调用了 AutoCAD API,触发了命令行激活。 </div><div><b>解决:</b>命令行句柄在 Enable() 时获取并缓存,Hook 回调中不调用任何 AutoCAD API。 </div><div><br></div><div>
<font size="4"><b>Q2: 输入法冻结</b></font> </div><div><b>原因:</b>在 kInitAppMsg 时调用 IMM API,AutoCAD 尚未完全初始化。 </div><div><b>解决:</b>在 kLoadDwgMsg 时启用功能。 </div><div><br></div><div>
<font size="4"><b>Q3: HwndWrapper 未被识别</b></font> </div><div><b><br></b></div><div><b>原因:</b>之前的代码用位置判断 HwndWrapper 是否在命令行区域内,但获取的命令行句柄可能不正确。</div><div><b>解决:</b>简化判断,只要类名包含 HwndWrapper 且属于 AutoCAD 进程就认为是目标区域。 </div><div><br></div><div>
<font size="4"><b>Q4: 目标区域内部切换触发重复处理</b></font> </div><div><b><br></b></div><div><b>原因:</b>焦点从 WPF 命令行切到动态输入 Edit 时,被判断为"离开+进入"。 </div><div><b>解决:</b>判断时检查 bGainInTarget && bLoseInTarget,这种情况不处理。</div><div><br>
<hr class="l">
<font size="5"><b><font color="#1e40af"><br></font></b></font></div><div><font size="5"><b><font color="#1e40af">八、依赖</font></b></font> </div><div><br></div><div>
#include <windows.h>
#include <imm. h="">
#pragma comment(lib, "imm32. lib")
// ObjectARX
#include <rxmfcapi.h>// acedGetAcadDockCmdLine, acedGetAcadTextCmdLine
<br><br><hr class="l">
<div align="center"><font color="#64748b">AutoCAD 输入法自动切换 - 技术总结 | ObjectARX 开发 | 2025</font></div></rxmfcapi.h></imm.></windows.h></div></div>
页:
[1]