作为商业软件,GoJs很容易使用,文档也很完备,不过项目中没有时间系统地按照文档学习,总是希望快速入门使用,所以将项目中遇到的问题精简一下,希望对后来者有些帮助。

开始使用

这里先展示一个最简单的例子,说明GoJs的使用。

<!DOCTYPE html> <!-- HTML5 document type -->
<html>

<head>
    <script src="https://unpkg.com/gojs/release/go-debug.js"></script>
</head>

<body>
    <div id="myDiagramDiv" style="width:1200px; height:850px; background-color: #DAE4E4;"></div>
    <script>
        var $ = go.GraphObject.make;

        myDiagram =
            $(go.Diagram, "myDiagramDiv",
                {
                    "undoManager.isEnabled": true
                });

        myDiagram.add(
            $(go.Node, "Auto",
                $(go.Shape, "RoundedRectangle",
                    {
                        fill: $(go.Brush, "Linear",
                            { 0.0: "Violet", 1.0: "Lavender" })
                    }),
                $(go.TextBlock, "测试文本!",
                    { margin: 5 }),
                $("Button", {
                    alignment: go.Spot.Right,
                    alignmentFocus: go.Spot.Left,
                    click: function(e,obj){ console.log(e); console.log(obj);alert(obj);}
                },
                    $(go.TextBlock, "+",  // the Button content
                        { font: "bold 8pt sans-serif" }))
            ));

    </script>
</body>

</html>

首先是引用GoJs库,可以有多个途径下载,可以通过npm,nuget等包管理工具,也可以直接下载。我们这里使用unpkg的引用。
然后就是使用 go.GraphObject.make创建图形和图形中的元素。这里先将 go.GraphObject.make简化定义为$,方便代码的编写与阅读,注意这不是必须的,也可以使用$$或者其它简化方式。结果如下:
GoJS 使用笔记-LMLPHP
这里的关键是go.GraphObject.make的使用,下面重点介绍这个函数。

使用go.GraphObject.make创建对象

一个图形可以看做由节点和连线组成,在GoJs中,图形元素是GraphObject,我们可以使用代码创建节点:

  var node = new go.Node(go.Panel.Auto);
  var shape = new go.Shape();
  shape.figure = "RoundedRectangle";
  shape.fill = "lightblue";
  node.add(shape);
  var textblock = new go.TextBlock();
  textblock.text = "你好!";
  textblock.margin = 5;
  node.add(textblock);
  diagram.add(node);

这种办法属于常规的编程方法,容易理解,但是需要定义大量的中间变量,如果需要创建的元素很多,就会感觉有些冗余。因此GoJs使用创建函数go.GraphObject.make简化创建过程。上面的代码写为:

  var $ = go.GraphObject.make;
  diagram.add(
    $(go.Node, "Auto",
      $(go.Shape, "RoundedRectangle", { fill: "lightblue" }),
      $(go.TextBlock, "你好!", { margin: 5 })
    ));

可读性好多了。 go.GraphObject.make的第一个参数是需要创建的类型,通常是GraphObject的子类,后续的参数可以有多个,可以是以下类型:

  • 属性/值对的JS简单对象,说明被创建对象的属性。
  • GraphObject,添加到被创建对象中的子对象,比如,上面的代码中在Node中增加Shape和TextBlock。
  • 字符串,针对特定对象的属性,比如对于TextBlock,就是设置文本值
  • 其它可能的Js对象,针对创建对象的不同。

Binding

基本用法

Binding顾名思义是绑定,将模型的属性与GraphObject对象的属性进行绑定。比如,有如下模型:

{ key: 23, say: "你好!" }

我们需要将say属性绑定到文本对象的text属性,可以使用下面的代码:

 var $ = go.GraphObject.make;
  myDiagram.nodeTemplate =
    $(go.Node, "Auto",
      . . .
      $(go.TextBlock, new go.Binding("text", "say"))
    )

转换函数

如果我们希望绑定的属性进行转换,可以使用转换函数,比如:

  new go.Binding("text", "say", function(v) { return "我说: " + v; })

单向绑定和双向绑定

单向绑定时只能是模型的属性改变GraphObject对象的属性,而双向绑定时,GraphObject对象的属性的改变可以改变模型的属性。双向绑定的写法是这样的:

new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify)

当loc转换为location时,使用go.Point.parse函数,当location转换为loc时,使用go.Point.stringify函数。

GraphObject

