开发实战:节点组合、嵌入、约束移动与折叠展开全解析)
X6 群组Group开发实战节点组合、嵌入、约束移动与折叠展开全解析【免费下载链接】X6 JavaScript diagramming library that uses SVG and HTML for rendering.项目地址: https://gitcode.com/GitHub_Trending/x6/X6本文以 X6 官方教程「群组」章节为核心系统讲解基于父子关系实现节点分组的核心能力如何通过 API 组合节点、如何开启embedding交互式嵌套节点、如何用translating.restrict限制子节点移动、如何监听位置事件自动扩展父节点以及如何自定义可展开/折叠的Group节点。读完本文你将掌握 X6 中群组相关全部配置项、事件与底层调用链可直接落地到流程图、组织架构图等业务场景。:::info 本章覆盖的能力如何组合节点如何通过交互方式组合节点如何在群组内限制子节点移动如何自动扩展父节点如何实现父节点的展开/折叠 :::一、组合节点基于父子关系的群组X6 通过**父子关系Parent-Child**实现群组子节点在逻辑上归属于父节点移动父节点时子节点跟随移动。所有与组合关系相关的 API 都定义在 Cell 基类 的 parent children 代码区下面按用途逐一说明。1.1 关系读取 API方法说明getParent()/getParentId()返回父节点Cell或父节点的 ID底层读取的是store.get(parent)getChildren()返回直接子节点数组底层通过store.get(children)中的 ID 列表向model查询实际 CellhasParent()是否有父节点isParentOf(child)/isChildOf(parent)判断两个节点是否为直接父子关系getAncestors({ deep })沿getParent()向上遍历收集所有祖先节点getDescendants({ deep, breadthFirst })收集所有后代节点支持深度优先默认与广度优先breadthFirst: true两种遍历方式deep: false时仅返回直接子节点eachChild/filterChild/getChildCount/getChildIndex/getChildAt子节点集合的遍历、过滤与索引工具从源码可以看到getParent()与getChildren()都会在首次调用时把结果缓存到_parent/_children属性见 cell.ts因此频繁读取关系是安全的。1.2 建立与解除关系建立父子关系有两种途径parent.addChild(child, options)把child挂到parent下。若child原本已有父节点addChild会先自动解除旧关系源码在 cell.ts 中处理了oldParent的 children 列表清理在节点配置数据中直接写入parent或children字段关系数据存储在节点store中。1.3 完整示例embed-edge官方示例 embed-edge/index.tsx 演示了最基本的组合用法const parent graph.addNode({ shape: custom-group-node, x: 40, y: 40, width: 360, height: 160, zIndex: 1, label: Parent\n(try to move me), }) parent.addChild(source) // 位于父节点内部的子节点 parent.addChild(target) // 位于父节点外部的子节点 graph.addEdge({ source, target, vertices: [ { x: 120, y: 60 }, { x: 200, y: 100 }, ], attrs: { line: { stroke: #8f8f8f, strokeWidth: 1 } }, })从该示例可以得到两个重要结论移动父节点时子节点跟着移动即便子节点位于父节点的可视边界之外。父子关系是逻辑上的不要求子节点的几何位置必须落在父节点内部默认将边的起点和终点的共同父节点视为该边的父节点。移动父节点时边的路径点vertices也会跟随移动。1.4 边与群组边Edge同样是 Cell可以被添加为某个节点的子节点。在下面的折叠示例中edge2被显式地addChild到群组aa下这样折叠aa时该边也会一并隐藏——这是群组场景中管理内部连线的一种常见做法。二、通过交互组合节点embedding 嵌入很多时候我们希望用户直接把一个节点拖进另一个节点从而自动建立父子关系。这时需要开启embedding选项。2.1 embedding 配置项embedding的完整类型定义见 graph/options.ts配置项类型默认值说明enabledbooleanfalse是否开启拖拽嵌入能力findParentbbox \| center \| topLeft \| topRight \| bottomLeft \| bottomRight \| 函数bbox指定拖拽过程中如何为被拖节点寻找合适的父节点。字符串模式表示用被拖节点的哪个几何参考点去命中候选父节点函数模式返回一个候选父节点数组。最终会选择z-index 最高视觉上最靠上的那个节点作为父节点frontOnlyboolean-为true时只考虑最上层节点是否可作为父节点为false时从前往后逐个测试直到找到合法父节点validate(args) boolean默认允许嵌入任意节点在嵌入过程中校验child/parent是否允许建立关系可用于按业务规则过滤2.2 使用 findParent 自定义父节点候选官方示例 embedding/index.tsx 给出了一个典型实现通过节点data中的parent: true标记哪些节点可以成为父节点再用包围盒相交判断是否命中const graph new Graph({ container: this.container, background: { color: #F2F7FA }, embedding: { enabled: true, findParent({ node }) { const bbox node.getBBox() return this.getNodes().filter((node) { const data node.getData{ parent: boolean }() if (data data.parent) { const targetBBox node.getBBox() return bbox.isIntersectWithRect(targetBBox) } return false }) }, }, })要点findParent的回调this指向Graph实例因此可以直接调用this.getNodes()示例用node.getBBox()获得被拖节点的包围盒再与每个候选父节点包围盒做isIntersectWithRect相交检测实现拖入即嵌入示例还监听了node:change:parent事件在父子关系建立后更新子节点标签用于实时反馈嵌入结果graph.on(node:change:parent, ({ node }) { node.attr({ label: { text: Child\n(embed) }, }) })2.3 与组合 API 的分工交互嵌入最终仍然落到第一节的父子关系机制上node:change:parent就是关系被建立时触发的事件。embedding解决的是如何交互式地触发关系建立而addChild/getParent等 API 解决的是如何编程式地读写关系。两者配合既能给用户提供直观的拖拽体验也能在代码中精确控制群组结构。三、限制子节点的移动范围当需要把子节点的移动范围限制在父节点内部时可在创建Graph实例时配置translating.restrict。3.1 配置类型translating配置定义在 graph/options.ts配置项类型默认值说明restrictboolean \| OptionItemCellView, RectangleLike \| number \| nullfalse限制节点的平移范围。设为true时限制在图区域边界内也可传入函数返回限制矩形autoOffsetboolean-节点移动后若与其他节点重叠是否自动偏移默认不偏移3.2 将子节点限制在父节点内官方示例 restrict/index.tsx 的做法是restrict传入一个函数函数接收节点的CellView若该节点存在父节点则返回父节点的包围盒作为移动边界const graph new Graph({ container: this.container, background: { color: #F2F7FA }, translating: { restrict(view) { if (view) { const cell view.cell if (cell.isNode()) { const parent cell.getParent() if (parent) { return parent.getBBox() // 子节点只能在父节点包围盒内移动 } } } return null // 无父节点时不做限制 }, }, }) const parent graph.addNode({ /* ... 宽 240 高 160 ... */ }) parent.addChild(child)关键点restrict返回null表示不做限制返回矩形RectangleLike或数字则将该矩形作为可移动区域该方案天然支持多层级群组嵌套的子节点会限制在其直接父节点内部而父节点本身又受其父级限制形成逐层约束这里限制的是节点的平移拖拽与第一节的父子关系叠加使用后子随父动 子不能拖出父的效果即可同时成立。四、自动扩展父节点监听位置变化动态调整尺寸拖拽子节点时希望父节点自适应地扩展/收缩尺寸让父节点始终完整包围所有子节点。官方示例 expand-shrink/index.tsx 通过监听node:change:position事件实现整体思路如下记录父节点原始状态当父节点自身移动或尺寸变化时把originSize/originPosition记录到节点的自定义属性node.prop中作为后续扩展计算的基准监听子节点位置变化任意节点位置改变时若其存在父节点则遍历父节点的所有子节点用child.getBBox().inflate(padding)得到带内边距的包围盒计算新的包围区域依次比较每个子节点包围盒的x、y与右上角corner取并集得到需要覆盖的矩形范围更新父节点一旦子节点越界hasChange为真通过parent.prop({ position, size })一次性更新父节点的位置与尺寸使父节点包围所有子节点。示例中有一个值得注意的细节——skipParentHandler标记。更新父节点时传入{ skipParentHandler: true }选项而node:change:size/node:change:position处理器开头都会检查该标记并直接返回从而避免父节点更新再次触发处理器、形成无限递归graph.on(node:change:size, ({ node, options }) { if (options.skipParentHandler) return // 记录 originSize ... }) graph.on(node:change:position, ({ node, options }) { if (options.skipParentHandler || ctrlPressed) return // 记录 originPosition、计算并更新父节点 ... })示例还支持实时调整内边距embedPadding默认 20并支持按住Ctrl/Cmd拖拽时跳过自动扩展通过node:embedding事件记录按键状态方便精细控制位置。核心计算片段let x originPosition.x let y originPosition.y let cornerX originPosition.x originSize.width let cornerY originPosition.y originSize.height let hasChange false children.forEach((child) { const bbox child.getBBox().inflate(this.embedPadding) const corner bbox.getCorner() if (bbox.x x) { x bbox.x; hasChange true } if (bbox.y y) { y bbox.y; hasChange true } if (corner.x cornerX) { cornerX corner.x; hasChange true } if (corner.y cornerY) { cornerY corner.y; hasChange true } }) if (hasChange) { parent.prop( { position: { x, y }, size: { width: cornerX - x, height: cornerY - y }, }, { skipParentHandler: true }, // 防止递归触发 ) }五、实现父节点的展开与折叠展开/折叠是群组最常用的高级能力。X6 官方示例 collapsable 采用自定义节点 自定义事件的方案先定义一个带折叠按钮的Group节点再在graph上监听按钮触发的自定义事件控制子节点的显隐。5.1 自定义 Group 节点完整实现见 collapsable/shape.tsimport { Node } from antv/x6 export class Group extends Node { private collapsed: boolean false private expandSize: { width: number; height: number } protected postprocess() { this.toggleCollapse(false) } isCollapsed() { return this.collapsed } toggleCollapse(collapsed?: boolean) { const target collapsed null ? !this.collapsed : collapsed if (target) { this.attr(buttonSign, { d: M 1 5 9 5 M 5 1 5 9 }) // 折叠态显示 号 this.expandSize this.getSize() this.resize(100, 32) // 折叠后缩小到标题栏尺寸 } else { this.attr(buttonSign, { d: M 2 5 8 5 }) // 展开态显示 - 号 if (this.expandSize) { this.resize(this.expandSize.width, this.expandSize.height) } } this.collapsed target } } Group.config({ markup: [ { tagName: rect, selector: body }, { tagName: text, selector: label }, { tagName: g, selector: buttonGroup, children: [ { tagName: rect, selector: button }, { tagName: path, selector: buttonSign }, ], }, ], attrs: { body: { refWidth: 100%, refHeight: 100%, stroke: none, fill: #fff }, label: { fontSize: 12, fill: #fff, refX: 32, refY: 10 }, buttonGroup: { refX: 8, refY: 8 }, button: { height: 14, width: 16, rx: 2, ry: 2, fill: #f5f5f5, stroke: #ccc, cursor: pointer, // 自定义事件点击按钮触发 event: node:collapse, }, buttonSign: { refX: 3, refY: 2, stroke: #808080 }, }, })实现要点postprocess()是节点实例化完成后的生命周期钩子这里用于初始化展开形态toggleCollapse(collapsed?)省略参数时自动取反折叠时记录原始尺寸expandSize并把节点缩到100 × 32的标题栏大小同时切换按钮图标buttonSign的路径d在 与 − 之间切换展开时恢复原始尺寸markup 结构buttonGroup是一个g分组内含矩形按钮button和路径图标buttonSign分别负责点击热区与图标绘制自定义事件在button的attrs中设置event: node:collapseX6 会在该元素被点击时派发名为node:collapse的图形事件——这就是自定义事件的注册方式无需手动绑定 DOM 监听器。5.2 在 graph 上监听折叠事件随后在graph上监听node:collapse根据父节点的展开/折叠状态显示或隐藏对应的子节点官方示例在基础版上还做了递归折叠处理详见 collapsable/index.tsxgraph.on(node:collapse, ({ node }: { node: Group }) { node.toggleCollapse() const collapsed node.isCollapsed() const collapse (parent: Group) { const cells parent.getChildren() if (cells) { cells.forEach((cell) { if (collapsed) { cell.hide() } else { cell.show() } // 若子节点本身是未折叠的 Group则递归处理其子节点 if (cell instanceof Group) { if (!cell.isCollapsed()) { collapse(cell) } } }) } } collapse(node) })要点toggleCollapse()负责节点的视觉形态尺寸与图标事件处理器负责子节点的显隐两者职责分离用node.getChildren()只取直接子节点若使用node.getDescendants()则可一次性取得所有后代。文档基础版本使用getDescendants()后对所有后代统一hide()/show()而示例实现为了支持折叠时保留未折叠子群组采用了按层级递归的方式hide()/show()是 Cell 提供的显隐方法被隐藏的 Cell 及其附属视图将从画布中移除渲染但关系数据保留展开时可无损恢复。5.3 组合出层级群组示例中通过createGroup/createNode/createEdge三个工厂函数构造了多层嵌套结构const a createGroup(a, 100, 40, 480, 280, #91d5ff) const aa createGroup(aa, 180, 100, 160, 140, #47C769) const aaa createGroup(aaa, 200, 160, 120, 40, #0491e4) const b createNode(b, 450, 200, 50, 50) a.addChild(aa) aa.addChild(aaa) a.addChild(b) // 边也可以作为子节点随群组一起显隐 aa.addChild(createEdge(edge2, aa, aaa, [{ x: 60, y: 140 }, { x: 60, y: 220 }]))这里体现了群组的三个进阶用法多层嵌套a嵌套aaaa再嵌套aaa折叠逻辑通过递归自动作用于任意层级子节点显隐不影响关系折叠只是隐藏视图getChildren()等关系 API 依然能正确返回数据边也可作为子节点把内部连线挂到群组下折叠时连线随群组一并隐藏视觉上干净利落。六、总结与配置速查围绕群组X6 提供了一条完整的能力链路需求核心配置 / API默认值参考实现编程式组合节点parent.addChild(child)、getParent()、getChildren()、getDescendants()无父子关系embed-edge拖拽嵌入节点graph选项embedding.enabled、embedding.findParent、embedding.validateenabled: false、findParent: bbox见 options.tsembedding限制子节点移动graph选项translating.restrict函数返回父节点 BBoxrestrict: falserestrict自动扩展父节点监听node:change:position/node:change:size配合skipParentHandler防递归-expand-shrink展开/折叠自定义节点 attrs.event: node:collapsegraph.on(node:collapse)-collapsable在实际项目中这五项能力通常组合使用用embedding让用户交互式建组用translating.restrict保证子节点不越界用位置监听实现父容器自适应最后用自定义Group节点提供折叠/展开入口。群组相关的所有关系 API 都收敛在 Cell 基类交互配置统一在 Graph 选项 中按需查阅即可快速接入自己的业务场景。【免费下载链接】X6 JavaScript diagramming library that uses SVG and HTML for rendering.项目地址: https://gitcode.com/GitHub_Trending/x6/X6创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考