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

[原创]《C#高级GDI+实战:从零开发一个流程图》第06章:繁琐?没扩展性?抽象!抽象!

呼延冰枫 2025-7-9 12:43:07
一、前言

前面的课程我们实现了两种形状:矩形、圆形,在第4章的时候就会发现,仅增加了个新形状,代码量及判断逻辑就翻倍不止,异常繁琐,可维护性很差,更没有扩展性可言。我们本节课就来解决这一点,解决的方法也很简单经典:抽象!
相信看完的你,一定会有所收获!
本文地址:https://www.cnblogs.com/lesliexin/p/18972184
二、先看效果

其实没什么特别的效果可看,和之前一样的功能效果,但也从侧面证明了抽象后的功能完善性。

我们下面就来讲解如何抽象。
三、形状基类

我们先看如何抽象出形状基类。
1,起个基类名

我们按“形状基类”直译即可:
1.png

可以看到上面打了马赛克,是因为我们现在还不知道定义为“接口类”还是“抽象类”,随着下文的推进,自然而然的就知道如何选择了。
2,通用属性:ID、Rect

我们先来看一下矩形和圆形的定义:
2.png

3.png

可以看到,两者的定义是一样的,所以我们直接将这两个属性添加到基类中即可:
4.png

看到这里读者可能就有疑问了:矩形和圆形的绘制是传入Rectangle这没问题,但如果我的形状是菱形、圆角矩形等,绘制时是需要多个GDI+函数组合绘制而成,那么这个Rectangle有什么用?
答:这个Rectangle在基类中,已经是狭义上的坐标及尺寸了,就是这个属性是不直接参与绘制的,只是用来标识当前形状所处的位置及形状所占的大小范围。
又问:那“不直接参与绘制”,我应该如何绘制呢?
答:下面我们就来讲一下。
3,抽象方法:Draw()

对于形状的绘制,我们并不由基类直接实现,而是交由派生类去实现,所以我们定义一个抽象方法即可:
5.png

可以看到绘制方法只有一个参数:Graphics,所以派生出来的形状,就用这个Graphics来绘制自己的长啥样,矩形、圆形等等都行,基类不管这些,基类只负责把画笔交给派生类,画什么样都是由派生类自己绘制的。
看到这里的读者可能又有疑问:为什么非得用抽象方法?接口方法不行吗?
答:行,但为了更好的复用,我们需要在基类中添加一些基础实现,派生类可直接用,不想用重写即可,所以我们的基类要定义为“抽象类”,而不是“接口类”。
而且随着功能的完善,基类也在不断的优化,也会有更多的基础实现,所以定义为“抽象类”会更合适。
第1小节的马赛克也可以去掉了:
6.png

我们下面来实现一个虚方法,添加基础的实现。
4,虚方法:GetCentertPoint()

我们在前面的课程中知道,连线连接的是形状的中心点,所以对于不同的形状都有获取中心点的方法:
7.png

既然是都有的方法,而且方法实现也基本一致,那么我们就来提取到基类当中。同时为了保证扩展性,我们定义为虚方法,让派生类可直接用、也可自行实现。
8.png

当然,读者这时候又会有疑问了:为什么连线要连中心点?而不是上下左右边各有连接点?
答:会有的,后面都会有的,别着急。我们本节课是为了将前面的课程抽象出来,解决的是前面课程的痛点,后面的课程才是扩展的时候。
5,其它属性:颜色、文本等

其实到前面基本上就已经完成了,但是为了方便使用,我们还需要添加一些额外的通用属性进来。
我们先来看一下之前绘制矩形和圆形时的方法:
9.png

从中我们可以提取到这些都有的属性:形状的背景色、文本内容、文本字体、文本颜色。
所以我们在基类中添加上这些属性,让派生类在绘制时能直接从自身取到,不需要额外获取,保持统一的处理逻辑。
10.png