GraphObject是抽象类,不能直接创建,GraphObject具有下面的一些特性。

尺寸

GraphObject有关尺寸的属性如下:

  • desiredSize,minSize和maxSize。width和height会转换为desiredSize。
  • angle和scale
  • stretch

GraphObject在Panel中绘制完成后,会有如下的只读属性:

  • nuturalBounds:表示基本尺寸,不受转换的影响
  • measureBounds: 表示在包含它的Panel中的尺寸
  • actualBounds:表示在包含它的Panel中的实际尺寸

顶层GraphObject一定是Part

Part是GraphObject的子类,表示顶层元素。顶层元素一定是Part,包括Node,Linke,Group以及Adornment,下面的属性用于获取相关的GraphObject:

  • panel:获取包含这个GraphObject的Panel
  • part: 获取这个GraphObject所在的Part。
  • layer: 获取这个GraphObject所在的Layer。
  • diagram:获取所在的Diagram。

Model

模型中保存了图形显示的数据,描述基本实体、它们的属性以及之间的关系。模型中的数据要尽量简单,可以很容易地序列化为JSON或者XML格式。有两种模型TreeModel和GraphLinksModel,前者保存树状结构的数据。模型中key值用来标识对象,必须是唯一的,下面是Model的使用实例:

myDiagram.model = new go.TreeModel();
        myDiagram.model.nodeDataArray = [{ "key": 0, "text": "Mind Map", "loc": "0 0" },
        { "key": 1, "parent": 0, "text": "Getting more time", "brush": "skyblue", "dir": "right", "loc": "77 -22" },
        { "key": 11, "parent": 1, "text": "Wake up early", "brush": "skyblue", "dir": "right", "loc": "200 -48" },
        { "key": 12, "parent": 1, "text": "Delegate", "brush": "skyblue", "dir": "right", "loc": "200 -22" }];

这是树形结构的数据。如果保存一般的图结构,需要使用GraphLinksModel。

自定义Node模板

个人认为方便的自定义模板是GoJs的强大功能之一,使用nodeTemplateMap可以很方便地定义各种类型的模板,只要在数据模型中指定模板的名称(使用category),就可以显示相应的图形。nodeTemplateMap的使用方法如下:

myDiagram.nodeTemplateMap.add("End",
        part
      );

这里,part就是显示的模板,比如,下面是一个part的定义,显示状态图的开始节点:

var partStart=    $(go.Node, "Spot", { desiredSize: new go.Size(75, 75) },
          new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
          $(go.Shape, "Circle",
            {
              fill: "#52ce60", /* green */
              stroke: null,
              portId: "",
              fromLinkable: true, fromLinkableSelfNode: true, fromLinkableDuplicates: true,
              toLinkable: true, toLinkableSelfNode: true, toLinkableDuplicates: true,
              cursor: "pointer"
            }),
          $(go.TextBlock, "开始",
            {
              font: "bold 16pt helvetica, bold arial, sans-serif",
              stroke: "whitesmoke"
            })
        );

对应的数据如下:

 {"id":-1, "loc":"155 -138", "category":"Start"}

数据中,category指定了模板类型,loc绑定到图元的位置,这里是双向绑定,也就是图元位置的变化,会改变数据模型中的数据。

如果只定义通用的模板,可以使用:

myDiagram.nodeTemplate=part;

这种情况下,没有指定category的数据都采用缺省模板显示。

自定义选中模板

当一个节点被选中时,我们希望使用不同的模板显示,比如,状态图中,一个被选中的节点中会出现添加链接的按钮,选中前:
GoJS 使用笔记-LMLPHP
选中后:
GoJS 使用笔记-LMLPHP
如果为使用缺省模板的节点定义选中模板,可以直接定义:

 myDiagram.nodeTemplate.selectionAdornmentTemplate = adornmentTemplate;

如果需要为使用nodeTemplateMap添加的自定义模板定义选中模板,可以使用如下方法:

