1. 项目概述:为什么要在ARX里折腾屏幕菜单?
如果你在AutoCAD二次开发这个圈子里混过一段时间,肯定会遇到一个经典难题:如何优雅地管理那些功能繁多、但又不想一股脑全塞进Ribbon工具栏或传统对话框里的命令和工具?尤其是在开发一些专业插件时,比如一个复杂的参数化构件库、一个图层状态管理器,或者一个需要频繁切换选项的绘图工具。传统的模态对话框会阻塞主界面,用户体验割裂;而把几十个按钮全堆在Ribbon上,又会显得杂乱无章,用户找起来也费劲。
这时候,一个可以停靠在屏幕侧边、随时展开收起、并且能承载丰富控件的“屏幕菜单”(或者叫“选项板”、“工具选项板”)就成了一个绝佳的解决方案。在AutoCAD的ObjectARX(ARX)开发体系中,CAdUiPaletteSet类就是官方为我们提供的、用来创建和管理这类屏幕菜单的核心武器库。
简单来说,这次我们要聊的,就是如何利用CAdUiPaletteSet这个类,在AutoCAD内部打造一个功能强大、交互流畅、用户体验媲美原生功能的侧边栏工具。这不仅仅是创建一个窗口那么简单,它涉及到ARX框架下的消息机制、资源管理、界面布局以及与AutoCAD主程序的深度集成。很多新手在初次接触时,往往卡在如何创建、如何添加内容、如何响应事件这些环节上。接下来,我就结合自己踩过的坑和积累的经验,把这个过程掰开揉碎了讲清楚。
2. 核心思路与架构设计
2.1 CAdUiPaletteSet 是什么?它能解决什么问题?
在深入代码之前,我们得先搞清楚CAdUiPaletteSet的定位。你可以把它想象成一个“标签页容器管理器”。它本身不是一个具体的工具面板,而是一个可以容纳多个CAdUiPalette(工具选项板)的框架窗口。这个框架窗口具备以下关键特性:
- 可停靠性:用户可以将其拖拽到AutoCAD主窗口的左侧、右侧或底部,并自动吸附对齐,也可以让其浮动。
- 标签页管理:一个
CAdUiPaletteSet可以包含多个CAdUiPalette,每个Palette作为一个标签页,用户通过点击标签头进行切换。 - 自动布局:框架负责管理其内部所有
Palette的显示、隐藏、尺寸以及滚动条。 - 与AutoCAD主题集成:其外观(颜色、字体等)会自动匹配当前AutoCAD的主题设置,保证视觉统一。
那么,它解决了什么问题呢?核心就是非模态、持久化、高效率的工具交互。相比于模态对话框,它不打断用户当前的绘图操作,用户可以一边看着选项板上的参数,一边在图形窗口中进行绘制。相比于固定的工具栏,它又能提供更丰富的控件(如树形列表、属性网格、图像按钮等)和更大的信息展示空间。
2.2 整体实现方案选型
在ARX中创建一个完整的屏幕菜单,通常遵循“总-分”结构:
- “总” - CAdUiPaletteSet:作为顶层容器,负责窗口的创建、显示、销毁以及管理下属的各个子面板。一个应用程序通常只需要一个
PaletteSet实例。 - “分” - CAdUiPalette:作为具体的内容载体。每个
Palette代表一个功能模块,比如“图库”、“属性”、“设置”等。我们需要从CAdUiPalette类派生自己的子类,并在其中创建具体的对话框资源(CDialog)或自定义控件。
为什么选择派生CAdUiPalette而不是直接使用CDialog?这是因为CAdUiPalette类封装了与CAdUiPaletteSet容器协同工作所需的特定接口和消息处理机制。直接使用CDialog无法获得自动停靠、标签页切换、主题适配等特性。派生后,我们可以在OnCreate或OnInitialUpdate等重载函数中创建并嵌入实际的对话框界面。
方案流程概览:
- 在ARX模块初始化时(如
acrxEntryPoint中的kInitAppMsg消息处理中),创建我们的自定义CAdUiPaletteSet实例。 - 创建并初始化一个或多个自定义的
CAdUiPalette派生类实例。 - 将这些
Palette实例添加到PaletteSet中。 - 在适当的时机(如响应某个命令)调用
PaletteSet的Display或SetVisible方法将其显示出来。 - 在模块卸载时(
kUnloadAppMsg),安全地销毁PaletteSet及其所有资源。
注意:
CAdUiPaletteSet和CAdUiPalette的生命周期管理至关重要。它们必须与ARX模块的生命周期绑定,确保在AutoCAD卸载你的程序前,所有窗口资源都已正确释放,否则可能导致内存泄漏或AutoCAD崩溃。
3. 核心细节解析与实操要点
3.1 自定义CAdUiPalette派生类的实现细节
这是整个屏幕菜单的“血肉”部分。创建一个典型的自定义Palette类,你需要做以下几件事:
1. 类声明与消息映射:
// MyToolPalette.h class CMyToolPalette : public CAdUiPalette { DECLARE_DYNAMIC(CMyToolPalette) // 支持运行时类型信息 public: CMyToolPalette(); virtual ~CMyToolPalette(); // 重写关键虚函数 virtual void OnCreate(CWnd* pParentWnd) override; virtual void OnDestroy() override; virtual BOOL OnSaveState(CAdUiState& state) const override; virtual BOOL OnRestoreState(const CAdUiState& state) override; // 自定义成员函数和数据 void UpdateToolList(); CListCtrl m_wndListCtrl; // 示例:一个列表控件 CButton m_btnRefresh; // 示例:一个按钮 protected: DECLARE_MESSAGE_MAP() afx_msg void OnSize(UINT nType, int cx, int cy); afx_msg void OnBtnRefreshClicked(); // ... 其他消息处理函数 private: CDialog* m_pDlg; // 指向内嵌对话框的指针 };2. 关键重载函数解析:
OnCreate: 这是Palette的“出生点”。在这里,你需要创建实际的界面。通常有两种方式:- 方式A:嵌入一个CDialog资源。这是最常用、最直观的方式。你可以先在资源编辑器里设计好一个对话框(记得设置
Style为Child,Border为None),然后在这里用Create函数创建它。
void CMyToolPalette::OnCreate(CWnd* pParentWnd) { CAdUiPalette::OnCreate(pParentWnd); // 先调用基类 m_pDlg = new CMyEmbeddedDlg(this); // CMyEmbeddedDlg是你从CDialog派生的类 if (m_pDlg && m_pDlg->Create(IDD_MY_EMBEDDED_DIALOG, this)) { m_pDlg->ShowWindow(SW_SHOW); } else { acutPrintf(_T("\nFailed to create embedded dialog!")); delete m_pDlg; m_pDlg = nullptr; } }- 方式B:动态创建控件。对于简单界面,也可以直接创建控件并手动布局。
void CMyToolPalette::OnCreate(CWnd* pParentWnd) { CAdUiPalette::OnCreate(pParentWnd); CRect rectClient; GetClientRect(&rectClient); // 创建列表控件 if (!m_wndListCtrl.Create(WS_CHILD | WS_VISIBLE | LVS_REPORT, CRect(10, 10, rectClient.Width()-10, rectClient.Height()-40), this, IDC_LIST_TOOLS)) { // 处理创建失败 } // 创建按钮 if (!m_btnRefresh.Create(_T("刷新"), WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, CRect(10, rectClient.Height()-30, 80, rectClient.Height()-10), this, IDC_BTN_REFRESH)) { // 处理创建失败 } }- 方式A:嵌入一个CDialog资源。这是最常用、最直观的方式。你可以先在资源编辑器里设计好一个对话框(记得设置
OnDestroy: 在这里进行资源清理,特别是删除动态创建的对象(如m_pDlg),避免内存泄漏。OnSaveState/OnRestoreState: 这两个函数用于实现用户界面状态的持久化。这是提升用户体验的关键!当用户调整了Palette的大小、位置,或者折叠/展开了某个区域,AutoCAD会调用OnSaveState让你保存这些状态。当Palette下次显示时,会调用OnRestoreState来恢复。BOOL CMyToolPalette::OnSaveState(CAdUiState& state) const { // 保存列表的列宽 for (int i = 0; i < m_wndListCtrl.GetHeaderCtrl()->GetItemCount(); ++i) { int width = m_wndListCtrl.GetColumnWidth(i); CString key; key.Format(_T("ColumnWidth_%d"), i); state.WriteInt(key, width); } // 保存某个复选框的勾选状态 state.WriteBool(_T("ShowPreview"), m_bShowPreview); return CAdUiPalette::OnSaveState(state); // 别忘了调用基类 } BOOL CMyToolPalette::OnRestoreState(const CAdUiState& state) { // 恢复列宽 for (int i = 0; i < m_wndListCtrl.GetHeaderCtrl()->GetItemCount(); ++i) { CString key; key.Format(_T("ColumnWidth_%d"), i); int width = 100; // 默认值 if (state.ReadInt(key, width)) { m_wndListCtrl.SetColumnWidth(i, width); } } // 恢复复选框状态 state.ReadBool(_T("ShowPreview"), m_bShowPreview); UpdateUI(); // 根据状态更新界面 return CAdUiPalette::OnRestoreState(state); }
3. 处理尺寸变化 (OnSize):当用户拖动Palette边框改变其大小时,OnSize消息会被触发。你必须在这里重新计算和调整内部控件的位置和大小,否则界面会混乱。
void CMyToolPalette::OnSize(UINT nType, int cx, int cy) { CAdUiPalette::OnSize(nType, cx, cy); if (m_pDlg && ::IsWindow(m_pDlg->GetSafeHwnd())) { // 让内嵌对话框充满整个Palette客户区 m_pDlg->MoveWindow(0, 0, cx, cy); } // 或者手动调整各个控件... // if (::IsWindow(m_wndListCtrl.GetSafeHwnd())) { ... } }3.2 创建与管理CAdUiPaletteSet
有了Palette,接下来就需要用PaletteSet把它们装起来。
1. 创建PaletteSet实例:通常在你的ARX应用类或一个全局管理类中,将CAdUiPaletteSet作为成员变量。
class CMyArxApp : public AcRxArxApp { // ... private: CAdUiPaletteSet* m_pMyPaletteSet; CMyToolPalette* m_pToolPalette; CMySettingsPalette* m_pSettingsPalette; };在初始化函数中创建它:
void CMyArxApp::createPaletteSet() { // 防止重复创建 if (m_pMyPaletteSet != nullptr && ::IsWindow(m_pMyPaletteSet->GetSafeHwnd())) return; // 1. 创建PaletteSet CString strPaletteSetTitle = _T("我的工具箱"); // 窗口标题 UINT nPaletteSetId = 0x1234; // 一个唯一的ID,用于状态持久化 m_pMyPaletteSet = new CAdUiPaletteSet(strPaletteSetTitle, nPaletteSetId); // 2. 设置PaletteSet的属性(可选,但推荐) m_pMyPaletteSet->SetAutoDelete(true); // 设置窗口关闭时自动删除C++对象 // 设置允许停靠的位置 m_pMyPaletteSet->SetDockableFloatingFlags( CAdUiPaletteSet::kDockableFloatingFlags_AllowDockLeft | CAdUiPaletteSet::kDockableFloatingFlags_AllowDockRight | CAdUiPaletteSet::kDockableFloatingFlags_AllowFloat ); // 设置初始位置和大小 m_pMyPaletteSet->SetMinimumSize(CSize(250, 300)); // 最小尺寸 // m_pMyPaletteSet->SetRect(...); // 设置初始矩形,通常让系统决定或读取保存的状态 // 3. 创建并添加Palette m_pToolPalette = new CMyToolPalette(); m_pSettingsPalette = new CMySettingsPalette(); // 添加Palette,并指定其在标签页上的显示名称 m_pMyPaletteSet->Add(_T("工具"), m_pToolPalette); m_pMyPaletteSet->Add(_T("设置"), m_pSettingsPalette); // 4. (可选)设置默认激活的标签页 m_pMyPaletteSet->SetActivePalette(m_pToolPalette); }2. 显示与隐藏PaletteSet:通常通过一个ARX命令来触发显示。
static void MyShowToolboxCommand() { CMyArxApp* pApp = getMyAppInstance(); // 获取你的应用实例 if (!pApp) return; pApp->ensurePaletteSetCreated(); // 确保PaletteSet已创建 CAdUiPaletteSet* pPalSet = pApp->getPaletteSet(); if (pPalSet) { if (!pPalSet->IsVisible()) { pPalSet->SetVisible(TRUE); // 显示 // 或者使用Display方法,它更强大,可以指定停靠位置 // pPalSet->Display(TRUE, CAdUiPaletteSet::kDockRight, NULL, CAdUiPaletteSet::kInitSizeDefault); } else { // 如果已经显示,可以将其激活到前台 pPalSet->SetFocus(); } } }3. 生命周期与销毁:在ARX模块的kUnloadAppMsg或应用类的析构函数中,必须安全地销毁PaletteSet。
void CMyArxApp::destroyPaletteSet() { if (m_pMyPaletteSet) { // 重要:先隐藏并销毁窗口 m_pMyPaletteSet->DestroyWindow(); // 如果SetAutoDelete(true),则窗口销毁时会自动删除对象,此时应将指针置空。 // 如果未设置自动删除,则需要手动delete。 // 这里假设设置了SetAutoDelete(true) m_pMyPaletteSet = nullptr; } // Palette对象会被PaletteSet在内部管理销毁,我们通常不需要手动delete它们。 m_pToolPalette = nullptr; m_pSettingsPalette = nullptr; }实操心得:关于
SetAutoDelete。我强烈建议设置为true。这能让CAdUiPaletteSet遵循MFC窗口对象的常见生命周期管理规则:当用户点击窗口关闭按钮或你调用DestroyWindow()时,C++对象会自动被删除。这比手动管理delete要安全得多,能有效避免“窗口已销毁但对象还在”或“重复删除”的经典错误。
4. 高级功能与交互实现
4.1 在Palette与AutoCAD图形空间之间通信
屏幕菜单不是孤岛,它需要与AutoCAD的主图形文档(AcDbDatabase)和编辑器(AcEdEditor)进行交互。例如,点击Palette上的一个图块名,将其插入到当前图中。
核心机制:使用ARX的全局函数、通知机制或自定义消息。
方法一:直接调用全局命令或函数。在你的Palette按钮点击事件处理函数中,可以调用acedCommand或acDocManager->sendStringToExecute来执行AutoCAD命令。
void CMyToolPalette::OnBtnInsertBlockClicked() { CString blockName = GetSelectedBlockName(); // 假设这个函数获取选中的图块名 if (blockName.IsEmpty()) return; // 方式1:使用acedCommand (适用于简单命令序列) struct resbuf *cmdList = acutBuildList( RTLB, // 开始列表 RTSTR, _T("_-INSERT"), // 命令名 RTSTR, blockName, // 图块名 RTPOINT, asDblArray(AcGePoint3d::kOrigin), // 插入点,这里简化了 RTSTR, _T(""), // X比例默认 RTSTR, _T(""), // Y比例默认 RTSTR, _T(""), // 旋转角默认 RTLE, // 结束列表 0); if (cmdList) { acedCommand(cmdList); acutRelRb(cmdList); } // 方式2:使用sendStringToExecute (更灵活,不阻塞当前调用栈) // CString cmdStr; // cmdStr.Format(_T("(command \"_-INSERT\" \"%s\") "), blockName); // acDocManager->sendStringToExecute(acDocManager->curDocument(), cmdStr); }方法二:使用自定义消息或事件机制。对于更复杂的交互,比如Palette需要根据当前图形选择集的变化来更新自身内容,建议使用观察者模式或ARX的通知器(AcApDocManagerReactor,AcEditorReactor)。
- 创建一个全局的或应用级的事件管理器。
- 让Palette类订阅相关事件(如“选择集已更改”、“文档已切换”)。
- 当AutoCAD中发生相应事件时,事件管理器通知所有订阅者(包括你的Palette)。
- Palette在收到通知后,调用自己的
UpdateToolList()等方法来刷新界面。
这种方式解耦了UI和业务逻辑,使代码更清晰、更易于维护。
4.2 实现拖放功能 (Drag & Drop)
让用户可以直接从你的Palette中将一个项目(如图标、图块名)拖拽到AutoCAD图形区域,这是一个极大的体验提升。实现起来稍复杂,但思路清晰:
在Palette控件上启动拖放:在你的列表或树形控件(
CListCtrl/CTreeCtrl)中,处理LVN_BEGINDRAG或TVN_BEGINDRAG通知消息。在这个消息处理函数中,你需要:- 获取被拖拽项的数据(如唯一的图块ID或路径)。
- 创建一个
COleDataSource对象。 - 将你的数据按照某种格式(如自定义剪贴板格式
CF_TEXT或CF_HDROP)放入COleDataSource。 - 调用
COleDataSource::DoDragDrop开始拖放操作。
在AutoCAD图形区域接受拖放:这需要你实现一个
AcEdDragDrop反应器(AcEdDragDropReactor)。你需要派生一个类,并重写其虚函数,特别是onDragEnter,onDragOver,onDrop。- 在
onDrop函数中,你可以从数据对象(COleDataObject)中提取出在第一步中放入的数据。 - 根据这些数据,执行相应的操作,比如调用
acedCommand插入图块。
- 在
注意事项:拖放功能的实现涉及到COM和OLE的基础知识,调试起来可能比较麻烦。务必确保数据格式在拖放两端保持一致,并且在
DoDragDrop和onDrop中处理好所有的错误情况。初次实现时,可以先做一个最简单的文本拖放进行验证。
4.3 样式与主题适配
为了让你的屏幕菜单看起来和AutoCAD原生组件一样专业,需要关注样式适配。
- 颜色和字体:
CAdUiPalette和其内部的控件默认会继承AutoCAD的主题。但如果你自定义绘制了某些区域,应该使用adui_get_system_color()等函数来获取当前主题的系统颜色,而不是硬编码颜色值。 - 高DPI支持:随着4K显示器的普及,高DPI适配变得重要。确保你的对话框资源在“属性”中设置了
True Type字体,并且考虑在OnCreate或OnSize中根据DPI缩放因子动态调整控件位置和字体大小。可以使用GetDeviceCaps获取DPI。 - 图标:使用与AutoCAD图标风格一致的图标(通常为16x16, 24x24, 32x32 PNG格式,带有透明通道)。可以在Palette的标题栏或按钮上使用
CAdUiBitmapButton等控件来获得更好的视觉效果。
5. 常见问题、调试技巧与性能优化
5.1 开发中常见的“坑”与解决方案
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
| PaletteSet创建后不显示 | 1. 未调用SetVisible(TRUE)或Display()。2. 创建PaletteSet的代码未在正确的时机执行(如命令尚未注册)。 3. 窗口句柄创建失败。 | 1. 在命令函数中单步调试,确认Display或SetVisible被调用。2. 检查 new CAdUiPaletteSet和Add方法是否成功,检查指针是否为空。3. 在 OnCreate中加入acutPrintf输出,看是否被调用。 |
| 内嵌对话框/控件显示为空白或错位 | 1.OnCreate中创建控件失败。2. 未正确处理 OnSize消息,控件尺寸未更新。3. 对话框资源风格设置错误(如不是 Child风格)。 | 1. 检查Create函数的返回值,检查资源ID是否正确。2. 在 OnSize中设置断点,检查cx, cy参数,并确认控件MoveWindow被调用。3. 在资源编辑器中检查对话框属性。 |
| 关闭AutoCAD时崩溃 | 1.CAdUiPaletteSet或CAdUiPalette对象未正确销毁,导致双重删除。2. 在析构函数或 OnDestroy中访问了已销毁的MFC对象。 | 1. 确保遵循“窗口销毁自动删除对象”的模式(SetAutoDelete(true)),并理清所有权关系。2. 在销毁前,使用 ::IsWindow(hWnd)判断窗口句柄是否有效。3. 使用调试工具(如Visual Studio的调试器)查看崩溃时的调用栈。 |
| 状态(如窗口位置、列宽)无法保存 | 1. 未重写OnSaveState/OnRestoreState,或重写但未正确读写数据。2. 保存的键名(Key)不唯一或格式错误。 3. 读写的数据类型与函数不匹配。 | 1. 确认这两个虚函数已被正确重写并调用。 2. 在 OnSaveState中,使用acutPrintf输出你保存的值,在OnRestoreState中输出读取的值,对比是否一致。3. 检查 CAdUiState的WriteInt,ReadInt等函数调用是否正确。 |
| 与AutoCAD交互(如执行命令)无效 | 1. 命令字符串格式错误。 2. 在错误的文档上下文或事务中执行。 3. acedCommand使用resbuf结构错误。 | 1. 先在AutoCAD命令行手动输入命令,确认语法正确。 2. 使用 acDocManager->curDocument()确保获取到当前活动文档。3. 使用 acutBuildList辅助构建resbuf,并仔细检查括号配对(RTLB/RTLE)。 |
5.2 调试技巧
- 使用
acutPrintf进行日志输出:这是ARX开发中最简单直接的调试手段。在关键函数入口、错误分支处输出信息,可以在AutoCAD的文本窗口(F2)看到。 - 利用Visual Studio调试器:将AutoCAD设置为调试启动项目。你可以在自己的ARX代码中设置断点,单步执行,观察变量。这对于追踪消息流、检查指针状态至关重要。
- 检查资源ID:对话框、控件、字符串的ID冲突是MFC开发中常见的静默错误。确保你的资源ID在
resource.h中定义唯一,并且与代码中使用的ID完全一致。 - 关注消息映射:如果某个消息(如按钮点击)没有响应,首先检查
BEGIN_MESSAGE_MAP和END_MESSAGE_MAP是否包含了对应该消息的条目,以及消息处理函数的声明和实现是否正确。
5.3 性能优化建议
- 惰性加载:如果某个Palette内容非常复杂(例如包含一个需要从网络加载大量数据的三维预览),不要在
OnCreate中就初始化所有内容。可以等到该Palette第一次被激活(OnSetActive)时再加载。 - 虚拟列表/树控件:如果
CListCtrl或CTreeCtrl需要显示成千上万行数据,务必使用虚拟模式(LVS_OWNERDATA)。在这种模式下,控件只保存索引,数据由你按需提供,可以极大减少内存占用和初始化时间。 - 避免频繁刷新:不要在任何可能被频繁调用的函数(如
OnTimer或无节制的消息处理函数)中进行全界面刷新。使用标志位(m_bNeedUpdate)和延迟更新机制(PostMessage一个自定义的更新消息)。 - 图形资源管理:图标、位图等资源在使用后应及时释放(
DeleteObject)。对于频繁使用的资源,可以设计一个缓存管理器,避免重复加载。
6. 一个完整的迷你示例:简易图块选项板
为了把上面的理论串联起来,这里给出一个极度简化的、但可运行的示例框架,实现一个只有一个标签页、一个列表显示图块名、点击插入的简易选项板。
步骤1:创建对话框资源在资源编辑器里创建一个ID为IDD_PALETTE_BLOCK的对话框,风格为Child,边框为None。上面放一个CListCtrl,ID设为IDC_LIST_BLOCKS。
步骤2:定义Palette派生类
// BlockPalette.h #pragma once #include <AdUiPalette.h> #include <AdUiPaletteSet.h> #include <afxtempl.h> // 用于CArray class CBlockPalette : public CAdUiPalette { DECLARE_DYNAMIC(CBlockPalette) public: CBlockPalette(); virtual ~CBlockPalette(); virtual void OnCreate(CWnd* pParentWnd) override; virtual void OnDestroy() override; virtual BOOL OnSaveState(CAdUiState& state) const override; virtual BOOL OnRestoreState(const CAdUiState& state) override; void RefreshBlockList(); // 刷新列表内容 protected: DECLARE_MESSAGE_MAP() afx_msg void OnSize(UINT nType, int cx, int cy); afx_msg void OnNMDblclkListBlocks(NMHDR* pNMHDR, LRESULT* pResult); private: CDialog* m_pEmbeddedDlg; CListCtrl m_wndList; CArray<CString, CString&> m_blockNames; // 存储图块名 };步骤3:实现Palette派生类
// BlockPalette.cpp #include "StdAfx.h" #include "BlockPalette.h" #include "resource.h" // 包含对话框资源ID IMPLEMENT_DYNAMIC(CBlockPalette, CAdUiPalette) BEGIN_MESSAGE_MAP(CBlockPalette, CAdUiPalette) ON_WM_SIZE() ON_NOTIFY(NM_DBLCLK, IDC_LIST_BLOCKS, &CBlockPalette::OnNMDblclkListBlocks) END_MESSAGE_MAP() CBlockPalette::CBlockPalette() : m_pEmbeddedDlg(nullptr) {} CBlockPalette::~CBlockPalette() {} void CBlockPalette::OnCreate(CWnd* pParentWnd) { CAdUiPalette::OnCreate(pParentWnd); // 创建内嵌对话框 m_pEmbeddedDlg = new CDialog(); if (!m_pEmbeddedDlg->Create(IDD_PALETTE_BLOCK, this)) { acutPrintf(_T("\nFailed to create block palette dialog.")); delete m_pEmbeddedDlg; m_pEmbeddedDlg = nullptr; return; } // 获取列表控件 m_wndList.SubclassDlgItem(IDC_LIST_BLOCKS, m_pEmbeddedDlg); m_wndList.InsertColumn(0, _T("图块名称"), LVCFMT_LEFT, 200); // 初始刷新列表 RefreshBlockList(); } void CBlockPalette::OnDestroy() { if (m_pEmbeddedDlg) { m_pEmbeddedDlg->DestroyWindow(); delete m_pEmbeddedDlg; m_pEmbeddedDlg = nullptr; } CAdUiPalette::OnDestroy(); } void CBlockPalette::OnSize(UINT nType, int cx, int cy) { CAdUiPalette::OnSize(nType, cx, cy); if (m_pEmbeddedDlg && ::IsWindow(m_pEmbeddedDlg->GetSafeHwnd())) { m_pEmbeddedDlg->MoveWindow(0, 0, cx, cy); // 调整列表控件大小,假设对话框客户区只有这个列表 if (::IsWindow(m_wndList.GetSafeHwnd())) { CRect rect; m_pEmbeddedDlg->GetClientRect(&rect); rect.DeflateRect(2, 2, 2, 2); // 留点边距 m_wndList.MoveWindow(rect); m_wndList.SetColumnWidth(0, rect.Width() - 20); // 调整列宽 } } } void CBlockPalette::RefreshBlockList() { if (!::IsWindow(m_wndList.GetSafeHwnd())) return; m_wndList.DeleteAllItems(); m_blockNames.RemoveAll(); // 这里简化:遍历当前数据库的块表,获取所有块定义名 AcDbDatabase* pDb = acdbHostApplicationServices()->workingDatabase(); if (!pDb) return; AcDbBlockTable* pBlockTable = nullptr; if (pDb->getBlockTable(pBlockTable, AcDb::kForRead) == Acad::eOk) { AcDbBlockTableIterator* pIter = nullptr; if (pBlockTable->newIterator(pIter) == Acad::eOk) { for (; !pIter->done(); pIter->step()) { AcDbBlockTableRecord* pRecord = nullptr; if (pIter->getRecord(pRecord, AcDb::kForRead) == Acad::eOk) { TCHAR* pName = nullptr; if (pRecord->getName(pName) == Acad::eOk) { // 过滤掉模型空间和图纸空间等特殊块 if (_tcscmp(pName, ACDB_MODEL_SPACE) != 0 && _tcscmp(pName, ACDB_PAPER_SPACE) != 0) { int idx = m_wndList.InsertItem(m_wndList.GetItemCount(), pName); m_blockNames.Add(pName); } acutDelString(pName); } pRecord->close(); } } delete pIter; } pBlockTable->close(); } } void CBlockPalette::OnNMDblclkListBlocks(NMHDR* pNMHDR, LRESULT* pResult) { LPNMITEMACTIVATE pNMItemActivate = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR); int nItem = pNMItemActivate->iItem; if (nItem >= 0 && nItem < m_blockNames.GetSize()) { CString cmdStr; cmdStr.Format(_T("_-INSERT \"%s\" "), m_blockNames[nItem]); acDocManager->sendStringToExecute(acDocManager->curDocument(), cmdStr); } *pResult = 0; } BOOL CBlockPalette::OnSaveState(CAdUiState& state) const { // 保存列表排序状态等(此处略) return CAdUiPalette::OnSaveState(state); } BOOL CBlockPalette::OnRestoreState(const CAdUiState& state) { // 恢复状态(此处略) return CAdUiPalette::OnRestoreState(state); }步骤4:创建PaletteSet并注册命令在你的主应用初始化代码中:
// 全局或应用类成员 CAdUiPaletteSet* g_pBlockPaletteSet = nullptr; CBlockPalette* g_pBlockPalette = nullptr; void InitMyPalette() { if (g_pBlockPaletteSet) return; g_pBlockPaletteSet = new CAdUiPaletteSet(_T("我的图块库"), 0xABCD); g_pBlockPaletteSet->SetAutoDelete(true); g_pBlockPalette = new CBlockPalette(); g_pBlockPaletteSet->Add(_T("图块"), g_pBlockPalette); } static void SHOWMYBLOCKSPALETTE() { InitMyPalette(); if (g_pBlockPaletteSet && !g_pBlockPaletteSet->IsVisible()) { g_pBlockPaletteSet->Display(TRUE); } } // 在 acrxEntryPoint 的 kInitAppMsg 中注册命令 SHOWMYBLOCKSPALETTE这个示例虽然简单,但涵盖了从创建、布局、内容填充到交互的完整链条。你可以在此基础上,逐步添加状态保存、过滤搜索、图标显示、拖放支持等高级功能。
开发ARX屏幕菜单是一个系统工程,它考验的不仅是对CAdUiPaletteSet这个类的熟悉程度,更是对MFC、Windows消息机制、ARX API以及良好软件设计理念的综合运用。从简单的工具面板到复杂的专业工作台,这个框架都能提供坚实的支撑。关键在于多动手、多调试,遇到问题时,善用官方文档、ObjectARX SDK的样例代码以及开发社区的资源。当你亲手打造的第一个屏幕菜单在AutoCAD中流畅运行起来时,那种成就感会让你觉得所有的折腾都是值得的。