news 2026/10/24 4:16:16

ObjectARX 2010 屏幕菜单停靠面板开发

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ObjectARX 2010 屏幕菜单停靠面板开发

简介:这是一份面向AutoCAD二次开发者的ObjectArx 2010屏幕菜单示例工程,聚焦如何基于DockControlBar创建可浮动、可停靠的自定义菜单,并完成命令响应与界面集成。压缩包共16个文件,以6个cpp源文件、5个h头文件为主,辅以rc资源脚本、sln解决方案和vcproj工程配置等,整体约31KB,可直接作为VS工程参照。已有1238人学习下载,适合具备C++与MFC基础、希望理解ObjectArx界面扩展机制的初中级开发者。工程围绕CAcUiDockControlBar派生、资源定义、启动注册、事件处理、显示隐藏及布局样式等环节给出可编译骨架,便于对照梳理菜单项与命令ID的绑定关系。工程内含文档数据、派生控制条、子对话框与资源头文件等模块,可观察从应用入口到控制条创建注册的调用链,理解MFC控件与AutoCAD命令消息的衔接方式,快速搭建个性化CAD工作界面并排查集成问题。

1. 屏幕菜单不该只是过去那种层层点选的老界面:为什么要在 ObjectArx 2010 里用 DockControlBar 重做它

老用户提到 ObjectArx 2010 屏幕菜单,脑子里多半是右边那条从 S 到 …、只能一层层点进去、还不能搜索的边条。但在我接到的实际需求里,客户嘴里的“屏幕菜单”根本不是这个东西:他们要在绘图区左侧常驻一块面板,按专业分组列出常用图块和命令,双击一下就把图块插入到当前图纸。这种做法用CAcUiDockControlBar实现最自然,DockControlBar会自动处理停靠、浮动、自动隐藏和主框架重排,你不用自己处理拖动停靠。适合已经会用 ObjectARX 注册命令、想从命令行交互升级到常驻图形界面的开发者。以下几件事均已实践验证:工程怎么配置、控件挂载到哪里、双击怎么派发、多图纸下如何刷新、退出为什么崩溃。

2. 把 IDD 模板变成停靠面板:CAcUiDockControlBar 的三段骨架与工程配置

从“能编译”到“能在 AutoCAD 里停靠”,中间有两类问题:一类来自链接配置,一类来自窗口层级错误。这一章把这两类问题都解决掉,你会得到一个能在主框架左侧停靠、能拖出来浮动、能关掉再开回来的空面板。

2.1 工程配置:MFC 共享 DLL 与依赖库清单

第一个绕不过去的限制是 MFC 链接方式。AutoCAD 2010 本体使用的是 MFC 共享 DLL(/MD)那一套运行时,你的 ARX 如果用“在静态库中使用 MFC”(/MT)编译,加载时很可能直接提示模块加载失败——注意,它并不会提示“MFC 不匹配”,所以很多人会误以为是代码问题,浪费一下午。工程属性里必须选“使用 MFC 在共享 DLL 中使用 MFC”,且 Debug/Release 要和 AutoCAD 本体对齐:拿 Debug 版 ARX 去挂 Release 版 CAD,行为不可预期。

第二个问题是字符集。2010 的官方样例基本都是 Unicode 字符集,ACHAR就是wchar_t。如果你从更早的代码中复制了一段char*拼命令的写法,编译可能能通过,但一执行命令就得不到响应。我一般直接把工程设成 Unicode,然后把所有achar和CString的类型统一到ACHAR上,少一次隐式转换就少一个坑。

依赖库这块,用表格列清楚比在链接器里盲目尝试要快得多:

库用途漏了会怎样
acad.lib命令注册、acedGetAcadFrame等接口LNK2019 找不到acedRegCmds之类的符号
acrx.libARX 运行时、acrxDynamicLinker入口函数里的unlockApplication直接链接失败
acui.libCAcUiDockControlBar、CAcUiDialog本方案的核心类无法链接
adui.libCAdUi基础控件,acui 依赖它只加 acui 会连带报一堆CAdUi*未解析
acdb.lib块表遍历、图块数据列表填不出内容
rxregsvc.lib命令注册服务acedRegCmds->addCommand未解析

提示:64 位 AutoCAD 2010 必须配 64 位的 ObjectARX SDK,acui.lib也有 x64 版本。32 位工程引用 x64 的 lib,报错信息是“模块计算机类型与目标计算机类型冲突”,跟代码无关。

另外,把“使用 MFC”打开之后,向导会生成一个CAcExtensionModule的派生类,并用ACRX_DEFINE_MFC_DYNCTRL宏把模块实例和 MFC 状态绑定起来。这层绑定是后面所有AfxGetStaticModuleState()能正常工作的前提,不要手动删除。

