找回密码
 立即注册

QQ登录

只需一步,快速开始

扫一扫,访问微社区

查看: 10|回复: 0

[研讨] 论使用HOOK实现命令行窗口自动切换英文输入

[复制链接]

已领礼包: 13个

财富等级: 恭喜发财

发表于 7 小时前 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
本帖最后由 LoveArx 于 2025-11-27 14:42 编辑

🔤 AutoCAD 输入法自动切换 - 技术总结
ObjectARX 开发技术文档

一、功能概述 在 AutoCAD 中,当焦点进入命令行或动态输入框时,自动将输入法切换为英文模式,避免用户手动切换输入法。 目标区域:

区域说明
传统命令行AutoCAD 2015 及以前版本的 MFC 命令行
WPF 命令行AutoCAD 2016 及以后版本的 WPF 命令行
动态输入框在绘图区光标旁边弹出的输入框



二、技术路线 整体架构:
┌─────────────────────────────────────────────────────────────┐ │ 整体架构 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ CBT Hook │───→│ 焦点变化检测 │───→│ IME 状态切换 │ │ │ │ (WH_CBT) │ │ │ │ (IMM API) │ │ │ └──────────┘ └──────────────┘ └──────────────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ HCBT_SETFOCUS IsTargetEditArea() ImmSetOpenStatus() │ │ │ └─────────────────────────────────────────────────────────────┘

核心流程:

用户按键 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 状态(保持当前状态)


三、核心组件 

  1. Hook 机制 
 使用 Windows CBT Hook 监控焦点变化:
 
  1. // 安装 Hook
  2. m_hHook = SetWindowsHookEx(
  3. WH_CBT, // CBT Hook 类型
  4. CBTHookProc, // 回调函数
  5. NULL, // 本进程
  6. GetCurrentThreadId() // 当前线程
  7. );
  8. // Hook 回调
  9. LRESULT CALLBACK CBTHookProc(int nCode, WPARAM wParam, LPARAM lParam)
  10. {
  11. if (nCode == HCBT_SETFOCUS)
  12. {
  13. HWND hWndGainFocus = (HWND)wParam; // 获得焦点的窗口
  14. HWND hWndLoseFocus = (HWND)lParam; // 失去焦点的窗口
  15. // 处理焦点变化...
  16. }
  17. return CallNextHookEx(m_hHook, nCode, wParam, lParam);
  18. }
 

  2. 目标区域识别

