mxGraph教程

Posted by lili on March 26, 2019

作为伪全栈工程师,在缺人的时候当然要冲出来。本文调研mxGraph的基本用法,用于绘制流程图。

目录

简介

根据维基,JGraph在2002年发布了最早的版本,是Java实现的。到了JGraph 5.x后使用JavaScript实现,更名为mxGraph,而原来的Java版本更名为JGraphX并且版本与mxGraph保持一致。Java和JavaScript版本的API是类似的,但是在今天B/S架构为主,因此更多的人是使用mxGraph。目前mxGraph(包括JGraphX)的最新版本是4.0.0。

在线绘图网站draw.io就是使用mxGraph实现的,官网也提供了一个非常复杂的例子。从这两个例子也可以看出,mxGraph的功能确实非常强大,可以实现复杂的流程图绘制。本文主要介绍mxGraph(JavaScript),因此mxGraph默认都是指JavaScript的版本,最后会简单的介绍Java版本。本文主要参考了官方教程官方文档。另外mxGraph 入门实例教程是一个非常好的教程,里面提供了不错的示例代码

教程

什么是Graph

mxGraph里的Graph指的是图论(Graph Theory)里的图而不是柱状图、饼图和甘特图等图(chart),因此想找这些图的读者可以结束阅读了。作为图论的图,它包含点和边,如下图所示。

图:一个简单的图

注意:在图论中,我们只关心有多少点和边,以及点和边是怎么连接的(包括边是有向还是无向的),我们并不关心点和边的形状、颜色以及它们的空间布局。把上图中的A移动一下位置对于图论来说仍然是一个相同的图。但是mxGraph作为一个可视化的JavaScript库,考虑的重点就是这些东西(否则还要它做什么,如果需要证明四色猜想或者用Dijkstra算法计算单源最短路径,估计很少人需要JavaScript库吧)。

主要用途

mxGraph有很多用途,下面是主要的一些用途。当然对于我们来说,是用它来定义流程图。

可视化

比如下图是交通图的可视化,我们可以把城市作为节点,因此节点的位置需要和实际的地图(经纬度)大致对应。

图:交通图的可视化

图的交互(编辑)

如下图所示,我们可以用mxGraph实现流程图,可以通过复杂的鼠标操作来编辑这个图,包括折叠和展开部分子图。

图:图的交互

图的布局

图的节点一多,就很难展示,怎么漂亮的对图的节点和边进行布局就非常重要,mxGraph支持树状(tree)布局、力导向(force-directed)布局和层次(hierarchical)布局等常见布局算法。而且可以把布局的计算放到后端服务器上进行,从而减少客户端的计算压力。层次布局示例如下图所示。

图:层次布局

图的分析

可以对图做一些聚类分析,也可以计算节点的最短路径等。比如下图显示了一条最短路径。

图:最短路径

架构

mxGraph在实际使用的架构如下图所示,分为三个部分:前端Web Client、Web Server和后端服务器。mxGraph是一个JavaScript库,除了js还包括图片和css等静态文件,通常放到一个Web Server上,比如Apache或者Nginx。当然很多后端服务器也能提供静态文件服务,比如Java的Apache Tomcat或者Jetty等。但是把前端(js)的静态文件放到 Nginx这样的服务器上性能更好,而且前后端分工比较清楚,互不干扰。用户通过前端的Web Client来访问包含mxGraph的网页,进行浏览和编辑,然后定期把修改同步到后端服务器上。当然也可以加载保存在服务器上的流程图,这样不同的用户可以实现流程图的共享。

图:mxGraph架构图

mxGraph的特点是:

  • 不依赖任何第三方库
  • 封装了SVG等Vector Graph语言,并且解决不同浏览器的兼容性问题
  • 所有数据(包括图的可视化的数据比如点的颜色形状等和用户业务数据)都保存在本地(JavaScript)里。因此即使没有网络,我们仍然可以编辑和修改,当网络恢复后我们再同步到服务器上就可以了。

获取代码

我们可以从github获取代码,这个目录包含如下内容:

  • doc/ 文档,包括教程和手册
  • dotnet/ .NET后端,我们忽略
  • java/ Java代码,包括Swing的前端和后端
  • javascript/ 我们关注的重点
  • /javascript/examples JavaScript例子
  • index.html 文档首页,可以本地打开