好了,到此我们基类就已经完成了,涵盖了前面课程所有的需求。下面是完整的基类代码,读者可自行查看:
点击查看代码
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Drawing;
  4. using System.Linq;
  5. using System.Text;
  6. using System.Threading.Tasks;
  7. namespace Elements
  8. {
  9.     /// <summary>
  10.     /// 形状基类
  11.     /// </summary>
  12.     public abstract class ShapeBase
  13.     {
  14.         /// <summary>
  15.         /// 形状ID
  16.         /// </summary>
  17.         public string Id { get; set; }
  18.         /// <summary>
  19.         /// 形状位置和尺寸
  20.         /// </summary>
  21.         public Rectangle Rect { get; set; }
  22.         /// <summary>
  23.         /// 背景颜色
  24.         /// </summary>
  25.         public Color BackgroundColor { get; set; }
  26.         /// <summary>
  27.         /// 形状内的文本
  28.         /// </summary>
  29.         public string Text { get; set; }
  30.         /// <summary>
  31.         /// 文本字体
  32.         /// </summary>
  33.         public Font TextFont { get; set; }
  34.         /// <summary>
  35.         /// 文本颜色
  36.         /// </summary>
  37.         public Color FontColor { get; set; }
  38.         //注:文章中说明,为了扩展性,不可能都在一个类中,不可能添加一个形状或连线就改代码,所以要把绘制方法放到基类中,由各个派生形状和连线自行绘制。
  39.         /// <summary>
  40.         /// 绘制形状
  41.         /// </summary>
  42.         /// <param name="g"></param>
  43.         public abstract void Draw(Graphics g);
  44.         /// <summary>
  45.         /// 获取形状的中心点
  46.         /// </summary>
  47.         /// <param name="shapeId"></param>
  48.         /// <returns></returns>
  49.         public virtual Point GetCentertPoint()
  50.         {
  51.             var line1X = Rect.X + Rect.Width / 2;
  52.             var line1Y = Rect.Y + Rect.Height / 2;
  53.             return new Point(line1X, line1Y);
  54.         }
  55.     }
  56. }
复制代码
四、连线基类

我们再来看如何抽象出连线基类,虽然我们目前只有直接这一个连线类型,但是并不妨碍我们抽象出基类,而且我们后面是要支持多种类型的连线样式的,在此便随着一直抽象出基类。
1,起个基类名

同样,我们根据“连线基类”直译出基类名:
11.png

2,通用属性:ID、开始形状、结束形状

我们先来看一下前面课程里直线连线的定义:
12.png

首先我们看到连线也是有ID的,用来唯一标识连线。
然后有开始形状ID和结束形状ID,和两个判断是矩形或圆形的变量,因为我们已经抽象出了形状基类,所以我们就不需要再这样写了,我们直接定义两个形状基类,分别作为开始形状和结束形状:
13.png

可以看到代码已经简洁了起来,这就是抽象的好处。
3,抽象方法:Draw()

同样的,对于连线的绘制,我们并不由基类直接实现,而是交由派生类去实现,所以我们定义一个抽象方法即可:
14.png

4,其它属性:颜色

相较于形状,连线就很简单了,只需要一个连线颜色属性即可:
15.png

当然,这只是暂时的,后续会随需求扩增属性。
好了,到此我们基类就已经完成了,涵盖了前面课程所有的需求。下面是完整的基类代码,读者可自行查看:
点击查看代码
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Drawing;
  4. using System.Linq;
  5. using System.Text;
  6. using System.Threading.Tasks;
  7. namespace Elements
  8. {
  9.     //注:说明,这里不能使用ID,要使用形状,当然后续可再改。
  10.     public abstract class LinkBase
  11.     {
  12.         /// <summary>
  13.         /// 连线ID
  14.         /// </summary>
  15.         public string Id { get; set; }
  16.         /// <summary>
  17.         /// 连线开始形状
  18.         /// </summary>
  19.         public ShapeBase StartShape { get; set; }
  20.         /// <summary>
  21.         /// 连线结束形状
  22.         /// </summary>
  23.         public ShapeBase EndShape { get; set; }
  24.         /// <summary>
  25.         /// 连线的颜色
  26.         /// </summary>
  27.         public Color BackgroundColor { get; set; }
  28.         /// <summary>
  29.         /// 绘制连线
  30.         /// </summary>
  31.         /// <param name="g"></param>
  32.         public abstract void Draw(Graphics g);
  33.     }
  34. }
复制代码
五、将基类放到独立的类库

除了形状和连线这些要抽象外,我们也要对程序进行“抽象”。
我们将基类的定义,以及后续派生出来的各种形状、连线等都放到一个单独的类库当中,这样就可以避免耦合太深,而且我们对基类的改动、增加新的形状和连线等也方便和清晰很多,不会过多影响对应的程序。
16.png

六、形状派生

我们有了基类,下面就来派生出矩形和圆形:
1,矩形

