简介:本资源是一份面向AutoCAD二次开发初学者与ObjectARX进阶实践者的完整屏幕菜单(DockControlBar)实现案例,聚焦AutoCAD 2010平台下基于ObjectARX 2010的界面扩展开发,解决自定义浮动/停靠式命令栏缺失、交互效率低等实际工程问题。压缩包共16个文件,含6个核心CPP源码(如MyDockControlBar.cpp、acrxEntryPoint.cpp)、5个H头文件(封装类声明与数据结构)、1个RC资源文件(定义菜单图标与字符串)、1个SLN解决方案及2个VCProj/SUO项目配置文件,总大小仅31KB,结构精简、模块职责清晰,便于快速编译调试与源码级理解。已有1236人学习下载,读者可直接获取可运行的ARX工程框架、完整的CAcUiDockControlBar派生类实现、资源绑定与命令注册逻辑、以及标准MFC事件响应机制(如OnCmdMsg重写),特别适合用于掌握ARX界面集成规范、复用DockControlBar模板或开展定制化CAD工具开发。
1. ARX 实现屏幕菜单(DockControlBar):不是加个按钮就完事,而是让 AutoCAD 界面真正听你指挥
很多刚接触 ObjectARX 的开发者以为,“加个屏幕菜单”就是调个acedPostCommand或者塞个CUI文件进去——结果发现菜单不响应、停靠栏位置乱跳、切换工作空间后直接消失。其实,真正的屏幕菜单(DockControlBar)是 AutoCAD 原生 UI 框架的深度延伸,它依赖 ARX 模块与 AcUi 库的协同注册、窗口消息链的主动接管、以及与 AcApDocument 生命周期的严格绑定。本资源是一套完整可运行的 ObjectARX 2010 工程源码包(含 VS2008 项目文件、编译后 ARX 文件、调试符号 PDB),聚焦于CDockControlBar的实战封装:它不依赖第三方 UI 库,不修改 CUI 文件,不劫持命令行,而是通过重载DockControlBar::OnCreateControl()、拦截WM_NOTIFY、适配AcApDocument::addReactor()实现菜单状态与当前图纸上下文实时同步。适合正在做定制化 CAD 插件、需要稳定嵌入工具面板的中高级 ARX 开发者,尤其适用于某高校机械制图教学系统、某公司标准化出图插件等需长期驻留、多文档切换不崩溃的工业级场景。
2. 为什么必须用 DockControlBar 而不是 Toolbar 或 PaletteSet?
2.1 屏幕菜单的本质:不是“显示控件”,而是“参与 AutoCAD 的停靠管理器”
AutoCAD 的 UI 架构中,CDockControlBar是 AcUi 库提供的原生停靠栏基类,它被设计为与CDockSite(即主窗口的停靠区域)深度集成。这和CAcUiPaletteSet(浮动面板)或传统CToolBar(仅限命令按钮)有根本区别:
CAcUiPaletteSet是独立窗口,无法自动吸附到状态栏/命令行上方,也不能随工作空间切换而保存位置;CToolBar在 AutoCAD 2010+ 中已被弃用,且其消息路由不经过 AcUi 消息泵,导致快捷键冲突、DPI 缩放错位;CDockControlBar则由AcUiDockManager统一调度,支持拖拽停靠、自动隐藏、多文档共享、高 DPI 自适应,并能响应ACAD_DOCKSTATE_CHANGED系统通知。
提示:本资源所有 UI 控件均继承自
CDockControlBar,而非CDialogBar或CControlBar。后者在 AutoCAD 2010 中会触发ASSERT断言(因CControlBar::EnableDocking()被禁用),这是新手最常翻车的第一步。
2.2 ObjectARX 2010 的关键约束:AcUi.lib 必须显式链接,且头文件顺序不能错
ObjectARX 2010 SDK 中,CDockControlBar定义在<acui.h>,但它的实现依赖<acuiDockManager.h>和<acuiDockSite.h>。若头文件包含顺序错误,或未链接AcUi.lib,编译会报以下三类典型错误:
error C2027: use of undefined type 'CDockControlBar' error LNK2001: unresolved external symbol "public: virtual void __thiscall CDockControlBar::OnCreateControl(void)" error C2504: 'CDockControlBar' : base class undefined正确做法是:在StdAfx.h中按如下顺序包含:
// StdAfx.h —— 必须严格按此顺序 #include "acadstr.h" #include "adesk.h" #include "rxobject.h" #include "rxboiler.h" #include "aced.h" #include "acdb.h" #include "acgi.h" #include "acui.h" // ← 必须在 acgi.h 之后、acuiDockManager.h 之前 #include "acuiDockManager.h" // ← 必须在 acui.h 之后 #include "acuiDockSite.h" // ← 必须在 acuiDockManager.h 之后同时,在项目属性 → 链接器 → 输入 → 附加依赖项中,必须显式添加AcUi.lib(注意不是AcUi2010.lib,ARX 2010 SDK 中统一为AcUi.lib)。漏掉它,即使编译通过,运行时加载 ARX 会立即弹出“无法解析 AcUi 导出函数”的错误对话框。
2.3 从零构建 DockControlBar:四步初始化流程不可跳过
一个可用的CDockControlBar不是 new 出来就能 show 的。它必须完成 AutoCAD 主窗口的“认领”过程。本资源采用标准四步法(已在CMyDockControlBar::InitInstance()中封装):
- 获取主窗口句柄:调用
acedGetAcadFrame()获取CAcAppWnd*,再通过GetSafeHwnd()转为HWND; - 创建 DockSite 实例:
CDockSite* pDockSite = new CDockSite();并调用pDockSite->Create(pMainFrame, ...); - 注册 DockControlBar 到 DockSite:
pDockSite->AddControlBar(this, AFX_IDW_DOCKBAR_TOP, FALSE);; - 设置停靠策略与初始状态:
EnableDocking(CBRS_ALIGN_ANY);+DockControlBar(pDockSite, AFX_IDW_DOCKBAR_TOP);。
注意:第 2 步中的
CDockSite必须是全局唯一实例(本资源中定义为static CDockSite* g_pDockSite = nullptr),多次 new 会导致内存泄漏和停靠逻辑错乱;第 4 步的DockControlBar()必须在OnInitDialog()之后调用,否则窗口尺寸为 0,控件不可见。
3. 核心功能实现:菜单按钮、状态同步与文档感知
3.1 自定义按钮控件:用 CButtonEx 替代 CButton,解决 DPI 缩放失真
AutoCAD 2010 默认启用 DPI 感知,但 MFC 原生CButton在缩放后图标模糊、文字锯齿。本资源采用CButtonEx(继承自CButton,重写OnPaint())实现高清渲染:
// CButtonEx.h class CButtonEx : public CButton { protected: afx_msg void OnPaint(); DECLARE_MESSAGE_MAP() }; // CButtonEx.cpp void CButtonEx::OnPaint() { CPaintDC dc(this); CRect rect; GetClientRect(&rect); // 启用双缓冲防闪烁 CDC memDC; memDC.CreateCompatibleDC(&dc); CBitmap bmp; bmp.CreateCompatibleBitmap(&dc, rect.Width(), rect.Height()); CBitmap* pOldBmp = memDC.SelectObject(&bmp); // 清空背景(使用系统颜色) memDC.FillSolidRect(&rect, ::GetSysColor(COLOR_BTNFACE)); // 绘制自定义图标(从资源 ID 加载,自动适配 DPI) HICON hIcon = AfxGetApp()->LoadIcon(IDI_MY_ICON); DrawIconEx(memDC.GetSafeHdc(), 4, 4, hIcon, 16, 16, 0, nullptr, DI_NORMAL); // 绘制文字(使用 GetTextMetrics 获取实际字体高度,避免截断) CFont* pOldFont = memDC.SelectObject(GetFont()); memDC.SetBkMode(TRANSPARENT); memDC.SetTextColor(::GetSysColor(COLOR_BTNTEXT)); CString str; GetWindowText(str); memDC.DrawText(str, &rect, DT_CENTER | DT_VCENTER | DT_SINGLELINE); dc.BitBlt(0, 0, rect.Width(), rect.Height(), &memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOldFont); memDC.SelectObject(pOldBmp); }逻辑说明:CButtonEx不依赖DrawItem()(该方法在 ARX 环境下易触发 GDI 句柄泄漏),而是完全接管OnPaint(),通过双缓冲 +DrawIconEx+DrawText组合实现 DPI 无损渲染。参数说明:DrawIconEx第 5、6 参数为图标宽高(固定 16×16),第 7 参数为帧索引(0),第 8 参数为掩码(nullptr),第 9 参数为绘制模式(DI_NORMAL表示正常绘制)。
3.2 文档状态监听:用 AcApDocManagerReactor 实现“当前图纸一变,菜单立刻响应”
屏幕菜单常需根据当前打开的图纸类型(如 DWG 版本、是否只读、图层状态)动态启用/禁用按钮。若轮询acDocManager->curDocument(),CPU 占用飙升。本资源采用AcApDocManagerReactor注册事件回调:
// CMyDocReactor.h class CMyDocReactor : public AcApDocManagerReactor { public: virtual void documentToBeActivated(const AcApDocument* pDoc) override { // 当前文档即将切换,提前保存旧状态 if (g_pDockControlBar) { g_pDockControlBar->OnDocumentChanging(pDoc); } } virtual void documentActivated(const AcApDocument* pDoc) override { // 新文档已激活,刷新菜单状态 if (g_pDockControlBar) { g_pDockControlBar->OnDocumentChanged(pDoc); } } }; // 在 ARX 初始化函数中注册 static CMyDocReactor g_docReactor; void MyArxApp::Initialize() { acDocManager->addReactor(&g_docReactor); }逻辑说明:documentToBeActivated和documentActivated是成对触发的钩子,比AcApDocumentReactor::commandWillStart()更早介入文档切换流程。参数说明:pDoc是AcApDocument*指针,可安全调用pDoc->database()获取AcDbDatabase*,进而查询dwgVersion()或isReadOnly()等属性。
3.3 命令绑定机制:不走 acedRegCmds,而是用 AcEdCommandStack 实现“按钮点击即执行”
很多教程教用acedRegCmds()->addCommand()注册命令,再让按钮调用acedPostCommand(_T("MYCMD"))。这会导致命令历史混乱、UNDO 不连贯、且无法获取命令执行上下文。本资源采用AcEdCommandStack直接压栈:
// 在按钮点击响应函数中 void CMyDockControlBar::OnBnClickedBtnExport() { // 创建命令对象(必须继承自 AcEdCommand) class CExportCmd : public AcEdCommand { public: virtual Adesk::Boolean execute(AcEdCommandContext* pCmdCtx) override { // 执行导出逻辑(此处省略具体实现) acutPrintf(_T("\nExport completed.")); return Adesk::kTrue; } }; // 压入命令栈(非注册!) AcEdCommandStack* pStack = acedGetCommandStack(); pStack->push(new CExportCmd()); }逻辑说明:push()将命令对象直接送入 AutoCAD 命令执行队列,享受完整的 UNDO/REDO、命令日志、上下文感知能力。参数说明:AcEdCommandContext* pCmdCtx包含当前文档、选择集、命令行输入等上下文,比acedPostCommand更底层、更可控。
4. 避坑:五个真实踩过的雷区与血泪修复方案
4.1 现象:ARX 加载后 DockControlBar 显示为空白,调试发现OnCreateControl()根本没被调用
原因:CDockControlBar::Create()被错误地放在OnInitDialog()之外调用,或Create()的父窗口句柄传入了NULL。CDockControlBar的Create()必须在CDialogBar::Create()成功返回后,且父窗口必须是有效的CDockSite或CAcAppWnd。
解决:确保Create()调用链为:CMyDockControlBar::Create()→CDockControlBar::Create()→CDialogBar::Create(),且Create()的第一个参数必须是pDockSite->GetSafeHwnd()(不是AfxGetMainWnd()->GetSafeHwnd())。
4.2 现象:切换工作空间(如从“草图与注释”切到“三维基础”)后,DockControlBar 消失且无法恢复
原因:AutoCAD 工作空间切换会销毁并重建 UI 元素,但CDockControlBar实例未被CDockSite重新注册。CDockSite在工作空间切换时会被析构,但g_pDockSite指针未置空,导致后续AddControlBar()失败。
解决:在CMyDocReactor::workspaceChanged()中监听工作空间变更,并强制重建CDockSite:
virtual void workspaceChanged(const TCHAR* wsName) override { if (g_pDockSite) { delete g_pDockSite; g_pDockSite = nullptr; } // 延迟重建(避免在销毁过程中创建) acedPostCommand(_T("MY_REBUILD_DOCK")); }并在命令MY_REBUILD_DOCK中重新new CDockSite()并AddControlBar()。
4.3 现象:按钮点击后 AutoCAD 崩溃,调用栈指向CDockControlBar::OnNotify()中的pNMHDR->hwndFrom为非法地址
原因:CDockControlBar接收WM_NOTIFY消息时,pNMHDR结构体由系统分配,但某些控件(如CComboBox)在DestroyWindow()后仍向已销毁的CDockControlBar发送通知。
解决:在CDockControlBar::OnNotify()中增加健壮性检查:
BOOL CMyDockControlBar::OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult) { NMHDR* pNMHDR = reinterpret_cast<NMHDR*>(lParam); if (!pNMHDR || !pNMHDR->hwndFrom || !::IsWindow(pNMHDR->hwndFrom)) { return FALSE; // 直接丢弃非法通知 } // 正常处理... return CDockControlBar::OnNotify(wParam, lParam, pResult); }4.4 现象:高 DPI 显示下按钮文字被截断,或图标位置偏移 2 像素
原因:CButtonEx::OnPaint()中硬编码了DrawIconEx的坐标(如4,4),未根据 DPI 缩放比例动态计算。Windows 125% 缩放时,GetDeviceCaps(LOGPIXELSX)返回 120,而默认坐标未乘以缩放系数。
解决:在OnPaint()开头获取缩放比例:
CDC* pDC = GetDC(); int dpiX = pDC->GetDeviceCaps(LOGPIXELSX); ReleaseDC(pDC); float scale = static_cast<float>(dpiX) / 96.0f; // 96 为标准 DPI int x = static_cast<int>(4.0f * scale); int y = static_cast<int>(4.0f * scale); DrawIconEx(..., x, y, ...);4.5 现象:ARX 卸载时 AutoCAD 崩溃,错误指向CDockSite::~CDockSite()
原因:CDockSite析构时尝试释放其管理的所有CDockControlBar,但CMyDockControlBar实例是全局静态对象(或由new分配但未delete),导致双重析构。
解决:在 ARXunload()回调中显式清理:
extern "C" AcRx::AppRetCode acrxEntryPoint(AcRx::AppMsgCode msg, void* pkt) { if (msg == AcRx::kUnloadAppMsg) { if (g_pDockControlBar) { g_pDockControlBar->DestroyWindow(); // 触发 OnDestroy() delete g_pDockControlBar; g_pDockControlBar = nullptr; } if (g_pDockSite) { delete g_pDockSite; g_pDockSite = nullptr; } } return AcRx::kRetOK; }5. 进阶技巧:让 DockControlBar 支持“最小化到状态栏”与“右键菜单定制”
5.1 实现状态栏最小化:拦截WM_SYSCOMMAND并重绘状态栏区域
AutoCAD 状态栏(ACAD_STATUSBAR)是一个标准CStatusBarCtrl,但其 HWND 不对外暴露。本资源通过FindWindowEx()定位,并利用CDockControlBar::OnSize()动态调整:
void CMyDockControlBar::OnSize(UINT nType, int cx, int cy) { CDockControlBar::OnSize(nType, cx, cy); if (nType == SIZE_MINIMIZED) { // 获取状态栏句柄(AutoCAD 2010 中固定为 "ACAD_STATUSBAR" 类名) HWND hStatusBar = FindWindowEx(AfxGetMainWnd()->GetSafeHwnd(), nullptr, _T("ACAD_STATUSBAR"), nullptr); if (hStatusBar) { // 在状态栏右侧绘制一个“恢复”按钮(模拟最小化效果) CRect rcStatus; ::GetWindowRect(hStatusBar, &rcStatus); ScreenToClient(&rcStatus); // 此处绘制小图标(代码略,使用 OwnerDraw Button) } } }更可靠的做法是:在OnSysCommand()中捕获SC_MINIMIZE,然后调用ShowWindow(SW_HIDE)并手动在状态栏区域CreateWindowEx()一个透明按钮,点击后ShowWindow(SW_SHOW)并SetFocus()。
5.2 右键菜单定制:重载OnContextMenu()并注入 AutoCAD 原生菜单项
CDockControlBar默认不响应右键,需手动启用:
// 在 Create() 后调用 ModifyStyle(0, WS_POPUP); // 允许右键 EnableToolTips(TRUE); void CMyDockControlBar::OnContextMenu(CWnd* pWnd, CPoint point) { CMenu menu; menu.CreatePopupMenu(); // 添加自定义项 menu.AppendMenu(MF_STRING, ID_MY_CMD_EXPORT, _T("导出当前视图")); // 插入 AutoCAD 原生项(如“锁定位置”) HMENU hAcadMenu = ::GetSystemMenu(AfxGetMainWnd()->GetSafeHwnd(), FALSE); if (hAcadMenu) { MENUITEMINFO mii = {0}; mii.cbSize = sizeof(mii); mii.fMask = MIIM_TYPE | MIIM_ID | MIIM_STATE; mii.fType = MFT_STRING; mii.wID = ID_ACAD_LOCK_POSITION; mii.dwTypeData = _T("锁定停靠位置"); mii.fState = MF_ENABLED; InsertMenuItem(menu.m_hMenu, 0, TRUE, &mii); } menu.TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, this); }逻辑说明:TrackPopupMenu()的坐标需转换为屏幕坐标(ClientToScreen(&point)),否则菜单出现在左上角。参数说明:TPM_LEFTALIGN表示左对齐弹出,TPM_RIGHTBUTTON表示模拟右键触发,避免与系统菜单冲突。
5.3 一个必须养成的习惯:每次DockControlBar::Create()后,强制调用RecalcLayout()
这是本资源在某跨平台系统项目中踩出的“后悔药”。现象是:首次加载 ARX 时 DockControlBar 位置正确,但重启 AutoCAD 后停靠位置偏移 10 像素。排查发现CDockSite内部布局缓存未刷新。解决方案极其简单,却常被忽略:
// 在 CMyDockControlBar::Create() 成功后立即执行 if (CDockControlBar::Create(...)) { // 关键:强制重算整个 DockSite 布局 if (g_pDockSite) { g_pDockSite->RecalcLayout(); } // 再执行 Dock DockControlBar(g_pDockSite, AFX_IDW_DOCKBAR_TOP); return TRUE; }RecalcLayout()会触发布局引擎重新计算所有停靠栏的尺寸与位置,确保DockControlBar的GetWindowRect()返回值与视觉位置一致。从那以后我每次Create()后都强制走一遍RecalcLayout(),哪怕多花 0.5ms,也比用户反馈“菜单歪了”强十倍。
希望帮到你。
本文还有配套的精品资源,点击获取