孤山朕爷 發表於 2025-7-16 10:50:00

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

<h1 id="一前言">一、前言</h1>
<p>上节课已经抽象出来了形状和连线,但是没解决程序复用的问题:现在所有的代码是写在窗口中的,如果想在其它程序想实现流程图,只能重新写代码或者复制粘贴代码,没办法简单复用,而且也无法保证功能的完整性和及时性。所以我们本节就来看一下,如何独立出一张“画布”控件,来解决此问题。</p>
<p><strong>相信看完的你,一定会有所收获!</strong></p>
<p>本文地址:https://www.cnblogs.com/lesliexin/p/18985184</p>
<h1 id="二先看效果">二、先看效果</h1>
<p>并没有什么特别的效果可看,主要是演示我们独立出来的“画布”控件功能完整性。</p>
<p><iframe src="https://player.bilibili.com/player.html?isOutside=true&amp;bvid=BV1LjTRz1EG7&amp;p=1&amp;autoplay=0" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true" width="668" height="376"></iframe></p>
<p>我们下面就来讲解如何实现。</p>
<h1 id="三创建类库及自定义控件">三、创建类库及自定义控件</h1>
<p>就像上节我们将抽象出来的形状和连线类都放到独立的类库中一样,我们同样将画布控件放到一个单独的类库中:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715105419509-736720436.png" alt="image" loading="lazy"></p>
<p>然后我们添加一个“自定义控件”,注意不是“用户控件”:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715105610417-29934668.png" alt="image" loading="lazy"></p>
<p>我们给画布起个名称:FCCanvas,就是FlowChartCanvas的简写。</p>
<p>这里为了方便编写教程,我们在后面增加V1、V2,用来区分。</p>
<p>创建好的结构如下:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715105942317-1559375267.png" alt="image" loading="lazy"></p>
<h1 id="四移植代码到自定义控件">四、移植代码到自定义控件</h1>
<p>现在有了单独的画布控件,我们就将之前在程序中实现代码移植过来,我们在FCCanvasV1上右键-&gt;查看代码,进入后台代码。</p>
<h3 id="1双缓冲">1,双缓冲</h3>
<p>首要的,我们在构造函数中添加开启双缓冲的代码:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715110153696-1155009175.png" alt="image" loading="lazy"></p>
<h3 id="2重写onpaint">2,重写OnPaint</h3>
<p>有过自定义控件的读者会知道,自定义控件就相当于一个“画布”,控制所展示的内容全是我们用代码“画”上去的,而绘制的方法就是在OnPaint方法中。</p>
<p>我们将之前代码里的DrawAll方法里的代码复制进来:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715113559976-97669362.png" alt="image" loading="lazy"></p>
<p>因为已经在OnPaint方法中,所以不再需要传入Graphics对象,直接使用e.Graphics即可,此即当前控件的对象。</p>
<h3 id="2重写鼠标相关事件">2,重写鼠标相关事件</h3>
<p>我们之前是在panel控件上操作,现在我们是在整个控件上操作,所以我们需要重写下相关事件,这些可重写的方法一般都是以On开头,如:OnMouseDown等。</p>
<h4 id="21onmousedown">2.1,OnMouseDown</h4>
<p>我们将之前代码中的MouseDown中的代码拷贝进来:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715130827344-730507310.png" alt="image" loading="lazy"></p>
<p>这里的变化有三点:</p>
<p>一是提示文本我们这里改为了触发事件的方式,我们定义了一个事件,通知订阅者使用,至于是否显示提示内容及如何显示提示内容我们控件不作管理。</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715131151265-1174039123.png" alt="image" loading="lazy"></p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715131333238-2030894330.png" alt="image" loading="lazy"></p>
<p>二是添加连线时,连线的颜色不再是随机生成,也是触发一个事件,由调用方决定连线的颜色是什么:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715131348284-1581715211.png" alt="image" loading="lazy"></p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715131422409-896603346.png" alt="image" loading="lazy"></p>
<p>为了防止调用方不订阅此事件,我们会默认连线颜色为黑色。</p>
<p>三是发起重新绘制的方式不一样了,之前是直接调用绘制所有方法DrawAll:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715131731957-858938912.png" alt="image" loading="lazy"></p>
<p>而现在我们也没有了DrawAll方法,DrawAll的实现被我们移植到了OnPaint方法中。所以我们直接调用控件自带的无效方法Invalidate(),来使窗口重绘:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715131849213-918617129.png" alt="image" loading="lazy"></p>
<p>内部逻辑简单而言就是:当我们调用Invalidate()后,系统会自动调用OnPaint方法,进而重绘。而这也是自定义控件的基础逻辑。</p>
<h4 id="22onmousemove">2.2,OnMouseMove</h4>
<p>同样的,我们将之前代码中的MouseMove中的代码拷贝进来:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715132255917-416469119.png" alt="image" loading="lazy"></p>
<p>可以看到几乎一样,也是最后一步改为调用无效方法Invalidate(),来使窗口重绘。</p>
<h4 id="23onmouseup">2.3,OnMouseUp</h4>
<p>同理:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715132352852-1348034400.png" alt="image" loading="lazy"></p>
<h3 id="3形状集合连线集合等定义">3,形状集合、连线集合等定义</h3>
<p>我们现在基本的实现都有了,那么就把之前的一些私有变量拿过来,像形状集合、连线集合、连线状态等:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715132611148-1409958431.png" alt="image" loading="lazy"></p>
<h3 id="4公共方法">4,公共方法</h3>
<p>现在整个FCCanvasV1内部已经自洽了,但是有个问题:如何与外部交互?如何添加形状?</p>
<p>我们现在就来开放一些公共方法,来实现与外部的交互。</p>
<h4 id="41添加形状方法">4.1,添加形状方法</h4>
<p>最核心的也是最基本的功能,就是添加形状的方法:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715132827424-774164696.png" alt="image" loading="lazy"></p>
<p>我们的方法支持一次添加多个形状,而且添加形状时会自动判断是否已经添加过。</p>
<p>注:我们看到方法名带了个前缀:FCC_,这样写看似不优雅,但是对于后续的开发和使用却有很大的便利,我们统一前缀,这样在写代码时敲入前缀就能看到所有的方法,而不需要再去思考,特别是对于其它人而言,不熟悉的情况下只能去看类的定义里有哪些方法才能去调用,而不像现在这样这么方便。这是经验之谈,当然加不加前缀完全是个人自由,想怎么写就怎么写,并不会影响功能。</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715133300002-1907584927.png" alt="image" loading="lazy"></p>
<h4 id="42清空方法">4.2,清空方法</h4>
<p>我们添加一个清空当前画布中所有形状和连线的方法,用于复原:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715133522080-2045050515.png" alt="image" loading="lazy"></p>
<h4 id="43刷新方法">4.3,刷新方法</h4>
<p>我们虽然可以通过调用控件的Invalidate()方法来刷新,但是不够直观,我们直接将其封装为一个方法:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715133645309-146517490.png" alt="image" loading="lazy"></p>
<h4 id="44添加连线和中止连线方法">4.4,添加连线和中止连线方法</h4>
<p>我们目前的程序支持添加连线和中止添加连线,所以我们同样开放出这两个方法:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715133743443-1413882656.png" alt="image" loading="lazy"></p>
<p>好了,到此为止,我们的V1版画布就已经完成了,可以实现之前课程里的所有效果了。下面是完整代码,大家可查看和尝试:</p>
<details>
<summary>点击查看代码</summary>
<pre><code>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

    /// &lt;summary&gt;
    /// 流程图画布
    /// &lt;/summary&gt;
    public class FCCanvasV1:Control
    {
      public FCCanvasV1()
      {
            SetStyle(ControlStyles.AllPaintingInWmPaint |
                ControlStyles.UserPaint |
                ControlStyles.OptimizedDoubleBuffer, true);
      }

      #region 公共事件

      /// &lt;summary&gt;
      /// 连线时的状态提示
      /// &lt;/summary&gt;
      public event Action&lt;string&gt; FCC_LinkState;
      /// &lt;summary&gt;
      /// 添加连线时,连线的颜色
      /// &lt;/summary&gt;
      public event Func&lt;Color&gt; FCC_LinkColor;

      #endregion

      #region 公共属性



      #endregion

      #region 公共方法

      //注:文章中说明,为了方便查看和演示有哪些方法和属性,所以固定开头,可依喜好不要此开头

      /// &lt;summary&gt;
      /// 向当前画布中添加形状
      /// &lt;/summary&gt;
      /// &lt;param name="sps"&gt;&lt;/param&gt;
      public void FCC_AddShapes(List&lt;ShapeBase&gt; sps)
      {
            if (sps == null || sps.Count == 0) return;
            foreach (var item in sps)
            {
                //根据ID去重
                if (!_shapes.Any(a =&gt; a.Id == item.Id))
                {
                  _shapes.Add(item);
                }
            }
            //令当前控件失效以重绘
            Invalidate();
      }
      
      /// &lt;summary&gt;
      /// 清空画布中的形状和连线
      /// &lt;/summary&gt;
      public void FCC_Clear()
      {
            _shapes.Clear();
            _links.Clear();
            Invalidate();
      }

      /// &lt;summary&gt;
      /// 刷新当前画布
      /// &lt;/summary&gt;
      public void FCC_Refresh()
      {
            Invalidate();
      }

      /// &lt;summary&gt;
      /// 开始连线
      /// &lt;/summary&gt;
      public void FCC_StartLink()
      {
            _isAddLink = true;
            _selectedStartShape = null;
            _selectedEndShape = null;
            FCC_LinkState?.Invoke("请点击第1个形状");
      }

      /// &lt;summary&gt;
      /// 中止/停止连线
      /// &lt;/summary&gt;
      public void FCC_StopLink()
      {
            _isAddLink = false;
            _selectedStartShape = null;
            _selectedEndShape = null;
            FCC_LinkState?.Invoke("");
            Invalidate();
      }

      #endregion

      #region 私有属性

      /// &lt;summary&gt;
      /// 形状集合
      /// &lt;/summary&gt;
      List&lt;ShapeBase&gt; _shapes = new List&lt;ShapeBase&gt;();
      /// &lt;summary&gt;
      /// 连线集合
      /// &lt;/summary&gt;
      List&lt;LinkBase&gt; _links = new List&lt;LinkBase&gt;();
      /// &lt;summary&gt;
      /// 当前是否有鼠标按下,且有矩形被选中
      /// &lt;/summary&gt;
      bool _isMouseDown = false;
      /// &lt;summary&gt;
      /// 最后一次鼠标的位置
      /// &lt;/summary&gt;
      Point _lastMouseLocation = Point.Empty;
      /// &lt;summary&gt;
      /// 当前被鼠标选中的矩形
      /// &lt;/summary&gt;
      ShapeBase _selectedShape = null;

      /// &lt;summary&gt;
      /// 添加连线时选中的第一个形状
      /// &lt;/summary&gt;
      ShapeBase _selectedStartShape = null;
      /// &lt;summary&gt;
      /// 添加连线时选中的第一个形状
      /// &lt;/summary&gt;
      ShapeBase _selectedEndShape = null;
      /// &lt;summary&gt;
      /// 是否正添加连线
      /// &lt;/summary&gt;
      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 =&gt; 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

    }
}

