[原创]《C#高级GDI+实战:从零开发一个流程图》第06章:繁琐?没扩展性?抽象!抽象!
<h1 id="一前言">一、前言</h1><p>前面的课程我们实现了两种形状:矩形、圆形,在第4章的时候就会发现,仅增加了个新形状,代码量及判断逻辑就翻倍不止,异常繁琐,可维护性很差,更没有扩展性可言。我们本节课就来解决这一点,解决的方法也很简单经典:抽象!</p>
<p><strong>相信看完的你,一定会有所收获!</strong></p>
<p>本文地址:https://www.cnblogs.com/lesliexin/p/18972184</p>
<h1 id="二先看效果">二、先看效果</h1>
<p>其实没什么特别的效果可看,和之前一样的功能效果,但也从侧面证明了抽象后的功能完善性。</p>
<p><iframe src="https://player.bilibili.com/player.html?isOutside=true&bvid=BV15jTRz1EyD&p=1&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>
<h3 id="1起个基类名">1,起个基类名</h3>
<p>我们按“形状基类”直译即可:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708090012398-1239141575.png" alt="image" loading="lazy"></p>
<p>可以看到上面打了马赛克,是因为我们现在还不知道定义为“接口类”还是“抽象类”,随着下文的推进,自然而然的就知道如何选择了。</p>
<h3 id="2通用属性idrect">2,通用属性:ID、Rect</h3>
<p>我们先来看一下矩形和圆形的定义:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708085833083-247734474.png" alt="image" loading="lazy"></p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708085851397-431871792.png" alt="image" loading="lazy"></p>
<p>可以看到,两者的定义是一样的,所以我们直接将这两个属性添加到基类中即可:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708092453881-1340836356.png" alt="image" loading="lazy"></p>
<p>看到这里读者可能就有疑问了:矩形和圆形的绘制是传入Rectangle这没问题,但如果我的形状是菱形、圆角矩形等,绘制时是需要多个GDI+函数组合绘制而成,那么这个Rectangle有什么用?</p>
<p>答:这个Rectangle在基类中,已经是狭义上的坐标及尺寸了,就是这个属性是<strong>不直接参与</strong>绘制的,只是用来标识当前形状所处的位置及形状所占的大小范围。</p>
<p>又问:那“不直接参与绘制”,我应该如何绘制呢?</p>
<p>答:下面我们就来讲一下。</p>
<h3 id="3抽象方法draw">3,抽象方法:Draw()</h3>
<p>对于形状的绘制,我们并不由基类直接实现,而是交由派生类去实现,所以我们定义一个抽象方法即可:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708093245588-901654870.png" alt="image" loading="lazy"></p>
<p>可以看到绘制方法只有一个参数:Graphics,所以派生出来的形状,就用这个Graphics来绘制自己的长啥样,矩形、圆形等等都行,基类不管这些,基类只负责把画笔交给派生类,画什么样都是由派生类自己绘制的。</p>
<p>看到这里的读者可能又有疑问:为什么非得用抽象方法?接口方法不行吗?</p>
<p>答:行,但为了更好的复用,我们需要在基类中添加一些基础实现,派生类可直接用,不想用重写即可,所以我们的基类要定义为“抽象类”,而不是“接口类”。</p>
<blockquote>
<p>而且随着功能的完善,基类也在不断的优化,也会有更多的基础实现,所以定义为“抽象类”会更合适。</p>
</blockquote>
<p>第1小节的马赛克也可以去掉了:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708093812656-1660488302.png" alt="image" loading="lazy"></p>
<p>我们下面来实现一个虚方法,添加基础的实现。</p>
<h3 id="4虚方法getcentertpoint">4,虚方法:GetCentertPoint()</h3>
<p>我们在前面的课程中知道,连线连接的是形状的中心点,所以对于不同的形状都有获取中心点的方法:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708094043273-368859934.png" alt="image" loading="lazy"></p>
<p>既然是都有的方法,而且方法实现也基本一致,那么我们就来提取到基类当中。同时为了保证扩展性,我们定义为虚方法,让派生类可直接用、也可自行实现。</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708094536582-1284678950.png" alt="image" loading="lazy"></p>
<p>当然,读者这时候又会有疑问了:为什么连线要连中心点?而不是上下左右边各有连接点?</p>
<p>答:会有的,后面都会有的,别着急。我们本节课是为了将前面的课程抽象出来,解决的是前面课程的痛点,后面的课程才是扩展的时候。</p>
<h3 id="5其它属性颜色文本等">5,其它属性:颜色、文本等</h3>
<p>其实到前面基本上就已经完成了,但是为了方便使用,我们还需要添加一些额外的通用属性进来。</p>
<p>我们先来看一下之前绘制矩形和圆形时的方法:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708095121700-1475487021.png" alt="image" loading="lazy"></p>
<p>从中我们可以提取到这些都有的属性:形状的背景色、文本内容、文本字体、文本颜色。</p>
<p>所以我们在基类中添加上这些属性,让派生类在绘制时能直接从自身取到,不需要额外获取,保持统一的处理逻辑。</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708100212660-2027454361.png" alt="image" loading="lazy"></p>
<p>好了,到此我们基类就已经完成了,涵盖了前面课程所有的需求。下面是完整的基类代码,读者可自行查看:</p>
<details>
<summary>点击查看代码</summary>
<pre><code>using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Elements
{
/// <summary>
/// 形状基类
/// </summary>
public abstract class ShapeBase
{
/// <summary>
/// 形状ID
/// </summary>
public string Id { get; set; }
/// <summary>
/// 形状位置和尺寸
/// </summary>
public Rectangle Rect { get; set; }
/// <summary>
/// 背景颜色
/// </summary>
public Color BackgroundColor { get; set; }
/// <summary>
/// 形状内的文本
/// </summary>
public string Text { get; set; }
/// <summary>
/// 文本字体
/// </summary>
public Font TextFont { get; set; }
/// <summary>
/// 文本颜色
/// </summary>
public Color FontColor { get; set; }
//注:文章中说明,为了扩展性,不可能都在一个类中,不可能添加一个形状或连线就改代码,所以要把绘制方法放到基类中,由各个派生形状和连线自行绘制。
/// <summary>
/// 绘制形状
/// </summary>
/// <param name="g"></param>
public abstract void Draw(Graphics g);
/// <summary>
/// 获取形状的中心点
/// </summary>
/// <param name="shapeId"></param>
/// <returns></returns>
public virtual Point GetCentertPoint()
{
var line1X = Rect.X + Rect.Width / 2;
var line1Y = Rect.Y + Rect.Height / 2;
return new Point(line1X, line1Y);
}
}
}
</code></pre>
</details>
<h1 id="四连线基类">四、连线基类</h1>
<p>我们再来看如何抽象出连线基类,虽然我们目前只有直接这一个连线类型,但是并不妨碍我们抽象出基类,而且我们后面是要支持多种类型的连线样式的,在此便随着一直抽象出基类。</p>
<h3 id="1起个基类名-1">1,起个基类名</h3>
<p>同样,我们根据“连线基类”直译出基类名:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708100752552-1236249907.png" alt="image" loading="lazy"></p>
<h3 id="2通用属性id开始形状结束形状">2,通用属性:ID、开始形状、结束形状</h3>
<p>我们先来看一下前面课程里直线连线的定义:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708100946673-1982707224.png" alt="image" loading="lazy"></p>
<p>首先我们看到连线也是有ID的,用来唯一标识连线。</p>
<p>然后有开始形状ID和结束形状ID,和两个判断是矩形或圆形的变量,因为我们已经抽象出了形状基类,所以我们就不需要再这样写了,我们直接定义两个形状基类,分别作为开始形状和结束形状:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708101324791-258167720.png" alt="image" loading="lazy"></p>
<p>可以看到代码已经简洁了起来,这就是抽象的好处。</p>
<h3 id="3抽象方法draw-1">3,抽象方法:Draw()</h3>
<p>同样的,对于连线的绘制,我们并不由基类直接实现,而是交由派生类去实现,所以我们定义一个抽象方法即可:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708101654573-874548057.png" alt="image" loading="lazy"></p>
<h3 id="4其它属性颜色">4,其它属性:颜色</h3>
<p>相较于形状,连线就很简单了,只需要一个连线颜色属性即可:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708101731983-1773527707.png" alt="image" loading="lazy"></p>
<blockquote>
<p>当然,这只是暂时的,后续会随需求扩增属性。</p>
</blockquote>
<p>好了,到此我们基类就已经完成了,涵盖了前面课程所有的需求。下面是完整的基类代码,读者可自行查看:</p>
<details>
<summary>点击查看代码</summary>
<pre><code>using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Elements
{
//注:说明,这里不能使用ID,要使用形状,当然后续可再改。
public abstract class LinkBase
{
/// <summary>
/// 连线ID
/// </summary>
public string Id { get; set; }
/// <summary>
/// 连线开始形状
/// </summary>
public ShapeBase StartShape { get; set; }
/// <summary>
/// 连线结束形状
/// </summary>
public ShapeBase EndShape { get; set; }
/// <summary>
/// 连线的颜色
/// </summary>
public Color BackgroundColor { get; set; }
/// <summary>
/// 绘制连线
/// </summary>
/// <param name="g"></param>
public abstract void Draw(Graphics g);
}
}
</code></pre>
</details>
<h1 id="五将基类放到独立的类库">五、将基类放到独立的类库</h1>
<p>除了形状和连线这些要抽象外,我们也要对程序进行“抽象”。</p>
<p>我们将基类的定义,以及后续派生出来的各种形状、连线等都放到一个单独的类库当中,这样就可以避免耦合太深,而且我们对基类的改动、增加新的形状和连线等也方便和清晰很多,不会过多影响对应的程序。</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708102230700-662615734.png" alt="image" loading="lazy"></p>
<h1 id="六形状派生">六、形状派生</h1>
<p>我们有了基类,下面就来派生出矩形和圆形:</p>
<h3 id="1矩形">1,矩形</h3>
<p>我们创建一个类,直接继承自形状基类,然后按提示重写抽象方法Draw(),直接将前面课程里绘制矩形的方法复制过来,然后改成相关属性就行了:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708103154727-463993638.png" alt="image" loading="lazy"></p>
<p>是不是很简单。</p>
<h3 id="2圆形">2,圆形</h3>
<p>同理可得:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708103443898-1325796764.png" alt="image" loading="lazy"></p>
<h3 id="3将派生类放到shapes文件夹中">3,将派生类放到Shapes文件夹中</h3>
<p>同样的,我们将派生出来的矩形类和圆形类放到Shapes文件夹中,方便查看及维护:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708103711603-1708928437.png" alt="image" loading="lazy"></p>
<p>我们后面课程所增加的形状都会放到Shapes文件夹中。</p>
<h1 id="七连线派生">七、连线派生</h1>
<p>同样的,我们将直线连线派生出来:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708103848016-1722466423.png" alt="image" loading="lazy"></p>
<p>可以看到,简单了很多,而且也用到了形状基类中的获取形状中心点的虚方法。</p>
<p>然后我们同样的将类放到Links文件夹中:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708104216368-1188255055.png" alt="image" loading="lazy"></p>
<h1 id="八程序改造">八、程序改造</h1>
<p>基类、派生类都有了,我们就来改造一下上节课的程序,看看会简化多少。</p>
<h3 id="1形状集合">1,形状集合</h3>
<p>之前的,我们需要分别定义不同形状和连线的集合:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708104338234-1186739477.png" alt="image" loading="lazy"></p>
<p>而现在,我们只需要定义一个形状基类集合和一个连线基类集合:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708104407745-1291776535.png" alt="image" loading="lazy"></p>
<h3 id="2绘制所有形状连线方法">2,绘制所有形状连线方法</h3>
<p>这部分简化倒不大,因为我们之前就已经使用类似抽象的方式,将绘制方法分别写到独立的方法中:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708105814277-376304766.png" alt="image" loading="lazy"></p>
<p>而现在,我们直接调用基类的Draw()方法即可,不用管具体是如何实现的,因为它们会自行绘制:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708105904257-1225210285.png" alt="image" loading="lazy"></p>
<p>写代码到这里,就感觉很舒服、很优雅,以后添加任何新形状、连线,都不需要改动了,终于不用再一遍遍重复写了。</p>
<h3 id="3鼠标相关事件">3,鼠标相关事件</h3>
<p>这里就不贴之前的实现代码了,太啰嗦了。直接看新实现的:</p>
<h4 id="31mousedown">3.1,MouseDown</h4>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708112113723-1783771261.png" alt="image" loading="lazy"></p>
<h4 id="32mousemove">3.2,MouseMove</h4>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708112142048-1734791327.png" alt="image" loading="lazy"></p>
<h4 id="33mouseup">3.3,MouseUp</h4>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708112205073-1831686547.png" alt="image" loading="lazy"></p>
<p>上面的代码,看下来是不是简单了很多,而且还有种熟悉的感觉?</p>
<p>没错!这和我们只有矩形时的实现几乎是一样的,从这也可以看出抽象后的实现简化程度有多少。</p>
<h3 id="4添加形状方法">4,添加形状方法</h3>
<p>当然,因为派生出来的矩形和圆形的属性已经变了,所以添加时也要做相应的改动:</p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708112449431-115554025.png" alt="image" loading="lazy"></p>
<p><img src="https://img2024.cnblogs.com/blog/1686429/202507/1686429-20250708112504685-1916136232.png" alt="image" loading="lazy"></p>
<p>好了,到此为止,我们终于改造完毕,在简化了大量代码后,仍实现了之前的效果,这就是抽象的力量。</p>
<p>下面是完整代码,大家可自行查看:</p>
<details>
<summary>点击查看代码</summary>
<pre><code>using Elements;
using Elements.Links;
using Elements.Shapes;
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 FormDemo05V1 : FormBase
{
public FormDemo05V1()
{
InitializeComponent();
DemoTitle = "第07节随课DemoPart1";
DemoNote = "效果:抽象图形基类,从基类派生出矩形、圆形,实现拖动、连线。";
SetStyle(ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint |
ControlStyles.OptimizedDoubleBuffer,true);
}
//注:文章中说明要添加Elements的项目引用。
/// <summary>
/// 形状集合
/// </summary>
List<ShapeBase> Shapes = new List<ShapeBase>();
/// <summary>
/// 连线集合
/// </summary>
List<LinkBase> Links = new List<LinkBase>();
Bitmap _bmp;
/// <summary>
/// 重新绘制当前所有矩形和连线
/// </summary>
/// <param name="g"></param>
void DrawAll(Graphics g1)
{
//创建内存绘图,将形状和连线绘制到此内存绘图上,然后再一次性绘制到控件上
_bmp = new Bitmap(panel1.Width, panel1.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(panel1.BackColor);
//绘制所有形状
foreach (var sp in Shapes)
{
sp.Draw(g);
}
//绘制所有连线
foreach (var ln in Links)
{
ln.Draw(g);
}
//绘制内存绘图到控件上
g1.DrawImage(_bmp, new PointF(0, 0));
}
//注:文章中说明;此处不过于抽象,后续章节会有
/// <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;
/// <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 = "矩形" + (Shapes.Count + 1),
Rect = new Rectangle()
{
X = 50,
Y = 50,
Width = 100,
Height = 100,
},
FontColor = Color.White,
BackgroundColor = GetColor(Shapes.Count),
Text = "矩形" + (Shapes.Count + 1),
TextFont = Font,
};
Shapes.Add(rs);
//重绘所有
DrawAll(panel1.CreateGraphics());
}
private void panel1_MouseDown(object sender, 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;
}
toolStripStatusLabel1.Text = "请点击第2个形状";
}
else
{
//判断第2个形状是否是第1个形状
if (sp != null)
{
//判断当前选中的矩形是否是第1步选中的矩形
if (_selectedStartShape.Id == sp.Id)
{
toolStripStatusLabel1.Text = "不可选择同一个形状,请重新点击第2个形状";
return;
}
}
if (sp != null)
{
//设置结束形状
_selectedEndShape = sp;
}
else
{
return;
}
//两个形状都设置了,便添加一条新连线
Links.Add(new LineLink()
{
Id = "连线" + (Links.Count + 1),
BackgroundColor=GetColor(Links.Count),
StartShape=_selectedStartShape,
EndShape=_selectedEndShape,
});
//两个形状都已选择,结束添加连线状态
_isAddLink = false;
toolStripStatusLabel1.Text = "";
//重绘以显示连线
DrawAll(panel1.CreateGraphics());
}
}
}
private void panel1_MouseMove(object sender, 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);
//重绘所有矩形
DrawAll(panel1.CreateGraphics());
}
}
private void panel1_MouseUp(object sender, MouseEventArgs e)
{
//当鼠标松开时
if (_isMouseDown)
{
//当且仅当:有鼠标按下且有矩形被选中时,才进行后续操作
//重置相关记录信息
_isMouseDown = false;
_lastMouseLocation = Point.Empty;
_selectedShape = null;
}
}
private void toolStripButton2_Click(object sender, EventArgs e)
{
_isAddLink = true;
_selectedStartShape = null;
_selectedEndShape = null;
toolStripStatusLabel1.Text = "请点击第1个形状";
}
private void toolStripButton3_Click(object sender, EventArgs e)
{
_isAddLink = false;
_selectedStartShape = null;
_selectedEndShape = null;
toolStripStatusLabel1.Text = "";
DrawAll(panel1.CreateGraphics());
}
private void toolStripButton4_Click(object sender, EventArgs e)
{
var rs = new EllipseShape()
{
Id = "圆形" + (Shapes.Count + 1),
Rect = new Rectangle()
{
X = 50,
Y = 50,
Width = 100,
Height = 100,
},
FontColor = Color.White,
BackgroundColor = GetColor(Shapes.Count),
Text = "圆形" + (Shapes.Count + 1),
TextFont = Font,
};
Shapes.Add(rs);
//重绘所有
DrawAll(panel1.CreateGraphics());
}
}
}
</code></pre>
</details>
<h1 id="九结语">九、结语</h1>
<p>本节课程是新阶段的开始,我们终于要在规范化与扩展性的基础上,开始新的旅途。</p>
<p>但是抽象到这一步就足够了吗?不足够!我们现在只是将形状和连线这些抽象了出来,但是具体的实现仍是写死在程序中,如果我新开了一个程序,怎么复用呢?</p>
<p>我们下节课就来解决复用的问题,我们将会独立出一张“画布”,所有的基础实现,如:添加形状、连线、拖动等都由“画布”自行实现,而程序只需要加载这张“画布”,然后调用“画布”提供的方法来达到想要的效果即可。敬请期待。</p>
<p>感谢大家的观看,本人水平有限,文章不足之处欢迎大家评论指正。</p>
<p>--</p><br><br>
来源:https://www.cnblogs.com/lesliexin/p/18972184
頁:
[1]