X6 节点工具(Node Tool)实战:自定义按钮、删除按钮、包围框与文本编辑
2026/9/17 14:33:26 网站建设 项目流程

X6 节点工具(Node Tool)实战:自定义按钮、删除按钮、包围框与文本编辑

【免费下载链接】X6🚀 JavaScript diagramming library that uses SVG and HTML for rendering.项目地址: https://gitcode.com/GitHub_Trending/x6/X6

导读

本文基于 X6 官方示例 节点工具(Node Tools) 展开,系统讲解如何在画布节点上挂载交互工具(Tool):包括按钮类工具(自定义按钮、删除按钮)、包围框(Boundary)和文本编辑器(Node Editor),并延伸到连接桩 ToolTip 的进阶玩法。读完本文,你将掌握tools配置、addTools/removeTools/setTools三组 API 的使用方式,理解按钮定位与包围框尺寸的底层计算逻辑,并能直接复用文中的所有示例代码。

一、示例页结构与运行方式

该示例页属于 X6 仓库site/examples/node/下的子模块,入口文档 index.en.md(中文版见 index.zh.md)本身仅声明页面标题与路由重定向,真正的演示内容由 demo 目录 下的五个可运行示例承载,示例清单定义在 meta.json:

示例文件主题说明
button.tsCustom Button 自定义按钮按钮点击后随机改变节点填充色
button-remove.tsDelete Button 删除按钮一键移除节点
boundary.tsBoundary Box 包围框高亮选中范围
editable.tsText Editor 文本编辑双击节点/边直接编辑文本
port-tooltip.tsxToolTip For Port 连接桩提示悬停连接桩显示提示气泡

运行这些示例无需额外配置,示例均在new Graph({ container, grid: true })基础上直接调用节点 API 完成,可复制到任意 X6 项目中验证。

二、节点工具的挂载机制:toolsaddToolsremoveTools

所有示例共用一套工具挂载机制。工具既可以随节点创建时静态声明(写入节点配置的tools数组),也可以在运行时通过 Cell 实例方法 动态增删:

  • node.addTools(tools, options):向节点追加一组工具;
  • node.setTools(tools, options):整体替换工具集合;
  • node.removeTools(options):清空当前节点的所有工具。

示例中典型的写法如下,静态声明与动态挂载两处配置完全一致:

// 静态声明:节点创建时即携带工具 const source = graph.addNode({ x: 180, y: 60, width: 100, height: 40, attrs: { body: { fill: '#f5f5f5', stroke: '#d9d9d9', strokeWidth: 1 } }, tools: [{ name: 'button', args: { /* ... */ } }], }) // 动态挂载:鼠标进入时添加工具,离开时移除 graph.on('node:mouseenter', ({ node }) => { if (node === target) node.addTools({ name: 'button', args: { /* ... */ } }) }) graph.on('node:mouseleave', ({ cell }) => { if (cell === target) cell.removeTools() })

工具项(Tool Item)本身是独立于节点可视区域之外渲染的 SVG/DOM 元素,由src/view/tool/下的 ToolItem 基类 与 ToolView 容器 统一管理,通过args传入的xyoffsetonClick等参数控制其表现与行为。

三、内置节点工具一览

X6 将工具以注册表(Registry)形式组织,节点工具的全部内置项定义在 src/registry/tool/index.ts 的nodeToolPresets中:

export const nodeToolPresets = { boundary: Boundary, button: Button, 'button-remove': Remove, 'node-editor': NodeEditor, }

即节点上内置四类原生工具:boundary(包围框)、button(自定义按钮)、button-remove(删除按钮)、node-editor(文本编辑器)。其中button-removenode-editor分别是Button与编辑器基类的派生/组合实现,后面逐一展开。工具名可直接写在tools数组的name字段,配合args传参,无需手动register

四、自定义按钮(button)

4.1 完整示例

button.ts 演示了两种挂载形态:源节点在创建时通过tools静态携带按钮;目标节点在node:mouseenter时动态追加按钮、node:mouseleave时移除。其核心配置如下:

