在上一篇文章 《記一次繪圖框架技術選型: 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位,不然導出圖片會有問題。