我们创建一个类,直接继承自形状基类,然后按提示重写抽象方法Draw(),直接将前面课程里绘制矩形的方法复制过来,然后改成相关属性就行了:
17.png

是不是很简单。
2,圆形

同理可得:
18.png

3,将派生类放到Shapes文件夹中

同样的,我们将派生出来的矩形类和圆形类放到Shapes文件夹中,方便查看及维护:
19.png

我们后面课程所增加的形状都会放到Shapes文件夹中。
七、连线派生

同样的,我们将直线连线派生出来:
20.png

可以看到,简单了很多,而且也用到了形状基类中的获取形状中心点的虚方法。
然后我们同样的将类放到Links文件夹中:
21.png

八、程序改造

基类、派生类都有了,我们就来改造一下上节课的程序,看看会简化多少。
1,形状集合

之前的,我们需要分别定义不同形状和连线的集合:
22.png

而现在,我们只需要定义一个形状基类集合和一个连线基类集合:
23.png

2,绘制所有形状连线方法

这部分简化倒不大,因为我们之前就已经使用类似抽象的方式,将绘制方法分别写到独立的方法中:
24.png

而现在,我们直接调用基类的Draw()方法即可,不用管具体是如何实现的,因为它们会自行绘制:
25.png

写代码到这里,就感觉很舒服、很优雅,以后添加任何新形状、连线,都不需要改动了,终于不用再一遍遍重复写了。
3,鼠标相关事件

这里就不贴之前的实现代码了,太啰嗦了。直接看新实现的:
3.1,MouseDown

26.png

3.2,MouseMove

27.png

3.3,MouseUp

28.png

上面的代码,看下来是不是简单了很多,而且还有种熟悉的感觉?
没错!这和我们只有矩形时的实现几乎是一样的,从这也可以看出抽象后的实现简化程度有多少。
4,添加形状方法

当然,因为派生出来的矩形和圆形的属性已经变了,所以添加时也要做相应的改动:
29.png

30.png

