
简介面向从事 AutoCAD 2010 二次开发、希望掌握 ObjectArx 屏幕菜单与 DockControlBar 停靠面板实现的中高级开发者这份案例围绕 CAcUiDockControlBar 派生类、界面资源定义、应用初始化注册、命令消息响应及显示隐藏等环节展开可用于理解 MFC 界面机制如何与 ARX 应用入口衔接并据此搭建可停靠的自定义菜单面板。压缩包共 16 个文件、约 31KB以 6 个 cpp 源文件和 5 个 h 头文件为主体辅以 rc 资源脚本、sln 解决方案与 vcproj 工程文件另含 suo 用户配置便于在 Visual Studio 中打开工程、对照类结构编译调试。已有 1239 人学习说明该案例在同类入门素材中具有一定参考热度。读者可从中获得完整的工程骨架、停靠控制条的类划分与事件处理思路以及界面资源、命令注册和布局样式之间的对应关系并通过重写虚函数接收命令、调用成员函数控制显隐适合作为排查面板可见性、停靠方向与命令分发的实践参照。1. 图纸里命令越堆越多屏幕菜单是最后的收纳口图纸里的命令越加越多键盘手速跟不上最后大家还是老老实实敲 L 和 C。真正让人下决心做一套 ObjectArx 2010 屏幕菜单的往往不是技术冲动而是某个下午发现三个同事在同一个对话框里重复填同样的参数。这套 ARX 屏幕菜单DockControlBar 可停靠面板干的事很朴素用 CAcUiDockControlBar 在主框架左侧挂一块常驻面板把高频命令、参数输入、拾取结果都塞进去跟着文档切换实时刷新用户不想看时随手拖走或叉掉。它适合手上已经有一堆零散 ARX/LISP 命令、想把入口收拢成图形界面的人也适合把企业内部工具从命令行搬上侧边栏的人代价是你得接受 MFC 的资源链与消息循环并且愿意为一次崩溃多调半小时。2. 工程骨架ObjectARX 2010 加 MFC 共享 DLL 的最小可跑配置把面板做出来之前工程类型和链接设置错一步后面所有代码都是白写。ObjectARX 2010 这一代仍然要求插件以 DLL 形式被 acad.exe 加载扩展名改成 .arx本质没变。最容易翻车的地方集中在三处MFC 的链接方式、平台位数、以及入口符号的导出方式。这一章按顺序把这三处钉死后面才好谈面板代码。2.1 工程类型与 SDK 目录接入新建工程时选「MFC DLL」不要选控制台 DLL 也不要选静态库在 MFC 使用方式上选「在共享 DLL 中使用 MFC」。这一条不是风格偏好原因很直接acad.exe 自身链接的是共享版 MFC你的 .arx 加载进去以后和宿主共用同一份 MFC 运行时状态资源链AFX_MODULE_STATE才对得上AfxGetStaticModuleState()才能正确切到本模块的对话框资源。如果这里错选「在静态库中使用 MFC」你会得到两套独立的 MFC 状态机表现为对话框资源找不到、断言直接打穿、甚至加载瞬间就崩。平台位数必须和宿主一致。AutoCAD 2010 同时存在 32 位与 64 位安装SDK 目录里也把两种位数的导入库分开放。目标机是 64 位 AutoCAD工程就得配 x64附加库目录指向 SDK 的 64 位库目录目标机是 32 位就配 Win32。混用的典型症状是链接期报LNK2019 无法解析的外部符号或者编译全过、加载时报找不到依赖模块。包含目录与库目录的配置我一般直接在工程属性里用属性表固定下来避免每个工程重复填附加包含目录: ARXSDK\inc;ARXSDK\inc-x64 附加库目录: ARXSDK\lib;ARXSDK\lib-x64 附加依赖项: acad.lib;acdb.lib;rxapi.lib;acui.libARXSDK换成你本机解压出来的 SDK 根目录即可32 位工程把带-x64的两项去掉。附加依赖项里的acui.lib是停靠面板的关键CAcUiDockControlBar和CAcUiDialog都在这个库里漏掉它会得到一堆「未定义的外部符号 CAcUiDockControlBar::Create」。具体依赖名以 SDK 自带示例工程.vcxproj里的AdditionalDependencies为准不同小版本的库名会有出入照抄示例最稳。输出配置上把「目标文件名」的后缀从.dll改成.arx配置类型仍然是 DLL。工程如果是 Unicode 字符集ObjectARX 2010 时代基本都是所有字符串用_T()或ACRX_T()包一层否则acutPrintf在中文环境下会打出乱码。这一点看起来是小节实际上第一次跑通时最容易在控制台看到一堆问号然后怀疑人生。2.2 AcRxArxApp 派生与入口符号ObjectARX 从 2007 起提供了AcRxArxApp基类和配套宏入口函数不用再手写acrxEntryPoint那一大段 switch。派生一个应用类重写初始化与卸载回调再用宏生成入口与版本函数// ArxApp.h #pragma once #include rxregsvc.h #include acrxArxApp.h // AcRxArxApp 基类 class CMyDockApp : public AcRxArxApp { public: CMyDockApp() : AcRxArxApp() {} // 插件加载注册命令、挂反应器 virtual AcRx::AppRetCode On_kInitAppMsg(void* pkt); // 插件卸载反注册、销毁面板 virtual AcRx::AppRetCode On_kUnloadAppMsg(void* pkt); // 供托管式加载使用的组件注册入口普通 ARX 留空即可 virtual void RegisterServerComponents() {} };// ArxApp.cpp #include stdafx.h #include ArxApp.h #include DockPanel.h // 该宏会生成 acrxEntryPoint 与 acrxGetApiVersion并带上导出修饰 IMPLEMENT_ARX_ENTRYPOINT(CMyDockApp) AcRx::AppRetCode CMyDockApp::On_kInitAppMsg(void* pkt) { // 允许运行期卸载不加这句 UNLOAD 会失败 acrxDynamicLinker-unlockApplication(pkt); // 声明本插件对多文档MDI安全 acrxRegisterAppMDIAware(pkt); InitDockCommands(); // 见第 4 章注册 MYDOCK 命令组 return AcRx::kRetOK; } AcRx::AppRetCode CMyDockApp::On_kUnloadAppMsg(void* pkt) { UninitDockCommands(); // 摘命令、拆面板顺序不能反 return AcRx::kRetOK; }IMPLEMENT_ARX_ENTRYPOINT已经把入口和版本函数一起生成好了所以工程里不要再自己写一遍extern C AcRx::AppRetCode acrxEntryPoint(...)也不要再加一个导出同名符号的.def文件两边同时存在就是重复定义链接期直接报错。这一点在接手别人老工程时特别常见老代码留着 2006 时代的.def新同事又加了宏两个人在同一份工程里互相打架。unlockApplication和acrxRegisterAppMDIAware这两个调用看着像仪式实际都有具体后果。前者决定ARXUNLOAD能不能成功卸载你的插件不调用的话卸载会被拒绝只能重启 CAD后者告诉宿主这个插件的全局数据在多文档切换时是安全的不调用时某些回调会在错误的文档上下文里触发你拿到的workingDatabase()可能不是用户当前看到的图纸。2.3 调试宿主与自动加载.arx不能双击运行调试必须把 acad.exe 设成宿主进程。在工程属性的「调试」页里命令填 AutoCAD 主程序路径命令参数留空或加/nologo减少启动画面。工作目录设成 AutoCAD 安装目录否则某些相对路径依赖会失效。真正省时间的是自动加载。把工程输出目录加进 AutoCAD 的支持文件搜索路径然后在支持路径下放一个纯文本的acad.rx一行写一个插件文件名MyDock.arx启动 CAD 时会按这个清单逐个加载F5 启动后断点直接命中On_kInitAppMsg不用每次手动 APPLOAD。这个机制在部分新版本上已经不再读取如果发现没生效就退回手动加载启动 CADAPPLOAD 选一次 .arx之后再调试时用「附加到进程」也能打断点只是每次改完代码要重新加载一次。我在两套机器上来回切的时候习惯把两种方式都留着省得排查问题时先怀疑是加载机制的问题。3. 停靠面板核心类拆解CAcUiDockControlBar 从创建到贴靠ObjectARX 的停靠面板不是一个控件而是三层结构叠起来的宿主框架的停靠区、控制栏本身、以及控制栏里的内容对话框。把这三层的关系理清楚后面改宽度、换主题、加控件都不会出现「改了没反应」的玄学。这一章按创建顺序拆先写控制栏派生类再写内容对话框最后把两者在命令里拼起来。3.1 面板载体类的头文件与消息映射CAcUiDockControlBar继承自 MFC 的CAdUiDockControlBar最终仍然是CControlBar所以停靠、浮动、显示隐藏这一套走的是 MFC 框架自己的机制。派生类只需要关心两件事OnCreate里把内容窗口建出来OnSize里让内容窗口跟着变尺寸。// DockPanel.h #pragma once #include acuidockbar.h // CAcUiDockControlBar #include DockPanelContent.h // 面板内容对话框 class CDockPanel : public CAcUiDockControlBar { DECLARE_DYNAMIC(CDockPanel) public: CDockPanel(); virtual ~CDockPanel(); // 创建并贴靠到主框架指定停靠区 BOOL AttachTo(CFrameWnd* pFrame, UINT nDockBarID, const CRect rcSize); // 文档切换时刷新面板内容 void RefreshFromDocument(AcApDocument* pDoc); protected: afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); afx_msg void OnSize(UINT nType, int cx, int cy); DECLARE_MESSAGE_MAP() private: CDockPanelContent m_wndContent; // 面板里真正的内容 };DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC这对宏不能省。MFC 在停靠、序列化窗口状态时会做运行期类型检查缺了它浮动再停靠回来时可能直接断言失败。这在开发期不一定复现往往是测试同事把面板拖出来又拖回去才炸。3.2 内容对话框与主题配色面板里那块内容我用CAcUiDialog派生而不是裸CDialog。原因只有一个主题。AutoCAD 2010 开始界面配色可以整体切换宿主框架的停靠栏会跟着变但你放进去的对话框如果还是系统默认的COLOR_BTNFACE就会出现「外框是深灰、里面一块白板」的割裂感。CAcUiDialog属于 AcUi 层它的绘制逻辑会读取当前主题色控件背景、静态文本前景都会跟着走。// DockPanelContent.h #pragma once #include acuidialog.h // CAcUiDialog class CDockPanelContent : public CAcUiDialog { DECLARE_DYNAMIC(CDockPanelContent) public: enum { IDD IDD_DOCK_PANEL_CONTENT }; CDockPanelContent(CWnd* pParent NULL); protected: virtual void DoDataExchange(CDataExchange* pDX); virtual BOOL OnInitDialog(); afx_msg void OnBnClickedPickEntity(); DECLARE_MESSAGE_MAP() };资源脚本里IDD_DOCK_PANEL_CONTENT建议从 1000 往后排别用 100 附近的低位 ID。低位段在 MFC 与宿主内部资源里占用较多撞上了不会报错只会表现成「对话框弹出来是另一个东西」或者控件句柄错乱排查起来非常费劲。血泪经验所有自己新增的 IDD、IDC 统一从 3000 起编并在资源符号表里加一行注释写清归属模块。3.3 创建、停靠与显示的三段式调用AttachTo是整个面板的关键它把「创建控制栏、允许停靠、贴到停靠区」这三步串起来。顺序错了或者少一步最常见的结果就是命令执行成功、控制台打印了日志但屏幕上什么都没有。// DockPanel.cpp #include stdafx.h #include DockPanel.h IMPLEMENT_DYNAMIC(CDockPanel, CAcUiDockControlBar) BEGIN_MESSAGE_MAP(CDockPanel, CAcUiDockControlBar) ON_WM_CREATE() ON_WM_SIZE() END_MESSAGE_MAP() CDockPanel::CDockPanel() {} CDockPanel::~CDockPanel() {} int CDockPanel::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CAcUiDockControlBar::OnCreate(lpCreateStruct) -1) return -1; // 内容对话框以控制栏为父窗口资源必须在本模块的 RC 里 if (!m_wndContent.Create(CDockPanelContent::IDD, this)) return -1; m_wndContent.ShowWindow(SW_SHOW); return 0; } void CDockPanel::OnSize(UINT nType, int cx, int cy) { CAcUiDockControlBar::OnSize(nType, cx, cy); // 拖动分隔条或从浮动切回停靠时内容跟着撑满 if (::IsWindow(m_wndContent.GetSafeHwnd())) m_wndContent.MoveWindow(0, 0, cx, cy); } BOOL CDockPanel::AttachTo(CFrameWnd* pFrame, UINT nDockBarID, const CRect rcSize) { if (pFrame NULL) return FALSE; // 第三步参数 false 表示内容由成员对话框自理不走 nIDDlg 模板 if (!Create(pFrame, IDD_DOCK_PANEL_CONTENT, rcSize, CBRS_LEFT | CBRS_SIZE_DYNAMIC, FALSE, 0)) return FALSE; EnableDocking(CBRS_ALIGN_ANY); // 允许贴到上下左右任意边 pFrame-DockControlBar(this, nDockBarID); pFrame-ShowControlBar(this, TRUE, FALSE); pFrame-RecalcLayout(); return TRUE; }Create的六个参数逐个看pFrame是宿主框架必须用acedGetAcadFrame()拿到的那一个不要 new 一个CFrameWndnID这里借用资源 ID 作为控制栏的子窗口 ID保证唯一即可rcSize是初始尺寸停靠后会按停靠区重算但浮动状态下的初始大小就是它CBRS_LEFT表示默认停靠方向CBRS_SIZE_DYNAMIC允许用户拖动改变大小两个都要最后一个布尔参数为TRUE时控件会自己按nIDDlg指定的对话框模板创建内容我选择FALSE是因为自建成员对话框更容易控制生命周期和消息走向。pFrame-DockControlBar(this, nDockBarID)里的nDockBarID用 MFC 的标准值左侧AFX_IDW_DOCKBAR_LEFT、右侧AFX_IDW_DOCKBAR_RIGHT、底部AFX_IDW_DOCKBAR_BOTTOM。这些是框架预置的停靠区AutoCAD 主框架本身已经把这些区域准备好直接贴上去就行。3.4 命令封装与全局单例面板对象必须全局唯一。用户连按两次命令不应该出现两块面板。用一个文件级静态指针守住命令进来先判断存在与否存在就只做显示。// DockCmd.cpp #include stdafx.h #include DockPanel.h #include aced.h static CDockPanel* g_pDockPanel NULL; void ShowMyDockPanel() { // ARX 里只要碰 MFC 资源第一行就切模块状态 AFX_MANAGE_STATE(AfxGetStaticModuleState()); CFrameWnd* pFrame acedGetAcadFrame(); if (pFrame NULL) return; if (g_pDockPanel NULL) { g_pDockPanel new CDockPanel(); if (!g_pDockPanel-AttachTo(pFrame, AFX_IDW_DOCKBAR_LEFT, CRect(0, 0, 260, 520))) { delete g_pDockPanel; g_pDockPanel NULL; acutPrintf(_T(\n面板创建失败先查资源 ID 与 MFC 共享设置。)); return; } } else { // 已存在只做显示第二个参数 TRUE 表示激活动作 pFrame-ShowControlBar(g_pDockPanel, TRUE, TRUE); pFrame-RecalcLayout(); } }AFX_MANAGE_STATE(AfxGetStaticModuleState())这一行是整个 ARX MFC 组合里最容易被忽略、后果又最严重的一句。它做的是把当前线程的 MFC 模块状态切换到你自己的 DLLCreateDialog、LoadString这类调用才会去你的资源段里找。漏掉的典型现象是命令执行后弹出的是宿主的某个对话框或者干脆弹不出来还没报错。我现在的习惯是只要函数体里出现任何 MFC 窗口类调用第一行无条件加这一句宁可多写也不去赌。4. 命令注册与生命周期加载、卸载、文档切换面板能显示只是及格线能不能干净卸载、能不能跟着图纸切换刷新才决定这东西能不能发给同事用。ARX 插件的生命周期由宿主控制卸载时机不归你管一旦资源没回收干净用户卸载插件后退出 CAD 的时候就会看到崩溃对话框——那时候已经没人记得是插件干的。4.1 命令组注册与反注册命令用命令组管理一组一个前缀卸载时整组摘掉不会漏。注册放在初始化回调里反注册放在卸载回调里两处成对出现// DockCmd.cpp 续 #define DOCK_CMD_GROUP _T(MYDOCK_CMDS) void InitDockCommands() { acedRegCmds-addCommand(DOCK_CMD_GROUP, // 命令组名 _T(MYDOCK), // 国际化命令名 _T(MYDOCK), // 本地命令名 ACRX_CMD_MODAL, // 模态命令执行期间可操作界面 ShowMyDockPanel); // 回调函数 } void UninitDockCommands() { // 先摘命令避免卸载过程中还能被再次触发 if (acedRegCmds ! NULL) acedRegCmds-removeGroup(DOCK_CMD_GROUP); UninitDockPanel(); // 再销毁面板见 4.2 }ACRX_CMD_MODAL是这类界面命令的正确标志。它表示命令执行期间用户仍然可以操作界面、切换文档不像ACRX_CMD_SESSION那样把整个会话锁住。如果你的面板命令需要在别的命令执行过程中被调用比如画线时按按钮切面板才需要叠加ACRX_CMD_TRANSPARENT但透明命令对文档锁的要求更高不确定时不要加。removeGroup只摘注册表里登记的项不会替你销毁任何窗口。把摘命令放在销毁面板前面是为了防止「面板正在销毁 → 用户又敲了一次命令」这种极端时序虽然概率低但在长时间运行的 CAD 会话里迟早会遇到。4.2 卸载时的资源回收顺序UninitDockPanel要处理的情况比想象中多面板可能停靠着、可能浮动着、可能已经被用户叉掉但对象还在。三种状态下销毁方式一致但显示状态要先归零// DockCmd.cpp 续 void UninitDockPanel() { if (g_pDockPanel NULL) return; AFX_MANAGE_STATE(AfxGetStaticModuleState()); CFrameWnd* pFrame acedGetAcadFrame(); if (pFrame ! NULL) { // 先从停靠框架上摘下来避免框架里留下悬空指针 pFrame-ShowControlBar(g_pDockPanel, FALSE, FALSE); } // DestroyWindow 会触发 WM_DESTROY内部对话框随之销毁 if (::IsWindow(g_pDockPanel-GetSafeHwnd())) g_pDockPanel-DestroyWindow(); delete g_pDockPanel; g_pDockPanel NULL; }顺序是固定的先隐藏、再DestroyWindow、最后delete。CAcUiDockControlBar是new出来的 MFC 窗口对象DestroyWindow负责拆 HWNDdelete负责释放 C 对象两步都要有。只delete不DestroyWindow主框架的停靠栏列表里会留下一个已释放的指针退出时框架遍历该列表就是标准的野指针崩溃。还有一个隐蔽点如果面板里挂了自己的反应器、定时器、或者开了指向文档数据库的长期指针这些必须在delete之前全部解绑。我吃过一次亏面板里存了一个指向AcDbDatabase的裸指针用于刷新列表结果用户先关了图纸再卸载插件刷新回调在已关闭的数据库上跑直接黑匣子。从那以后面板里只存AcDbObjectId用之前判空、判库不存裸指针。4.3 文档切换反应器与面板联动面板是常驻的图纸是切换的两者要同步只能靠反应器。AcApDocManagerReactor的documentActivated回调在文档真正激活之后触发是刷新面板数据的合适时机// DocReactor.h #pragma once #include dbapserv.h #include acdocman.h class CDocSwitchReactor : public AcApDocManagerReactor { public: virtual void documentActivated(AcApDocument* pDoc); };// DocReactor.cpp #include stdafx.h #include DocReactor.h #include DockPanel.h extern CDockPanel* GetDockPanelPtr(); // 暴露全局面板指针 void CDocSwitchReactor::documentActivated(AcApDocument* pDoc) { if (pDoc NULL) return; CDockPanel* pBar GetDockPanelPtr(); if (pBar NULL) return; // 只读取文档信息时无需加锁但要保证不跨文档持有对象 pBar-RefreshFromDocument(pDoc); }反应器的注册与注销同样成对// 初始化阶段 static CDocSwitchReactor* g_pDocReactor NULL; g_pDocReactor new CDocSwitchReactor(); acDocManager-addReactor(g_pDocReactor); // 卸载阶段 if (g_pDocReactor ! NULL) { acDocManager-removeReactor(g_pDocReactor); delete g_pDocReactor; g_pDocReactor NULL; }RefreshFromDocument内部只做只读操作从pDoc-database()拿图纸信息、统计图层数、刷新列表控件。如果面板上有「保存到图纸」这类写操作那必须acDocManager-lockDocument(pDoc)锁住文档再动手锁的粒度和时机按具体业务定。反应器里做重活是常见误区回调执行在 UI 线程上你在这里跑一个遍历十万实体的循环用户看到的现象是「切图纸卡三秒」而不会想到是插件。5. 避坑排查面板不显示、崩溃与资源错乱的五条记录下面这五条都是我在不同机器、不同版本上真实撞过的按现象、原因、解决三段写方便对照自己手上的工程。5.1 命令执行了但屏幕上没有面板现象敲MYDOCK命令行没有报错acutPrintf的调试输出也打出来了但界面没有任何变化。原因Create返回了TRUE只代表窗口对象建好了控制栏默认是隐藏状态另一种情况是pFrame-DockControlBar调用了但主框架对应的停靠区没有EnableDockingMFC 会静默忽略这次停靠请求。解决确认AttachTo里ShowControlBar(this, TRUE, FALSE)和pFrame-RecalcLayout()都调到了如果仍然不显示把DockControlBar的第二个参数换成0试试让 MFC 自己选停靠区能显示就说明是停靠区 ID 的问题。实在排查不出来临时在Create之后加一句SetWindowPos(CWnd::wndTop, 0, 0, 260, 520, SWP_SHOWWINDOW)强制显示至少能先确认窗口本身是好的。5.2 面板一显示就崩溃现象命令一敲AutoCAD 直接退出没有任何可读的错误或者弹出断言框停在AfxGetModuleState附近。原因九成是 MFC 模块状态没切。CDockPanel的OnCreate里调用了m_wndContent.Create(...)这个调用会去当前模块的资源段找IDD_DOCK_PANEL_CONTENT而当前线程的模块状态还停在 acad.exe找不到资源就返回FALSE我这边又写了return -1控制栏创建失败后面的代码拿着半成品对象继续用就崩了。解决所有进入 MFC 世界的入口函数第一行加AFX_MANAGE_STATE(AfxGetStaticModuleState())包括命令回调、反应器回调、OnCreate之前调用的工厂函数。同时检查工程是否真的是「共享 DLL 中使用 MFC」这一条错了加一百遍宏也没用。5.3 卸载插件后退出 CAD 崩在收尾阶段现象ARXUNLOAD显示卸载成功继续用 CAD 一切正常但关闭程序时弹崩溃。原因面板对象没销毁或者销毁了但没从停靠框架上摘下来。框架在退出时会遍历所有控制栏遇到已经delete掉的指针就是野指针访问。解决严格按 4.2 的三步顺序执行另外在On_kUnloadAppMsg的最开头就调用UninitDockPanel()不要等到最后因为后面如果还有别的清理逻辑抛出异常比如某个反应器回调里访问了已失效对象面板就永远轮不到销毁。5.4 深色主题下面板里一片白板现象宿主界面是深灰配色停靠栏外框跟着变了但面板内容区还是亮白文字几乎看不清。原因内容对话框用了裸CDialog它的背景画刷取的是系统COLOR_BTNFACE不认 AcUi 主题。解决内容对话框改成CAcUiDialog派生静态文本、分组框这类自绘控件也换成 AcUi 提供的对应类实在需要自绘的控件在OnCtlColor里根据当前主题返回对应画刷。判断当前是不是深色主题可以用宿主提供的主题查询接口不同小版本名字有差别稳妥做法是自己封装一层主题变化时统一下发一次重绘。5.5 编译全过、加载时报找不到模块现象x64 工程编译链接全部成功APPLOAD加载时报「无法加载模块」或者提示缺少某个 DLL。原因典型的位数不匹配或者工程误用了静态 MFC。还有一种情况是 Debug 版插件加载到了 Release 版 CAD 里调试运行时库对不上。解决先确认dumpbin /headers看 PE 类型是 x本文还有配套的精品资源点击获取