找回密码
 立即注册
首页 业界区 业界 [原创]《C#高级GDI+实战:从零开发一个流程图》第07章: ...

[原创]《C#高级GDI+实战:从零开发一个流程图》第07章:来吧,自定义“画布”控件!

刃减胸 2025-9-26 10:55:44
一、前言

上节课已经抽象出来了形状和连线,但是没解决程序复用的问题:现在所有的代码是写在窗口中的,如果想在其它程序想实现流程图,只能重新写代码或者复制粘贴代码,没办法简单复用,而且也无法保证功能的完整性和及时性。所以我们本节就来看一下,如何独立出一张“画布”控件,来解决此问题。
相信看完的你,一定会有所收获!
本文地址:https://www.cnblogs.com/lesliexin/p/18985184
二、先看效果

并没有什么特别的效果可看,主要是演示我们独立出来的“画布”控件功能完整性。

我们下面就来讲解如何实现。
三、创建类库及自定义控件

就像上节我们将抽象出来的形状和连线类都放到独立的类库中一样,我们同样将画布控件放到一个单独的类库中:
1.png

然后我们添加一个“自定义控件”,注意不是“用户控件”:
2.png

我们给画布起个名称:FCCanvas,就是FlowChartCanvas的简写。
这里为了方便编写教程,我们在后面增加V1、V2,用来区分。
创建好的结构如下:
3.png

四、移植代码到自定义控件

现在有了单独的画布控件,我们就将之前在程序中实现代码移植过来,我们在FCCanvasV1上右键->查看代码,进入后台代码。
1,双缓冲

首要的,我们在构造函数中添加开启双缓冲的代码:
4.png

2,重写OnPaint

有过自定义控件的读者会知道,自定义控件就相当于一个“画布”,控制所展示的内容全是我们用代码“画”上去的,而绘制的方法就是在OnPaint方法中。
我们将之前代码里的DrawAll方法里的代码复制进来:
5.png

因为已经在OnPaint方法中,所以不再需要传入Graphics对象,直接使用e.Graphics即可,此即当前控件的对象。
2,重写鼠标相关事件

我们之前是在panel控件上操作,现在我们是在整个控件上操作,所以我们需要重写下相关事件,这些可重写的方法一般都是以On开头,如:OnMouseDown等。
2.1,OnMouseDown

我们将之前代码中的MouseDown中的代码拷贝进来:
6.png

这里的变化有三点:
一是提示文本我们这里改为了触发事件的方式,我们定义了一个事件,通知订阅者使用,至于是否显示提示内容及如何显示提示内容我们控件不作管理。
7.png

8.png

二是添加连线时,连线的颜色不再是随机生成,也是触发一个事件,由调用方决定连线的颜色是什么:
9.png

10.png

为了防止调用方不订阅此事件,我们会默认连线颜色为黑色。
三是发起重新绘制的方式不一样了,之前是直接调用绘制所有方法DrawAll:
11.png

而现在我们也没有了DrawAll方法,DrawAll的实现被我们移植到了OnPaint方法中。所以我们直接调用控件自带的无效方法Invalidate(),来使窗口重绘:
12.png

内部逻辑简单而言就是:当我们调用Invalidate()后,系统会自动调用OnPaint方法,进而重绘。而这也是自定义控件的基础逻辑。
2.2,OnMouseMove

同样的,我们将之前代码中的MouseMove中的代码拷贝进来:
13.png

可以看到几乎一样,也是最后一步改为调用无效方法Invalidate(),来使窗口重绘。
2.3,OnMouseUp

同理:
14.png

3,形状集合、连线集合等定义

我们现在基本的实现都有了,那么就把之前的一些私有变量拿过来,像形状集合、连线集合、连线状态等:
15.png

4,公共方法

现在整个FCCanvasV1内部已经自洽了,但是有个问题:如何与外部交互?如何添加形状?
我们现在就来开放一些公共方法,来实现与外部的交互。
4.1,添加形状方法

最核心的也是最基本的功能,就是添加形状的方法:
16.png

