简介:面向需要在AutoCAD 2010中定制屏幕菜单的ObjectArx开发者,这份示例工程演示了通过CAcUiDockControlBar派生自定义控制条,并完成注册、事件响应与布局管理的关键流程。工程源码包含DockControlBar实现、子对话框、入口函数与资源定义,有助于理解ARX与MFC协作构建用户界面的机制。压缩包共16个文件,以6个cpp源文件、5个h头文件为主,另有sln/vcproj工程文件、rc资源脚本及SUO状态文件,整体仅31KB,适合快速阅读和对照学习。该示例已吸引1227人学习查看,说明其对于初次接触AutoCAD界面扩展的开发者具有较高参考价值。借助这套代码,可以掌握DockControlBar的创建和停靠控制,学会将自定义命令与菜单项关联,并了解在ARX启动注册阶段集成UI的典型写法,为后续开发完整CAD工具集打下基础。 做AutoCAD二次开发有些年头了,菜单、工具栏、Ribbon这些常规交互几乎都用ObjectARX做过一轮。但前阵子接到一个行业绘图增强需求,甲方点名要复刻那种老式的“屏幕菜单”——菜单面板沿着绘图区右侧常驻,点击即走,命令即发。这个需求乍一看不复杂,真正落地时才发现里面全是细节。最后我是用ObjectARX配合DockControlBar实现完整的屏幕菜单面板,整个过程踩了不少坑,也沉淀出一些经验。这篇文章就把这套实现思路、关键代码和个人踩坑心得完整梳理一遍,给要做类似功能的朋友一个可参考的路径。
1. 方案选型:屏幕菜单为什么用 DockControlBar
1.1 屏幕菜单解决什么痛点
AutoCAD自带的屏幕菜单默认隐藏,很多年轻用户甚至没见过它。但对老工程师来说,屏幕菜单是他们几十年的肌肉记忆:不需要去顶部菜单逐级翻找,不需要记命令行,命令面板常驻屏幕边缘,点一下就触发对应操作。尤其在做专业图纸时,把几十条高频命令按“绘图、修改、标注、图库”分层组织,效率比多少级下拉菜单都来得直接。
我这次的需求就是典型的场景驱动:目标用户是一群用惯了经典界面的工程师,他们不需要复杂的Ribbon定制,只需要一块稳定、跟手、点击即执行的面板。屏幕菜单的价值就在这里——它天然适合“量大、分级、高频”的命令集合,比堆一排工具栏更整齐,比重做Ribbon面板成本低得多。
1.2 DockControlBar 与 PaletteSet 的取舍
很多同行接到这个需求,第一反应是用CAdUiPaletteSet。确实,PaletteSet是做属性面板、图层面板的常用方案,但屏幕菜单有一个特殊要求:它必须紧贴绘图窗口边缘,点击菜单后不能打断当前命令状态。PaletteSet在设计上更偏向“工具窗口”,带标题栏、默认有折叠动画,焦点管理和刷新策略偏重,交互上总有一种“面板抢走了控制权”的生涩感。
DockControlBar则不一样。它来自AdUi底层的一套停靠机制,本质上把MFC的CControlBar挂进AutoCAD主框架,行为更接近“CAD主窗口的一部分”。我对比两个方案时列了张表:
| 对照项 | DockControlBar | CAdUiPaletteSet |
|---|---|---|
| 窗口本质 | MFC控制栏,停靠机制直接 | AdUi工具窗口,独立绘制 |
| 点击焦点行为 | 可配合WS_EX_NOACTIVATE做到点击不抢焦点 | 默认会激活面板,需要额外处理 |
| 停靠灵活度 | 可在主框架四个边缘任意停靠 | 固定区域浮动,行为较受限 |
| 与命令交互 | 点击后直接释放控制权 | 面板拥有独立消息循环,容易中断命令 |
| 适合场景 | 经典屏幕菜单、命令面板 | 属性检查器、图元管理器 |
这个对比不一定适用于所有情况,但针对“屏幕菜单”这个具体形态,DockControlBar明显更贴题。实际做完以后我也确认,那种点完菜单立即画图、键盘输入不中断的体验,只有DockControlBar能做到位。
1.3 模块划分与类设计
动工前我先定了模块边界,避免后面代码搅成一团。整个方案拆成四个部分:
- 模块入口类:负责ARX加载和卸载时注册命令、创建与回收控制栏;
- 面板主类CScreenMenuBar:从CAdUiDockControlBar派生,负责停靠、尺寸、可见性;
- 菜单视图控件:内部使用CTreeCtrl,树节点通过ItemData绑定命令ID;
- 命令分发器:把节点点击翻译成acedCommand调用,并处理命令状态刷新。
这样拆的好处是,面板UI和命令逻辑不耦合。后面如果想把树换成图标网格,或者增加搜索框,只需要动视图控件那一层,命令分发器完全不用改。实际开发时,我把命令去重、菜单配置外置到XML也是基于这个结构做的,扩展很顺手。
2. 核心原理:DockControlBar 在 AutoCAD 里如何工作
2.1 CAdUiDockControlBar 与 MFC 消息循环
理解DockControlBar的关键,是先明白AutoCAD主程序本身是个MFC应用,ARX模块以动态库方式加载进同一个进程。也就是说,我们的面板不是独立进程的窗口,而是在AutoCAD主框架窗口上创建的子窗口,并注册到主框架的停靠管理机制中。
CAdUiDockControlBar正是AdUi库对MFC控制栏的封装扩展。它复用CFrameWnd::DockControlBar那套停靠逻辑,同时针对AutoCAD做了适配。创建成功后,AutoCAD负责处理拖动、吸附、浮动这些行为,不需要开发者手动维护窗口位置。
这里有个非常关键的坑:AutoCAD主框架并非在ARX加载一开始就绪。如果直接在acrxEntryPoint的kInitAppMsg里创建控制栏,大概率会拿到空的框架指针。正确做法是推迟到一段延时任务或某个用户命令触发后再创建,我使用的是注册一个“SCREENMENU”命令,由用户执行时完成面板创建,这样最稳妥。
2.2 菜单的组织方式:树控件还是按钮组
屏幕菜单的传统样式是一列按层级“下钻”的大按钮——顶层命令列表,点一下就进入下一层。但在MFC面板里复刻这种效果,我评估了两种方案:
- 树控件CTreeCtrl:天然支持多级层级,节点自带展开收起状态,实现简单,显示密度稳定。缺点是需要自己处理高亮、图标索引和项选中后的视觉反馈。
- 自绘按钮组:更像老版屏幕菜单,观感复古,但动态增删层级、做状态高亮都要手动管理,工程量大不少。
我最终选了CTreeCtrl,但做了一层样式定制:隐藏了根节点前的展开箭头,通过点击事件自行判断是“展开子层”还是“执行命令”。这样展示层级时不会像普通文件资源管理器那样“树感”太强,视觉上更接近经典屏幕菜单。
2.3 焦点问题:为什么点完菜单命令就断
这是整个实现里最折磨人的问题,值得提前说透。
普通窗口被鼠标点击后会自动获得焦点,这本来没啥。但在AutoCAD里,命令行的输入目标是绘图文档窗口。如果面板把焦点抢走了,用户点完菜单想继续画图,键盘输入就全乱了,要么按了没反应,要么命令中断。我第一次做的时候就被这个问题坑得很惨:菜单点得飞快,图怎么画都不对。
解决办法有两层。第一层,在创建面板时给窗口加WS_EX_NOACTIVATE样式,告诉系统“这个窗口不需要被激活”,鼠标点击时焦点仍然留在原来的绘图窗口。第二层,在触发命令时用acedCommand直接执行,而不是把字符串发给命令行队列,这样命令是立即在当前文档上下文中执行的,不依赖后续焦点状态。
3. 实操过程:从建工程到屏幕菜单跑起来
3.1 工程配置与资源准备
开发环境上,我常用的是Visual Studio加对应版本的ObjectARX SDK,具体版本需要根据目标AutoCAD版本选,比如AutoCAD 2020配VS2019,AutoCAD 2024配VS2022。工程类型选择MFC DLL,配置里务必打开“使用MFC共享库”,否则CAdUiDockControlBar的链接会出各种幺蛾子。字符集统一用Unicode,这个不用犹豫。
资源准备方面,DockControlBar本身是窗口,不需要对话框模板,直接Create就行。资源文件主要放三类东西:控制栏标题的字符串资源、树节点用的位图和图标、命令执行时可能用到的小按钮图片。我习惯把图标统一整理成一个16x16的图标列表,树节点通过索引引用,避免每棵节点都加载一遍位图。
3.2 创建并注册 DockControlBar
重点代码是控制栏类的创建和停靠注册。下面这段是我核心实现,关键逻辑都标了注释:
// ScreenMenuBar.h class CScreenMenuBar : public CAdUiDockControlBar { public: CScreenMenuBar() = default; BOOL Create(LPCTSTR lpszTitle, CWnd* pParentWnd); protected: CTreeCtrl m_tree; afx_msg void OnTreeClick(NMHDR* pNMHDR, LRESULT* pResult); DECLARE_MESSAGE_MAP() };// ScreenMenuBar.cpp BOOL CScreenMenuBar::Create(LPCTSTR lpszTitle, CWnd* pParentWnd) { // CSize(240, 600) 指定初始尺寸,比例按屏幕边缘区域设计 if (!CAdUiDockControlBar::Create(lpszTitle, pParentWnd, CSize(240, 600), TRUE, AFX_IDW_CONTROLBAR_FIRST + 32)) { return FALSE; } // 允许停靠在上下左右四个边缘 EnableDocking(CBRS_ALIGN_LEFT | CBRS_ALIGN_RIGHT | CBRS_ALIGN_TOP | CBRS_ALIGN_BOTTOM); // 关键:点击面板不让它抢焦点 ModifyStyleEx(0, WS_EX_NOACTIVATE); // 创建树控件,铺满整个面板 m_tree.Create(WS_CHILD | WS_VISIBLE | TVS_HASLINES | TVS_LINESATROOT, CRect(0, 0, 0, 0), this, 1001); // 填充菜单数据,每个节点通过 SetItemData 绑定命令 ID PopulateTree(m_tree); return TRUE; }然后在模块入口处完成停靠注册。我之前强调过不能在ARX加载时直接创建,稳妥的做法是注册一个“SCREENMENU”命令,用户执行后创建:
static CScreenMenuBar* g_pScreenMenu = nullptr; static void CreateScreenMenuBar() { if (g_pScreenMenu != nullptr) return; CFrameWnd* pFrame = acedGetAcadFrame(); if (pFrame == nullptr) return; g_pScreenMenu = new CScreenMenuBar(); if (!g_pScreenMenu->Create(_T("专业屏幕菜单"), pFrame)) { delete g_pScreenMenu; g_pScreenMenu = nullptr; return; } pFrame->DockControlBar(g_pScreenMenu, AFX_IDW_DOCKBAR_RIGHT); }acedGetAcadFrame()是获取AutoCAD主框架窗口的经典入口,拿到之后创建控制栏,再通过DockControlBar把它停靠在右侧。第一次把面板拉起来的时候,那种“面板镶嵌进CAD”的感觉还是挺有成就感的。
3.3 菜单命令回调与状态联动
停靠和显示解决之后,真正的业务逻辑在点击回调里。树控件需要处理NM_CLICK通知,先判断用户点击的是目录节点还是命令节点,再进行相应操作:
void CScreenMenuBar::OnTreeClick(NMHDR* pNMHDR, LRESULT* pResult) { CTreeCtrl& tree = m_tree; CPoint pt; GetCursorPos(&pt); tree.ScreenToClient(&pt); HTREEITEM hItem = tree.HitTest(pt); if (hItem == nullptr) return; DWORD_PTR cmdId = tree.GetItemData(hItem); if (cmdId == 0) { // 目录节点:展开收起子级 tree.Expand(hItem, TVE_TOGGLE); } else { // 命令节点:交给命令分发器执行 ExecuteCommandById((int)cmdId); } *pResult = 0; }命令执行封在ExecuteCommandById里,内部用acedCommand调用真实的AutoCAD命令。我使用的是旧式acedCommand,因为它在历史SDK中兼容性最好,执行时机可控:
void CScreenMenuBar::ExecuteCommandById(int cmdId) { // cmdId 与命令表一一对应,查表得到命令字符串 const TCHAR* cmdStr = LookupCommandString(cmdId); if (cmdStr == nullptr) return; // 绘制类命令建议这样直接执行,而不是 SendStringToExecute acedCommand(RTSTR, cmdStr, RTNONE); // 较新的 SDK 也可以用 acedCmd 的变体,效果一致 }注意不要用SendStringToExecute,它会把命令放到队列里异步执行,在屏幕菜单这种高频点击场景下,用户可能连续点了好几个命令,队列会混乱。acedCommand同步执行,一次只处理一个,配合键盘输入也更顺。
状态联动我做得比较轻量:每次执行完命令,树控件会刷新一遍选中项,并且用计时器延时几百毫秒清除高亮,避免“上次点的菜单一直亮着”误导用户。
4. 踩坑记录:常见问题与排查技巧
4.1 面板抢焦点导致命令输入失效
这个坑在前面提过,这里给出完整的排查方法。出现“点完菜单后键盘输不进坐标”的现象时,先确认两件事:第一,面板窗口的扩展样式里有没有WS_EX_NOACTIVATE;第二,命令触发使用的是不是acedCommand同步执行。
我后期检查过一个客户反馈的类似问题,发现他们对面板做了皮肤美化,重写窗口过程时把原始样式弄丢了。所以提醒一句:如果代码里有自定义窗口过程,务必在WM_NCCREATE或创建完成后重新确保WS_EX_NOACTIVATE存在,不然一切努力白费。
4.2 卸载模块时崩溃或残留
ARX卸载时控制栏没有干净销毁,是常见的崩溃原因。正确顺序是:先从主框架上移除停靠关系,再销毁窗口,最后释放对象。反过来的话,主框架可能还在向已销毁的窗口发消息,直接崩溃。
我采用的清理代码如下:
static void DestroyScreenMenuBar() { if (g_pScreenMenu == nullptr) return; CFrameWnd* pFrame = acedGetAcadFrame(); if (pFrame != nullptr) pFrame->DockControlBar(g_pScreenMenu); // 先解除停靠 g_pScreenMenu->DestroyWindow(); // 销毁窗口 delete g_pScreenMenu; // 释放对象 g_pScreenMenu = nullptr; }在kUnloadAppMsg分支里调用这个方法时,还要注意当前是否已有命令在执行。如果ARX卸载发生在某条命令还没结束时,直接销毁面板易出问题。我习惯在卸载前先结束所有已注册命令,清空Undo栈的引用,再清理面板。
4.3 高DPI与不同版本适配
AutoCAD从2017版本开始逐步强化高DPI支持,MFC控件在高DPI下如果不做适配,字体和图标会糊成一片。我的做法是在程序入口处显式设置进程DPI感知,这个要在任何窗口创建之前完成:
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);另外,树控件在高DPI下高度需要动态计算,不能写死。我在OnSize里根据当前DPI缩放行高,实测在125%、150%缩放下都能正常显示。
不同ObjectARX版本对MFC控件的支持也有差异。我整理了一个对照表供参考:
| ObjectARX版本 | 对应VC++ | 常见注意点 |
|---|---|---|
| 2020 | VS2019 | 默认Unicode,高DPI需手动适配 |
| 2022 | VS2022 | CAdUiDockControlBar头文件路径变化 |
| 2024 | VS2022 | 推荐使用Per-Monitor V2 DPI感知 |
4.4 配置外置:菜单内容可维护性
最后一个值得分享的点是菜单内容不要写死在代码里。我前期为了赶进度把菜单项写死了,结果客户隔三差五要增删命令,每次都要重新编译。后来把菜单结构抽成XML,启动时解析,树节点动态生成。改动菜单只需要改配置文件,不用动代码。这也是DockControlBar方案的一个优势:面板只是容器,内容层完全可以灵活替换。
XML配置大致是这个结构:
<Menu name="专业绘图"> <Category name="绘图"> <Cmd name="直线" command="LINE" /> <Cmd name="矩形" command="RECTANG" /> </Category> <Category name="标注"> <Cmd name="线性标注" command="DIMLINEAR" /> </Category> </Menu>解析时对应生成树节点,命令名直接存到节点数据里,点击时逐层查找并执行。这样菜单维护成本和代码维护成本彻底分离,项目交付后的迭代效率提升非常明显。
最后再分享一点实际体会
这套屏幕菜单做完整理下来,我发现真正困难的不是DockControlBar本身,而是跟AutoCAD的交互节奏对齐。面板停靠、点击、命令执行、焦点归还,每一步都要顺着宿主程序的脾气来,稍微急一点就会出问题。我个人最深的体会是:界面代码宁可多写几十行,也要把焦点问题处理好;菜单内容一定要外置,否则后期维护会把自己累死。项目上线之后,几个老工程师用得很顺手,说就像回到了CAD 2004时代。如果你也要做类似功能,希望这篇文章能帮你少走一段弯路。
本文还有配套的精品资源,点击获取