var partStart=    $(go.Node, "Spot", { desiredSize: new go.Size(75, 75) },
          new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
          $(go.Shape, "Circle",
            {
              fill: "#52ce60", /* green */
              stroke: null,
              portId: "",
              fromLinkable: true, fromLinkableSelfNode: true, fromLinkableDuplicates: true,
              toLinkable: true, toLinkableSelfNode: true, toLinkableDuplicates: true,
              cursor: "pointer"
            }),
          $(go.TextBlock, "开始",
            {
              font: "bold 16pt helvetica, bold arial, sans-serif",
              stroke: "whitesmoke"
            })
        );
        partStart.selectionAdornmentTemplate =$(go.Adornment, "Spot",
          $(go.Panel, "Auto",
            $(go.Shape, "RoundedRectangle", roundedRectangleParams,
            { fill: null, stroke: "#7986cb", strokeWidth: 3 }),
            $(go.Placeholder)  // a Placeholder sizes itself to the selected Node
          ),
          // the button to create a "next" node, at the top-right corner
          $("Button",
            {
              alignment: go.Spot.TopRight,
              click: addNodeAndLink  // this function is defined below
            },
            $(go.Shape, "PlusLine", { width: 6, height: 6 })
          )
        );

        myDiagram.nodeTemplateMap.add("Start",
        partStart
        );

上面的代码中,需要先定义模板的part,然后增加选中模板,最后,使用nodeTemplateMap.add方法进行添加。

节点和连接的上下文菜单

对于节点和菜单的缺省模板,可以直接使用contextMenu定义上下文菜单,比如:

        myDiagram.nodeTemplate.contextMenu =
            $("ContextMenu",
                $("ContextMenuButton",
                    $(go.TextBlock, "显示属性"),
                    { click: function (e, obj) {
                      var data = myDiagram.model.findNodeDataForKey(obj.part.key);
                      alert(data.complex.p1);
                      } })
            );

对于使用nodeTemplateMap定义的自定义模板,需要在模板上先定义上下文菜单,然后再将模板添加到nodeTemplateMap中,下面的代码定义了状态图中结束节点的上下文菜单:

var partEnd=$(go.Node, "Spot", { desiredSize: new go.Size(75, 75) },
          new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
          $(go.Shape, "Circle",
            {
              fill: "maroon",
              stroke: null,
              portId: "",
              fromLinkable: true, fromLinkableSelfNode: true, fromLinkableDuplicates: true,
              toLinkable: true, toLinkableSelfNode: true, toLinkableDuplicates: true,
              cursor: "pointer"
            }),
          $(go.Shape, "Circle", { fill: null, desiredSize: new go.Size(65, 65), strokeWidth: 2, stroke: "whitesmoke" }),
          $(go.TextBlock, "结束",
            {
              font: "bold 16pt helvetica, bold arial, sans-serif",
              stroke: "whitesmoke"
            })
        );
        partEnd.contextMenu=
            $("ContextMenu",
                $("ContextMenuButton",
                    $(go.TextBlock, "显示属性"),
                    { click: function (e, obj) {
                      var data = myDiagram.model.findNodeDataForKey(obj.part.key);
                      alert(data.complex.p1);
                      } })
            );
      myDiagram.nodeTemplateMap.add("End",
        partEnd
      );

连接的上下文菜单定义与节点相同,示例代码如下:

        myDiagram.linkTemplate.contextMenu =
            $("ContextMenu",
                $("ContextMenuButton",
                    $(go.TextBlock, "显示属性"),
                    { click: function (e, obj) {
                      alert(obj.part.data.expression);
                      } })
            );

节点和连接关联数据的访问

很多图形编辑器不容易使用的一个原因是编辑器的数据模型与业务的数据模型很难匹配。业务数据模型经常比较复杂,不仅仅是键值对能够完全表示的,很多情况下需要使用复杂的对象描述。GoJs在这一点上做得非常好,图形相关的数据模型可以和图形进行绑定,并且数据模型中可以包括复杂的数据对象,比如下面的节点中包括了复合的对象:

{"id":-1, "loc":"155 -138", "category":"Start","complex":{"p1":"自定义属性"}}

对象的读取也不复杂,在访问节点数据的示例代码如下:

        myDiagram.nodeTemplate.contextMenu =
            $("ContextMenu",
                $("ContextMenuButton",
                    $(go.TextBlock, "显示属性"),
                    { click: function (e, obj) {
                      var data = myDiagram.model.findNodeDataForKey(obj.part.key);
                      alert(data.complex.p1);
                      } })
            );

访问连接数据的示例代码如下:


        myDiagram.linkTemplate.contextMenu =
            $("ContextMenu",
                $("ContextMenuButton",
                    $(go.TextBlock, "显示属性"),
                    { click: function (e, obj) {
                      console.log(e);
                      console.log(obj.part);
                      alert(obj.part.data.expression);
                      } })
            );

GoJs的命令

