版权所有,引用请注明出处:<<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序列化