好了,到此为止,我们终于改造完毕,在简化了大量代码后,仍实现了之前的效果,这就是抽象的力量。
下面是完整代码,大家可自行查看:
点击查看代码
  1. using Elements;
  2. using Elements.Links;
  3. using Elements.Shapes;
  4. using System;
  5. using System.Collections.Generic;
  6. using System.ComponentModel;
  7. using System.Data;
  8. using System.Drawing;
  9. using System.Linq;
  10. using System.Text;
  11. using System.Threading.Tasks;
  12. using System.Windows.Forms;
  13. namespace FlowChartDemo
  14. {
  15.     public partial class FormDemo05V1 : FormBase
  16.     {
  17.         public FormDemo05V1()
  18.         {
  19.             InitializeComponent();
  20.             DemoTitle = "第07节随课Demo  Part1";
  21.             DemoNote = "效果:抽象图形基类,从基类派生出矩形、圆形,实现拖动、连线。";
  22.             SetStyle(ControlStyles.AllPaintingInWmPaint |
  23.                 ControlStyles.UserPaint |
  24.                 ControlStyles.OptimizedDoubleBuffer,true);
  25.         }
  26.                
  27.         //注:文章中说明要添加Elements的项目引用。
  28.         
  29.         /// <summary>
  30.         /// 形状集合
  31.         /// </summary>
  32.         List<ShapeBase> Shapes = new List<ShapeBase>();
  33.         /// <summary>
  34.         /// 连线集合
  35.         /// </summary>
  36.         List<LinkBase> Links = new List<LinkBase>();
  37.         Bitmap _bmp;
  38.         /// <summary>
  39.         /// 重新绘制当前所有矩形和连线
  40.         /// </summary>
  41.         /// <param name="g"></param>
  42.         void DrawAll(Graphics g1)
  43.         {
  44.             //创建内存绘图,将形状和连线绘制到此内存绘图上,然后再一次性绘制到控件上
  45.             _bmp = new Bitmap(panel1.Width, panel1.Height);
  46.             var g = Graphics.FromImage(_bmp);
  47.             //设置显示质量
  48.             g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
  49.             g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
  50.             g.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
  51.             g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
  52.             g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
  53.             g.Clear(panel1.BackColor);
  54.             //绘制所有形状
  55.             foreach (var sp in Shapes)
  56.             {
  57.                 sp.Draw(g);
  58.             }
  59.             //绘制所有连线
  60.             foreach (var ln in Links)
  61.             {
  62.                 ln.Draw(g);
  63.             }
  64.             //绘制内存绘图到控件上
  65.             g1.DrawImage(_bmp, new PointF(0, 0));
  66.         }
  67.         //注:文章中说明;此处不过于抽象,后续章节会有
  68.         /// <summary>
  69.         /// 当前是否有鼠标按下,且有矩形被选中
  70.         /// </summary>
  71.         bool _isMouseDown = false;
  72.         /// <summary>
  73.         /// 最后一次鼠标的位置
  74.         /// </summary>
  75.         Point _lastMouseLocation = Point.Empty;
  76.         /// <summary>
  77.         /// 当前被鼠标选中的矩形
  78.         /// </summary>
  79.         ShapeBase _selectedShape = null;
  80.         /// <summary>
  81.         /// 添加连线时选中的第一个形状
  82.         /// </summary>
  83.         ShapeBase _selectedStartShape = null;
  84.         /// <summary>
  85.         /// 添加连线时选中的第一个形状
  86.         /// </summary>
  87.         ShapeBase _selectedEndShape = null;
  88.         /// <summary>
  89.         /// 是否正添加连线
  90.         /// </summary>
  91.         bool _isAddLink = false;
  92.         /// <summary>
  93.         /// 获取不同的背景颜色
  94.         /// </summary>
  95.         /// <param name="i"></param>
  96.         /// <returns></returns>
  97.         Color GetColor(int i)
  98.         {
  99.             switch (i)
  100.             {
  101.                 case 0: return Color.Red;
  102.                 case 1: return Color.Green;
  103.                 case 2: return Color.Blue;
  104.                 case 3: return Color.Orange;
  105.                 case 4: return Color.Purple;
  106.                 default: return Color.Red;
  107.             }
  108.         }
  109.         private void toolStripButton1_Click(object sender, EventArgs e)
  110.         {
  111.             var rs = new RectShape()
  112.             {
  113.                 Id = "矩形" + (Shapes.Count + 1),
  114.                 Rect = new Rectangle()
  115.                 {
  116.                     X = 50,
  117.                     Y = 50,
  118.                     Width = 100,
  119.                     Height = 100,
  120.                 },
  121.                 FontColor = Color.White,
  122.                 BackgroundColor = GetColor(Shapes.Count),
  123.                 Text = "矩形" + (Shapes.Count + 1),
  124.                 TextFont = Font,
  125.             };
  126.             Shapes.Add(rs);
  127.             //重绘所有
  128.             DrawAll(panel1.CreateGraphics());
  129.         }
  130.         private void panel1_MouseDown(object sender, MouseEventArgs e)
  131.         {
  132.             //当鼠标按下时
  133.             //取最上方的形状
  134.             var sp = Shapes.FindLast(a => a.Rect.Contains(e.Location));
  135.            
  136.             if (!_isAddLink)
  137.             {
  138.                 //当前没有处理连线状态
  139.                 if (sp != null)
  140.                 {
  141.                     //设置状态及选中矩形
  142.                     _isMouseDown = true;
  143.                     _lastMouseLocation = e.Location;
  144.                     _selectedShape = sp;
  145.                 }
  146.             }
  147.             else
  148.             {
  149.                 //正在添加连线
  150.                 if (_selectedStartShape == null)
  151.                 {
  152.                     //证明没有矩形和圆形被选中则设置开始形状
  153.                     if (sp != null)
  154.                     {
  155.                         //设置开始形状
  156.                         _selectedStartShape = sp;
  157.                     }
  158.                     toolStripStatusLabel1.Text = "请点击第2个形状";
  159.                 }
  160.                 else
  161.                 {
  162.                     //判断第2个形状是否是第1个形状
  163.                     if (sp != null)
  164.                     {
  165.                         //判断当前选中的矩形是否是第1步选中的矩形
  166.                         if (_selectedStartShape.Id == sp.Id)
  167.                         {
  168.                             toolStripStatusLabel1.Text = "不可选择同一个形状,请重新点击第2个形状";
  169.                             return;
  170.                         }
  171.                     }
  172.                     if (sp != null)
  173.                     {
  174.                         //设置结束形状
  175.                         _selectedEndShape = sp;
  176.                     }
  177.                     else
  178.                     {
  179.                         return;
  180.                     }
  181.                     //两个形状都设置了,便添加一条新连线
  182.                     Links.Add(new LineLink()
  183.                     {
  184.                         Id = "连线" + (Links.Count + 1),
  185.                         BackgroundColor=GetColor(Links.Count),
  186.                         StartShape=_selectedStartShape,
  187.                         EndShape=_selectedEndShape,
  188.                     });
  189.                     //两个形状都已选择,结束添加连线状态
  190.                     _isAddLink = false;
  191.                     toolStripStatusLabel1.Text = "";
  192.                     //重绘以显示连线
  193.                     DrawAll(panel1.CreateGraphics());
  194.                 }
  195.             }
  196.         }
  197.         private void panel1_MouseMove(object sender, MouseEventArgs e)
  198.         {
  199.             //当鼠标移动时
  200.             //如果处于添加连线时,则不移动形状
  201.             if (_isAddLink) return;
  202.             if (_isMouseDown)
  203.             {
  204.                 //当且仅当:有鼠标按下且有矩形被选中时,才进行后续操作
  205.                 //改变选中矩形的位置信息,随着鼠标移动而移动
  206.                 //计算鼠标位置变化信息
  207.                 var moveX = e.Location.X - _lastMouseLocation.X;
  208.                 var moveY = e.Location.Y - _lastMouseLocation.Y;
  209.                 //将选中形状的位置进行同样的变化
  210.                 var oldXY = _selectedShape.Rect.Location;
  211.                 oldXY.Offset(moveX, moveY);
  212.                 _selectedShape.Rect = new Rectangle(oldXY, _selectedShape.Rect.Size);
  213.                 //记录当前鼠标位置
  214.                 _lastMouseLocation.Offset(moveX, moveY);
  215.                 //重绘所有矩形
  216.                 DrawAll(panel1.CreateGraphics());
  217.             }
  218.         }
  219.         private void panel1_MouseUp(object sender, MouseEventArgs e)
  220.         {
  221.             //当鼠标松开时
  222.             if (_isMouseDown)
  223.             {
  224.                 //当且仅当:有鼠标按下且有矩形被选中时,才进行后续操作
  225.                 //重置相关记录信息
  226.                 _isMouseDown = false;
  227.                 _lastMouseLocation = Point.Empty;
  228.                 _selectedShape = null;
  229.             }
  230.         }
  231.         private void toolStripButton2_Click(object sender, EventArgs e)
  232.         {
  233.             _isAddLink = true;
  234.             _selectedStartShape = null;
  235.             _selectedEndShape = null;
  236.             toolStripStatusLabel1.Text = "请点击第1个形状";
  237.         }
  238.         private void toolStripButton3_Click(object sender, EventArgs e)
  239.         {
  240.             _isAddLink = false;
  241.             _selectedStartShape = null;
  242.             _selectedEndShape = null;
  243.             toolStripStatusLabel1.Text = "";
  244.             DrawAll(panel1.CreateGraphics());
  245.         }
  246.         private void toolStripButton4_Click(object sender, EventArgs e)
  247.         {
  248.             var rs = new EllipseShape()
  249.             {
  250.                 Id = "圆形" + (Shapes.Count + 1),
  251.                 Rect = new Rectangle()
  252.                 {
  253.                     X = 50,
  254.                     Y = 50,
  255.                     Width = 100,
  256.                     Height = 100,
  257.                 },
  258.                 FontColor = Color.White,
  259.                 BackgroundColor = GetColor(Shapes.Count),
  260.                 Text = "圆形" + (Shapes.Count + 1),
  261.                 TextFont = Font,
  262.             };
  263.             Shapes.Add(rs);
  264.             //重绘所有
  265.             DrawAll(panel1.CreateGraphics());
  266.         }
  267.     }
  268. }
复制代码
九、结语

本节课程是新阶段的开始,我们终于要在规范化与扩展性的基础上,开始新的旅途。
但是抽象到这一步就足够了吗?不足够!我们现在只是将形状和连线这些抽象了出来,但是具体的实现仍是写死在程序中,如果我新开了一个程序,怎么复用呢?
我们下节课就来解决复用的问题,我们将会独立出一张“画布”,所有的基础实现,如:添加形状、连线、拖动等都由“画布”自行实现,而程序只需要加载这张“画布”,然后调用“画布”提供的方法来达到想要的效果即可。敬请期待。
感谢大家的观看,本人水平有限,文章不足之处欢迎大家评论指正。
-[END]-

来源:豆瓜网用户自行投稿发布,如果侵权,请联系站长删除

相关推荐

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