简介面向AutoCAD二次开发者的ObjectARX 2010实战示例聚焦用C与MFC创建可停靠屏幕菜单解决DockControlBar从派生、资源定义到注册显示的学习需求。压缩包共16个文件约31KB以6个cpp和5个h源码为主体配合rc资源脚本、sln解决方案与vcproj工程文件另有suo用户配置构成可直接在Visual Studio中打开的工程骨架。已有1239人学习说明该案例在ARX界面扩展入门中具有一定参考价值。读者可从中梳理CAcUiDockControlBar派生类、子对话框、命令消息响应及启动注册的完整组织方式并借助现成工程对照调试停靠栏的显示隐藏与布局设置适合具备C基础、希望掌握AutoCAD自定义界面开发的中级开发者。1. 从模态对话框到可停靠面板ObjectArx 2010 屏幕菜单值不值得换做图层批处理的工具最早用模态对话框用户改一次图层就要关一次窗绘图区被挡得严严实实改三层要开关三次窗口。后来换成 ObjectArx 2010 屏幕菜单的形态——用CAcUiDockControlBar挂一块可停靠面板到绘图区右侧点哪层亮哪层窗口不用再关。DockControlBar 本质上是 MFCCControlBar家族在 ARX 里的封装停靠、浮动、吸附、自动隐藏这些脏活都由宿主框架代管你只管往里塞控件。适合把一批高频操作常驻在界面上的人图层、块、标注样式、图纸集切换。不适合只弹一次、跑完就退的批处理命令那种场景模态对话框反而更省事。2. 把面板挂上主框架CAcUiDockControlBar 派生与 Create 的六个参数这一章从工程配置一路做到面板能被命令拉起来。代码不多但每一步都有前置条件漏一个就是能编译不能显示或者一显示就崩溃。2.1 工程与库的前置条件MFC 共享 DLL、acui 依赖和入口点ObjectArx 2010 配套的编译器是 VS2008 SP1工程向导会生成一个 MFC 扩展 DLL 的骨架。MFC 的链接方式我一般选共享 DLL静态链接在宿主进程里会出状态问题后面避坑章会专门讲。附加包含目录要指向 SDK 的inc和inc-x64附加库目录指向lib-win32或lib-x64这两个目录按目标平台二选一不能混。需要链接的库至少包括acad.lib、acrx.lib、acdb.lib、acge.lib加上面板基类所在的acui.lib和它依赖的adui.lib。头文件顺序有讲究先 MFC 的afxwin.h、afxext.h再 SDK 的arxHeaders.h最后才是acuiDockBar.h。顺序反过来会在 MFC 的类型定义上撞车报一堆CString重定义。// StdAfx.h —— 顺序别乱 #include afxwin.h #include afxext.h #include afxdialogex.h #include arxHeaders.h // AcRx、AcDb、aced 系列入口 #include acuiDockBar.h // CAcUiDockControlBar #include aduiTheme.h // 主题相关浅色深色都靠它入口点这块是新手最容易糊弄过去的地方。三个动作缺一不可解锁应用、声明 MDI 感知、注册命令组。extern C AcRx::AppRetCode acrxEntryPoint(AcRx::AppMsgCode msg, void* pkt) { switch (msg) { case AcRx::kInitAppMsg: acrxDynamicLinker-unlockApplication(pkt); // 不解锁收不到卸载消息 acrxRegisterAppMDIAware(pkt); // 面板是应用级的只加载一次 acedRegCmds-addCommand(_T(MYSCRMENU), _T(MYSCRMENU_SHOW), _T(MYSCRMENU_SHOW), ACRX_CMD_MODAL | ACRX_CMD_TRANSPARENT, ShowScreenMenu); break; case AcRx::kUnloadAppMsg: acedRegCmds-removeGroup(_T(MYSCRMENU)); DestroyScreenMenu(); // 先拆窗口再让 ARX 走 break; } return AcRx::kRetOK; }unlockApplication决定你的 DLL 能不能被卸载不调用的话卸载命令会直接拒绝执行。acrxRegisterAppMDIAware决定多文档切换时 ARX 是被反复加载还是只加载一次面板这种全局 UI 必须走这条。命令组的名字我用一个独立的前缀卸载时整组摘掉避免残留命令在命令行里按回车报未知命令。2.2 从 CAdUiDockControlBar 派生一个最小可跑的面板类面板类继承CAcUiDockControlBar它上面还有一层CAdUiDockControlBar真正干停靠活的是后者前者补了主题和若干 CAD 特有的绘制细节。一个能跑的最小骨架只需要消息映射、构造函数和OnCreate。class CMyScreenMenuBar : public CAcUiDockControlBar { DECLARE_DYNAMIC(CMyScreenMenuBar) public: CMyScreenMenuBar(); virtual ~CMyScreenMenuBar(); protected: CListCtrl m_wndList; afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); afx_msg void OnSize(UINT nType, int cx, int cy); afx_msg void OnClose(); DECLARE_MESSAGE_MAP() }; BEGIN_MESSAGE_MAP(CMyScreenMenuBar, CAcUiDockControlBar) ON_WM_CREATE() ON_WM_SIZE() ON_WM_CLOSE() END_MESSAGE_MAP()OnClose单独拎出来是有原因的后面第 4 章第一条坑就栽在这。基类默认的关闭行为在不同小版本上表现不完全一致有的只是隐藏有的会走到销毁所以我自己接管固定成只隐藏。子控件在这里创建不要在对话框模板的OnInitDialog里做因为基类用模板只是拿来做布局载体你并不拥有一个真正意义上的CDialog生命周期。int CMyScreenMenuBar::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CAcUiDockControlBar::OnCreate(lpCreateStruct) -1) return -1; AFX_MANAGE_STATE(AfxGetStaticModuleState()); // 后面的资源都在本模块里找 if (!m_wndList.Create(WS_CHILD | WS_VISIBLE | LVS_REPORT | LVS_SINGLESEL | LVS_SHOWSELALWAYS, CRect(0, 0, 200, 520), this, IDC_LAYER_LIST)) return -1; m_wndList.InsertColumn(0, _T(图层), LVCFMT_LEFT, 120); m_wndList.InsertColumn(1, _T(状态), LVCFMT_LEFT, 56); m_wndList.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES); return 0; }AFX_MANAGE_STATE放在子控件创建之前是因为CListCtrl::Create内部要查控件类注册信息一旦模块状态跑偏到acad.exe那边返回的就是空句柄。2.3 Create 的六个参数与挂载时序真正的挂载在命令函数里。Create的签名参数不算多但每一个填错都有对应症状我把踩过的取值整理在下面这张表里。参数典型取值填错的症状lpszTitle固定的中文名如_T(快捷屏菜单)浮动时标题栏空白位置记忆失效pParentWnd必须是acedGetAcadFrame()面板浮在主窗口外面不随 CAD 最小化rect宽 200260、高 480600宽度给 0 会被吸成一条线bHasGripperTRUE没有抓手拖动不了分隔条nIDTemplate对话框资源 ID资源不存在直接返回FALSEdwStyleWS_CHILD | WS_VISIBLE | CBRS_LEFT不给CBRS_*默认贴左边给错贴错边命令函数里做三件事判重、创建、失败兜底。static CMyScreenMenuBar* g_pScreenMenu NULL; void ShowScreenMenu() { AFX_MANAGE_STATE(AfxGetStaticModuleState()); if (g_pScreenMenu ! NULL ::IsWindow(g_pScreenMenu-m_hWnd)) { g_pScreenMenu-ShowControlBar(TRUE, FALSE, FALSE); g_pScreenMenu-ReloadLayers(); // 每次唤出都刷一遍当前文档 return; } g_pScreenMenu new CMyScreenMenuBar(); CRect rc(0, 0, 220, 560); if (!g_pScreenMenu-Create(_T(快捷屏菜单), acedGetAcadFrame(), rc, TRUE, IDD_SCREENMENU_FORM, WS_CHILD | WS_VISIBLE | CBRS_LEFT)) { delete g_pScreenMenu; g_pScreenMenu NULL; acutPrintf(_T(\n面板创建失败先检查对话框模板 ID 和样式。)); return; } g_pScreenMenu-ReloadLayers(); }pParentWnd传错是最隐蔽的问题。传NULL或者传当前的AcApDocument对应窗口代码能跑面板也确实出现了但它是独立顶层窗口CAD 主窗口最小化时它不动用户切走再切回来就找不到了。必须传acedGetAcadFrame()让宿主的框架管理器接管它。Create内部的时序是加载对话框模板做客户区、注册到框架、按dwStyle尝试停靠。所以模板必须先存在。模板的样式要求是Child、Border选NoneVisible可以不勾因为外层由WS_VISIBLE控制。我见过有人把模板设成Popup结果Create返回FALSE日志里什么都没有纯靠猜。面板起来之后建议补一次布局刷新acedGetAcadFrame()-RecalcLayout();不刷新的话在某些界面配置下右侧工具栏会压住面板一截得拖动一下主窗口才回正。这行不写也能用写了体验更接近原生。3. 让面板吃到数据MFC 模块状态、文档切换和列表刷新面板挂上去只是空壳真正决定它好不好用的是数据链路。这一章解决三个问题控件里的资源从哪来、切文档时面板怎么知道、命令怎么驱动面板显示隐藏。3.1 AFX_MANAGE_STATE 与跨模块资源ARX 是 DLLMFC 有自己的一套模块状态链。宿主是acad.exe你的代码在自己的 DLL 里两个模块各有各的资源句柄、各有各的CWinApp实例。凡是跨越 DLL 边界执行 MFC 代码的地方都要把状态切回来。必须加的四个位置命令入口函数、消息响应函数、定时器回调、任何会被宿主直接调用的导出函数。这几个地方一旦漏掉症状五花八门。void CMyScreenMenuBar::OnSize(UINT nType, int cx, int cy) { CAcUiDockControlBar::OnSize(nType, cx, cy); if (m_wndList.GetSafeHwnd() NULL) return; AFX_MANAGE_STATE(AfxGetStaticModuleState()); CRect rcClient; GetClientRect(rcClient); rcClient.DeflateRect(4, 4, 4, 4); m_wndList.MoveWindow(rcClient); // 两列按比例分避免拖窄后状态列被挤没 int nTotal rcClient.Width(); m_wndList.SetColumnWidth(0, nTotal - 64); m_wndList.SetColumnWidth(1, 60); }AFX_MANAGE_STATE是个 RAII 对象作用域结束自动还原所以放在函数里靠前的位置别放循环里反复构造。OnSize里还要注意判空窗口第一次收到WM_SIZE时子控件可能还没建好不判就是空指针崩溃。资源这块有个细节。对话框模板、图标、字符串表都编在你的 ARX 里编号和宿主里的资源编号可能撞。MFC 找资源是按模块状态链顺序找的状态不对就会先命中宿主那份拿到完全不相干的字符串或者干脆找不到。所以模板 ID 我一般手工改成 30000 以上的区间离宿主的常用编号远一点。3.2 让面板跟随当前文档反应器与列表刷新面板是应用级的只有一份但数据是文档级的每个 DWG 一份。切文档时面板不刷新用户看到的就是上一张图的图层点一下改的是当前图的图层这种错位非常要命。读图层的代码如下注意打开模式和迭代器的释放void CMyScreenMenuBar::ReloadLayers() { if (m_wndList.GetSafeHwnd() NULL) return; m_wndList.SetRedraw(FALSE); m_wndList.DeleteAllItems(); AcDbDatabase* pDb acdbHostApplicationServices()-workingDatabase(); AcDbLayerTable* pLt NULL; if (pDb NULL || pDb-getLayerTable(pLt, AcDb::kForRead) ! Acad::eOk) { m_wndList.SetRedraw(TRUE); return; } AcDbLayerTableIterator* pIt NULL; pLt-newIterator(pIt); int nRow 0; for (; !pIt-done(); pIt-step()) { AcDbLayerTableRecord* pRec NULL; if (pIt-getRecord(pRec, AcDb::kForRead) ! Acad::eOk) continue; m_wndList.InsertItem(nRow, pRec-getName()); m_wndList.SetItemText(nRow, 1, pRec-isOff() ? _T(关) : _T(开)); pRec-close(); // 迭代器拿到的记录必须还回去 nRow; } delete pIt; // 迭代器是 new 出来的自己删 pLt-close(); m_wndList.SetRedraw(TRUE); m_wndList.Invalidate(); }几个参数说明一下。AcDb::kForRead是只读打开不占写锁多文档并发时不会互相阻塞。pRec-isOff()读的是图层关闭标志不是冻结冻结要用isFrozen()这两个在业务上是两回事界面文案最好分开。SetRedraw(FALSE)包住整个填充过程几十上百个图层时能明显减少闪烁。如果要跟随文档自动刷新挂一个AcApDocManagerReactor重写documentActivated和documentToBeDeactivated在回调里调用面板的ReloadLayers。反应器对象在kInitAppMsg里 new在kUnloadAppMsg里先removeReactor再 delete。反应器回调发生在文档上下文里别忘了同样加模块状态切换。如果偷懒不挂反应器退而求其次在WM_SETFOCUS里刷一次也能挡住大部分场景但用户切回来点别处就不灵了属于糊弄自己的做法。3.3 从命令驱动面板显示、隐藏与刷新面板的显隐不要直接调ShowWindow走ShowControlBar它会把停靠布局一起重算。void ToggleScreenMenu() { AFX_MANAGE_STATE(AfxGetStaticModuleState()); if (g_pScreenMenu NULL || !::IsWindow(g_pScreenMenu-m_hWnd)) { ShowScreenMenu(); return; } BOOL bVisible g_pScreenMenu-IsVisible(); g_pScreenMenu-ShowControlBar(!bVisible, FALSE, FALSE); if (!bVisible) g_pScreenMenu-ReloadLayers(); }ShowControlBar三个参数分别是目标可见性、是否延迟显示、是否延迟重算布局。第三个参数我固定传FALSE让布局立刻生效传TRUE的话在某些界面下会出现面板已经显示但位置还停在上一次的地方。用户点右上角关闭时走的是OnClose这里接管成只隐藏void CMyScreenMenuBar::OnClose() { ShowControlBar(FALSE, FALSE, FALSE); // 不调基类避免窗口被真正销毁后 g_pScreenMenu 变成野指针 }对应的界面按钮就简单了点一下调ToggleScreenMenu或者做个双击切换图层开关的处理NM_DBLCLK里拿到行号用AcDbLayerTable::getAt拿记录改成kForWrite改完close。整个链路是界面事件 → 文档事务 → 面板刷新别在事务没提交的时候刷新列表读到的还是旧值。4. 真机上翻车的五个现场屏菜单的排查清单开发机上跑通跟用户机上能用是两码事。这一章的五个问题都是我在交付后被反馈回来的按「现象 → 原因 → 解决」顺序写遇到同类症状可以直接对号。4.1 面板第二次调不出来命令像没执行现象第一次执行命令面板正常出现用户点关闭后再执行同一个命令命令行没报错界面毫无反应。原因基类在关闭时把窗口销毁了g_pScreenMenu指针还指着那块已经回收的内存::IsWindow判断之前就访问了不可用的m_hWnd或者判断通过但窗口句柄已经复用。有的小版本走隐藏有的走销毁行为不统一。解决OnClose里接管成ShowControlBar(FALSE, ...)只隐藏不销毁所有访问面板的地方先用::IsWindow判一遍句柄有效性判不过就当作没创建重新走创建流程。4.2 卸载 ARX 后 CAD 退出直接崩现象ARXUNLOAD卸载成功命令行提示已卸载接着关闭 CAD弹崩溃报告。原因kUnloadAppMsg里只摘了命令组没拆面板窗口。窗口还挂在框架的停靠管理器里宿主退出时按自己的顺序析构撞上一个已经卸载的 DLL 里的窗口过程直接翻车。解决卸载分支里先DestroyWindow再delete指针置空。顺序不能反先 delete 再 DestroyWindow 会走一遍已释放对象的虚函数表。void DestroyScreenMenu() { if (g_pScreenMenu NULL) return; if (::IsWindow(g_pScreenMenu-m_hWnd)) g_pScreenMenu-DestroyWindow(); delete g_pScreenMenu; g_pScreenMenu NULL; }另外卸载流程最好在kUnloadAppMsg最前面执行别放在最后中间任何一步失败都会导致面板残留。4.3 面板一打开就断言停在 afxwin1.inl现象Debug 版一执行命令就弹断言框调用栈里全是 MFC 内部函数看不出自己代码哪行错。原因MFC 状态不对。要么命令入口漏了AFX_MANAGE_STATE要么工程用了静态链接 MFC宿主和 DLL 各有一份 MFC 运行时句柄体系对不上。解决命令入口、消息响应、回调函数统一补模块状态切换工程属性改成共享 DLL 方式链接 MFC和宿主保持一致。静态链接看起来省依赖实际在 ARX 里是自找麻烦。4.4 Create 返回 FALSE但没有任何报错现象命令执行完Create直接返回失败自己兜底的日志打出来面板不出现。原因排第一的是对话框模板 ID 根本没建或者 ID 定义在resource.h里但资源文件没同步排第二是模板样式不对设成了Popup而不是Child排第三是资源编号和宿主的撞了MFC 在错误的模块里找。解决先在资源视图确认模板存在且样式正确再把 ID 手工挪到高位区间最后在Create前后打印GetLastError()虽然 MFC 内部吞掉了大部分错误码但能捞到一点是一点。4.5 拖窄之后控件错位列表只剩一列现象面板宽度拖到 120 像素以下左侧列表整块消失或者状态列跑到可视区外。原因OnSize里没重排子控件控件还是按创建时的绝对坐标待着超出客户区就被裁掉。解决凡是放子控件的面板OnSize里必须调MoveWindow按客户区重算列宽按比例分同时给主列设一个下限宽度小于阈值时干脆隐藏次要列。GetClientRect拿到的宽高已经扣掉了边框DeflateRect再留 4 像素边距视觉上不至于贴着边。5. 像原生面板那样收尾主题、停靠记忆和最小宽度功能通了之后用户对面板的容忍度会立刻下降到外观层面。这一章三个点都是「不写也能用写了才不像外挂」的部分。5.1 深色主题下面板白得刺眼宿主切深色主题之后面板背景还是系统灰列表头白底黑字在一堆深色面板中间非常扎眼。CAdUiDockControlBar这一层提供了主题对象通过GetTheme()拿到颜色枚举定义在aduiTheme.h。做法是在OnCtlColor里按当前主题返回背景刷和文字色同时把刷子缓存成成员别每次都建。HBRUSH CMyScreenMenuBar::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { HBRUSH hbr CAcUiDockControlBar::OnCtlColor(pDC, pWnd, nCtlColor); CAdUiTheme* pTheme GetTheme(); if (pTheme NULL || pTheme-GetThemeState() kAdUiThemeStateUseSystem) return hbr; // 跟随系统时交回基类别自作主张 COLORREF clrBk pTheme-GetThemeColor(kAdUiThemeColorBackground); COLORREF clrTx pTheme-GetThemeColor(kAdUiThemeColorText); pDC-SetBkColor(clrBk); pDC-SetTextColor(clrTx); if (m_brBk.GetSafeHandle() NULL) m_brBk.CreateSolidBrush(clrBk); return (HBRUSH)m_brBk.GetSafeHandle(); }颜色枚举名各版本略有差异动手前对着本机的aduiTheme.h核一遍别照抄。m_brBk是CBrush成员析构时自动释放如果每次OnCtlColor都新建一个刷子画一百次就漏一百个 GDI 对象任务管理器里能看到句柄数一路涨这也是很多人反馈「用久了界面变卡」的真实原因。主题切换的消息我也接了一下宿主切换主题时会广播收到后Invalidate(TRUE)重建整个面板比逐个子控件刷新省事。5.2 停靠位置记不住每次启动都回默认位宿主的布局记忆是按面板标识来的。标识稳定位置就稳定标识一变宿主当成新面板位置回默认。标识主要来自两处创建时传的标题字符串和对话框模板的 ID。标题里带版本号、带构建日期、带用户名的一律会丢位置。我见过最典型的是把标题写成CString拼接的_T(快捷面板 v) 版本号改一次版本所有人的面板位置重置一次。固定成_T(快捷屏菜单)就没事了。如果还想记住「上次是不是可见」宿主不管这个得自己存。我一般写注册表键用固定路径值存 0/1。// 退出时保存初始化时读取 void SavePanelVisible(bool bVisible) { AfxGetApp()-WriteProfileInt(_T(ScreenMenu), _T(Visible), bVisible ? 1 : 0); } bool LoadPanelVisible() { return AfxGetApp()-GetProfileInt(_T(ScreenMenu), _T(Visible), 0) 1; }存的是可见性意图不是窗口当前状态。用户上次手动关了面板下次启动就不要自作主张弹出来上次是开着的启动后恢复。kInitAppMsg里读完这个值为真就调一次ShowScreenMenu。注意初始化时机要放在所有 ARX 加载完成之后太早调主框架还没准备好acedGetAcadFrame()可能返回空。参数说明WriteProfileInt的第二个参数是键名第三个是值GetProfileInt的第三个参数是默认值第一次运行时键不存在返回默认值这个默认值决定新用户第一次装上是开还是关我一般给 0不主动打扰。5.3 最小宽度、图标与标题栏面板能被拖到只剩一条缝用户拖窄之后再想拖回来抓手常常点不中。解决办法是把最小宽度锁住重写OnWindowPosChanging在窗口位置尺寸变化前修正。void CMyScreenMenuBar::OnWindowPosChanging(WINDOWPOS* lpwndpos) { const int kMinWidth 180; if ((lpwndpos-flags SWP_NOSIZE) 0 lpwndpos-cx kMinWidth) lpwndpos-cx kMinWidth; CAcUiDockControlBar::OnWindowPosChanging(lpwndpos); }kMinWidth取值要跟列表两列宽度对齐180 是我这边主列 116 加状态列 60 的实践值比这个更窄状态列就没法看了。图标这一步容易忘。浮动状态下标题栏左上角默认是空白看着像没做完的工具。在OnCreate末尾加一行SetIcon把 ARX 资源里的图标挂上去注意图标尺寸要备两套16×16 和 32×32 各一份宿主的 DPI 会自己挑。标题文字本身不建议动态改改一次位置记忆就丢一次前面说过。需要显示当前图层数量这类动态信息放在面板内部的标签控件里别动标题。6. 该用 PaletteSet 还是 DockControlBar一套三遍验证法选型上有个常见分歧同样是常驻面板宿主还提供了AcTcUiPaletteSet那套调色板机制带选项卡、能放多页、自动处理主题和布局。两者怎么选我的判断标准很直接单页、内容固定的轻量面板用CAcUiDockControlBar代码量少一个类加一个模板就能跑需要多页签、内容会动态增减、要跟工具选项板抢位置的用PaletteSet。维度CAcUiDockControlBarAcTcUiPaletteSet页签支持无单页原生多页签代码量一个类 一个模板需要派生调色板类主题跟随手工处理OnCtlColor基本自动位置记忆依赖标题和模板 ID 稳定内置适合场景单一功能常驻面板多功能集合面板功能写完到交付之间我会固定跑三遍验证缺一遍都不敢发出去。第一遍叫「开合一百次」。写个脚本或者手动循环执行显隐命令一百次之后看任务管理器的 GDI 对象数和 USER 对象数。这两个数字稳定在初始值附近才算过关每轮涨一点就是漏刷子、漏字体、漏窗口句柄。这一遍最容易抓到OnCtlColor里建刷子的那种低级错误。第二遍叫「卸载重载」。ARXLOAD加载、ARXUNLOAD卸载来回三轮中间穿插打开和关闭面板。第三轮之后执行命令应该还能正常拉出面板不崩不花。这一遍专门验证kUnloadAppMsg里的拆窗口顺序以及反应器有没有干净移除。第三遍叫「跨文档乱切」。开三个 DWGA 图打开面板切到 B 图看列表是不是 B 图的图层切到 C 图再切回 A 图看面板是否还在、数据是否对。这一遍验证的是文档反应器和workingDatabase()的配合acedGetAcadFrame()在文档切换期间拿到的框架句柄不会变但数据库会。早期我习惯只跑第一遍觉得开合没问题就成了结果交付后最常被投诉的偏偏是卸载崩溃和切文档数据对不上。从那以后我每次改完跟面板相关的代码都把这三遍强制走一遍哪怕只改了一行标题字符串——标题一动位置记忆就废这种退化只有切文档加重启才能暴露出来。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?