我们的方法支持一次添加多个形状,而且添加形状时会自动判断是否已经添加过。
注:我们看到方法名带了个前缀:FCC_,这样写看似不优雅,但是对于后续的开发和使用却有很大的便利,我们统一前缀,这样在写代码时敲入前缀就能看到所有的方法,而不需要再去思考,特别是对于其它人而言,不熟悉的情况下只能去看类的定义里有哪些方法才能去调用,而不像现在这样这么方便。这是经验之谈,当然加不加前缀完全是个人自由,想怎么写就怎么写,并不会影响功能。
17.png

4.2,清空方法

我们添加一个清空当前画布中所有形状和连线的方法,用于复原:
18.png

4.3,刷新方法

我们虽然可以通过调用控件的Invalidate()方法来刷新,但是不够直观,我们直接将其封装为一个方法:
19.png

4.4,添加连线和中止连线方法

我们目前的程序支持添加连线和中止添加连线,所以我们同样开放出这两个方法:
20.png

好了,到此为止,我们的V1版画布就已经完成了,可以实现之前课程里的所有效果了。下面是完整代码,大家可查看和尝试:
点击查看代码
  1. using Elements;
  2. using Elements.Links;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Drawing;
  6. using System.Linq;
  7. using System.Text;
  8. using System.Threading.Tasks;
  9. using System.Windows.Forms;
  10. namespace FlowChartCanvas
  11. {
  12.     //注:随文说明:不是【用户控件】,直接在类继承CONTROL
  13.     /// <summary>
  14.     /// 流程图画布
  15.     /// </summary>
  16.     public class FCCanvasV1:Control
  17.     {
  18.         public FCCanvasV1()
  19.         {
  20.             SetStyle(ControlStyles.AllPaintingInWmPaint |
  21.                 ControlStyles.UserPaint |
  22.                 ControlStyles.OptimizedDoubleBuffer, true);
  23.         }
  24.         #region 公共事件
  25.         /// <summary>
  26.         /// 连线时的状态提示
  27.         /// </summary>
  28.         public event Action<string> FCC_LinkState;
  29.         /// <summary>
  30.         /// 添加连线时,连线的颜色
  31.         /// </summary>
  32.         public event Func<Color> FCC_LinkColor;
  33.         #endregion
  34.         #region 公共属性
  35.         #endregion
  36.         #region 公共方法
  37.         //注:文章中说明,为了方便查看和演示有哪些方法和属性,所以固定开头,可依喜好不要此开头
  38.         /// <summary>
  39.         /// 向当前画布中添加形状
  40.         /// </summary>
  41.         /// <param name="sps"></param>
  42.         public void FCC_AddShapes(List<ShapeBase> sps)
  43.         {
  44.             if (sps == null || sps.Count == 0) return;
  45.             foreach (var item in sps)
  46.             {
  47.                 //根据ID去重
  48.                 if (!_shapes.Any(a => a.Id == item.Id))
  49.                 {
  50.                     _shapes.Add(item);
  51.                 }
  52.             }
  53.             //令当前控件失效以重绘
  54.             Invalidate();
  55.         }
  56.         
  57.         /// <summary>
  58.         /// 清空画布中的形状和连线
  59.         /// </summary>
  60.         public void FCC_Clear()
  61.         {
  62.             _shapes.Clear();
  63.             _links.Clear();
  64.             Invalidate();
  65.         }
  66.         /// <summary>
  67.         /// 刷新当前画布
  68.         /// </summary>
  69.         public void FCC_Refresh()
  70.         {
  71.             Invalidate();
  72.         }
  73.         /// <summary>
  74.         /// 开始连线
  75.         /// </summary>
  76.         public void FCC_StartLink()
  77.         {
  78.             _isAddLink = true;
  79.             _selectedStartShape = null;
  80.             _selectedEndShape = null;
  81.             FCC_LinkState?.Invoke("请点击第1个形状");
  82.         }
  83.         /// <summary>
  84.         /// 中止/停止连线
  85.         /// </summary>
  86.         public void FCC_StopLink()
  87.         {
  88.             _isAddLink = false;
  89.             _selectedStartShape = null;
  90.             _selectedEndShape = null;
  91.             FCC_LinkState?.Invoke("");
  92.             Invalidate();
  93.         }
  94.         #endregion
  95.         #region 私有属性
  96.         /// <summary>
  97.         /// 形状集合
  98.         /// </summary>
  99.         List<ShapeBase> _shapes = new List<ShapeBase>();
  100.         /// <summary>
  101.         /// 连线集合
  102.         /// </summary>
  103.         List<LinkBase> _links = new List<LinkBase>();
  104.         /// <summary>
  105.         /// 当前是否有鼠标按下,且有矩形被选中
  106.         /// </summary>
  107.         bool _isMouseDown = false;
  108.         /// <summary>
  109.         /// 最后一次鼠标的位置
  110.         /// </summary>
  111.         Point _lastMouseLocation = Point.Empty;
  112.         /// <summary>
  113.         /// 当前被鼠标选中的矩形
  114.         /// </summary>
  115.         ShapeBase _selectedShape = null;
  116.         /// <summary>
  117.         /// 添加连线时选中的第一个形状
  118.         /// </summary>
  119.         ShapeBase _selectedStartShape = null;
  120.         /// <summary>
  121.         /// 添加连线时选中的第一个形状
  122.         /// </summary>
  123.         ShapeBase _selectedEndShape = null;
  124.         /// <summary>
  125.         /// 是否正添加连线
  126.         /// </summary>
  127.         bool _isAddLink = false;
  128.         Bitmap _bmp;
  129.         #endregion
  130.         #region 私有方法
  131.         #endregion
  132.         #region 重写方法
  133.         protected override void OnPaint(PaintEventArgs e)
  134.         {
  135.             _bmp = new Bitmap(Width, Height);
  136.             var g = Graphics.FromImage(_bmp);
  137.             //设置显示质量
  138.             g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
  139.             g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
  140.             g.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
  141.             g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
  142.             g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
  143.             g.Clear(BackColor);
  144.             //绘制所有形状
  145.             foreach (var sp in _shapes)
  146.             {
  147.                 sp.Draw(g);
  148.             }
  149.             //绘制所有连线
  150.             foreach (var ln in _links)
  151.             {
  152.                 ln.Draw(g);
  153.             }
  154.             //绘制内存绘图到控件上
  155.             e.Graphics.DrawImage(_bmp, new PointF(0, 0));
  156.             //释放资源
  157.             g.Dispose();
  158.             base.OnPaint(e);
  159.         }
  160.         protected override void OnMouseDown(MouseEventArgs e)
  161.         { //当鼠标按下时
  162.             //取最上方的形状
  163.             var sp = _shapes.FindLast(a => a.Rect.Contains(e.Location));
  164.             if (!_isAddLink)
  165.             {
  166.                 //当前没有处理连线状态
  167.                 if (sp != null)
  168.                 {
  169.                     //设置状态及选中矩形
  170.                     _isMouseDown = true;
  171.                     _lastMouseLocation = e.Location;
  172.                     _selectedShape = sp;
  173.                 }
  174.             }
  175.             else
  176.             {
  177.                 //正在添加连线
  178.                 if (_selectedStartShape == null)
  179.                 {
  180.                     //证明没有矩形和圆形被选中则设置开始形状
  181.                     if (sp != null)
  182.                     {
  183.                         //设置开始形状
  184.                         _selectedStartShape = sp;
  185.                     }
  186.                     FCC_LinkState?.Invoke("请点击第2个形状");
  187.                 }
  188.                 else
  189.                 {
  190.                     //判断第2个形状是否是第1个形状
  191.                     if (sp != null)
  192.                     {
  193.                         //判断当前选中的矩形是否是第1步选中的矩形
  194.                         if (_selectedStartShape.Id == sp.Id)
  195.                         {
  196.                             FCC_LinkState?.Invoke("不可选择同一个形状,请重新点击第2个形状");
  197.                             return;
  198.                         }
  199.                     }
  200.                     if (sp != null)
  201.                     {
  202.                         //设置结束形状
  203.                         _selectedEndShape = sp;
  204.                     }
  205.                     else
  206.                     {
  207.                         return;
  208.                     }
  209.                     //两个形状都设置了,便添加一条新连线
  210.                     _links.Add(new LineLink()
  211.                     {
  212.                         Id = "连线" + Guid.NewGuid().ToString(),//这里就不能用数量了,防止重复
  213.                         BackgroundColor = FCC_LinkColor?.Invoke() ?? Color.Black,
  214.                         StartShape = _selectedStartShape,
  215.                         EndShape = _selectedEndShape,
  216.                     });
  217.                     //两个形状都已选择,结束添加连线状态
  218.                     _isAddLink = false;
  219.                     FCC_LinkState?.Invoke("");
  220.                     //令当前控件失效以重绘
  221.                     Invalidate();
  222.                 }
  223.             }
  224.             base.OnMouseDown(e);
  225.         }
  226.         protected override void OnMouseMove(MouseEventArgs e)
  227.         {  
  228.             //当鼠标移动时
  229.             //如果处于添加连线时,则不移动形状
  230.             if (_isAddLink) return;
  231.             if (_isMouseDown)
  232.             {
  233.                 //当且仅当:有鼠标按下且有矩形被选中时,才进行后续操作
  234.                 //改变选中矩形的位置信息,随着鼠标移动而移动
  235.                 //计算鼠标位置变化信息
  236.                 var moveX = e.Location.X - _lastMouseLocation.X;
  237.                 var moveY = e.Location.Y - _lastMouseLocation.Y;
  238.                 //将选中形状的位置进行同样的变化
  239.                 var oldXY = _selectedShape.Rect.Location;
  240.                 oldXY.Offset(moveX, moveY);
  241.                 _selectedShape.Rect = new Rectangle(oldXY, _selectedShape.Rect.Size);
  242.                 //记录当前鼠标位置
  243.                 _lastMouseLocation.Offset(moveX, moveY);
  244.                 //令当前控件失效以重绘
  245.                 Invalidate();
  246.             }
  247.             base.OnMouseMove(e);
  248.         }
  249.         protected override void OnMouseUp(MouseEventArgs e)
  250.         {
  251.             //当鼠标松开时
  252.             if (_isMouseDown)
  253.             {
  254.                 //当且仅当:有鼠标按下且有矩形被选中时,才进行后续操作
  255.                 //重置相关记录信息
  256.                 _isMouseDown = false;
  257.                 _lastMouseLocation = Point.Empty;
  258.                 _selectedShape = null;
  259.             }
  260.             base.OnMouseUp(e);
  261.         }
  262.         #endregion
  263.     }
  264. }