tools: [ { name: 'button', args: { markup: [ { tagName: 'circle', selector: 'button', attrs: { r: 14, stroke: '#fe854f', strokeWidth: 2, fill: 'white', cursor: 'pointer', }, }, { tagName: 'text', textContent: 'Btn', selector: 'icon', attrs: { fill: '#fe854f', fontSize: 10, textAnchor: 'middle', pointerEvents: 'none', y: '0.3em', }, }, ], x: '100%', y: '100%', offset: { x: -20, y: -20 }, onClick({ cell }: { cell: Cell }) { const fill = Color.randomHex() cell.attr({ body: { fill }, label: { fill: Color.invert(fill, true) }, }) }, }, }, ],

点击按钮后调用cell.attr(...)修改节点的bodylabel填充色,实现一键换肤。动态挂载版本额外展示了按钮点击改变描边样式的效果(随机描边色 +strokeDasharray/strokeDashoffset递增的虚线动画效果)。

4.2 参数详解(对应源码 src/registry/tool/button.ts)

参数类型说明
markupMarkup数组按钮的自定义 SVG 结构,可包含多个元素并按selector命名
x/ynumber \| string按钮锚点在节点包围盒内的位置,支持百分比字符串(如'100%'定位到右下角),底层通过NumberExt.normalizePercentage(x, bbox.width)归一化
offsetnumber \| { x, y }锚点基础上的额外偏移;传数字时 x、y 同时生效
rotateboolean是否跟随节点旋转(结合cell.getAngle()处理)
useCellGeometryboolean默认true,使用节点几何数据计算包围盒
onClick(args) => void点击回调,回调参数含{ e, cell, view, btn },其中btn为按钮工具实例

按钮的定位逻辑在getNodeMatrix()中:先取节点包围盒(必要时按角度旋转),再以包围盒中心为原点,将归一化后的x/yoffset组合成 SVG 变换矩阵Dom.transform(...)应用到按钮容器上。onClick在基类的onMouseDown中触发,事件绑定了mousedowntouchstart,因此同时支持鼠标与触屏操作。

五、删除按钮(button-remove)

5.1 使用方式

button-remove.ts 用法与button完全一致,只需把name换成'button-remove'

tools: [ { name: 'button-remove', args: { x: '100%', y: 0, offset: { x: -10, y: 10 }, }, }, ],