区域识别方法
传统命令行acedGetAcadDockCmdLine() / acedGetAcadTextCmdLine() 的子窗口
WPF 命令行类名包含 HwndWrapper
动态输入框父窗口链中有 CAcDynInputWndControl

  1. // 判断是否为 WPF 命令行
  2. bool IsWpfCommandLine(HWND hWnd) const
  3. {
  4. if (!hWnd) return false;
  5. if (! IsAutoCADWindow(hWnd)) return false;
  6. TCHAR szClassName[256] = {0};
  7. GetClassName(hWnd, szClassName, _countof(szClassName));
  8. // 只要是 HwndWrapper 就认为是目标区域
  9. if (_tcsstr(szClassName, _T(\"HwndWrapper\")) != nullptr)
  10. return true;
  11. return false;
  12. }
  13. // 判断是否为动态输入窗口
  14. bool IsDynamicInputWindow(HWND hWnd) const
  15. {
  16. if (! hWnd) return false;
  17. if (!IsAutoCADWindow(hWnd)) return false;
  18. TCHAR szClassName[256] = {0};
  19. GetClassName(hWnd, szClassName, _countof(szClassName));
  20. // Edit 控件,检查父窗口链
  21. if (_tcsicmp(szClassName, _T(\"Edit\")) == 0)
  22. {
  23. HWND hParent = GetParent(hWnd);
  24. while (hParent)
  25. {
  26. TCHAR szParentText[64] = {0};
  27. GetWindowText(hParent, szParentText, _countof(szParentText));
  28. if (_tcsstr(szParentText, _T(\"CAcDynInputWndControl\")) != nullptr)
  29. return true;
  30. hParent = GetParent(hParent);
  31. }
  32. }
  33. return false;
  34. }
 

  3. IME 操作 

 
  1. // 检查当前是否为中文模式
  2. bool IsChineseMode() const
  3. {
  4. HWND hWnd = GetFocus();
  5. HIMC hImc = ImmGetContext(hWnd);
  6. if (!hImc) return false;
  7. BOOL bOpen = ImmGetOpenStatus(hImc);
  8. DWORD dwConv = 0, dwSent = 0;
  9. ImmGetConversionStatus(hImc, &dwConv, &dwSent);
  10. ImmReleaseContext(hWnd, hImc);
  11. return bOpen && (dwConv & IME_CMODE_NATIVE);
  12. }
  13. // 设置为英文模式
  14. void SetEnglishMode()
  15. {
  16. HWND hWnd = GetFocus();
  17. HIMC hImc = ImmGetContext(hWnd);
  18. if (!hImc) return;
  19. ImmSetOpenStatus(hImc, FALSE);
  20. ImmReleaseContext(hWnd, hImc);
  21. }

 

四、关键注意点 

  ⚠️ 1. 不要在 Hook 回调中调用 AutoCAD API 

 在 Hook 回调中调用 AutoCAD API(如 acedGetAcadDockCmdLine())会触发命令行激活,导致首字母丢失。 ❌ 错误写法:
  1. LRESULT CALLBACK CBTHookProc(... ) {
  2. acedGetAcadDockCmdLine(); // 不要在这里调用!
  3. }
✅ 正确写法:
  1. bool CImeAutoSwitch::Enable() {
  2. CWnd* p = acedGetAcadDockCmdLine();
  3. s_hDockCmdLine = p ? p->m_hWnd : NULL;
  4. }
 

  ⚠️ 2. 使用 m_hWnd 而不是 GetSafeHwnd() 根据 ObjectARX 文档,跨 MFC DLL 时直接使用 m_hWnd 更安全: ✅ 推荐:
  1. CWnd* pCmdLine = acedGetAcadDockCmdLine();
  2. HWND hWnd = pCmdLine ? pCmdLine->m_hWnd : NULL;
⚠️ 可能有问题:
  1. HWND hWnd = pCmdLine->GetSafeHwnd();
  2. // 跨 MFC DLL 调用成员函数可能出问题
 

  ⚠️ 3. 区分目标区域内部切换 目标区域内部切换(如从 WPF 命令行切到动态输入框的 Edit)不应该触发"进入/离开"事件:
  1. bool bGainInTarget = IsTargetEditArea(hWndGainFocus);
  2. bool bLoseInTarget = IsTargetEditArea(hWndLoseFocus);
  3. if (bGainInTarget && bLoseInTarget) {
  4. // 目标区域内部切换(如 WPF→Edit),不处理
  5. }
  6. else if (bGainInTarget && !bLoseInTarget) {
  7. // 进入目标区域,切换到英文
  8. OnEnterEditArea(hWndGainFocus);
  9. }
  10. else if (!bGainInTarget && bLoseInTarget) {
  11. // 离开目标区域
  12. OnLeaveEditArea(hWndLoseFocus);
  13. }
 

  ⚠️ 4. 使用状态标志避免重复处理
  1. // 进入时
  2. if (! m_bInEditBox) {
  3. OnEnterEditArea(hWnd);
  4. }
  5. // 离开时
  6. if (m_bInEditBox) {
  7. OnLeaveEditArea(hWnd);
  8. }
 

  ⚠️ 5. 正确的启用时机 ✅ 正确:kLoadDwgMsg
  1. case AcRx::kLoadDwgMsg:
  2. CImeAutoSwitch::GetInstance().Enable();
  3. break;
  4. // 此时命令行已创建
❌ 错误:kInitAppMsg
  1. case AcRx::kInitAppMsg:
  2. // 太早了!命令行可能未创建
  3. // 可能导致输入法冻结
 

  ⚠️ 6. DockCmdLine 和 TextCmdLine 的区别

函数返回窗口
acedGetAcadDockCmdLine()可停靠的命令行面板(外层容器)
acedGetAcadTextCmdLine()文本命令行窗口(实际输入区域)

┌─────────────────────────────────────────────────┐ │ AutoCAD 命令行面板 (DockCmdLine) │ │ ┌───────────────────────────────────────────┐ │ │ │ 命令: LINE │ │ ← TextCmdLine │ │ 指定第一点: │ │ (文本输入区) │ │ _ │ │ │ └───────────────────────────────────────────┘ │ │ [最近输入] [历史记录] [其他按钮...] │ └─────────────────────────────────────────────────┘ ↑ DockCmdLine (整个可停靠面板)



五、代码结构

CImeAutoSwitch (单例) │ ├── Enable() // 启用,获取命令行句柄,安装 Hook ├── Disable() // 禁用,卸载 Hook │ ├── CBTHookProc() // 静态回调,监控焦点变化 │ ├── IsTargetEditArea() // 判断是否在目标区域 │ │ ├── IsCommandLineWindow() // 传统命令行 │ │ ├── IsWpfCommandLine() // WPF 命令行 │ │ └── IsDynamicInputWindow() // 动态输入 │ │ │ ├── OnEnterEditArea() // 进入:切换到英文 │ └── OnLeaveEditArea() // 离开:不做操作 │ ├── IsChineseMode() // 检查当前是否中文 ├── SetEnglishMode() // 切换到英文 │ └── 静态成员 ├── s_hDockCmdLine // 命令行面板句柄 └── s_hTextCmdLine // 文本命令行句柄



六、兼容性

AutoCAD 版本命令行类型识别方法
2015 及以前传统 MFCacedGetAcadDockCmdLine() 子窗口
2016 及以后WPF类名含 HwndWrapper
所有版本动态输入父窗口含 CAcDynInputWndControl


七、常见问题 

  Q1: 首字母丢失 
原因:在 Hook 回调中调用了 AutoCAD API,触发了命令行激活。 
解决:命令行句柄在 Enable() 时获取并缓存,Hook 回调中不调用任何 AutoCAD API。 

  Q2: 输入法冻结 
原因:在 kInitAppMsg 时调用 IMM API,AutoCAD 尚未完全初始化。 
解决:在 kLoadDwgMsg 时启用功能。 

  Q3: HwndWrapper 未被识别 

原因:之前的代码用位置判断 HwndWrapper 是否在命令行区域内,但获取的命令行句柄可能不正确。
解决:简化判断,只要类名包含 HwndWrapper 且属于 AutoCAD 进程就认为是目标区域。 

  Q4: 目标区域内部切换触发重复处理 

原因:焦点从 WPF 命令行切到动态输入 Edit 时,被判断为"离开+进入"。 
解决:判断时检查 bGainInTarget && bLoseInTarget,这种情况不处理。

 

八、依赖 

 
  1. #include
  2. #include
  3. #pragma comment(lib, \"imm32. lib\")
  4. // ObjectARX
  5. #include // acedGetAcadDockCmdLine, acedGetAcadTextCmdLine



AutoCAD 输入法自动切换 - 技术总结 | ObjectARX 开发 | 2025
论坛插件加载方法
发帖求助前要善用【论坛搜索】功能,那里可能会有你要找的答案;
如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请把帖子标题加上【已解决】;
如何回报帮助你解决问题的坛友,一个好办法就是给对方加【D豆】,加分不会扣除自己的积分,做一个热心并受欢迎的人!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|申请友链|Archiver|手机版|小黑屋|辽公网安备|晓东CAD家园 ( 辽ICP备15016793号 )

GMT+8, 2025-11-27 21:47 , Processed in 0.188810 second(s), 28 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表