[原創]《C#高級GDI+實戰:從零開發一個流程圖》第07章:來吧,自定義“畫布”控件!
一、前言
上節課已經抽象出來了形狀和連線,但是沒解決程序復用的問題:現在所有的代碼是寫在窗口中的,如果想在其它程序想實現流程圖,只能重新寫代碼或者復制粘貼代碼,沒辦法簡單復用,而且也無法保證功能的完整性和及時性。所以我們本節就來看一下,如何獨立出一張“畫布”控件,來解決此問題。
相信看完的你,一定會有所收獲!
本文地址:http://www.rzrgm.cn/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]-

浙公網安備 33010602011771號