复制代码
五、使用画布控件

我们的画布控件已经完成,下面就来看一下如何去使用它。
1,引用画布类库

因为我们的画布在独立的类库中,所以我们先引用类库:
21.png

2,添加画布控件

首先,界面与之前并无变化:
22.png

不过我们不再在中间的panel中绘制,而是将我们的画布控件添加到panel当中。我们在构造函数中使用代码的方式添加控件:
23.png

当然也可以能通过工具箱拖动添加,不过不太建议,特别当自定义控件复杂的情况下,代码的方式更好控制和编写。
我们订阅两个事件,分别用来设置状态文本和获取颜色:
24.png

3,按钮调用画布方法

现在这些按钮不再自行实现了,而是直接调用画布的对应方法即可。
3.1,添加矩形按钮

25.png

3.2,添加圆形按钮

26.png

3.3,开始连线

27.png

3.4,中止连线

28.png

好了,到此为止我们就已经实现了之前课程里的效果。
下面是完整代码,大家可自己查看和编译:
点击查看代码
  1. using Elements;
  2. using Elements.Links;
  3. using Elements.Shapes;
  4. using FlowChartCanvas;
  5. using System;
  6. using System.Collections.Generic;
  7. using System.ComponentModel;
  8. using System.Data;
  9. using System.Drawing;
  10. using System.Linq;
  11. using System.Text;
  12. using System.Threading.Tasks;
  13. using System.Windows.Forms;
  14. namespace FlowChartDemo
  15. {
  16.     public partial class FormDemo06V1 : FormBase
  17.     {
  18.         public FormDemo06V1()
  19.         {
  20.             InitializeComponent();
  21.             DemoTitle = "第08节随课Demo  Part1";
  22.             DemoNote = "效果:加载画布、并添加形状、连线等。";
  23.             //添加画布控件
  24.             _fcc = new FCCanvasV1();
  25.             _fcc.FCC_LinkColor += _fcc_FCC_LinkColor;
  26.             _fcc.FCC_LinkState += _fcc_FCC_LinkState;
  27.             _fcc.Dock = DockStyle.Fill;
  28.             panel1.Controls.Add(_fcc);
  29.         }
  30.         private void _fcc_FCC_LinkState(string obj)
  31.         {
  32.             toolStripStatusLabel1.Text = obj;
  33.         }
  34.         private Color _fcc_FCC_LinkColor()
  35.         {
  36.             return GetColor(_linkColorIndex++);
  37.         }
  38.         FCCanvasV1 _fcc;
  39.         /// <summary>
  40.         /// 形状颜色序号
  41.         /// </summary>
  42.         int _shapeColorIndex = 0;
  43.         /// <summary>
  44.         /// 连线颜色序号
  45.         /// </summary>
  46.         int _linkColorIndex = 0;
  47.                
  48.         /// <summary>
  49.         /// 获取不同的背景颜色
  50.         /// </summary>
  51.         /// <param name="i"></param>
  52.         /// <returns></returns>
  53.         Color GetColor(int i)
  54.         {
  55.             switch (i)
  56.             {
  57.                 case 0: return Color.Red;
  58.                 case 1: return Color.Green;
  59.                 case 2: return Color.Blue;
  60.                 case 3: return Color.Orange;
  61.                 case 4: return Color.Purple;
  62.                 default: return Color.Red;
  63.             }
  64.         }
  65.         private void toolStripButton1_Click(object sender, EventArgs e)
  66.         {
  67.             var rs = new RectShape()
  68.             {
  69.                 Id = "矩形" + Guid.NewGuid().ToString(),//这里就不能用数量了,防止重复
  70.                 Rect = new Rectangle()
  71.                 {
  72.                     X = 50,
  73.                     Y = 50,
  74.                     Width = 100,
  75.                     Height = 100,
  76.                 },
  77.                 FontColor = Color.White,
  78.                 BackgroundColor = GetColor(_shapeColorIndex++),
  79.                 Text = "矩形" + _shapeColorIndex,
  80.                 TextFont = Font,
  81.             };
  82.             _fcc.FCC_AddShapes(new List<ShapeBase>() { rs });
  83.             _fcc.FCC_Refresh();
  84.         }
  85.         private void toolStripButton4_Click(object sender, EventArgs e)
  86.         {
  87.             var rs = new EllipseShape()
  88.             {
  89.                 Id = "圆形" + Guid.NewGuid().ToString(),//这里就不能用数量了,防止重复
  90.                 Rect = new Rectangle()
  91.                 {
  92.                     X = 50,
  93.                     Y = 50,
  94.                     Width = 100,
  95.                     Height = 100,
  96.                 },
  97.                 FontColor = Color.White,
  98.                 BackgroundColor = GetColor(_shapeColorIndex++),
  99.                 Text = "圆形" + _shapeColorIndex,
  100.                 TextFont = Font,
  101.             };
  102.             _fcc.FCC_AddShapes(new List<ShapeBase>() { rs });
  103.             _fcc.FCC_Refresh();
  104.         }
  105.         private void toolStripButton2_Click(object sender, EventArgs e)
  106.         {
  107.             _fcc.FCC_StartLink();
  108.         }
  109.         private void toolStripButton3_Click(object sender, EventArgs e)
  110.         {
  111.             _fcc.FCC_StopLink();
  112.         }
  113.     }
  114. }
复制代码
六、结语

可以看到我们更多的是使用,而不是编写。有了我们自定义的画布控件,完全不需要过多的考虑,只需要调用画布的方法就行了,复用性很强。
现在所有的角色都已登场,后面就要在这个地基上添砖加瓦,构造我们自己的流程图。
我们下节课就来添加一些其它的形状,如:菱形、平行四边形、圆角矩形等,到时候会发现原来这么的顺理成章,敬请期待。
感谢大家的观看,本人水平有限,文章不足之处欢迎大家评论指正。
-[END]-

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册