☰
ObjectArx 2010屏幕菜单可停靠面板开发避坑
2026/10/28 10:36:06 网站建设 项目流程

简介:面向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宽 200~260、高 480~600宽度给 0 会被吸成一条线
bHasGripperTRUE没有抓手,拖动不了分隔条
nIDTemplate对话框资源 ID资源不存在直接返回FALSE
dwStyleWS_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选None,Visible可以不勾,因为外层由WS_VISIBLE控制。我见过有人把模板设成Popup,结果Create返回FALSE,日志里什么都没有,纯靠猜。

面板起来之后建议补一次布局刷新:

acedGetAcadFrame()->RecalcLayout();

不刷新的话,在某些界面配置下右侧工具栏会压住面板一截,得拖动一下主窗口才回正。这行不写也能用,写了体验更接近原生。

3. 让面板吃到数据:MFC 模块状态、文档切换和列表刷新

面板挂上去只是空壳,真正决定它好不好用的是数据链路。这一章解决三个问题:控件里的资源从哪来、切文档时面板怎么知道、命令怎么驱动面板显示隐藏。

3.1 AFX_MANAGE_STATE 与跨模块资源

ARX 是 DLL,MFC 有自己的一套模块状态链。宿主是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里的拆窗口顺序,以及反应器有没有干净移除。

第三遍叫「跨文档乱切」。开三个 DWG,A 图打开面板,切到 B 图看列表是不是 B 图的图层,切到 C 图再切回 A 图,看面板是否还在、数据是否对。这一遍验证的是文档反应器和workingDatabase()的配合,acedGetAcadFrame()在文档切换期间拿到的框架句柄不会变,但数据库会。

早期我习惯只跑第一遍,觉得开合没问题就成了,结果交付后最常被投诉的偏偏是卸载崩溃和切文档数据对不上。从那以后我每次改完跟面板相关的代码,都把这三遍强制走一遍,哪怕只改了一行标题字符串——标题一动位置记忆就废,这种退化只有切文档加重启才能暴露出来。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询