LoveArx 发表于 3 小时前

论使用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>&nbsp;</div><div><br></div><div>&nbsp;
<font size="4"><b>1. Hook 机制</b></font>&nbsp;</div><div>&nbsp;使用 Windows CBT Hook 监控焦点变化:</div><div>&nbsp;
// 安装 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);
}
&nbsp;</div><div><br></div><div>&nbsp;
<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;
}
&nbsp;</div><div><br></div><div>&nbsp;
<font size="4"><b>3. IME 操作</b></font>&nbsp;</div><div><br></div><div>&nbsp;
// 检查当前是否为中文模式
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, &amp;dwConv, &amp;dwSent);

    ImmReleaseContext(hWnd, hImc);

    return bOpen &amp;&amp; (dwConv &amp; IME_CMODE_NATIVE);
}

// 设置为英文模式
void SetEnglishMode()
{
    HWND hWnd = GetFocus();
    HIMC hImc = ImmGetContext(hWnd);
    if (!hImc) return;

    ImmSetOpenStatus(hImc, FALSE);
    ImmReleaseContext(hWnd, hImc);
}
</div><div><br>&nbsp;

<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>&nbsp;</div><div><br></div><div>&nbsp;
<font size="4"><b>⚠️ 1. 不要在 Hook 回调中调用 AutoCAD API</b></font>&nbsp;</div><div><br></div><div>&nbsp;在 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-&gt;m_hWnd : NULL;
}
&nbsp;</div><div><br></div><div>&nbsp;
<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-&gt;m_hWnd : NULL;


<font color="#f59e0b"><b>⚠️ 可能有问题:</b></font>

HWND hWnd = pCmdLine-&gt;GetSafeHwnd();
// 跨 MFC DLL 调用成员函数可能出问题
&nbsp;</div><div><br></div><div>&nbsp;
<font size="4"><b>⚠️ 3. 区分目标区域内部切换</b></font>

目标区域内部切换(如从 WPF 命令行切到动态输入框的 Edit)不应该触发"进入/离开"事件:


bool bGainInTarget = IsTargetEditArea(hWndGainFocus);
bool bLoseInTarget = IsTargetEditArea(hWndLoseFocus);

if (bGainInTarget &amp;&amp; bLoseInTarget) {
    // 目标区域内部切换(如 WPF→Edit),不处理
}
else if (bGainInTarget &amp;&amp; !bLoseInTarget) {
    // 进入目标区域,切换到英文
    OnEnterEditArea(hWndGainFocus);
}
else if (!bGainInTarget &amp;&amp; bLoseInTarget) {
    // 离开目标区域
    OnLeaveEditArea(hWndLoseFocus);
}
&nbsp;</div><div><br></div><div>&nbsp;
<font size="4"><b>⚠️ 4. 使用状态标志避免重复处理</b></font>


// 进入时
if (! m_bInEditBox) {
    OnEnterEditArea(hWnd);
}

// 离开时
if (m_bInEditBox) {
    OnLeaveEditArea(hWnd);
}
&nbsp;</div><div><br></div><div>&nbsp;
<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:
    // 太早了!命令行可能未创建
    // 可能导致输入法冻结
&nbsp;</div><div><br></div><div>&nbsp;
<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>&nbsp;</div><div><br></div><div>&nbsp;
<font size="4"><b>Q1: 首字母丢失</b></font>&nbsp;</div><div><b>原因:</b>在 Hook 回调中调用了 AutoCAD API,触发了命令行激活。&nbsp;</div><div><b>解决:</b>命令行句柄在 Enable() 时获取并缓存,Hook 回调中不调用任何 AutoCAD API。&nbsp;</div><div><br></div><div>&nbsp;
<font size="4"><b>Q2: 输入法冻结</b></font>&nbsp;</div><div><b>原因:</b>在 kInitAppMsg 时调用 IMM API,AutoCAD 尚未完全初始化。&nbsp;</div><div><b>解决:</b>在 kLoadDwgMsg 时启用功能。&nbsp;</div><div><br></div><div>&nbsp;
<font size="4"><b>Q3: HwndWrapper 未被识别</b></font>&nbsp;</div><div><b><br></b></div><div><b>原因:</b>之前的代码用位置判断 HwndWrapper 是否在命令行区域内,但获取的命令行句柄可能不正确。</div><div><b>解决:</b>简化判断,只要类名包含 HwndWrapper 且属于 AutoCAD 进程就认为是目标区域。&nbsp;</div><div><br></div><div>&nbsp;
<font size="4"><b>Q4: 目标区域内部切换触发重复处理</b></font>&nbsp;</div><div><b><br></b></div><div><b>原因:</b>焦点从 WPF 命令行切到动态输入 Edit 时,被判断为"离开+进入"。&nbsp;</div><div><b>解决:</b>判断时检查 bGainInTarget &amp;&amp; bLoseInTarget,这种情况不处理。</div><div><br>&nbsp;

<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>&nbsp;</div><div><br></div><div>&nbsp;
#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]
查看完整版本: 论使用HOOK实现命令行窗口自动切换英文输入