2.2 从 IDD 模板到停靠条:派生类、Create 调用与 OnCreate 挂控件

CAcUiDockControlBar的用法很像“把一个对话框模板贴到一个控制条上”。资源视图里新建一个 Dialog,ID 取IDD_SCREEN_MENU_DLG,样式改成 Child、Border 设 None、Visible 勾上、Title Bar 去掉;然后在上面放一个 List Control(ID 用IDC_LIST_MENU),View 设 Report,想显示图标就把 Style 里的 Owner Draw Fixed 先保持关闭,用CImageList挂上去即可。控件位置随便摆,尺寸后面在OnSize里统一重算。

派生类的头文件我一般写成这样:

// ScreenMenuBar.h #pragma once #include "acui.h" // CAcUiDockControlBar 在这个头里 class CScreenMenuBar : public CAcUiDockControlBar { public: CScreenMenuBar(); virtual ~CScreenMenuBar(); // 三个参数:父窗口、控制条 ID、对话框模板 ID BOOL Create(CWnd* pParent, UINT nCtrlID, UINT nDlgTemplateID); protected: virtual void OnSize(UINT nType, int cx, int cy); virtual BOOL OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult); afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); afx_msg void OnDestroy(); DECLARE_MESSAGE_MAP() private: void FillFromBlockTable(); void RunCommand(const CString& sCmd); CListCtrl m_lstMenu; // 子类化模板上的 List Control };

Create的实现要把两个容易写错的地方一次做对:父窗口给acedGetAcadFrame()而不是当前文档窗口,样式里带上CBRS_LEFT | CBRS_RIGHT | CBRS_SIZE_DYNAMIC,否则拖不动也停不了。

BOOL CScreenMenuBar::Create(CWnd* pParent, UINT nCtrlID, UINT nDlgTemplateID) { if (pParent == NULL) pParent = acedGetAcadFrame(); // 主框架,不是 acdbCurDwg 的窗口 if (!CAcUiDockControlBar::Create(_T("屏幕菜单"), pParent, nCtrlID, nDlgTemplateID, WS_CHILD | WS_VISIBLE | CBRS_LEFT | CBRS_RIGHT | CBRS_SIZE_DYNAMIC)) return FALSE; EnableDocking(CBRS_ALIGN_LEFT | CBRS_ALIGN_RIGHT); // 只允许左右停靠 return TRUE; }

参数说明:nCtrlID是这个控制条在主框架里的唯一标识,建议取0xE800–0xE8FF区间里靠后的值(这段是 MFC 给控制条预留的),不要让主框架里两个控制条使用同一个 ID;nDlgTemplateID就是刚才那个 Child 样式的对话框模板。Create返回 FALSE 时先别怀疑 API,九成是模板样式不对或者资源句柄指到了acad.exe,下一节的排查清单里会细说。

控件怎么挂?基类用模板创建了控制条自身的窗口,模板上的控件就是它的直接子窗口,所以在OnCreate里直接子类化:

int CScreenMenuBar::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CAcUiDockControlBar::OnCreate(lpCreateStruct) == -1) return -1; if (!m_lstMenu.SubclassDlgItem(IDC_LIST_MENU, this)) { acutPrintf(_T("\n屏幕菜单:找不到 IDC_LIST_MENU,检查模板与模块状态。")); return -1; } m_lstMenu.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES); m_lstMenu.InsertColumn(0, _T("名称"), LVCFMT_LEFT, 120); m_lstMenu.InsertColumn(1, _T("类型"), LVCFMT_LEFT, 60); FillFromBlockTable(); return 0; }

SubclassDlgItem的第二个参数传this,是因为控制条的窗口就是模板窗口本身;如果你以后改成“空模板 + 自己Create一个CAcUiDialog子对话框来承载控件”的做法(逻辑复杂时确实更好维护),这里就要换成那个对话框对象。子类化失败时不要强行继续执行——CListCtrl的m_hWnd是空的,后面InsertItem会直接触发断言。

2.3 注册命令与卸载顺序:入口函数里必须按序做对的三件事

初始化顺序错一步,后面都不对。kInitAppMsg里按“解锁 → 注册 MDI → 建面板 → 注册命令 → 挂反应器”的顺序做;kUnloadAppMsg里严格反着来,尤其是面板必须先销毁再删除。