它默认渲染为一个红色圆形(#FF1D00)内嵌白色“×”图标的删除按钮,点击后节点连同按钮一起从画布移除。

5.2 默认配置与删除逻辑(源码 src/registry/tool/button.ts)

Remove继承自Button,其默认markup为圆形 + 两条交叉线段组成的删除图标,默认点击行为是:

onClick({ view, btn }) { btn.parent.remove() // 移除按钮本身 view.cell.remove({ ui: true, toolId: btn.cid }) // 删除节点 }

即先移除按钮所在工具组(ToolView),再以{ ui: true }语义删除对应节点,避免触发 UI 无关的回调;toolId用于在删除时精确跳过该工具自身。其余定位参数(xyoffsetdistance等)与button通用。

六、包围框(boundary)

6.1 使用方式

boundary.ts 用包围框高亮节点,静态挂载与动态挂载并存:

tools: [ { name: 'boundary', args: { padding: 5, attrs: { fill: '#7c68fc', stroke: '#9254de', strokeWidth: 1, fillOpacity: 0.2, }, }, }, ],

效果是节点四周出现一圈带透明填充、紫色描边的矩形框。动态版本在鼠标悬停时node.addTools({ name: 'boundary', args: { attrs: { ... } } }),移出时node.removeTools()

6.2 参数与渲染原理(源码 src/registry/tool/boundary.ts)

参数类型默认值说明
paddingnumber \| SideOptions10包围框与节点边缘的间距,支持{ top, right, bottom, left }分别设置
useCellGeometrybooleantrue是否基于节点几何数据计算包围盒
rotatebooleantrue时包围框随节点旋转,否则按旋转后的轴对齐包围盒(bbox.bbox(angle)
attrsSimpleAttrsfill: 'none'stroke: '#333'stroke-width: 0.5stroke-dasharray: '5, 5'pointer-events: 'none'包围框 rect 的样式属性

渲染时工具会:

  1. 通过Util.getViewBBox(view, useCellGeometry)取得节点包围盒;
  2. NumberExt.normalizeSides(padding)归一化 padding 并调用moveAndExpand外扩矩形;
  3. 若节点有旋转角度,依据rotate选择“随动旋转”或“轴对齐重算”;
  4. 将最终bbox写入容器 rect 的x/y/width/height属性。

默认样式是虚线描边且pointer-events: none,因此不会拦截鼠标事件,适合做纯视觉的高亮反馈。

七、文本编辑(node-editor / edge-editor)

7.1 使用方式

editable.ts 展示了节点与边上的文本编辑器:

// 节点 tools: [ { name: 'node-editor', args: { attrs: { backgroundColor: '#EFF4FF' }, }, }, ], // 边(同理) tools: [ { name: 'edge-editor', args: { attrs: { backgroundColor: '#fff' }, }, }, ],

挂载后双击节点或边,即可在其上直接输入文本(contentEditable模式),编辑结果写回模型。

7.2 底层实现(源码 src/registry/tool/editor.ts)

NodeEditor/EdgeEditor由编辑器基类CellEditor派生。关键设计点:

  • 触发方式:工具渲染时监听cell:dblclickonRender中绑定),双击进入编辑态;
  • 编辑介质:创建一个contentEditable = 'true'的 HTMLdiv覆盖在节点/边上,样式(fontSizefontFamilycolorbackgroundColor)取自args.attrs
  • 定位逻辑:未指定x/y时编辑器居中于节点包围盒(translate(-50%, -50%)),并经过graph.localToGraph将局部坐标换算为画布坐标,兼容缩放场景(graph.scale());
  • 交互边界:绑定mousedown/touchstart与文档级mouseup/touchend/touchcancel,保证编辑结束、失焦行为的一致处理。

相关行为在 editor 测试 中有覆盖,可作为自定义编辑器行为的参考。

八、连接桩 ToolTip(port-tooltip)

最后一个示例 port-tooltip.tsx 不是内置工具,而是“回调 + 第三方 Tooltip 组件”组合出的交互:利用GraphonPortRendered回调,在连接桩 DOM 渲染完成后为每个桩注册鼠标事件,再结合 antd 的Tooltip显示提示文本:

const graph = new Graph({ container: this.container, width: 800, height: 600, grid: true, onPortRendered({ contentContainer, port, node }) { const text = node.portProp(port.id, 'tip') as string registerPortTooltip(contentContainer, text) }, }) // 连接桩上自定义 tip 属性 ports: { groups: { bottom: { position: 'bottom', attrs: { circle: { magnet: true } } } }, items: [{ id: 'port-1', group: 'bottom', tip: 'port-1-tip' }], }

registerPortTooltipmouseenter时把portProp(port.id, 'tip')读到的文本写入ant-tooltip-inner,并将气泡定位到鼠标附近;mouseleave时把气泡移到屏幕外(-1000px)。用 React 渲染示例时,Tooltip组件常驻页面并设置overlayClassName="x6-tooltip"作为移动载体。该模式也可迁移到任意 UI 组件库,仅需将Tooltip换成对应实现。

九、常见问题与最佳实践

  • 何时用静态tools数组,何时用addTools常驻型工具(如始终显示的删除按钮、包围框)适合写在节点配置里;交互型工具(悬停才出现)应放在node:mouseenter/node:mouseleave事件中动态增删,注意用节点引用做判断,避免所有节点共享同一套工具。
  • removeTools()会清空全部工具:如果只想移除单个工具,需自行管理工具实例或使用setTools整体重设。
  • 按钮遮挡交互:自定义markup时,图标元素建议设置pointerEvents: 'none'(示例中的text元素即如此),让点击事件只落在按钮容器上。
  • 包围框拦截事件:若包围框影响拖拽,保持默认的pointer-events: none即可;需要可交互的框再覆盖attrs
  • 百分比定位依赖包围盒x: '100%'y: '100%'按节点宽高归一化,offset再做像素级微调,二者组合可精确控制按钮落在节点边角内侧或外侧。

十、结语

节点工具是 X6 交互体系中最常用的一环:buttonbutton-remove解决“点一下做什么”的入口问题,boundary提供轻量高亮,node-editor让文本就地可编辑,而onPortRendered+ Tooltip 的组合则展示了向连接桩扩展交互的通用思路。结合 节点工具注册表 与各工具源码(button.ts、boundary.ts、editor.ts),你既可以直接复用官方示例,也可以基于ToolItem基类派生自己的业务工具。

【免费下载链接】X6🚀 JavaScript diagramming library that uses SVG and HTML for rendering.项目地址: https://gitcode.com/GitHub_Trending/x6/X6

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询