- 桌面应用
- 图形学
【免费下载链接】LibreCAD
LibreCAD is a cross-platform 2D CAD program. It can read DXF/DWG, and write DXF/DWG/PDF/SVG files. It supports point/line/circle/ellipse/parabola/hyperbola/spline primitives. The GUI is highly customizable, and has dozens of translations.
导读
LibreCAD 是一款跨平台的开源 2D CAD 程序(见 README.md),其原生 DXF 格式并不支持图层的层级结构。本文围绕 ChangeLogs/LAYER_TREE_VIEW.md 介绍的Layer Tree View(图层树视图)控件,讲解如何在纯命名约定的基础上为图层建立树状层级、借助图层类型前缀/后缀实现标注、信息、替代位置等辅助图层的自动归类,以及如何利用正则过滤、拖放重组、分组批量操作来提升大型工程图的图层管理效率。读完本文,你将掌握图层树的两种显示模式、五类预定义图层类型、完整工具栏动作与右键菜单操作,以及该控件全部可配置项在源码中的实现与存储位置。
一、问题背景:DXF 图层为何需要"树"
在大型工程图中,图层数量可能非常多,且同一图层常常被复用于单一零件/视图;标注、零件的替代位置、注释与草图等附加信息也往往需要独立的图层。常见做法是:
- 用统一前缀为属于同一零件/视图的图层命名;
- 依靠命名中的公共逻辑前缀进行"逻辑分组"。
但人工命名只能让"分组的味道"更接近,依然存在局限:前缀一旦拼写不一致,分组就会失效。更根本的问题是,DXF 格式本身不支持图层层级,LibreCAD 原生图层结构同样没有层级概念(见原文档 The Problem 一节)。
二、解决方案:命名约定驱动 + 纯 UI 实现
Layer Tree View 的核心理念可以概括为两点:
- 层级完全由命名约定推导:假设图层名中存在某个统一的"分隔子串"(默认是
-),该子串将图层名切分成若干层级片段,第 1 个片段是顶层、最后一个片段是实际图层名,中间片段自动成为"虚拟图层"(组节点)。 - 纯 UI 实现,不影响底层存储:控件不改变
RS_Layer内部结构与 DXF 存储格式,原始平面图层列表(RS_LayersList)不受影响,两个控件可以并行使用(见原文档 Implementation notes 一节)。
从源码看,这一设计得到了印证:LC_LayerTreeItem中的m_name保存的是"解析后的原始名称片段",m_displayName是 UI 展示名,m_fullName是完整路径;而LC_LayerTreeItem对RS_Layer仅持有一个引用(m_layer),本身并不创建新的图层对象,见 lc_layertreeitem.h。RS_Layer类中的m_layerType字段也明确注释为"transient field, should be updated by LayerTreeWidget"(瞬态字段,应由图层树控件更新),见 rs_layer.h。
源码中的层级构建流程
在 lc_layertreemodel.cpp 的rebuildModel()中,层级还原逻辑非常清晰:
// split by parts using defined separator of layer levels QStringList nameParts = layerFullName.split(m_options->layerLevelSeparator, Qt::KeepEmptyParts); ... for (int indent = 0; indent < partsCount-1; ++indent){ const QString& subName = nameParts.at(indent); virtualRoot = virtualRoot -> getOrCreateVirtualChild(subName); ... } if (partsCount > 0){ layerName = nameParts.at(partsCount-1); }即:先用layerLevelSeparator拆分全名,除最后一段外的每一段通过getOrCreateVirtualChild()逐级挂到虚拟节点下,最后一段作为实际图层的显示名。
三、功能总览
根据原文档 Features 一节,控件提供的能力可分为以下几类(各项功能均可在 lc_layertreewidget.cpp 中找到对应实现槽函数):
3.1 常规功能
- 两种显示模式:层级树状(tree)与平面列表(flat list)。
LC_LayerTreeWidget中的m_flatListMode控制该模式,toggleFlatView()槽函数负责切换,见 lc_layertreewidget.h; - 基于正则表达式的过滤:可"过滤图层"或"高亮匹配图层的图层";
- 拖放(Drag&Drop)重组:通过拖动节点调整层级结构;
- 图层层级的分组操作:对虚拟图层(组节点)的点击会递归作用于其全部后代图层。
3.2 预定义的图层类型
原文档定义了五类图层,源码中由LC_LayerTreeItem::LayerType枚举实现(见 lc_layertreeitem.h):
| 类型 | 英文枚举值 | 含义 | 默认命名后缀 |
|---|---|---|---|
| 普通图层 | NORMAL | 常规图层,存放实体 | 无 |
| 标注图层 | DIMENSIONAL | 为对应普通图层存放尺寸标注 | + |
| 信息图层 | INFORMATIONAL | 存放注释与草图 | _meta |
| 替代位置图层 | ALTERNATE_POSITION | 同一零件在不同位置的表达 | _pos |
| 虚拟图层 | VIRTUAL | 其他类型图层的"容器",类似文件夹 | 由分隔符切分出的中间片段 |
注意:
RS_Layer::LayerType枚举同样包含这五类(见 rs_layer.h),LC_LayerTreeItem与RS_Layer共用同一套类型体系,类型标记会在rebuildModel()中通过layer->setLayerType(type)写回图层对象。
类型识别完全依赖"名称是否以指定后缀结尾",这在rebuildModel()中有明确实现:
if (layerName.endsWith(m_options->alternatePositionLayerNameSuffix)){ type = RS_Layer::LayerType::ALTERNATE_POSITION; ... } else if (layerName.endsWith(m_options->informationalLayerNameSuffix)){ type = RS_Layer::LayerType::INFORMATIONAL; ... } else if (layerName.endsWith(m_options->dimensionalLayerNameSuffix)){ type = RS_Layer::LayerType::DIMENSIONAL; ... }这里有个重要细节:同名辅助图层会被自动挂到其主图层(primary layer)之下。例如Doors+会被识别为Doors的标注子图层;若Doors尚不存在,则它会以Doors为主节点挂载(findPrimaryLayerChild查找同父节点下已存在的同名主图层,见 lc_layertreemodel.cpp)。因此主图层必须排在辅助图层之前——源码注释明确指出依赖"incoming list 已正确排序",即主图层因排序在先已被插入树中。
3.3 工具栏动作
initButtonsBar()(见 lc_layertreewidget.cpp)构建了完整按钮条,与文档列举一一对应:
- 显示所有图层(Show all Layers)
- 隐藏除当前图层外的所有图层(Hide all Layers except current)
- 显示/隐藏次级图层(Show/Hide Secondary Layers,按钮可勾选,默认可见)
- 仅显示活动图层(Show Active Layer Only)
- 全部展开(Expand All,树模式)
- 全部折叠(Collapse All,树模式)
- 折叠次级图层(Collapse Secondary,树模式)
- 全部锁定 / 全部解锁(Lock/Unlock All Layers)
- 添加图层(Add a layer)
- 添加标注图层(Add Dimensions Layer)
- 移除图层(Remove layer)
- 编辑/重命名图层(Modify layer attributes / rename)
- 平面列表模式切换(Flat List Mode)
此外,updateToolBarButtons()会根据当前状态(如是否树模式)动态启用/禁用部分按钮。
3.4 过滤框(Filtering Box)
过滤框由过滤输入框(QLineEdit)、Highlight Mode勾选框与设置按钮组成,见initFilterAndSettingsSection()(lc_layertreewidget.cpp):
- 高亮模式(Highlight Mode):默认勾选。匹配正则的图层项在列表中高亮显示;
- 过滤模式:取消勾选后,仅显示匹配正则的图层项,其余全部隐藏。
两种模式由m_layerTreeModel->setFilteringRegexp(mask, highlightMode)驱动(见 lc_layertreemodel.h),模型内部以QRegularExpression m_filteringRegexp保存表达式,并在rebuildModel()中对每个图层名执行hasMatch()匹配。
3.5 图层树视图(Layers Tree View)
- 展示基于命名约定的逻辑结构;
- 显示图层类型图标(普通、虚拟、标注、信息、替代位置各有独立图标,见 lc_layertreemodel.h);
- 提供图层标志列:冻结(可见性)、锁定、打印、构造,模型列定义见 lc_layertreemodel.h;
- 标志列支持 GROUP 操作:点击虚拟图层(组)某一列的标志,会递归应用到其后代图层。其实现位于
slotTreeClicked()(lc_layertreewidget.cpp):虚拟图层通过layerItem->collectLayers(...)收集后代并统一调用manageLayersVisibilityFlag/manageLayersLockFlag/manageLayersPrintFlag/manageLayersConstructionFlag; - 拖放重组层级(
LC_LayerTreeView重写dragEnterEvent/dropEvent,模型提供performReStructure(),见 lc_layertreeview.h); - 双击虚拟图层:展开其后代子项;
- 双击实际图层:弹出图层编辑对话框;
- 项目感知的右键上下文菜单(见下文);
- 可自定义外观(颜色、显示项);
- 悬停显示完整图层名作为工具提示(
showToolTips选项控制); - 可按层级深度对图层名进行可选缩进(
showIndentedName选项控制)。
3.6 图层操作(Operations With Layers)
原文档列出的全部操作在源码中均有对应实现:
| 操作 | 说明 | 主要源码位置 |
|---|---|---|
| 图层创建 | 支持按图层类型使用默认画笔 | invokeLayerAddDialog/getDefaultPen() |
| 图层编辑 | 名称、画笔、类型、构造标志等 | LC_LayerDialogEx(lc_layerdialog_ex.h) |
| 层级感知重命名 | 主图层重命名时可按选项联动重命名后代(次级)图层 | renamePrimaryLayer()(lc_layertreemodel.cpp) |
| 单个图层删除 | 删除指定图层 | doRemoveLayers |
| 层级感知删除 | 删除"图层及其子图层"或"仅子图层" | doRemoveLayersFromSource(source, removeChildrenOnly)(lc_layertreewidget.cpp) |
| 添加指定类型的子图层 | 为普通图层添加标注/信息/替代位置子层 | addDimensionalLayerForSelectedItem等 |
| 改变图层类型 | 普通 ↔ 标注/信息/替代位置互转 | convertLayerTypeToXxx |
| 冻结除选中图层外的所有图层 | Freeze Others | hideOtherThanSelectedLayers |
| 复制图层(层级感知) | 复制层级结构并拷贝属性,不复制实体 | doCreateLayersCopy(..., false) |
| 复制图层(层级感知) | 复制结构+属性,并同时复制源图层上的实体 | doCreateLayersCopy(..., true) |
| 移除空图层 | 删除不含实体的图层 | removeEmptyLayers |
其中"复制"与"复制含内容"的区别值得展开:doCreateLayersCopy()先通过m_layerTreeModel->createLayersCopy()生成结构副本,再在LC_DocumentModificationBatch事务内逐个copyLayerAttributes()复制属性;若duplicateEntities为真,则遍历文档实体,将源图层上的实体clone()后设置到副本图层(duplicateLayerEntities(),见 lc_layertreewidget.cpp)。属性复制覆盖构造/转换/打印标志、画笔的颜色/宽度/线型/透明度/屏幕宽度/虚线偏移等,并同步冻结状态。
3.7 实体操作(Operations With Entities)
- 选择图层实体(层级感知):选中图层(含子层)上的实体;
- 移动当前选中实体到图层(
moveSelectionToLayer); - 复制当前选中实体到图层(
duplicateSelectionToLayer)。
两者最终都进入doMoveSelectionToLayer(layerItem, duplicate, resolvePens)(lc_layertreewidget.cpp):通过undoableModify事务克隆实体并设置目标图层,移动模式额外从文档移除原实体;resolvePens=true时还会把By Layer/By Block的画笔解析为具体值,保证实体迁移到目标图层后外观不变。
3.8 右键上下文菜单
onCustomContextMenu()(lc_layertreewidget.cpp)根据点击对象动态生成菜单:
- 虚拟图层:添加子图层、重命名、移除(子树)、复制结构(子树)、复制内容(子树)、选择实体(子树);
- 实际普通图层:编辑图层属性、移除图层、添加标注/信息/替代位置子图层(仅在该类型子层尚不存在时出现)、移除子图层、类型转换(无子层时)、选择图层实体、创建图层副本、复制含内容图层、移动/复制当前选中实体到该图层;
- 空白区域:添加图层;
- 全局动作(对所有场景):冻结其他图层、解冻所有图层、隐藏除当前外图层、锁定/解锁所有图层、启用/禁用所有图层打印、导出单个图层/图层子树/可见图层、查找并移除空图层。
四、命名约定与配置项(Customization Options)
这是控件最核心的可配置部分。所有选项由LC_LayerTreeModelOptions结构体承载(见 lc_layertreemodel_options.h),并通过load()/save()持久化到 LibreCAD 设置文件的Widget.LayerTree组(见 lc_layertreemodel_options.cpp)。
4.1 命名约定(Naming Convention)
| 配置项(源码成员) | 持久化键名 | 默认值 | 作用 |
|---|---|---|---|
layerLevelSeparator | namingLayerSeparator | - | 图层层级的分隔子串,用于切分图层名各级 |
informationalLayerNameSuffix | namingInfoSuffix | _meta | 标识信息图层(注释/草图)的后缀 |
dimensionalLayerNameSuffix | namingDimSuffix | + | 标识标注图层的后缀 |
alternatePositionLayerNameSuffix | namingAltSuffix | _pos | 标识替代位置图层的后缀 |
copiedNamePathPrefix | namingCopyPrefix | (Copy | 自动生成副本名前缀(防止名称冲突) |
copiedNamePathSuffix | namingCopySuffix | ) | 自动生成副本名后缀(与序号组合) |
举个直观例子:图层名Walls-Outer-North会被解析为层级Walls→Outer→North;Walls-Inner_meta则成为Walls-Inner的信息子图层;Walls-Outer-South_pos是Walls-Outer-South的替代位置子图层。这一推导逻辑与 tests/lc_layertreemodel_tests.cpp 中的测试数据完全一致(测试用Doors、Doors+、Walls-Inner、Walls-Inner_meta、Walls-Outer-North、Walls-Outer-South_pos构建了三层深度:组Walls与Walls-Outer,以及Doors、Walls-Inner下的次级图层)。
4.2 显示选项(Display Settings)
| 配置项 | 持久化键名 | 默认值 | 作用 |
|---|---|---|---|
showIndentedName | showIndentedName | true | 图层名按层级缩进 |
identSize | indentSize | 4 | 缩进宽度(像素级间距) |
showToolTips | showToolTips | true | 悬停时显示完整图层名提示 |
renameSecondaryLayersOnPrimaryRename | renameSecondaryOnPrimary | true | 主图层重命名时联动重命名次级图层 |
hideLayerTypeIcons | hideLayerTypeIcons | false | 是否隐藏图层类型图标 |
dragDropEnabled | dragDropEnabled | true | 是否允许拖放重组 |
showGrid | showGrid | true | 树视图中是否显示网格线 |
4.3 颜色(Colors)
| 配置项 | 持久化键名 | 默认值(浅色) | 作用 |
|---|---|---|---|
matchedItemColor | (跟随loadDefaults) | blue | 正则匹配项的高亮颜色 |
virtualLayerBgColor | virtualLayerBgColor | rgb(245,245,245) | 虚拟图层背景色 |
selectedItemBgColor | selectedItemBgColor | rgb(245,245,245) | 选中项背景色 |
activeLayerBgColor | activeLayerBgColor | white | 活动图层背景色 |
注意:loadDefaults(bool isDark)会根据当前配色方案是否为深色自动切换默认值——深色方案下虚拟/选中/活动图层的背景取自系统QPalette(AlternateBase/Highlight/Mid),见 lc_layertreemodel_options.cpp。
4.4 每种图层类型的默认画笔(Default Pens)
| 配置项 | 持久化键名 | 默认值 | 说明 |
|---|---|---|---|
defaultPenNormal | NormalLayer | 黑色、Width00、实线 | 普通图层创建对话框中的默认画笔 |
defaultPenDimensional | DimensionalLayer | 蓝色、Width02、实线 | 标注图层 |
defaultPenInformational | InfoLayer | 品红、Width02、实线 | 信息图层 |
defaultPenAlternatePosition | AltPosLayer | 青色、Width00、实线 | 替代位置图层 |
getDefaultPen(layerType)按类型返回对应画笔,图层创建对话框会将该画笔预置为初始值(见 lc_layertreemodel_options.cpp)。画笔通过RS_Settings::writePen/readPen持久化,可在"图层树设置"对话框中修改。
4.5 设置对话框
上述绝大多数配置均可通过过滤框右侧的设置按钮(invokeSettingsDialog)打开设置对话框进行可视化调整,其 UI 定义位于 lc_layertreeoptionsdialog.ui。保存时会调用LC_LayerTreeModelOptions::save(),写入上述持久化键。
五、控件的注册与使用方式
图层树控件以停靠窗口(QDockWidget)形式集成在主程序中。LC_WidgetFactory::createLayerTreeWidget()(见 lc_widgetfactory.cpp)创建名为"Layers Tree"(layer_tree_dockwidget)的停靠窗口,并实例化LC_LayerTreeWidget:
QDockWidget* dock = createDockWidget(tr("Layers Tree"), "layer_tree_dockwidget", tr("Layers Tree")); const auto widget = new LC_LayerTreeWidget(actionHandler, dock, "Layer Tree"); dock->setWidget(widget);因此使用方式非常直接:在 LibreCAD 主界面的 View/窗口菜单中找到"Layers Tree"停靠面板打开即可,它可与传统的"Layers"列表面板(QG_LayerWidget,见同文件 lc_widgetfactory.cpp)并行使用,二者数据源一致。
LC_LayerTreeWidget实现了RS_LayerListListener接口,监听图层列表的增删改、激活与冻结/锁定/打印/构造标志变化(见 lc_layertreewidget.h),保证树视图与文档状态实时同步。
六、实现说明(Implementation Notes)与架构评价
原文档对实现思路给出了坦诚的说明:
- 纯 UI 实现:不触及图层的内部结构或底层存储格式,原始平面图层列表不受影响,两个控件可并行;
- 最小化重构:尽量少改动既有代码,避免引入回归;
- 零新增依赖:仅使用纯 C++/Qt 代码实现;
- 隔离性:实现高度自包含,但也因此在架构层面略显"游离",文档指出未来可能重构的候选对象包括:动作类(actions)、实体操作代码、以及原始图层列表(
RS_LayersList)。
从源码观察可以补充两点佐证:
RS_Layer新增的m_layerType是瞬态字段,仅由 LayerTreeWidget 更新,不会写入 DXF,这正是"不影响底层存储格式"的直接体现(见 rs_layer.h);- 图层树模型的内存管理已纳入测试覆盖:
tests/lc_layertreemodel_tests.cpp通过堆探针验证模型与控件销毁后不留内存泄漏,并复现了文档描述的树构建行为(6 个图层 + 2 个虚拟组 = 8 行)与 100 次循环构建后堆不再增长的结论(见 lc_layertreemodel_tests.cpp)。
七、本地化(Localization)
控件中所有可见字符串均通过tr()包裹,具备完整的本地化能力;实际翻译值由各语言翻译文件(.ts)提供。例如过滤框提示"Filter"、按钮提示"Show all layers"、"Add Dimensions Layer"等均可在 librecad/ts 目录下的语言文件中找到对应条目。
八、最佳实践小结
结合原文档与源码实现,使用图层树控件时建议遵循以下约定:
- 统一层级分隔符:全图使用同一个分隔子串(默认
-)组织层级,如Chassis-Frame-Bracket; - 主图层先创建:由于次级图层会自动挂到同名主图层之下,应确保主图层(如
Doors)先于其辅助层(如Doors+)存在; - 按后缀自动分类:尺寸标注统一使用
+后缀、注释统一使用_meta、替代位置统一使用_pos,即可自动归入对应子图层并享受分组标志操作; - 善用正则过滤:高亮模式下用正则快速定位,过滤模式下只保留目标图层,适合超多图层的图纸;
- 充分利用分组操作:对虚拟图层(组)执行冻结/锁定/打印切换、移除、复制等操作,可一次性作用于整棵子树;
- 用 Copy 与 Duplicate 区分需求:仅要结构拷贝用"Create Layer Copy",连实体一起复制用"Duplicate Layer With Content";
- 命名与层级调整:通过拖放重组层级、批量重命名次级图层(
renameSecondaryLayersOnPrimaryRename)可随时修正命名约定,无需手工逐层修改。
结语
Layer Tree View 以极低侵入的方式解决了 DXF 无图层层级这一历史约束:全部依赖命名约定,纯 UI 层实现,不修改底层存储,同时把图层管理从"手工前缀分组"升级为"可折叠树 + 类型子层 + 正则过滤 + 分组批量操作"的完整工作流。无论你是处理数百图层的复杂装配图,还是希望统一管理尺寸与注释,都可以直接在主界面打开 "Layers Tree" 面板投入使用。
- 桌面应用
- 图形学
【免费下载链接】LibreCAD
LibreCAD is a cross-platform 2D CAD program. It can read DXF/DWG, and write DXF/DWG/PDF/SVG files. It supports point/line/circle/ellipse/parabola/hyperbola/spline primitives. The GUI is highly customizable, and has dozens of translations.
相关推荐
LeetCode-Go 题解:199. Binary Tree Right Side View —— 基于层序遍历的二叉树右视图
LeetCode Go 题解:199. Binary Tree Right Side View —— 基于层序遍历的二叉树右视图 导读 本文讲解 LeetCod
示例工程LeetCode-Go 题解 0199:二叉树右视图(Binary Tree Right Side View)层序遍历实现详解
LeetCode Go 题解 0199:二叉树右视图(Binary Tree Right Side View)层序遍历实现详解 本文以 LeetCode Go
示例工程Angel 计算图中的层(Layer)体系详解:输入层、线性层、Join 层与损失层
Angel 计算图中的层(Layer)体系详解:输入层、线性层、Join 层与损失层 本文以 Angel 的 计算图(AngelGraph) https://l
人工智能机器学习分布式训练图计算后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考