
一、前言
上节课已经抽象出来了形状和连线,但是没解决程序复用的问题:现在所有的代码是写在窗口中的,如果想在其它程序想实现流程图,只能重新写代码或者复制粘贴代码,没办法简单复用,而且也无法保证功能的完整性和及时性。所以我们本节就来看一下,如何独立出一张“画布”控件,来解决此问题。
相信看完的你,一定会有所收获!
本文地址:https://www.cnblogs.com/lesliexin/p/18985184
二、先看效果
并没有什么特别的效果可看,主要是演示我们独立出来的“画布”控件功能完整性。
我们下面就来讲解如何实现。
三、创建类库及自定义控件
就像上节我们将抽象出来的形状和连线类都放到独立的类库中一样,我们同样将画布控件放到一个单独的类库中:
然后我们添加一个“自定义控件”,注意不是“用户控件”:
我们给画布起个名称:FCCanvas,就是FlowChartCanvas的简写。
这里为了方便编写教程,我们在后面增加V1、V2,用来区分。
创建好的结构如下:
四、移植代码到自定义控件
现在有了单独的画布控件,我们就将之前在程序中实现代码移植过来,我们在FCCanvasV1上右键->查看代码,进入后台代码。
1,双缓冲
首要的,我们在构造函数中添加开启双缓冲的代码:
2,重写OnPaint
有过自定义控件的读者会知道,自定义控件就相当于一个“画布”,控制所展示的内容全是我们用代码“画”上去的,而绘制的方法就是在OnPaint方法中。
我们将之前代码里的DrawAll方法里的代码复制进来:
因为已经在OnPaint方法中,所以不再需要传入Graphics对象,直接使用e.Graphics即可,此即当前控件的对象。
2,重写鼠标相关事件
我们之前是在panel控件上操作,现在我们是在整个控件上操作,所以我们需要重写下相关事件,这些可重写的方法一般都是以On开头,如:OnMouseDown等。
2.1,OnMouseDown
我们将之前代码中的MouseDown中的代码拷贝进来:
这里的变化有三点:
一是提示文本我们这里改为了触发事件的方式,我们定义了一个事件,通知订阅者使用,至于是否显示提示内容及如何显示提示内容我们控件不作管理。
二是添加连线时,连线的颜色不再是随机生成,也是触发一个事件,由调用方决定连线的颜色是什么:
为了防止调用方不订阅此事件,我们会默认连线颜色为黑色。
三是发起重新绘制的方式不一样了,之前是直接调用绘制所有方法DrawAll:
而现在我们也没有了DrawAll方法,DrawAll的实现被我们移植到了OnPaint方法中。所以我们直接调用控件自带的无效方法Invalidate(),来使窗口重绘:
内部逻辑简单而言就是:当我们调用Invalidate()后,系统会自动调用OnPaint方法,进而重绘。而这也是自定义控件的基础逻辑。
2.2,OnMouseMove
同样的,我们将之前代码中的MouseMove中的代码拷贝进来:
可以看到几乎一样,也是最后一步改为调用无效方法Invalidate(),来使窗口重绘。
2.3,OnMouseUp
同理:
3,形状集合、连线集合等定义
我们现在基本的实现都有了,那么就把之前的一些私有变量拿过来,像形状集合、连线集合、连线状态等:
4,公共方法
现在整个FCCanvasV1内部已经自洽了,但是有个问题:如何与外部交互?如何添加形状?
我们现在就来开放一些公共方法,来实现与外部的交互。
4.1,添加形状方法
最核心的也是最基本的功能,就是添加形状的方法:
我们的方法支持一次添加多个形状,而且添加形状时会自动判断是否已经添加过。
注:我们看到方法名带了个前缀:FCC_,这样写看似不优雅,但是对于后续的开发和使用却有很大的便利,我们统一前缀,这样在写代码时敲入前缀就能看到所有的方法,而不需要再去思考,特别是对于其它人而言,不熟悉的情况下只能去看类的定义里有哪些方法才能去调用,而不像现在这样这么方便。这是经验之谈,当然加不加前缀完全是个人自由,想怎么写就怎么写,并不会影响功能。
4.2,清空方法
我们添加一个清空当前画布中所有形状和连线的方法,用于复原:
4.3,刷新方法
我们虽然可以通过调用控件的Invalidate()方法来刷新,但是不够直观,我们直接将其封装为一个方法:
4.4,添加连线和中止连线方法
我们目前的程序支持添加连线和中止添加连线,所以我们同样开放出这两个方法:
好了,到此为止,我们的V1版画布就已经完成了,可以实现之前课程里的所有效果了。下面是完整代码,大家可查看和尝试:
点击查看代码
using Elements; using Elements.Links; using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace FlowChartCanvas { //注:随文说明:不是【用户控件】,直接在类继承CONTROL /// <summary> /// 流程图画布 /// </summary> public class FCCanvasV1:Control { public FCCanvasV1() { SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer, true); } #region 公共事件 /// <summary> /// 连线时的状态提示 /// </summary> public event Action<string> FCC_LinkState; /// <summary> /// 添加连线时,连线的颜色 /// </summary> public event Func<Color> FCC_LinkColor; #endregion #region 公共属性 #endregion #region 公共方法 //注:文章中说明,为了方便查看和演示有哪些方法和属性,所以固定开头,可依喜好不要此开头 /// <summary> /// 向当前画布中添加形状 /// </summary> /// <param name="sps"></param> public void FCC_AddShapes(List<ShapeBase> sps) { if (sps == null || sps.Count == 0) return; foreach (var item in sps) { //根据ID去重 if (!_shapes.Any(a => a.Id == item.Id)) { _shapes.Add(item); } } //令当前控件失效以重绘 Invalidate(); } /// <summary> /// 清空画布中的形状和连线 /// </summary> public void FCC_Clear() { _shapes.Clear(); _links.Clear(); Invalidate(); } /// <summary> /// 刷新当前画布 /// </summary> public void FCC_Refresh() { Invalidate(); } /// <summary> /// 开始连线 /// </summary> public void FCC_StartLink() { _isAddLink = true; _selectedStartShape = null; _selectedEndShape = null; FCC_LinkState?.Invoke("请点击第1个形状"); } /// <summary> /// 中止/停止连线 /// </summary> public void FCC_StopLink() { _isAddLink = false; _selectedStartShape = null; _selectedEndShape = null; FCC_LinkState?.Invoke(""); Invalidate(); } #endregion #region 私有属性 /// <summary> /// 形状集合 /// </summary> List<ShapeBase> _shapes = new List<ShapeBase>(); /// <summary> /// 连线集合 /// </summary> List<LinkBase> _links = new List<LinkBase>(); /// <summary> /// 当前是否有鼠标按下,且有矩形被选中 /// </summary> bool _isMouseDown = false; /// <summary> /// 最后一次鼠标的位置 /// </summary> Point _lastMouseLocation = Point.Empty; /// <summary> /// 当前被鼠标选中的矩形 /// </summary> ShapeBase _selectedShape = null; /// <summary> /// 添加连线时选中的第一个形状 /// </summary> ShapeBase _selectedStartShape = null; /// <summary> /// 添加连线时选中的第一个形状 /// </summary> ShapeBase _selectedEndShape = null; /// <summary> /// 是否正添加连线 /// </summary> bool _isAddLink = false; Bitmap _bmp; #endregion #region 私有方法 #endregion #region 重写方法 protected override void OnPaint(PaintEventArgs e) { _bmp = new Bitmap(Width, Height); var g = Graphics.FromImage(_bmp); //设置显示质量 g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; g.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality; g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality; g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; g.Clear(BackColor); //绘制所有形状 foreach (var sp in _shapes) { sp.Draw(g); } //绘制所有连线 foreach (var ln in _links) { ln.Draw(g); } //绘制内存绘图到控件上 e.Graphics.DrawImage(_bmp, new PointF(0, 0)); //释放资源 g.Dispose(); base.OnPaint(e); } protected override void OnMouseDown(MouseEventArgs e) { //当鼠标按下时 //取最上方的形状 var sp = _shapes.FindLast(a => a.Rect.Contains(e.Location)); if (!_isAddLink) { //当前没有处理连线状态 if (sp != null) { //设置状态及选中矩形 _isMouseDown = true; _lastMouseLocation = e.Location; _selectedShape = sp; } } else { //正在添加连线 if (_selectedStartShape == null) { //证明没有矩形和圆形被选中则设置开始形状 if (sp != null) { //设置开始形状 _selectedStartShape = sp; } FCC_LinkState?.Invoke("请点击第2个形状"); } else { //判断第2个形状是否是第1个形状 if (sp != null) { //判断当前选中的矩形是否是第1步选中的矩形 if (_selectedStartShape.Id == sp.Id) { FCC_LinkState?.Invoke("不可选择同一个形状,请重新点击第2个形状"); return; } } if (sp != null) { //设置结束形状 _selectedEndShape = sp; } else { return; } //两个形状都设置了,便添加一条新连线 _links.Add(new LineLink() { Id = "连线" + Guid.NewGuid().ToString(),//这里就不能用数量了,防止重复 BackgroundColor = FCC_LinkColor?.Invoke() ?? Color.Black, StartShape = _selectedStartShape, EndShape = _selectedEndShape, }); //两个形状都已选择,结束添加连线状态 _isAddLink = false; FCC_LinkState?.Invoke(""); //令当前控件失效以重绘 Invalidate(); } } base.OnMouseDown(e); } protected override void OnMouseMove(MouseEventArgs e) { //当鼠标移动时 //如果处于添加连线时,则不移动形状 if (_isAddLink) return; if (_isMouseDown) { //当且仅当:有鼠标按下且有矩形被选中时,才进行后续操作 //改变选中矩形的位置信息,随着鼠标移动而移动 //计算鼠标位置变化信息 var moveX = e.Location.X - _lastMouseLocation.X; var moveY = e.Location.Y - _lastMouseLocation.Y; //将选中形状的位置进行同样的变化 var oldXY = _selectedShape.Rect.Location; oldXY.Offset(moveX, moveY); _selectedShape.Rect = new Rectangle(oldXY, _selectedShape.Rect.Size); //记录当前鼠标位置 _lastMouseLocation.Offset(moveX, moveY); //令当前控件失效以重绘 Invalidate(); } base.OnMouseMove(e); } protected override void OnMouseUp(MouseEventArgs e) { //当鼠标松开时 if (_isMouseDown) { //当且仅当:有鼠标按下且有矩形被选中时,才进行后续操作 //重置相关记录信息 _isMouseDown = false; _lastMouseLocation = Point.Empty; _selectedShape = null; } base.OnMouseUp(e); } #endregion } }
五、使用画布控件
我们的画布控件已经完成,下面就来看一下如何去使用它。
1,引用画布类库
因为我们的画布在独立的类库中,所以我们先引用类库:
2,添加画布控件
首先,界面与之前并无变化:
不过我们不再在中间的panel中绘制,而是将我们的画布控件添加到panel当中。我们在构造函数中使用代码的方式添加控件:
当然也可以能通过工具箱拖动添加,不过不太建议,特别当自定义控件复杂的情况下,代码的方式更好控制和编写。
我们订阅两个事件,分别用来设置状态文本和获取颜色:
3,按钮调用画布方法
现在这些按钮不再自行实现了,而是直接调用画布的对应方法即可。
3.1,添加矩形按钮
3.2,添加圆形按钮
3.3,开始连线
3.4,中止连线
好了,到此为止我们就已经实现了之前课程里的效果。
下面是完整代码,大家可自己查看和编译:
点击查看代码
using Elements; using Elements.Links; using Elements.Shapes; using FlowChartCanvas; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace FlowChartDemo { public partial class FormDemo06V1 : FormBase { public FormDemo06V1() { InitializeComponent(); DemoTitle = "第08节随课Demo Part1"; DemoNote = "效果:加载画布、并添加形状、连线等。"; //添加画布控件 _fcc = new FCCanvasV1(); _fcc.FCC_LinkColor += _fcc_FCC_LinkColor; _fcc.FCC_LinkState += _fcc_FCC_LinkState; _fcc.Dock = DockStyle.Fill; panel1.Controls.Add(_fcc); } private void _fcc_FCC_LinkState(string obj) { toolStripStatusLabel1.Text = obj; } private Color _fcc_FCC_LinkColor() { return GetColor(_linkColorIndex++); } FCCanvasV1 _fcc; /// <summary> /// 形状颜色序号 /// </summary> int _shapeColorIndex = 0; /// <summary> /// 连线颜色序号 /// </summary> int _linkColorIndex = 0; /// <summary> /// 获取不同的背景颜色 /// </summary> /// <param name="i"></param> /// <returns></returns> Color GetColor(int i) { switch (i) { case 0: return Color.Red; case 1: return Color.Green; case 2: return Color.Blue; case 3: return Color.Orange; case 4: return Color.Purple; default: return Color.Red; } } private void toolStripButton1_Click(object sender, EventArgs e) { var rs = new RectShape() { Id = "矩形" + Guid.NewGuid().ToString(),//这里就不能用数量了,防止重复 Rect = new Rectangle() { X = 50, Y = 50, Width = 100, Height = 100, }, FontColor = Color.White, BackgroundColor = GetColor(_shapeColorIndex++), Text = "矩形" + _shapeColorIndex, TextFont = Font, }; _fcc.FCC_AddShapes(new List<ShapeBase>() { rs }); _fcc.FCC_Refresh(); } private void toolStripButton4_Click(object sender, EventArgs e) { var rs = new EllipseShape() { Id = "圆形" + Guid.NewGuid().ToString(),//这里就不能用数量了,防止重复 Rect = new Rectangle() { X = 50, Y = 50, Width = 100, Height = 100, }, FontColor = Color.White, BackgroundColor = GetColor(_shapeColorIndex++), Text = "圆形" + _shapeColorIndex, TextFont = Font, }; _fcc.FCC_AddShapes(new List<ShapeBase>() { rs }); _fcc.FCC_Refresh(); } private void toolStripButton2_Click(object sender, EventArgs e) { _fcc.FCC_StartLink(); } private void toolStripButton3_Click(object sender, EventArgs e) { _fcc.FCC_StopLink(); } } }
六、结语
可以看到我们更多的是使用,而不是编写。有了我们自定义的画布控件,完全不需要过多的考虑,只需要调用画布的方法就行了,复用性很强。
现在所有的角色都已登场,后面就要在这个地基上添砖加瓦,构造我们自己的流程图。
我们下节课就来添加一些其它的形状,如:菱形、平行四边形、圆角矩形等,到时候会发现原来这么的顺理成章,敬请期待。
感谢大家的观看,本人水平有限,文章不足之处欢迎大家评论指正。
-[END]-