GoJs的命令,比如删除、重做、取消等等通过类CommandHandler实现。命令可以通过代码执行,也可以通过快捷键执行。下面的代码执行undo操作:

 myDiagram.commandHandler.undo();

下面是GoJs常用的命令和对应的快捷键:

  • Del 或者 Backspace 激活 CommandHandler.deleteSelection,删除
  • Ctrl-X 或者 Shift-Del 激活 CommandHandler.cutSelection,剪切
  • Ctrl-C 或者 Ctrl-Insert 激活 CommandHandler.copySelection,拷贝
  • Ctrl-V 或者 Shift-Insert 激活 CommandHandler.pasteSelection,粘贴
  • Ctrl-A 激活 CommandHandler.selectAll,全选
  • Ctrl-Z 或者 Alt-Backspace 激活 CommandHandler.undo,取消
  • Ctrl-Y 或者 Alt-Shift-Backspace 激活 CommandHandler.redo,重做
  • 空格键 激活 CommandHandler.scrollToPart,滚动到部件
    • (减号)激活CommandHandler.decreaseZoom,缩小zoom
    • (加号)激活 CommandHandler.increaseZoom,放大zoom
  • Ctrl-0 激活 CommandHandler.resetZoom ,重置zoom
  • Shift-Z 激活 CommandHandler.zoomToFit,设置zoom到适合图形大小
  • Ctrl-G 激活 CommandHandler.groupSelection , 组合
  • Ctrl-Shift-G 激活 CommandHandler.ungroupSelection,取消组合
  • F2 激活 CommandHandler.editTextBlock,编辑
  • Esc 激活 CommandHandler.stopCommand,取消命令

GoJs 上下文菜单

前面介绍了节点和链接的上下文菜单,在图形的背景上也可以设置上下文菜单,设置方法很简单,直接在背景的contextMenu上设置就可以了,示例代码如下:

    myDiagram.contextMenu =
        GO("ContextMenu",
            GO("ContextMenuButton",
                GO(go.TextBlock, "撤销"),
                {
                    click: function (e, obj) {
                        myDiagram.commandHandler.undo();
                    }
                })
        );

可以对ContextMenuButton设置尺寸,比如,增加宽和高的属性:

GO("ContextMenuButton",
                GO(go.TextBlock, "撤销"),
                {
                    width: 160, height: 120,
                    click: function (e, obj) {
                        myDiagram.commandHandler.undo();
                    }
                }),

也可以为ContextMenu设置属性,添加完菜单按钮后面,增加属性设置:

myDiagram.contextMenu =
        GO("ContextMenu",

            GO("ContextMenuButton",
                GO(go.TextBlock, "撤销"),
                {
                    click: function (e, obj) {
                        myDiagram.commandHandler.undo();
                    }
                }),
            GO("ContextMenuButton",
                GO(go.TextBlock, "重做"),
                {
                    click: function (e, obj) {
                        myDiagram.commandHandler.redo();
                    }
                }),
            {width:200}
        );

GoJs 生成图片并回传服务器

GoJs提供在客户端生成流程图的blob数据,然后通过浏览器进行下载,这种方式不需要服务端的支持,示例代码如下:

myDiagram.makeImageData({ returnType: "blob", scale: 3, detail: 0.9, callback: saveBlobToServer});

这里生成的blob数据会由自定义的回调函数处理,在回调函数中,可以编写通过浏览器的下载代码,或者将流程图数据回传到服务器的代码。这里,我们希望将图片回传服务器进行保存:

            function saveBlobToServer(blob) {
                var fd = new FormData();
                fd.append('fname', 'myBlobFile.png');
                fd.append('data', blob);
                $.ajax({
                    type: 'POST',
                    url: root + 'Upload/SaveImage',
                    data: fd,
                    processData: false,
                    contentType: false
                }).done(function (data) {
                    if (!data) alert("保存完成");
                    else alert(data);
                });
            }

服务器端使用Asp.Net Core:

       [HttpPost]
        public IActionResult SaveImage()
        {
            var files = Request.Form.Files;
            var fn = Request.Form["fname"];
            if (files.Count > 0)
            {
                var pic = files[0];
                var fileName = fn;// Path.Combine(rootpath, pic.FileName);
                if (System.IO.File.Exists(fileName)) System.IO.File.Delete(fileName);
                using (var stream = new FileStream(fileName, FileMode.CreateNew))
                {
                    pic.CopyTo(stream);
                }
            }

            return Content("");
        }
02-23 19:48