javascript目录包括:

$ tree -L 1
.
├── examples
├── index.html
├── mxClient.js
├── mxClient.min.js
└── src

我们通常需要把mxClient.js(或者mxClient.min.js,这个文件去掉了换行,因此更小,但是不便于调试)和src目录复制到Nginx等服务器上。src包含了所有了js、css和图片等内容。

Hello World

我们首先来看Hello World示例,也可以git clone代码后直接用浏览器打开本地的文件。

引入mxGraph

在HMTL的head部分加入如下代码:

<script type="text/javascript">
  mxBasePath = 'javascript/src';
</script>
<script type="text/javascript" src="javascript/src/js/mxClient.js"></script>

引入mxGraph和别的库不同的地方是需要知道mxBasePath,也就是前面我们说的src目录的路径。

浏览器兼容性检查

使用mxGraph前我们首先需要检查用户的浏览器是否支持,代码如下:

<script type="text/javascript";>
function main(container)
{
  // 检查浏览器是否支持 
  if (!mxClient.isBrowserSupported())
  {
    // 如果不支持显示错误信息
    mxUtils.error('Browser is not supported!', 200, false);
  }
  ...

Container

我们需要在某个Element里显示mxGraph,通常当然是放到一个DIV里。我们在HTML的BODY里定义一个DIV,然后在构造mxGraph的时候通过getElementById找到它并传入。

<body onload="main(document.getElementById('graphContainer'))">

   <!-- Creates a container for the graph with a grid wallpaper -->
   <div id="graphContainer"
      style="overflow:hidden;width:321px;height:241px;background:url('editors/images/grid.gif')">
   </div>
</body>

我们在div里指定的样式是div的宽度、高度以及overflow:hidden(这可以去掉滚动条),另外作为流程图的编辑,我们通常在使用网格的背景(每个网格10px),这样便于对齐,另外这样看起来更加专业一点。除此之外,其它的所有与图相关的样式都是在mxGraph里通过后面介绍的样式来修改。

Graph对象的定义

var model = new mxGraphModel();
var graph = new mxGraph(container, model);

container就是我们前面定义的DIV,我们也可以不自己构造mxGraphModel,这样也是可以的:

var graph = new mxGraph(container);

添加点和边

// 得到默认的parent用于插入cell。这通常是root的第一个孩子。
var parent = graph.getDefaultParent();
        
// 开始事务
model.beginUpdate();
try
{
  var v1 = graph.insertVertex(parent, null, 'Hello,', 20, 20, 80, 30);
  var v2 = graph.insertVertex(parent, null, 'World!', 200, 150, 80, 30);
  var e1 = graph.insertEdge(parent, null, '', v1, v2);
}
finally
{
  // 提交事务
  model.endUpdate();
}

所有的修改都需要在beginUpdate和endUpdate之间,而且一旦调用了beginUpdate,就一定要调用endUpdate,所以这里的代码把endUpdate放到finally里以确保它一定会被调用。而beginUpdate一定不能放到try里面,否则如果beginUpdate抛出异常,也会调用endUpdate,这是不行的!

最终的效果如下图所示:

图:Hell World

mxGraph中的继承

为了改写mxGraph的默认行为,我们通常需要继承某个父类并且重载某个父类方法。mxGraph使用prototype来实现继承,这是JavaScript实现类和继承的标准方法,对此不了解的读者可以参考A Beginner’s Guide to JavaScript’s PrototypeJavaScript Inheritance and the Prototype Chain,非常通俗易懂,我这种只学了半个月JavaScript的人都能看懂!

比如mxEditor要继承mxEventSource类,那么就可以这样实现:

mxEditor.prototype = new mxEventSource()
mxEditor.prototype.constructor = mxEditor

继承mxGraph

为了继承mxGraph,我们定义如下构造函数(如果不懂的话,请仔细阅读上面的两篇参考文章,JavaScript里类就是构造函数!):

function MyGraph(container)
{
   //调用父类的构造函数
   mxGraph.call(this, container);
}

//通过prototype继承mxGraph
MyGraph.prototype = new mxGraph();
MyGraph.prototype.constructor = MyGraph;

我们通常希望这个类可以序列化成XML,从而可以保存到后端服务器里(比如数据库里),因此我们通常需要如下的代码:

var codec = mxCodecRegistry.getCodec(mxGraph);
codec.template = new MyGraph();
mxCodecRegistry.register(codec);

重载

比如我们可以重载mxGraph的isSelectable方法,这个方法用来决定某个cell(后面会解释,cell就是点和边等)是否可以被选中。

MyGraph.prototype.isSelectable = function(cell)
{
   var selectable = mxGraph.prototype.isSelectable.apply(this, arguments);
   var geo = this.model.getGeometry(cell);
   return selectable &&(geo == null || !geo.relative);
}

我们可以首先调用父类的方法”mxGraph.prototype.isSelectable.apply(this, arguments)”,然后再加上我们的判断逻辑。不了解this和arguments关键词的读者可以参考这里这里

当然我们也可以不调用父类的方法,比如:

mxGraph.prototype.isSelectable = function(cell)
{
   var geo = this.model.getGeometry(cell);
   return selectable && (geo == null || !geo.relative);
}

我们也可以定义添加新的函数,比如:

MyGraph.prototype.getXml = function()
{
   var enc = new mxCodec();
   return enc.encode(this.getModel());
}

上面的代码给MyGraph类添加了getXml函数,但是新加的函数只能被我们自己调用,不可能被mxGraph框架回调,而前面的isSelectable通常我们不会主动调用它,而是mxGraph框架在合适的时候回调这个函数,从而改变mxGraph选择的行为。

基本概念

cell

Cell在mxGraph中可以代表组(Group)、节点(Vertex)和边(Edge),mxCell 这个类封装了Cell的操作。

事务

对于图的修改,我们需要放到beginUpdate之间endUpdate,这里面的所有操作就是一个事务。和数据库的事务类似,它要么都成功要么都失败,而且mxGraph的回滚(undo)也是以事务为单位的。因此正确的写法是首先调用beginUpdate;然后把图的修改放到try里;最后在finally里调用endUpdate。代码类似如下结构:

model.beginUpdate();
try
{
  //更新点和边
}
finally
{
  model.endUpdate();
}

mxGraphModel

mxGraphModel里真正的存放图的数据,但是我们通常并不直接操作mxGraphModel,而是通过mxGraph的函数间接操作mxGraphModel。

insertVertex和insertEdge

要在图中插入点,我们可以使用mxGraph.insertVertex(parent, id, value, x, y, width, height, style)。这个函数的参数如下:

  • parent 所有的节点都有parent,从而构成一棵树。我们通常使用graph.getDefaultParent()作为parent。

  • id 点的id,需要唯一,如果null,mxGraph会自动帮我们生成唯一的id。

  • value UserObject。我们可以把业务逻辑的对象存放在这里

  • x, y, width, height 左上的坐标以及宽和高

  • style 后文会详细介绍

关于UserObject这里再做一下简单的解释。对于mxGraph来说,数据包括两部分:用于显示的数据和UserObject。显示的数据包括点的大小、位置等”样式”,而UserObject是用于存放业务逻辑对象。

插入边需要调用mxGraph.insertEdge(parent, id, value, source, target, style) ,parent、id和style的含义和insertVetex相同,而:

  • source表示边的起点

  • target表示边的终点

mxGeometry

mxGeometry表示一个点或者边的控制点的几何位置信息,它包含左上左边和宽高信息:

function mxGeometry(x,y,width,height){}

所谓边的控制点是指某些非直线比如折线的中间点,比如下图所示。当然对于控制点来说,width和height就没有意义了。另外边是没有位置信息的,它由起点和终点确定(当然边可以有线宽等属性,用于控制线的粗细)。

图:控制点

mxGeometry有一个很重要的布尔属性relative,它用来说明位置是绝对坐标(左边原点是屏幕的最左上角)还是相对位置(相对其parent)。

绝对坐标很好理解,我们下面通过图来了解什么是相对位置,比如下图:

图:点的相对位置

如果是相对位置,那么点(0.5, 0.5)的左上角(注意不是中心)位于父节点的中心。(1, 1)的左上角位于父节点的最右下角;(-0.5, 0.5)的位置如上图所示。

前面说了边并没有自己的位置,但是边有一个标签(label),它显示出来就是一个长方形(rect)。下面我们来说一下label的相对定位。

mxGraph通过(x, y)来相对定位边的label。我们先看直线(线段)的边,x=0表示label位于线段的中间,而x=-1表示label位于线段的左端,x=1表示label位于右端。y表示垂直于直线的方向的位置。如下图所示:

图:label的相对位置

上图是通过如下代码画出来的:

const e1 = graph.insertEdge(parent, null, '30%', v1, v2);
e1.geometry.x = 1;
e1.geometry.y = 100;

样式

我们可以通过样式来改变cell的外观。如下图所示,mxStyleSheet可以看成一个Hashtable,key是样式名,value是很多具体的样式的数组。每一个具体的样式就是”strokeColor=red”这样的k-v对。因此每个样式实际是一组具体的样式,具体的样式可能是设置字体颜色为红色、设置透明的为50等等。

图:label的相对位置

比如在上图中,有一个defaultVertex样式,它是默认的点的样式;而defaultEdge是默认边的样式。当然一个样式也可以同时应用于点和边。除了defaultVertex和defaultEdge等系统预先定义的样式,我们也可以自己定义样式:

var style = new Object();
style[mxConstants.STYLE_SHAPE] = mxConstants.SHAPE_RECTANGLE;
style[mxConstants.STYLE_OPACITY] = 50;
style[mxConstants.STYLE_FONTCOLOR]= '#774400';
graph.getStylesheet().putCellStyle('ROUNDED',style);

比如上面的代码就定义了名为ROUNDED的样式,定义之后我们就可以在创建节点或者边的时候使用这个自定义样式:

var v1 = graph.insertVertex(parent, null, 'Hello', 20, 20, 80, 30, 'ROUNDED');

当然我们也可以使用这个样式,但是又修改部分具体的样式,我们需要用分号(;)来加入具体样式:

var v1 = graph.insertVertex(parent, null, 'Hello',  20, 20, 80, 30, 'ROUNDED;strokeColor=red;fillColor=green');

我们也可以不使用任何样式而直接指定具体样式:

var v1 = graph.insertVertex(parent, null, 'Hello', 20, 20, 80, 30, ';strokeColor=red;fillColor=green');

因此上面的代码指定画笔的颜色是红色而填充的颜色是绿色,除此之外没有任何样式。

查看样式的小技巧

mxGraph 所有样式在这里可以查看,打开网站后可以看到以 STYLE_开头的是样式常量。但是这些样式常量并不能展示样式的效果。下面教大家一个查看样式效果的小技巧,使用 draw.ioGraphEditor (它们都是使用 mxGraph 进行开发的) 的Edit Style功能可以查看当前Cell样式。

比如现在我想将边的样式设置成:红色、折线、粗3pt。在 Style 面板手动修改样式后,再点击 Edit Style 就可以看到对应的样式代码。

图:使用GraphEditor设置和查看样式

图:点击Edit Style编辑样式

序列化与反序列化

为了把本地的JavaScript对象保存到服务器或者从服务器恢复到JavaScript对象,我们需要序列化和反序列化。mxGraph还是使用古董级的XML的方案。

我们可以使用下面的代码把整个图(包括图的样式和UserObject)都序列化成XML,这样我们可以通过Ajax请求把它保存到服务器上:

    var encoder = new mxCodec();
    var result = encoder.encode(graph.getModel());
    var xmlString = mxUtils.getXml(result);

保存到服务器后我们随时可以再把它取下来,然后反序列化重新加载Graph:

    var doc = mxUtils.parseXml(xmlString);
    var codec = new mxCodec(doc);
    codec.decode(doc.documentElement, graph.getModel());

高级功能

高级功能包括分组、折叠等,我没看懂,暂时也用不到,因此就不研究了。

更多示例

更多示例参考这里,另外这里也有一些简单的示例。

Java教程

Java代码在这里,但这是比较古老的ant项目,我把它改成了一个maven项目,可以在这里来clone。

示例代码都在com.mxgraph.example包下,示例的说明在这里