在上一篇文章 《记一次绘图框架技术选型: jsPlumb VS mxGraph》 中,提到了我为何要去学习 mxGraph。在入门时我遇到了如下几个问题javascript
经过本身对着官方文档死磕了一段时间并在公司项目中进行实践后,慢慢开始掌握这个框架的使用。下面我就根据个人学习经验写一篇比较适合入门的文章。css
官方列了比较多文档,其中下面这几份是比较有用的。html
在看完个人文章后但愿系统地学习 mxGraph 仍是要去阅读这些文档的,如今能够暂时不看。由于刚开始就堆这么多理论性的东西,对入门没有好处。前端
这篇教程分为两部分,第一部分结合我写的一些例子讲解基础知识。第二部分则利用第一部分讲解的知识开发一个小项目 pokemon-diagram。本教程会使用到 ES6 语法,而第二部分的项目是用 Vue 写的。阅读本教程须要你掌握这两项预备知识。vue
咱们来分析一下官方的 HelloWorld 实例是怎样经过 script 标签引入 mxGraph 的java
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Hello World</title> </head> <body> <div id="graphContainer"></div> </body> <script> mxBasePath = '../src'; </script> <script src="../src/js/mxClient.js"></script> <script> // ...... </script> </html>
首先要声名一个全局变量 mxBasePath
指向一个路径,而后引入 mxGraph。node
mxBasePath
指向的路径做为 mxGraph 的静态资源路径。上图是 HelloWorld 项目的 mxBasePah
,这些资源除了 js 目录 ,其余目录下的资源都是 mxGraph 运行过程当中所须要的,因此要在引入 mxGraph 前先设置 mxBasePath
。 git
再来看看 javascript 目录下有两个 mxClient.js
版本。 一个在 javascript/src/js/mxClient.js
,另外一个在 javascript/mxClient.js
,后者是前者打包后的版本,因此二者是能够替换使用的。若是你的项目是使用 script 标签引入 mxGraph,能够参考我这个库。github
模块化引入能够参考 pokemon-diagram 的这个文件 static/mxgraph/index.js编程
/*** 引入 mxgraph ***/ // src/graph/index.js import mx from 'mxgraph'; const mxgraph = mx({ mxBasePath: '/static/mxgraph', }); //fix BUG https://github.com/jgraph/mxgraph/issues/49 window['mxGraph'] = mxgraph.mxGraph; window['mxGraphModel'] = mxgraph.mxGraphModel; window['mxEditor'] = mxgraph.mxEditor; window['mxGeometry'] = mxgraph.mxGeometry; window['mxDefaultKeyHandler'] = mxgraph.mxDefaultKeyHandler; window['mxDefaultPopupMenu'] = mxgraph.mxDefaultPopupMenu; window['mxStylesheet'] = mxgraph.mxStylesheet; window['mxDefaultToolbar'] = mxgraph.mxDefaultToolbar; export default mxgraph; /*** 在其余模块中使用 ***/ // src/graph/Graph.js import mxgraph from './index'; const { mxGraph, mxVertexHandler, mxConstants, mxCellState, /*......*/ } = mxgraph;
这里有两点须要注意的
mx
方法传入的配置项 mxBasePath
指向的路径必定要是一个能够经过 url 访问的静态资源目录。举个例子,pokemon-diagram 的 static 目录是个静态资源目录,该目录下有 mxgraph/css/common.css
这么个资源,经过http://localhost:7777
能够访问 pokemon-diagram 应用,那么经过 http://localhost:7777/static/mxgraph/css/common.css
也应该是能够访问 common.css
才对这部分会使用到我本身编写的一些例子。你们能够先把代码下载下来,这些例子都是不须要使用 node 运行的,直接双击打开文件在浏览器运行便可。
Cell
在 mxGraph 中能够表明组(Group)
、节点(Vertex)
、边(Edge)
,mxCell 这个类封装了 Cell
的操做,本教程不涉及到组
的内容。下文若出现 Cell
字眼能够看成 节点
或 边
。
官方的 HelloWorld 的例子向咱们展现了如何将节点插入到画布。比较引人注意的是 beginUpdate
与 endUpdate
这两个方法,这两个方法在官方例子中出镜频率很是高,咱们来了解一下他们是干吗用的,嗯,真是只是了解一下就能够了,由于官方对两个方法的描述对入门者来讲真的是比较晦涩难懂,并且我在实际开发中基本用不上这两个方法。能够等掌握这个框架基本使用后再回过头来研究。下面的描述来源这个文档,我来简单归纳一下有关这两个方法的相关信息。
beginUpdate、endUpdate
用于建立一个事务,一次 beginUpdate
必须对应一次 endUpdate
beginUpdate 必定要放到 try 块以外
beginUpdate必定要放到 finally 块
按照官方这个说明,若是我不须要撤消/重作功能,是否是能够不使用这两个方法呢。我试着把这两个方法从 HelloWorld 例子的代码中删掉,结果程序仍是能够正常运行。
mxGraph.prototype.insertVertex = function(parent, id, value, x, y, width, height, style, relative) { // 设置 Cell 尺寸及位置信息 var geometry = new mxGeometry(x, y, width, height); geometry.relative = (relative != null) ? relative : false; // 建立一个 Cell var vertex = new mxCell(value, geometry, style); // ... // 标识这个 Cell 是一个节点 vertex.setVertex(true); // ... // 在画布上添加这个 Cell return this.addCell(vertex, parent); };
上面是经简化后的 insertVertex 方法。 insertVertex 作了三件事,先是设置几何信息,而后建立一个节点,最后将这个节点添加到画布。insertEdge 与 insertVertex 相似,中间过程会调用 vertex.setEdge(true)
将 Cell
标记为边。从这里咱们也能够得知不管节点
仍是边
在 mxGraph 中都是由 mxCell 类表示,只是在该类内部标识当前 Cell
是 节点
仍是 边
。
function mxGeometry(x,y,width,height){}
mxGeometry 类表示 Cell
的几何信息,宽高比较好理解,只对节点有意义,对边没意义。下面经过 02.geometry.html 这个例子说明如x、y
的做用。
mxGeometry
还有一个很重要的布尔属性 relative
,
relative
为 false
的节点,表示以画布左上角为基点进行定位,x、y
使用的是绝对单位
上一小节提到 insertVertex
内部会建立 mxGeometry
类。使用 mxGraph.insertVertex
会建立一个 mxGeometry.relative
为 false 的节点,如 A 节点
relative
为 true
的节点,表示以父节点左上角为基点进行定位,x、y
使用的是相对单位
使用 mxGraph.insertVertex
会建立一个 relative 为 false 的节点。若是你要将一个节点添加到另外一个节点中须要在该方法调用的第9个参数传入 true
,将 relative
设置为 true
。这时子节点使用相对坐标系,以父节点左上角做为基点,x、y 取值范围都是 [-1,1]
。如 C节点 相对 B节点定位。
relative
为 true
的边,x、y
用于定位 label
使用 mxGraph.insertEdge
会建立一条 relative 为 true 的边。x、y 用于定位线条上的 label,x 取值范围是 [-1,1]
,-1 为起点,0 为中点,1 为终点
。y 表示 label 在边的正交线上移到的距离。第三个例子能帮忙你们理解这种状况。
const e1 = graph.insertEdge(parent, null, '30%', v1, v2); e1.geometry.x = 1; e1.geometry.y = 100;
由 03.stylesheet.html 这个例子咱们得知 mxGraph 提供两种设置样式的方式。
第一种是设置全局样式
。mxStylesheet 类用于管理图形样式,经过 graph.getStylesheet() 能够获取当前图形的 mxStylesheet
对象。mxStylesheet
对象的 styles
属性也是一个对象,该对象默认状况下包含两个对象defaultVertexStyle、defaultEdgeStyle
,修改这两个对象里的样式属性对全部线条/节点都生效
。
第二种是命名样式
。先建立一个样式对象,而后使用 mxStylesheet.putCellStyle 方法为 mxStylesheet.styles
添加该样式对象并命名。在添加 Cell 的时候,将样式写在参数中。格式以下
[stylename;|key=value;]
分号前能够跟命名样式名称或者一个样式的 key、value 对。
ROUNDED
是一个内置的命名样式,对节点设置有圆角效果,对边设置则边的拐弯处为圆角。
例子中设置折线有一个须要注意的地方。
// 设置拖拽边的过程出现折线,默认为直线 graph.connectionHandler.createEdgeState = function () { const edge = this.createEdge(); return new mxCellState(graph.view, edge, graph.getCellStyle(edge)); };
虽然调用 insertEdge
方法时已经设置了线条为折线,可是在拖拽边过程当中依然是直线。上面这段代码重写了 createEdgeState 方法,将拖动中的边样式设置成与静态时的边样式一致,都是折线。
mxGraph 全部样式在这里能够查看,打开网站后能够看到以 STYLE_
开头的是样式常量。可是这些样式常量并不能展现样式的效果。下面教你们一个查看样式效果的小技巧,使用 draw.io 或 GraphEditor (这两个应用都是使用 mxGraph 进行开发的) 的 Edit Style
功能能够查看当前 Cell 样式。
好比如今我想将边的样式设置成:折线、虚线、绿色、拐弯为圆角、粗3pt。在 Style 面板手动修改样式后,再点击 Edit Style
就能够看到对应的样式代码。
为了方便观察我手动格式化了样式,注意最后一行以 entry
或 exit
开头的样式表明的是边出口/入口的靶点坐标,下一小节会进行讲解。
关于如何设置靶点能够参考 04.anchors.html ,下面也是以这个 Demo 进行讲解两个用户操做的例子,对比不一样的操做对于获取靶点信息的影响。
将鼠标悬浮中 A 节点中心,待节点高亮时链接到 B 节点的一个靶点上
而后将 A 节点拖拽到 B 节点右边
能够看到若是从图形中心拖出线条,这时边的出口值 exit
为空,只有入口值 entry
。若是拖动节点 mxGraph 会智能地调整线条出口方向。如节点 A 的链接靶点原来是在右边,节点拖动到节点 B 右边后靶点也跟着发生了变化,跑到了左边,而节点 B 的链接靶点一直没变。
此次将鼠标悬浮到 A 节点的一个靶点,待靶点高亮时链接到 B 节点的一个靶点上
而后将 A 节点拖拽到 B 节点右边
能够看到此次全部值都有了,链接后拖动节点 A,链接靶点的位置也固定不变,mxGraph 不像第一个例子同样调整链接靶点位置。之因此产生这样的差别是由于第一个例子的边是从节点中心拖出的,并无出口靶点的信息,而第二个例子则是明确地从一个靶点中拖出一条边。
mxGraph 框架是使用面向对象的方式进行编写的,该框架全部类带 mx 前缀。在接下来的例子你会看到不少这种形式的方法重写(Overwrite)
。
const oldBar = mxFoo.prototype.bar; mxFoo.prototype.bar = function (...args)=> { // ..... oldBar.apply(this,args); // ..... };
这一小节经过 05.consistuent.html 这个例子,讲解节点组合须要注意的地方。
组合节点后默认状况下,父节点是可折叠的,要关闭折叠功能须要将 foldingEnabled
设为 false
。
graph.foldingEnabled = false;
若是但愿在改变父节点尺寸时,子节点与父节点等比例缩放,须要开启 recursiveResize
。
graph.recursiveResize = true;
下面是这个例子最重要的两段代码。
/** * Redirects start drag to parent. */ const getInitialCellForEvent = mxGraphHandler.prototype.getInitialCellForEvent; mxGraphHandler.prototype.getInitialCellForEvent = function (me) { let cell = getInitialCellForEvent.apply(this, arguments); if (this.graph.isPart(cell)) { cell = this.graph.getModel().getParent(cell); } return cell; }; // Redirects selection to parent graph.selectCellForEvent = function (cell) { if (this.isPart(cell)) { mxGraph.prototype.selectCellForEvent.call(this, this.model.getParent(cell)); return; } mxGraph.prototype.selectCellForEvent.apply(this, arguments); };
这两个方法重写(Overwrite)
了原方法,思路都是判断若是该节点是子节点则替换成父节点去执行剩下的逻辑。
getInitialCellForEvent 在鼠标按下(mousedown事件,不是click事件)时触发,若是注释掉这段代码,不使用父节点替换,当发生拖拽时子节点会被单独拖拽,不会与父节点联动。使用父节点替换后,本来子节点应该被拖拽,如今变成了父节点被拖拽,实现联动效果。
selectCellForEvent 实际上是 getInitialCellForEvent
内部调用的一个方法。这个方法的做用是将 cell 设置为 selectionCell
,设置后可经过 mxGraph.getSelectionCell 可获取得该节点。与 getInitialCellForEvent
同理,若是不使用父节点替换,则 mxGraph.getSelectionCell
获取到的会是子节点。项目实战咱们会使用到 mxGraph.getSelectionCell
这个接口。
这部分我主要挑一些这个项目比较重要的点进行讲解。
下面以项目的这个节点为例,讲解如何组合节点
const insertVertex = (dom) => { // ... const nodeRootVertex = new mxCell('鼠标双击输入', new mxGeometry(0, 0, 100, 135), `node;image=${src}`); nodeRootVertex.vertex = true; // ... const title = dom.getAttribute('alt'); const titleVertex = graph.insertVertex(nodeRootVertex, null, title, 0.1, 0.65, 80, 16, 'constituent=1;whiteSpace=wrap;strokeColor=none;fillColor=none;fontColor=#e6a23c', true); titleVertex.setConnectable(false); const normalTypeVertex = graph.insertVertex(nodeRootVertex, null, null, 0.05, 0.05, 19, 14, `normalType;constituent=1;fillColor=none;image=/static/images/normal-type/forest.png`, true); normalTypeVertex.setConnectable(false); // ..... };
单单 nodeRootVertex
就是长这个样子。经过设置自定义的 node
样式(见 Graph 类 _putVertexStyle 方法)与 image
属性设置图片路径配合完成。
由于默认状况下一个节点只能有一个文本区和一个图片区,要增长额外的文本和图片就须要组合节点。在 nodeRootVertex
上加上 titleVertex
文本节点和 normalTypeVertex
图片节点,最终达到这个效果。
有时须要为不一样子节点设置不一样的鼠标悬浮图标,如本项目鼠标悬浮到 normalTypeVertex
时鼠标变为手形,参考 AppCanvas.vue 的 setCursor 方法,重写 mxGraph.prototype.getCursorForCell
能够实现这个功能。
const setCursor = () => { const oldGetCursorForCell = mxGraph.prototype.getCursorForCell; graph.getCursorForCell = function (...args) { const [cell] = args; return cell.style.includes('normalType') ? 'pointer' : oldGetCursorForCell.apply(this, args); }; };
下面这段代码是编辑内容比较经常使用的设置
// 编辑时按回车键不换行,而是完成输入 this.setEnterStopsCellEditing(true); // 编辑时按 escape 后完成输入 mxCellEditor.prototype.escapeCancelsEditing = false; // 失焦时完成输入 mxCellEditor.prototype.blurEnabled = true;
默认状况下输入内容时若是按回车键内容会换行,但有些场景有禁止换行的需求,但愿回车后完成输入,经过graph.setEnterStopsCellEditing(true) 设置能够知足需求。
重点说说 mxCellEditor.prototype.blurEnabled 这个属性,默认状况下若是用户在输入内容时鼠标点击了画布以外的不可聚焦区域(div、section、article等),节点内的编辑器是不会失焦的,这致使了 LABEL_CHANGED 事件不会被触发。但在实际项目开发中通常咱们会指望,若是用户在输入内容时鼠标点击了画布以外的地方就应该算做完成一次输入,而后经过被触发的 LABEL_CHANGED
事件将修改后的内容同步到服务端。经过 mxCellEditor.prototype.blurEnabled = true
这行代码设置能够知足咱们的需求。
const titleVertex = graph.insertVertex(nodeRootVertex, null, title, 0.1, 0.65, 80, 16, 'constituent=1;whiteSpace=wrap;strokeColor=none;fillColor=none;fontColor=#e6a23c', true);
对于非输入的文本内容,默认状况下即使文本超出容器宽度也是不会换行的。咱们项目中宽度为 80 的 titleVertex 正是这样一个例子。
要设置换行须要作两件事,第一是经过这行代码 mxGraph.setHtmlLabels(true),使用 html 渲染文本(mxGraph 默认使用 svg的text 标签渲染文本)。第二是像上面的 titleVertex 的样式设置同样,添加一句 whiteSpace=wrap。
如今介绍一下 Model 这个概念,Model 是当前图形的数据结构化表示。mxGraphModel 封装了 Model 的相关操做。
你能够启动项目,画一个这样的图,而后点击输出XML。为了保的 xml 与下面的一致,须要先拖出智爷,再拖出超级皮卡丘,最后链接边。
控制台应该输出这样一份 xml
<mxGraphModel> <root> <mxCell id="0"/> <mxCell id="1" parent="0"/> <mxCell id="4" value="Hello" style="node;image=/static/images/ele/ele-005.png" vertex="1" data="{"id":1,"element":{"id":1,"icon":"ele-005.png","title":"智爷"},"normalType":"water.png"}" parent="1"> <mxGeometry x="380" y="230" width="100" height="135" as="geometry"/> </mxCell> ........ </root> </mxGraphModel>
每个 mxCell 节点都有 parent 属性指向父节点。咱们对 value="Hello" 这个 mxCell 节点手动格式化。
<mxCell id="4" value="Hello" style="node;image=/static/images/ele/ele-005.png" vertex="1" data="{"id":1,"element":{"id":1,"icon":"ele-005.png","title":"智爷"},"normalType":"water.png"}" parent="1"> <mxGeometry x="380" y="230" width="100" height="135" as="geometry"/> </mxCell>
data 值是原对象经 JSON.stringify 获得的,经转义后就变成了上面的样子。控制台还打印了一个 mxGraphModel 对象,对比上面的 xml 与 下图的节点对象,能够发现它们只是同一个 Model 的不一样表现形式,xml 正是将 mxGraph.model 格式化而成的。
本项目监听事件写在 AppCanvas.vue 的 _listenEvent 方法,能够在这个方法了解一些经常使用的事件。下图来自 mxGraph 类的方法调用依赖图,咱们能够从这里看出整个框架的事件流动。
本项目的 _listenEvent 方法用到两个事件监听对象。
mxEventSource
有 mxEvent.UNDO、mxEvent.CHANGE
两个事件,经过监听 mxEvent.CHANGE
事件能够获取当前被选中的 Cell
。mxGraph
类有不少 XXX_CELLS
、CELLS_XXXED
这种形式的事件,这部分我还没弄懂,下面仅以添加事件为例探讨这两类事件的区别。
Cell
的时候会触发两个事件 ADD_CELLS
、CELLS_ADDED
, 先触发 CELLS_ADDED
后触发 ADD_CELLS
。ADD_CELLS
在 addCells
方法中触发,而 CELLS_ADDED
在 cellsAdded
方法中触发。而对于 addCells 与 cellsAdded 官方文档的说明并不能体现出二者的区别,再深究下去就要查阅源码了。按经验而言后触发的事件会携带更多的信息,因此平时开发我会监听 ADD_CELLS
事件。MOVE_CELLS、CELLS_MOVED
、REMOVE_CELLS、CELLS_REMOVED
等事件与此相似。从上面的方法调用依赖图中咱们能够看到,insertVertex
、insertEdge
最终都被看成 Cell
处理,在后续触发的事件也没有对 节点/边
进行区分,而是统一看成 Cell
事件。因此对于一个 Cell
添加事件,须要本身区别是添加了节点仍是添加了边。
graph.addListener(mxEvent.CELLS_ADDED, (sender, evt) => { const cell = evt.properties.cells[0]; if (graph.isPart(cell)) { return; } if (cell.vertex) { this.$message.info('添加了一个节点'); } else if (cell.edge) { this.$message.info('添加了一条线'); } });
还有就是对于子节点添加到父节点的状况(如本项目将 titleVertex 、normalTypeVertex 添加到 nodeRootVertex)也是会触发 Cell
添加事件的。一般对于这些子节点不做处理,能够像 05.consistuent.html 同样用一个 isPart
判断过滤掉。
上面提到过 mxGraph 继承自 mxEventSource,调用父类的 fireEvent 可触发自定义事件。下面是一个简单的例子
mxGraph.addListener('自定义事件A',()=>{ // do something ..... }); // 触发自定义事件 mxGraph.fireEvent(new mxEventObject('自定义事件A');
在本项目 Graph 类的 _configCustomEvent 方法我也实现了两个自定义事件。当边开始拖动时会触发 EDGE_START_MOVE
事件,当节点开始拖动时会触发 VERTEX_START_MOVE
事件。
mxGraph 导出图片的思路是先在前端导出图形的 xml 及计算图形的宽高,而后将 xml、宽、高,这有三项数据发送给服务端,服务端也使用 mxGraph 提供的 API 将 xml 转换成图片。服务端若是是使用 Java 能够参考官方这个例子,下面主要介绍前端须要作的工做。
导出图片可使用 mxImageExport 类,该类的文档有一段能够直接拿来使用的代码。
// ... var xmlCanvas = new mxXmlCanvas2D(root); var imgExport = new mxImageExport(); imgExport.drawState(graph.getView().getState(graph.model.root), xmlCanvas); var bounds = graph.getGraphBounds(); var w = Math.ceil(bounds.x + bounds.width); var h = Math.ceil(bounds.y + bounds.height); var xml = mxUtils.getXml(root); // ...
但这段代码会将整块画布截图,而不是以最左上角的元素及最右下角的元素做为边界截图。若是你有以元素做为边界的需求,则须要调用 xmlCanvas.translate 调整裁图边界。
//..... var xmlCanvas = new mxXmlCanvas2D(root); xmlCanvas.translate( Math.floor((border / scale - bounds.x) / scale), Math.floor((border / scale - bounds.y) / scale), ); //.....
完整截图代码能够参考本项目 Graph 类的 exportPicXML 方法。
若是节点像个人项目同样使用到图片,而导出来的图片的节点没有图片。能够从两个方向排查问题,先检查发送的 xml 里的图片路径是不是可访问的,以下面是项目“导出图片”功能打印的 xml 里的一个图片标签。
<image x="484" y="123" w="72" h="72" src="http://localhost:7777/static/images/ele/ele-005.png" aspect="0" flipH="0" flipV="0"/>
要保证 http://localhost:7777/static/images/ele/ele-005.png
是可访问的。若是图片路径没问题再检查一下使用的图片格式,原本我在公司项目中节点内使用的图片是 svg 格式,导出图片失败,多是 mxGraph 不支持这个格式,后来换成 png 以后问题就解决了。
还有就是若是导出的图片里的节点的某些颜色跟设置的有差别,那多是设置样式时写了3位数的颜色像 #fff
,颜色必定要使用完整的6位,不然导出图片会有问题。