一、前言

上节课已经抽象出来了形状和连线,但是没解决程序复用的问题:现在所有的代码是写在窗口中的,如果想在其它程序想实现流程图,只能重新写代码或者复制粘贴代码,没办法简单复用,而且也无法保证功能的完整性和及时性。所以我们本节就来看一下,如何独立出一张“画布”控件,来解决此问题。

相信看完的你,一定会有所收获!

本文地址: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]-

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

  1. 适合新手:从零开发一个IM服务端(基于Netty,有完整源码)

    本文由“yuanrw”分享,博客:juejin.im/user/5cefab8451882510eb758606,收录时内容有改动和修订. 0.引言 站长提示:本文适合IM新手阅读,但最好有一定的网络 ...

  2. 【Android开发VR实战】三.开发一个寻宝类VR游戏TreasureHunt

    转载请注明出处:http://blog.csdn.net/linglongxin24/article/details/53939303 本文出自[DylanAndroid的博客] [Android开发 ...

  3. iOS开发UI篇—Quartz2D(自定义UIImageView控件)

    iOS开发UI篇—Quartz2D(自定义UIImageView控件) 一.实现思路 Quartz2D最大的用途在于自定义View(自定义UI控件),当系统的View不能满足我们使用需求的时候,自定义 ...

  4. 【Android开发日记】之入门篇(十四)——Button控件+自定义Button控件

        好久不见,又是一个新的学期开始了,为什么我感觉好惆怅啊!这一周也发生了不少事情,节假日放了三天的假(好久没有这么悠闲过了),实习公司那边被组长半强制性的要求去解决一个后台登陆的问题,结果就是把 ...

  5. 1   开发一个注重性能的JDBC应用程序不是一件容易的事. 当你的代码运行很慢的时候JDBC驱动程序并不会抛出异常告诉你。   本系列的性能提示将为改善JDBC应用程序的性能介绍一些基本的指导原则,这其中的原则已经被许多现有的JDBC应用程序编译运行并验证过。 这些指导原则包括:    正确的使用数据库MetaData方法    只获取需要的数据    选用最佳性能的功能    管理连

    1 开发一个注重性能的JDBC应用程序不是一件容易的事. 当你的代码运行很慢的时候JDBC驱动程序并不会抛出异常告诉你. 本系列的性能提示将为改善JDBC应用程序的性能介绍一些基本的指导原则,这其中的 ...

  6. 【VS开发】单文档中往视图中加入控件

    [VS开发]单文档中往视图中加入控件 标签(空格分隔): [VS开发] 分隔视图的但文档窗口,要显示控件,推荐使用CFormView或者CCtrlView,前者和对话框的做法一致. 在MainFram ...

  7. Winform自定义键盘控件开发及使用

    最近有学员提出项目中要使用键盘控件,系统自带的osk.exe不好用,于是就有了下面的内容: 首先是进行自定义键盘控件的开发,其实核心大家都知道,就是利用SendKeys.Send发送相应 的字符,但是 ...

  8. 使用前端开发工具包WijmoJS - 创建自定义DropDownTree控件(包含源代码)

    概述 最近,有客户向我们请求开发一个前端下拉控件,需求是显示了一个列表,其中包含可由用户单独选择的项目控件,该控件将在下拉列表中显示多选TreeView(树形图). 如今WijmoJS已经实现了该控件 ...

  9. 【Nodejs】326- 从零开发一个node命令行工具

    本文由 IMWeb 社区授权转载自腾讯内部 KM 论坛.点击阅读原文查看 IMWeb 社区更多精彩文章. 什么是命令行工具? 命令行工具(Cmmand Line Interface)简称cli,顾名思 ...

  10. Django实战总结 - 快速开发一个数据库查询工具

    一.简介 Django 是一个开放源代码的 Web 应用框架,由 Python 写成. Django 只要很少的代码就可以轻松地完成一个正式网站所需要的大部分内容,并进一步开发出全功能的 Web 服务 ...

随机推荐

  1. eolinker校验规则之 Json Path定位:返回值每一项数组内值校验

    如下图,获取H5首页菜单,验证菜单名是否正确 找到对应的接口,查看返回数据,菜单名字存放在TabBar下的3个数组内 Eolinker传统的JSON参数定位(json结构定位)只能校验第一个数组内的p ...

  2. JVM 垃圾回收调优的主要目标是什么?

    JVM 垃圾回收调优的主要目标 JVM 垃圾回收调优的目标是为了提升应用的性能,优化垃圾回收过程中的停顿时间和吞吐量.调优的核心目标通常包括以下几点: 1. 减少垃圾回收的停顿时间 停顿时间(Stop ...

  3. Windows管理小工具

    Windows 管理小工具 概述 Windows 管理小工具 是一个基于批处理脚本的多功能工具,旨在帮助用户快速管理 Windows 系统中的常见设置和功能.通过简单的菜单操作,用户可以轻松完成 Wi ...

  4. EBC Rev.5中的中间品、消费品,资本品,HS2012,HS2017与EBC Rev.5对照表,各种贸易标准对照表....

    最近在做一个国际供应链问题研究,用到了中间品这个概念,在各大论坛逛了好久居然没有找到一个说清楚了的,某些论坛需要什么点数才能下载,论坛用户更是自视甚高,一副这也不懂的姿态审视所有刚入门这个领域的新人, ...

  5. QtWidget项目-仿腾讯QQ音乐

    本博客主要介绍本人写的个人项目 - QtWidget5 仿腾讯QQ音乐项目. 效果演示 项目详情 源码 Gitee地址:https://gitee.com/run-little-peach/my-qq ...

  6. c++并发编程实战-第2章 线程管控

    线程的基本管控 每个应用程序都至少拥有一个线程,即运行main函数的线程,称为主线程,它由c++运行时系统启动.我们可以在软件运行中产生其他线程,它们以指定的函数作为入口函数.当main函数返回后,程 ...

  7. 【晴神宝典刷题路】codeup+pat 题解索引(更新ing

    记录一下每天的成果,看多久能刷完伐 c2 c/c++快速入门 <算法笔记>2.3小节--C/C++快速入门->选择结构 习题4-10-1 奖金计算 <算法笔记>2.4小节 ...

  8. File与IO流之File练习

    创建文件夹,并在其中创建文件 package Java_test; import java.io.*; public class Test { public static void main(Stri ...

  9. 「Temp」目录

    吃吃吃 \(\color{orange}{Eon\ 今天吃什么}\) Temp \(\color{magenta}{代码模板}\) Trick \(\color{magenta}{常见错误}\) \( ...

  10. python基础—基本数据类型—数字,字符串,列表,元组,字典

    1.运算符 (1)基本运算符 +  加法   -   减法 *   乘法 /   除法 **   幂 //   取整(除法) %   取余(除法) (2)判断某个东西是否在某个东西里面包含 in no ...