static CScreenMenuBar* g_pMenuBar = NULL; static CScreenMenuDocReactor* g_pDocReactor = NULL; void InitScreenMenuApp() { AFX_MANAGE_STATE(AfxGetStaticModuleState()); // 切模块状态,资源才找得到 if (g_pMenuBar == NULL) { g_pMenuBar = new CScreenMenuBar(); // 0xE8F0 是控制条 ID,IDD_SCREEN_MENU_DLG 是对话框模板 if (!g_pMenuBar->Create(acedGetAcadFrame(), 0xE8F0, IDD_SCREEN_MENU_DLG)) { delete g_pMenuBar; g_pMenuBar = NULL; acutPrintf(_T("\n屏幕菜单创建失败,检查模板样式与资源句柄。")); } } } void UnloadScreenMenuApp() { AFX_MANAGE_STATE(AfxGetStaticModuleState()); if (g_pMenuBar != NULL) { if (::IsWindow(g_pMenuBar->GetSafeHwnd())) { acedGetAcadFrame()->ShowControlBar(g_pMenuBar, FALSE, FALSE); g_pMenuBar->DestroyWindow(); // 先摘下来,再删 } delete g_pMenuBar; g_pMenuBar = NULL; } } extern "C" AcRx::AppRetCode acrxEntryPoint(AcRx::AppMsgCode msg, void* pkt) { switch (msg) { case AcRx::kInitAppMsg: acrxDynamicLinker->unlockApplication(pkt); acrxRegisterAppMDIAware(pkt); // 不加这句,多图纸下只认第一张 InitScreenMenuApp(); acedRegCmds->addCommand(_T("SCREENMENU_ARX"), _T("SMENUSHOW"), _T("SMENUSHOW"), ACRX_CMD_MODAL, ShowMenuCmd); break; case AcRx::kUnloadAppMsg: acedRegCmds->removeGroup(_T("SCREENMENU_ARX")); UnloadScreenMenuApp(); break; } return AcRx::kRetOK; }

三处细节值得单独说。unlockApplication必须在做任何事之前调用,否则后续创建窗口时会因为模块被锁而失败。acrxRegisterAppMDIAware是给主框架使用的声明,不调用它,AutoCAD 会认为你的应用只服务于第一张图纸,新建图纸后你的面板还在,但和当前文档脱节了。最后,卸载阶段“先ShowControlBar(FALSE)再DestroyWindow再delete”这个顺序不能颠倒:直接delete会让主框架的停靠列表里留一个野指针,关 CAD 时就会出现经典的退出崩溃。

3. 让屏幕菜单点得动:块表填充、双击派发与面板缩放跟随

面板能停靠了,接下来最影响体验的是三件事:列表里得显示有用的数据、双击后确实要能插入东西、窗口拖窄不能把控件裁掉。这三件事都不难,但每一件都有个“看起来能跑、实际会出问题”的写法。

3.1 菜单数据从哪来:块表遍历与配置节区两条路

做屏幕菜单,数据源无非两种:直接从当前图纸的块表抓,或者从一个外部配置里读。前者零配置、装完就能用,适合“把本图所有图块列出来”的场景;后者可控性强,能分组、能排顺序、能给中文别名,适合给甲方做的成品工具。我一般先做块表版本把功能跑通,再叠加配置文件,两步走比一上来就设计配置格式靠谱。

块表遍历的写法要小心资源的 open/close 配对,尤其是提前continue的分支:

void CScreenMenuBar::FillFromBlockTable() { m_lstMenu.DeleteAllItems(); AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase(); AcDbBlockTable* pBt = NULL; if (pDb->getBlockTable(pBt, AcDb::kForRead) != Acad::eOk) return; AcDbBlockTableIterator* pIt = NULL; pBt->newIterator(pIt); int nRow = 0; for (; !pIt->done(); pIt->step()) { AcDbBlockTableRecord* pRec = NULL; if (pIt->getRecord(pRec, AcDb::kForRead) != Acad::eOk) continue; const ACHAR* pName = NULL; pRec->getName(pName); // 过滤匿名块、模型空间/图纸空间(它们都以 * 开头) if (pName != NULL && pName[0] != _T('*') && !pRec->isDynamicBlock()) { int nIdx = m_lstMenu.InsertItem(nRow, pName); m_lstMenu.SetItemText(nIdx, 1, _T("块")); m_lstMenu.SetItemData(nIdx, (DWORD_PTR)pRec->objectId().asOldId()); nRow++; } pRec->close(); // 一定要关,哪怕是 continue 之前 } pIt->close(); pBt->close(); }

参数上只有两个地方需要调整:isDynamicBlock()这个判断看你要不要列动态块,列出来的话双击插入的是它的默认状态,用户还得自己去属性面板改参数;SetItemData把AcDbObjectId存进去只是为了后面能精确插入——不过要注意,跨图纸切换后这些 ID 就失效了,所以真正派发命令时我更倾向于用块名而不是 ID,下面一节会说为什么。

如果走配置文件路线,格式建议简单到极致:一个 INI 或者一个自定义的 Xrecord 就够,别上数据库。INI 每行分组|显示名|块名|命令,读进CListCtrl按分组插分隔行,比块表方案多花的代码不超过一百行,但可维护性高一个档次。

3.2 双击条目怎么执行命令:sendStringToExecute 与命令串拼装

这是最容易出错的地方。在面板的回调里直接调用acedCommand或acedInvoke,等于在非模态窗口的消息处理过程中重入命令栈,轻则命令执行一半就停、重则 CAD 直接退出。正确做法是把命令串交给文档管理器,让它在本轮消息处理结束后再执行。

void CScreenMenuBar::RunCommand(const CString& sCmd) { AcApDocument* pDoc = acDocManager->curDocument(); if (pDoc == NULL || pDoc->database() == NULL) return; // activate=true 让命令在当前文档里立刻执行 AcApDocManager::sendStringToExecute(pDoc, sCmd + _T("\n"), true, false, false); // 命令发出去后焦点还在面板上,用户会以为卡住了 if (acedGetAcadDwgView() != NULL) acedGetAcadDwgView()->SetFocus(); }

sendStringToExecute的后三个参数是activate、wrapUpInactiveDoc、echoCommand:第一个必须为 true,否则命令可能被塞进一个“待激活”队列里不执行;第三个留 false,否则命令行会把整串命令回显出来,看起来体验很差。

双击的处理放在OnNotify里,比ON_NOTIFY宏更直观,也方便统一拦截:

BOOL CScreenMenuBar::OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult) { NMHDR* pHdr = (NMHDR*)lParam; if (pHdr->idFrom == IDC_LIST_MENU && pHdr->code == NM_DBLCLK) { int nSel = m_lstMenu.GetNextItem(-1, LVNI_SELECTED); if (nSel != -1) { CString sName = m_lstMenu.GetItemText(nSel, 0); CString sCmd; // -INSERT 后面跟块名,然后等用户点插入点 if (sName.Find(_T(' ')) >= 0) sCmd.Format(_T("_.-INSERT\n\"%s\""), (LPCTSTR)sName); else sCmd.Format(_T("_.-INSERT\n%s"), (LPCTSTR)sName); RunCommand(sCmd); } *pResult = 0; return TRUE; // 吃掉消息,不要再往基类冒 } return CAcUiDockControlBar::OnNotify(wParam, lParam, pResult); }

块名带空格的处理是个经验细节:-INSERT的块名参数如果直接

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 4:15:35

东莞废水治理厂家实力与用户口碑:鼎力环保客户口碑力荐

东莞废水治理厂家实力与用户口碑:鼎力环保客户口碑力荐 废水治理定制厂家哪家靠谱?这是东莞及周边制造企业老板在环保整改时问得最多的一句话。随着环保监管持续加严,污水排放抽检频次逐年提高,排放标准不断收紧,工业企业一旦污水…

作者头像 李华
网站建设 2026/10/24 4:15:17

Avatar 骨骼映射:从任意骨架到标准人形的统一表示

一、问题:同一套动画如何服务高矮胖瘦各种角色 你有一组动画资源:一段走、一段跑、一段待机。它们是为某一个具体角色录的。 现在你要把它们用在另一个角色上——身高差 30 厘米,肩膀更宽,胳膊更长,手臂比例还不一样。 直接把动画播过去会出什么问题? 脚步会打滑:源角…

作者头像 李华
网站建设 2026/10/24 4:15:17

C# 与 C++ 之间传字符串:一次 P/Invoke 调用的逐字节内存追踪

一、先规定一下"看清楚"的标准 1.1 三个必须分清的概念 读内存问题,最容易混淆的是下面这三样东西。它们在语言里长得像,在内存里完全是三码事。 概念 是什么 谁负责它的生死 地址 一个指向某处的数字 谁创建指针,谁决定它还指向谁 内容 地址指向的那些字节 内容…

作者头像 李华
网站建设 2026/10/24 4:15:06

叁方GEO 和传统 SEO 外包的区别:可验证指标、交付物与风险

叁方GEO 和传统 SEO 外包的区别:可验证指标、交付物与风险 叁方GEO 和传统 SEO 外包的区别:可验证指标、交付物与风险> 本文为技术实践笔记,讨论生成式引擎优化(GEO)的内容结构、收录推送与回测方法。不涉及商业推广…

作者头像 李华