</code></pre>
</details>
<h1 id="五使用画布控件">五、使用画布控件</h1>
<p>我们的画布控件已经完成,下面就来看一下如何去使用它。</p>
<h3 id="1引用画布类库">1,引用画布类库</h3>
<p>因为我们的画布在独立的类库中,所以我们先引用类库:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715134026888-1051407799.png" alt="image" loading="lazy"></p>
<h3 id="2添加画布控件">2,添加画布控件</h3>
<p>首先,界面与之前并无变化:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715134809605-1696478166.png" alt="image" loading="lazy"></p>
<p>不过我们不再在中间的panel中绘制,而是将我们的画布控件添加到panel当中。我们在构造函数中使用代码的方式添加控件:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715134721186-456334504.png" alt="image" loading="lazy"></p>
<blockquote>
<p>当然也可以能通过工具箱拖动添加,不过不太建议,特别当自定义控件复杂的情况下,代码的方式更好控制和编写。</p>
</blockquote>
<p>我们订阅两个事件,分别用来设置状态文本和获取颜色:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715135528369-1649198833.png" alt="image" loading="lazy"></p>
<h3 id="3按钮调用画布方法">3,按钮调用画布方法</h3>
<p>现在这些按钮不再自行实现了,而是直接调用画布的对应方法即可。</p>
<h4 id="31添加矩形按钮">3.1,添加矩形按钮</h4>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715135619032-1761018788.png" alt="image" loading="lazy"></p>
<h4 id="32添加圆形按钮">3.2,添加圆形按钮</h4>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715135640940-710925039.png" alt="image" loading="lazy"></p>
<h4 id="33开始连线">3.3,开始连线</h4>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715135701899-2031459160.png" alt="image" loading="lazy"></p>
<h4 id="34中止连线">3.4,中止连线</h4>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250715135718623-1705676680.png" alt="image" loading="lazy"></p>
<p>好了,到此为止我们就已经实现了之前课程里的效果。</p>
<p>下面是完整代码,大家可自己查看和编译:</p>
<details>
<summary>点击查看代码</summary>
<pre><code>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节随课DemoPart1";
            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;

      /// &lt;summary&gt;
      /// 形状颜色序号
      /// &lt;/summary&gt;
      int _shapeColorIndex = 0;
      /// &lt;summary&gt;
      /// 连线颜色序号
      /// &lt;/summary&gt;
      int _linkColorIndex = 0;
               
      /// &lt;summary&gt;
      /// 获取不同的背景颜色
      /// &lt;/summary&gt;
      /// &lt;param name="i"&gt;&lt;/param&gt;
      /// &lt;returns&gt;&lt;/returns&gt;
      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&lt;ShapeBase&gt;() { 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&lt;ShapeBase&gt;() { 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();
      }

    }


}

</code></pre>
</details>
<h1 id="六结语">六、结语</h1>
<p>可以看到我们更多的是<strong>使用</strong>,而不是<strong>编写</strong>。有了我们自定义的画布控件,完全不需要过多的考虑,只需要调用画布的方法就行了,复用性很强。</p>
<p>现在所有的角色都已登场,后面就要在这个地基上添砖加瓦,构造我们自己的流程图。</p>
<p>我们下节课就来添加一些其它的形状,如:菱形、平行四边形、圆角矩形等,到时候会发现原来这么的顺理成章,敬请期待。</p>
<p>感谢大家的观看,本人水平有限,文章不足之处欢迎大家评论指正。</p>
<p>--</p><br><br>
来源:https://www.cnblogs.com/lesliexin/p/18985184
頁: [1]
查看完整版本: [原创]《C#高级GDI+实战:从零开发一个流程图》第07章:来吧,自定义“画布”控件!