版權所有,引用請注明出處:<<http://www.cnblogs.com/dragon/p/5203663.html >>
本文所用示例下載FlowChart.zip
一個用Netron開發的實際應用請看:發布一個免費開源軟件-- PAD流程圖繪制軟件PADFlowChart
一、 概述
Netron是一個開源的圖形開發庫,它還有一個輕量級的版本叫NetronLight,本文不討論NetronLight。
在NetronGraphLib里,需要重點理解的是四個類,這四個類理解了,NetonGraphLib就掌握了大半部分:
- GraphControl:代表的是畫布對象,所有的圖形對象都是在畫布上展現,同時畫布對象管理着圖形對象的各種行為,如拖動,變形,選中以及連接等。也可以通過畫布對象訪問到所有的圖形對象。
- Shape:代表的是一個圖形對象
- Connector:代表的是圖形對象上的連接點,兩個連接點之間可以產生一條連接線。連接點對象只能依附圖形對象而存在。
- Connection:代表的是兩個連接點之間的連接線
Shape、Connector、Connection共通的一些屬性:
- Site:用來引用GraphControl畫布對象
- IsSelected:表明對象是否處於選中狀態
- IsHovered:鼠標是否正懸停在對象上
- Paint():用來畫出對象的方法
- Hit():檢測對象是否被矩形包含或包含某個坐標點
- OnMouseDown、OnMouseMove、OnMouseUp:鼠標事件委托,可以捕捉鼠標事件
二、 GraphControl控件
將NetronGraphLib添加到解決方案
打開一個Form設計窗口,這時你會看到在工具箱里多了一個NetronGraphLib組件面板
將面板里的GraphControl組件拖到Form窗體上,如下圖
GraphControl組件是用來畫圖的畫布,而且這個組件可以自動幫我們管理圖形的移動,變形,選擇等操作。
下面我們看看該組件的一些屬性設置
滾動條
AutoScroll=True:設置當圖形超出畫布邊界時滾動條是否會自動顯現
RestrictToCanvas=False:設置圖形是否可以被移動到超出當前畫布的位置
這兩個屬性配合起來可以實現將圖形拖動到超出當前畫布位置時畫布自動出現滾動條
比如下面的圖形顯示在GraphControl上,
當拖動圖形到畫布之外時,滾動條自動出現了
停靠
通常情況下我們都會設置Dock屬性為Full,一般情況下沒有什么問題,但是當你在窗體下方添加了狀態條,而又設置了滾動條時,會出現水平方向的滾動條被狀態條擋住顯示不了的問題。為了解決這個問題,需要將Dock設置為None,同時設置Anchor為Top,Bottom,Left,Right.這樣就可以達到Dock為Full時一樣的效果,同時狀態條也能正常顯示。
EnableLayout=False:這個屬性如果選擇True,則畫布會在你拖動兩個相互連接的圖形時出現動畫效果,但是你無法控制圖形最后停止的位置
當EnableLayout=True的時候GraphLayoutAlgorithm可以設定產生動畫效果的算法
最后,我們把GraphControl控件的Name屬性設為graphControl,方便在代碼中引用
GraphControl的其它常用屬性和方法:
- AddShape():可以往畫布上添加一個對象並顯示
- Shapes:可以訪問graphControl管理的所有圖形對象
- Connections:可以訪問所有的Connection對象
- SelectedShapes:可以訪問所有被選中的圖形對象
- Abstract:這個數據成員可能會讓人比較困惑,這個Abstract是GraphAbstract類型的對象,其實就是用來管理Shapes、Connections等數據的一個數據類,GraphControl的Shapes成員和Connections成員也是通過訪問Abstract來得到的。
三、 開發圖形:Shape
我們打算畫一個矩形圖形,如下圖
1、 建立一個SequenceShape對象,從Shape類繼承
using Netron.GraphLib; public class SequenceShape : Shape{ }
2、 改寫Shape的InitEntity方法
我們需要在該方法中初始化對象的矩形坐標,以及設定畫邊框的畫筆Pen和Shape的背景顏色ShapeColor,
protected override void InitEntity() { base.InitEntity(); Pen = new Pen(Color.FromArgb(167, 58, 95)); ShapeColor = Color.FromArgb(255, 253, 205); Rectangle = new RectangleF(100,10 0, 100, 50); }
不要忘了先調用基類的InitEntity方法
3、改寫Shape的Paint方法,進行具體的畫圖
public override void Paint(Graphics g) { base.Paint(g); g.FillRectangle(new SolidBrush(ShapeColor), Rectangle); g.DrawRectangle(Pen, System.Drawing.Rectangle.Round(Rectangle)); }
FillRectangle方法是用來填充矩形的背景色,DrawRectangle是用來畫矩形的邊框。至於為什么要先填背景色再畫邊框,是因為先畫邊框會導致邊框有兩條邊被背景色覆蓋掉。
4、在畫布上顯示圖形
要在畫布上顯示圖形,只需生成Shape實例並加入畫布對象graphControl
我們在點擊工具欄btn_sequence按鈕的事件中加入生成SequenceShape實例的代碼
private void btn_sequence_Click(object sender, EventArgs e) { SequenceShape shape = new SequenceShape(); graphControl.AddShape(shape); }
運行程序,下面是效果圖
點擊btn_sequence,在graphControl畫布上就可以畫出矩形圖形了,而且還具有了選中、移動、改變大小等功能,這些功能都是由graphControl對象自動管理的。
四、 增加圖形對象的功能
1、 讓矩形圖形顯示文本
在InitEntity()方法中添加下面代碼
protected override void InitEntity() { …… Text = "順序圖形"; Font = new Font("宋體", 10); …… }
在Paint()方法中添加畫出文本的代碼
public override void Paint(Graphics g) { …… if (!string.IsNullOrEmpty(Text)) g.DrawString(Text,this.Font, this.TextBrush,System.Drawing.RectangleF.Inflate(Rectangle,0,-2)); }
下面是效果圖
2、 實現雙擊圖形修改圖形中的文本
這個功能也是畫圖軟件常見的功能,我們需要在Shape的鼠標雙擊事件中創建一個TextBox控件,然后將圖形的文本傳給TextBox控件,同時添加相應TextBox的LostFocus事件的代碼,使TextBox消失
在InitEntity()方法中添加掛鈎OnMouseDown事件的代碼
protected override void InitEntity() { …… OnMouseDown += SequenceShape_OnMouseDown; }
然后在SequenceShape_OnMouseDown中生成TextBox控件並顯示
private void SequenceShape_OnMouseDown(object sender, MouseEventArgs e) { if (e.Clicks == 2 && e.Button == MouseButtons.Left) { if (m_tb == null) { m_tb = new TextBox(); } m_tb.Location = System.Drawing.Point.Round(Rectangle.Location); m_tb.Width = (int) Rectangle.Width; m_tb.Height = (int)Rectangle.Height; m_tb.BackColor = ShapeColor; m_tb.Multiline = true; m_tb.Text = Text; m_tb.SelectionLength = Text.Length; m_tb.LostFocus += T_tb_LostFocus; (Site as Control).Controls.Add(m_tb); m_tb.Show(); m_tb.Focus(); m_tb.ScrollToCaret(); } } private void T_tb_LostFocus(object sender, EventArgs e) { Text = m_tb.Text; m_tb.Hide(); (Site as Control).Controls.Remove(m_tb); }
Shape.Site就是GraphControl對象在Shape中的引用,在調用GraphControl.AddShape()時會設置Shape.Site。只是在Shape中的Site類型是IGraphSite類型,而GraphControl對象則是繼承了System.Windows.Forms .ScrollableControl, IGraphSite接口和 IGraphLayout接口
另外要說明的一點是,在NetronGraphLib中,Shape對象的OnMouseDown是通過GraphControl. OnMouseDown來調用的,而在NetronGraphLib原來的設計中,鼠標雙擊事件被GraphControl. OnMouseDown檢測到后,會顯示圖形對象的屬性,然而並沒有繼續調用Shape.OnMouseDown,所以Shape永遠接收不到鼠標雙擊事件。而筆者在所附代碼里修復了這個問題,將鼠標雙擊事件繼續傳遞給Shape
3、 Shape類的一些常用方法和數據成員
- 位置信息:X、Y、Location、Left、Right、Width、Height
- Rectangle:包含Shape的矩形框。要重設Shape的位置,需要重新生成一個Rectangle對象賦值給它,而不能通過改變Rectangle的屬性來重設Shape的位置。
- Location:Shape的左上角坐標點
- Abstract:可以通過它訪問到GraphControl上的所有其他Shape和Connection
- Connectors:Shape對象上的所有連接點集合
- IsSelected:用來判斷Shape是否處於選中狀態
- Site:對GraphControl畫布對象的引用
- Tracker:ShapeTracker類型的對象,代表的是當Shape被選中的時候在Shape周圍畫出的表示選中狀態的選中框。這個對象的生成是在IsSelected屬性里設置的。所以要知道Shape對象有沒有被選中,只要查詢IsSelected==True或Tracker!=null都可以
- ShapeMenu():用來返回圖形對象的右鍵菜單
五、 連接圖形:Connector和Connection
有了Shape圖形對象后,我們要在圖形對象之間畫連接線。
1、 添加連接點
首先,我們要給圖形對象添加連接點Connector
給SequenceShape添加下面的Connector數據成員
private Connector m_leftConnector; private Connector m_rightConnector;
在InitEntity()方法中添加對Connector數據成員的初始化代碼
protected override void InitEntity() { …… m_leftConnector = new Connector(this,"Left",true); Connectors.Add(m_leftConnector); m_rightConnector = new Connector(this, "Right", true); Connectors.Add(m_rightConnector); …… }
Connector構造函數的第一個參數是Shape對象,代表的Connector對象依附的圖形對象
第二個參數是Connector的名字,這個名字比較重要,當你要從其它對象訪問該Connector時,就可以用SequenceShape.Connectors[“Left”]來訪問到m_leftConnector了
第三個參數表示是否允許Connector有多個連接
2、 重寫Shape的ConnectionPoint方法:
我們還要重寫Shape的ConnectionPoint方法,返回每一個Connector的具體坐標。因為當你在代碼中用Connector.Location查詢Connector的坐標時,它就是通過查詢自己所依附的Shape的ConnectionPoint方法來返回自己的坐標值的。通過Connector.BelongsTo可以得到Connector所依附的Shape對象。
public override PointF ConnectionPoint(Connector c) { if (c == m_leftConnector) { return new PointF(Rectangle.Left, Rectangle.Top + Rectangle.Height / 2); } if (c == m_rightConnector) { return new PointF(Rectangle.Right, Rectangle.Top + Rectangle.Height / 2); } return new PointF(0, 0); }
這時我們再運行程序,把鼠標移到Shape上,就可以看到Shape已經有了左右兩個連接點;當鼠標移動到連接點上方時,鼠標會變成一個小的綠色方塊,表示現在可以通過按下鼠標並拖動來畫一條連接線Connection。這些功能都是通過GraphControl的OnMouseDown, OnMouseMove, OnMouseUp里的代碼自動實現的。我們在后續篇章中還要討論GraphControl里關於處理鼠標事件的代碼。
3、 Connector的常用屬性和方法
- ConnectionGrip():返回一個矩形對象,代表了Connector四周的一個正方形小塊,當鼠標移動到這個方塊內時,鼠標會變成綠色小方塊,表示此時按下鼠標可以拖動出連接線
- AllowNewConnectionsFrom:false表示該連接點只能接受從其它連接點拖動過來的連接線,而不能從該連接點拖出一條連接線
- AllowNewConnectionsTo:false時只能拖出不能拖入連接線
- BelongsTo:Connector所依附的Shape對象
- Connections:所有和該Connector連接的Connection集合
- ConnectorLocation、ConnectionShift、AdjacentPoint:這三個屬性決定了NetronGraphLib提供的Connection將如何畫出。
- ConnectorLocation屬性可以是East, South, West, North, Omni和Unknown。
AdjacentPoint屬性表示的是Connector向着Shape圖形對象外延伸ConnectionShift距離的一個點。如果ConnectionLocation是North,則AdjacentPoint是從Connector所在坐標點向正上方延伸出的一個點;如果是East則是向右方延伸出的一個點。如果是Omni則忽略ConnecionShift,AdjacentPoint就是Connector自身所在的坐標點。
在用系統提供的Connection連接圖形對象時,Connection的Paint方法先從From Connector所在坐標點向AdjacentPoint畫一條直線,然后再畫直線到To Connector的AdjacentPoint,最后從AdjacentPoint再畫直線到To Connector的坐標點。
六、 自定義Connection類
如果Connection定義的畫法不能滿足你的需求,你就需要設計自己的Connection對象。但是NetronGraphLib並不能很好的支持自定義的Connection,在NetronGraphLib的代碼里,是在Connection. LinePath屬性的設置代碼中,給Connection.ConnectionPainter和Connection.Tracker設置不同的對象,從而決定如何畫出Connection。可是如果你想加入自己的Painter和Tracker,就必須修改Connection.LinePath屬性的設置方法。這恐怕不是一個好的解決方法。
所以為了能夠方便的加入自己設計的Connection,筆者修改了GraphControl.OnMouseDown里的代碼。因為在這段代碼里設定了當用戶在一個Connector上按下鼠標時,會生成一個Connection對象。
我做的修改如下:
在NetronGraphLib中定義一個IConnectable接口,這個接口定義了一個方法Connection CreateConnection(Connector connector),用來根據鼠標點擊的Connector以及Connector依附的Shape來產生一個Connection對象。
然后在OnMouseDown里檢查當前鼠標按下的Connector依附的Shape是否實現了IConnectable接口,如果實現,則調用CreateConnection方法來返回一個Connection對象。因為通常在用戶按下鼠標點擊Connector的時候,就可以根據點擊的Shape和Shape上具體哪個Connector返回不同的Connection了。
所以現在如果要實現你自己的Connetion,你需要做的就是
- 從Netron.GraphLib.Connection類繼承並設計你自己的Connection類
- 使你自己設計的Shape類繼承IConnectable接口並實現CreateConnection方法,在CreateConnection方法中根據傳入的Connector返回你自己的Connection類
1、 實現自定義連接線
我們打算實現如下圖所示的連接線,當圖形右側的連接點拖動到下一個圖形左側連接點時,連接線始終呈現為一條水平線和垂直線;而當從圖形左側的連接點拖動到下一個圖形左側的連接點時,連接線始終呈現為一條垂直線
我們先定義自己的連接線類
public class FlowChartConnection : Connection{}
然后我們覆寫Connection類的GetConnectionPoints方法,該方法返回Connection連接線上的多個點,Connection.Paint方法調用GetConnectionPoints來畫出一條折線
public override PointF[] GetConnectionPoints() { PointF[] points; PointF t_to = PointF.Empty; PointF t_from = Point.Empty; if (From == null) return null; if (From?.Name == "Left" && To?.Name == "Left") { //如果是左側點連接左側點,則返回垂直線 //To?.Name中的?表示會先檢查To是否為null points = new PointF[2]; points[0] = From.Location; points[1] = new PointF(From.Location.X, To.Location.Y); return points; } points = new PointF[3]; t_from = From.Location; t_to = (To != null) ? To.Location : ToPoint; points[0] = t_from; points[1] = new PointF(t_to.X, t_from.Y); points[2] = t_to; return points; }
要說明的幾點:
a) From、To都是Connector類型,不管用戶是從左到右還是從右到左拖動,From指的都是鼠標按下開始拖動出Connection連接線時的Connector,To都是放開鼠標時所在的Connector;
b) 在拖動的過程中,To是空值null,這時ToPoint屬性指示了拖動過程中鼠標所在位置
c) 在NetronGraphLib中有個Bug,在Connection的構造函數里,調用了InitConnection()方法,而InitConnection方法中生成了DefaultPainter對象,DefaultPainter構造函數調用基類ConnectionPainter的構造函數時調用了Connection .GetConnectionPoints()方法。而在此時Connection.From還沒有賦值。所以如果你覆寫GetConnectionPoints時沒有檢查From是否為空值,系統就會拋出異常。我去掉了ConnectionPainter構造函數調用GetConnectionPoints()方法的代碼,這樣可以保證GetConnectionPoints被調用時From不為空值
如果你還有別的需求,可以進一步覆寫Paint方法,自己編寫Connection的畫法
接下來我們要使SequenceShape繼承IConnectable接口並實現CreateConnection方法返回我們自定義的FlowChartConnection
- 添加Using:
using Netron.GraphLib.Interfaces;
- 添加IConnectable接口繼承:
public class SequenceShape : Shape, IConnectable
- 實現CreateConnection方法:
public Connection CreateConnection(Connector connector) { return new FlowChartConnection(); }
當然你也可以根據傳入的connector參數決定返回不同的Connection對象
2、 Connection的常用屬性和方法
- Insert():這個方法以兩個Connector作為參數,將Connection對象加入GraphControl.Connections,以及From Connector和To Connector的Connections集合中。如果你是在代碼中自己生成兩個連接點之間的Connection,通常你需要做的就是如下兩行代碼
Connection connection = new Connection(); connection.Insert(fromConnector, toConnector);
- PaintLabel():我們有時需要覆寫這個方法來為連接線加上文字顯示
- Remove():這個方法不是NetronGraphLib原有的。因為如果你需要自己在代碼中生成Connection對象時,通常也會遇到需要刪除Connection對象的時候,Connection對象自帶一個Delete()方法可以起到這個功能,但是不幸的是這個方法是從基類繼承過來的私有方法,所以我加了這個Remove方法來調用Delete方法。Delete方法從From Connector和To Connector以及GraphControl的Connections集合中移除該Connection
請繼續閱讀Netron開發快速上手(二):Netron序列化