一、目标
在 CATIA V5 的 CATStateCommand 中捕获用户的键盘输入(包括普通字母键、方向键、功能键以及 Ctrl/Shift/Alt 组合键)。
二、结论先行
| 方案 | 是否可行 | 备注 |
|---|---|---|
| CAA 公开 API(CATCommand 通知、CATDialogAgent、AddCallback 等) | 不可行 | 普通按键事件不会通过 CAA 公开的事件/通知体系传播 |
Windows SetWindowsHookEx(WH_KEYBOARD) 线程钩子 |
可行 ✓ | 在 Windows 消息循环层面拦截,线程级、无需 DLL 注入 |
最终采用方案:Windows WH_KEYBOARD 线程级键盘钩子。
三、失败方案详细记录
方案 1:重写 AnalyseNotification
CATBoolean TestCmd::AnalyseNotification(CATCommand* cmd, CATNotification* notif)
- 预期:作为 CATCommand 的虚函数,收到传播到本命令的所有通知
- 实际:从未被调用。在 CATStateCommand 中,Dialog Engine 接管了通知分发,不会调用用户重写的
AnalyseNotification
方案 2:AddAnalyseNotificationCB + CATKeyboardEvent
AddAnalyseNotificationCB(pViewer, CATKeyboardEvent::ClassName(),
(CATCommandMethod)&TestCmd::OnKeyEvent, NULL);
- 预期:订阅 CATViewer 上的键盘事件通知
- 实际:回调注册成功(
CB > 0),但从未触发 - 原因:CATViewer 不在 TestCmd 的父命令树分支上。
AddAnalyseNotificationCB只能接收沿 CATCommand 树向上冒泡的通知,而键盘事件根本不通过 CATCommand 通知机制传播
方案 3:CATDialogAgent + AcceptOnNotify
CATDialogAgent* pAgent = newCATDialogAgent("KeyAgent");
pAgent->AcceptOnNotify(pViewer, "CATKeyboardEvent");
- 预期:Dialog Engine 将键盘事件路由到 Agent
- 实际:Agent 从未触发
- 原因:Dialog Engine 不处理
CATKeyboardEvent。Agent 的AcceptOnNotify机制只对 Dialog 控件(CATDlgXxx)产生的通知有效
方案 4:AddCallback(全局函数)+ CATViewer
::AddCallback(this, pViewer, CATKeyboardEvent::ClassName(),
(CATSubscriberMethod)&TestCmd::OnKeyCallback, NULL);
- 预期:通过 CATEventSubscriber 的回调机制直接订阅 CATViewer 的键盘事件
- 实际:AB=7510(注册成功),但回调从未触发
- 原因:CATViewer 虽然是
CATEventSubscriber的子类,但它并不是键盘事件的发布者**。键盘事件由底层的CATVizViewer通过DispatchCallbacks发出,走的是CATVizViewer→CATViewer::InterpretKeybdEvent的内部管线**
方案 5:直接使用 CATVizViewer
CATVizViewer* pVizViewer = pViewer->GetVizViewer();
pVizViewer->GetKeyboardDevice();
- 预期:从
CATVizViewer获取CATKeyboardDevice并注册回调 - 实际:编译阶段直接报错
CAAV5-ERROR: unauthorized API for CAA V5: CATVizViewer - 原因:
CATVizViewer被 CATIA CAA 许可检查标记为非授权 API**,禁止在 CAA V5 合规代码中使用**
方案 6:AddCallback + CATViewer + NULL 事件名
::AddCallback(this, pViewer, NULL,
(CATSubscriberMethod)&TestCmd::OnKeyCallback, NULL);
- 预期:从 CATViewer 订阅所有事件(包括键盘)
- 实际:
CB = -1(注册失败) - 原因:
AddCallback不接受 NULL 事件名
方案 7:AddAnalyseNotificationCB(NULL, NULL) — 通配订阅
AddAnalyseNotificationCB(NULL, NULL,
(CATCommandMethod)&TestCmd::OnAnyNotif, NULL);
-
预期:捕获所有源、所有类型的 CATCommand 通知
-
**实际部分成功:只有 Ctrl 和 Shift 键触发了回调
[S1-AnalyseNotif] sender=CATNavigDefault notif=CATNotification**
-
关键发现:
-
只有修饰键(Ctrl/Shift)由
CATNavigDefault(导航命令)消费并产生通知 -
普通字母键(A、B、C…)、方向键、Enter 等完全没有任何 CATCommand 通知产生
方案 8:CATDlgEditor 输入控件
- 分析:
CATDlgEditor有GetEditModifyNotification、SetInputCallback等 API - 结论:需要 UI 控件(文本编辑框)才能接收输入,不适用于 3D 查看器场景
四、CATIA V5 键盘事件架构分析
通过以上 8 种方案的测试,得出以下结论:
物理键盘
↓
Windows 消息 (WM_KEYDOWN / WM_KEYUP)
↓
CATIA 主消息循环
↓
CATKeyboardDevice (内部类,无公开头文件)
↓
CATVizViewer::DispatchCallbacks (非授权 API)
↓
CATViewer::InterpretKeybdEvent (内部方法)
↓
┌──────────────────────────────────────────┐
│ 此处键盘事件被 CATIA 内部消费 │
│ ・修饰键 (Ctrl/Shift) → CATNavigDefault │
│ ・普通字母键 → 不产生任何 CATCommand 通知 │
│ ・快捷键 → 直接由 CommandHeader 消费 │
└──────────────────────────────────────────┘
核心发现:CATIA V5 的键盘事件从 CATKeyboardDevice 开始,经 CATVizViewer 分发到 CATViewer,然后被内部消费。这个过程不经过 CATCommand 通知树,也不触发 CATEventSubscriber 回调(即使 CATViewer 是 CATEventSubscriber 的子类)。
普通字母键的事件在 CATIA 内部处理链中静默终结,不会产生任何 CAA 层面可以订阅的通知或事件。
五、最终方案:Windows WH_KEYBOARD 线程钩子
原理
使用 SetWindowsHookEx(WH_KEYBOARD, ...) 在 Windows 消息循环层面拦截键盘消息,在 CATIA 处理之前就能看到所有按键。
关键特性
| 特性 | 说明 |
|---|---|
| 钩子类型 | WH_KEYBOARD(线程级) |
| 作用范围 | 仅当前线程(CATIA UI 线程) |
| DLL 注入 | 不需要 |
| 对 CATIA 的影响 | 通过 CallNextHookEx 传递,不影响 CATIA 正常键盘处理 |
| 捕获范围 | 所有按键,包括字母、数字、方向键、功能键、组合键 |
核心代码
头文件(TestCmd.h):
#include"CATStateCommand.h"
#include<windows.h>
classTestCmd : public CATStateCommand
{
DeclareResource(TestCmd, CATStateCommand)
public:
TestCmd();
virtual ~TestCmd();
virtualvoidBuildGraph();
private:
static LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam);
static TestCmd* _pActiveInstance;
HHOOK _hKeyboardHook;
};
实现(TestCmd.cpp 关键部分):
// 安装钩子(在 BuildGraph 中)
_hKeyboardHook = SetWindowsHookEx(
WH_KEYBOARD,
KeyboardProc,
NULL, // hInstance = NULL 表示线程钩子
GetCurrentThreadId()); // 当前线程
// 钩子回调
LRESULT CALLBACK TestCmd::KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode == HC_ACTION && _pActiveInstance)
{
BOOL isKeyDown = !(lParam & (1 << 31)); // bit31=0 表示按下
BOOL isRepeat = (lParam & (1 << 30)); // bit30=1 表示重复
if (isKeyDown && !isRepeat)
{
// wParam = 虚拟键码 (VK_xxx)
BOOL ctrl = (GetKeyState(VK_CONTROL) & 0x8000) != 0;
BOOL shift = (GetKeyState(VK_SHIFT) & 0x8000) != 0;
BOOL alt = (GetKeyState(VK_MENU) & 0x8000) != 0;
// 在这里处理按键逻辑
}
}
returnCallNextHookEx(NULL, nCode, wParam, lParam); // 必须传递
}
// 卸载钩子(在析构函数中)
if (_hKeyboardHook)
{
UnhookWindowsHookEx(_hKeyboardHook);
_hKeyboardHook = NULL;
}
注意事项
- 必须调用
CallNextHookEx:否则会阻断 CATIA 的键盘处理 - 静态回调函数:
SetWindowsHookEx要求 C 风格回调,通过静态成员_pActiveInstance访问实例 - 生命周期管理:析构时必须
UnhookWindowsHookEx,否则钩子残留会导致异常 - 线程安全:线程钩子只在安装线程(UI 线程)上触发,无跨线程问题
- 系统库链接:
user32.lib(包含SetWindowsHookEx)在 mkmk 环境下自动链接,无需手动配置
六、关键经验教训
6.1 CATIA V5 CAA 事件体系的局限性
- CATCommand 通知树:只传播
SendNotification显式发出的通知,键盘事件不在其中 - CATEventSubscriber 回调:需要明确的发布者-事件对应关系;CATViewer 虽是 EventSubscriber 子类,但不发布键盘事件
- CATDialogAgent:只处理 Dialog 控件产生的通知,不处理底层设备事件
- CATVizViewer / CATKeyboardDevice:是键盘事件的实际载体,但被标记为非授权 API****
6.2 调试方法论
- 三策略并行诊断法:同时注册多种机制(通配通知、Viewer 回调、键盘事件回调),对比哪个触发,快速定位问题
- 二分法排查:先确认事件是否存在(修饰键 vs 普通键),再缩小问题范围
- 打印回调注册返回值:
CB > 0表示注册成功但不代表会触发;CB = -1表示注册失败
6.3 CAA V5 授权限制
-
mkmk 编译器的
CAAV5-ERROR: unauthorized API检查位于预处理阶段 -
不能通过强制包含头文件或强制转换绕过
-
遇到此错误意味着该 API 在 CAA 合规级别下不可用,需要寻找替代方案
6.4 Windows Hook 作为备选
-
当 CATIA 框架层面没有公开 API 时,回退到 Windows 平台 API 是可行的
-
线程级钩子(
GetCurrentThreadId())比全局钩子安全,不影响其他进程 -
WH_KEYBOARD在消息分发到窗口过程之前触发,可以先于 CATIA 看到按键
七、扩展参考
可能的替代/增强方案
| 方案 | 说明 | 适用场景 |
|---|---|---|
WH_KEYBOARD_LL |
低级键盘钩子,捕获更底层(需单独线程) | 需要拦截系统键(如 Win 键) |
WH_GETMESSAGE |
消息钩子,可看到完整 MSG 结构 |
需要区分消息目标窗口 |
Raw Input API |
RegisterRawInputDevices+ WM_INPUT |
需要区分多个键盘设备 |
| Subclass CATIA 窗口 | SetWindowLongPtr(GWLP_WNDPROC)+ WM_KEYDOWN |
只关注特定窗口的按键 |
CATKeyboardEvent 内部键值定义(仅供参考)
LeftKey=1, RightKey=2, UpKey=3, DownKey=4
EscapeKey=11, ControlKey=13, EnterKey=14, AltKey=15
这些值在 CATKeyboardEvent.h 中定义,但由于键盘事件不通过 CAA API 传播,实际无法使用。
评论