一、前言
技術沒有先進落后之分,只有合不合適,
WinForm有著非常多的優點,在使用WinForm久了之后,難免會覺得WinForm自帶的某些控制元件外觀上有些許樸素、或者功能上有些不如意,自然而然便想去美化這些控制元件,或者給控制元件添加一些額外功能,而這便是自定義控制元件的意義所在,
自定義控制元件的難度并不大,但是卻處在一個比較尷尬的位置:
1,一般的教材不會講——因為還是有難度的,而且一般用不上;
2,而網上或書上所找到的自定義控制元件相關知識教程里,大多都是給一個已完成的自定義控制元件,再附上原始碼,只有了了注釋和說明,畢竟難度不大,懂的自然懂,而且對懂的人來說,看別人的自定義控制元件往往是為了看一下實作的思路或某個點的實作方法,因為很多都是一點就透,
對于初學者而言,要想掌握自定義控制元件,就需要花費不少的時間去學習那些源代碼、去模仿、去練習、去摸索,最后一步步去歸納總結出適合自己的一條路,當掌握了之后,回頭看去,會發現其實真的不難,耗費的時間與學習的難度并不成正比,這些額外的時間就花費在了摸索和總結上了,
我也是這樣一步步走來的,所以不想讓大家再花費這么多的時間去掌握一項并不太難的知識,便有了這篇文章,
在本文中,我會從零開始,帶著大家一步一步去實作一個自定義控制元件,同時會分享一些我的經驗之談,相信看完的你,一定會有所識訓,
本篇的自定義控制元件是:TrackBar
本文地址:https://www.cnblogs.com/lesliexin/p/13265707.html
二、前期分析
(一)為什么需要去自定義控制元件?
我們來分析一下為什么要去自定義控制元件,
以本文要實作的TrackBar為例,最主要的原因便 是系統自帶的TrackBar太過樸素,所以需要一款比較好看的TrackBar控制元件,
系統自帶的TrackBar:

預想的TrackBar樣式:

(二)實作目標
在實作一個自定義控制元件前,我們要確定一下我們要實作的目標,比如外觀、功能、特點等,
1,外觀
個人經驗之談
在設計預想樣式時,可以何用任何方式,只要自己可以看明白就行,但是還是推薦使用繪圖軟體去做一個示意圖,主要是因為在自定義控制元件時,往往會需要用到一些坐標、寬、高等值,特別是和GDI+有關時,使用繪圖軟體則可以去準確和清晰的標注出來這些資訊,并進行相關的計算,
我想實作的TrackBar的外觀樣式如下:

2,功能
參考系統的TrackBar,可以將所需要的功能歸為下面幾點:
(1)支持滑鼠點擊,

(2)支持滑鼠拖動,

(3)支持修改顏色,

3,特點
既然全實作自己的TrackBar,肯定要有自己的特點,
(1)支持顏色調整,包括背景色和前景色,

(2)支持圓角顯示,和直角顯示,

(三)技術分析
在自定義控制元件的目標定好之后,接下來便是分析實作上述目標所需要的技術,
1,整體實作
自定義的TrackBar從邏輯上可以分為兩層:背景條(Bar)和滑塊(Slider),

在具體實作時也是按照這兩層的思路去分層實作,
2,主要技術
通過上面的分析的示意,我們發現GDI+可以實作上述目標,所以我們的主要技術便是——GDI+,
3,圓角和直角的實作
直角可以使用GDI+中的Graphics.DrawLine去實作,那么圓角怎么實作呢?
其實也很簡單,仍然使用Graphics.DrawLine實作,不過在創建Pen時,需要設定一下LineCap,通過LineCap可以實作多種樣式,除了圓角外,還有菱形、箭頭等等,
具體的設定后文會講解,此處不再贅述,
MSDN中關于LineCap的說明如下:
指定可用線帽樣式,Pen 物件以該線帽結束一段直線,
三、開始實作
(一)前期準備
1,創建自定義控制元件類別庫專案
個人經驗之談
建議創建自定義控制元件時,將自定義控制元件寫在一個單獨的類別庫里,主要的目的是提高復用性,同時也方便管理,以及方便控制元件間的相互呼叫,
關于控制元件間的相互呼叫:
因為控制元件除了單個的自定義控制元件外,還有用戶控制元件(UserControl)——實作某些復雜功能的時候,往往就需要用到用戶控制元件,用戶控制元件往往是多個控制元件的組合,所以將控制元件放到一個類別庫中可以方便的呼叫,修改也方便,
啟動VS(本文使用的VS2019),添加新的 類別庫(.NET Framework)專案,起好專案名稱并選好位置,點擊創建,
個人經驗之談
關于框架的選擇,
在實際應用當中,框架版本要根據自定義控制元件所服務的專案去選擇,因為是自定義控制元件,所以兼容性很高,往往.Net 2.0就可以實作絕大部分效果,所以,可以根據具體的專案去選擇框架的版本,當然也可以選一個.Net 2.0,然后在實作完成之后編譯成不同框架版本,
2,添加類
在專案名稱上右擊,選擇添加-類,輸入類名:LTrackBar.cs,確定,
個人經驗之談
關于類名
在起自定義控制元件的名稱時,最好不要和系統控制元件名稱一樣,那樣會導致二義性,平白增加代碼量,
所以可以統一加一個前綴或后綴,如:TextBoxEx,PanelPlus,本文便是統一加上前綴”L“——LTrackBar
3,添加繼承
在添加繼承時,根據具體的需要去選擇不同的繼承,比如要對ComboBox的一拉選項添加不同的顏色,就繼承ComboBox并進行重繪;比如要讓TextBox支持透明,就繼承TextBox進行重寫等等,
在本例的LTrackBar中,通過前文的分析發現很簡單,所以可以繼承基礎的Control類,
(1)添加繼承
在類名后輸入”:Control“

(2)添加參考
上一步里會發現”Control“顯示代表錯誤的波浪線,我們將滑鼠懸浮在上面,在彈出的提示按鈕上點擊,選擇”將參考添加到System.Windows.Forms.dll",然后"Control"下面的波浪線將會消失,并變為淺藍色,

↓

(3)修改可訪問性,
由于是一個單獨的類別庫,并且LTrackBar是一個獨立的控制元件,所以我們需要將類的可訪問性修改為Public,

4,添加自定義屬性
個人經驗之談
關于引數命名
對于公共引數,個人建議添加一個統一的前綴,主要原因有兩點:
1,在視圖設計界面中的屬性視窗中,無論是“按分類排序”還是“按字母排序”,都可以使控制元件所公開的自定義屬性集中在一起,
按分類排序:
按字母排序:
2,在代碼編輯界面,可以在輸入統一的前綴后,將該控制元件的所以自定義屬性都在代碼提示視窗中顯示在一起,方便選擇,
(1)顏色相關
通過前文可知,我們涉及到的顏色有兩個——背景條顏色和滑塊顏色,所以我們添加兩個屬性,其中的“Invalidate()”是為了在修改該屬性值后立刻使控制元件重繪,

(2)圓角相關

(3)最大值與最小值
如TrackBar一樣,我們也需要有最大值和最小值,由于我的需要很簡單,所以只支持整型(int),
首先,最小值應該大于0,然后最小值要小于最大值,所以最小值如下:

其次,最大值也應該大于最小值,

(4)當前值
用來獲取或設定當前LTrackBar所代表的值,
當前值需要在最大值和最小值之間,同時我們需要知道值發生了變化,所以添加了一個委托事件LValueChanged,關于委托和事件此處不展開講,因為不懂也不影響使用,就像固定公式一樣往上套就行了,只需要知道其作用是讓呼叫本控制元件的人知道當前的值發生了變化,

(5)方向
LTrackBar支持橫向顯示,也支持豎向顯示,
在橫向顯示時,分為兩種情況:1,左端為最小值(L_Minimum),右端為最大值(L_Maximum);2,左端為最大值(L_Maximum),右端為最小值(L_Minimum),
在豎向顯示時,分為兩種情況:1,頂部為最小值(L_Minimum),底部為最大值(L_Maximum);2,頂部為最大值(L_Maximum),底部為最小值(L_Minimum),
綜上,共有4種情況,所以我們先創建一個列舉,
同樣為了方便統一管理,新建一個類專門存放列舉資訊,

之后,創建一個Orientation列舉型別的屬性:

上面的那兩個if陳述句的作用是為了實作在改變方向后,自動交換控制元件的寬和高,
(6)寬度/高度
像TrackBar只能在設計器中調整寬度一樣,LTrackBar也只能調整寬度(橫向顯示時)或高度(豎向顯示),所以需要一個屬性來控制,

為了實作只能調整寬度/高度,需要重寫SetBoundsCore方法,MSDN上關于SetBoundsCore的說明如下:
我們需要對其進行重寫,以限制只能調整寬度或高度:

由于VS的強大,所以在重寫時非常方便:

(7)增加描述資訊
在公開屬性上加入Catagory(分組),Description(描述),之后便可以在屬性視窗看到相應的分類和說明,


5,添加事件
為了獲取LTrackBar的當前值,以及在值改變時執行某些操作,所以需要增加一個事件,事件資料則為當前值(L_Value),
(1)新建類,繼承自EventArgs,

(2)新建委托和事件

6,重寫方法
通過前文的分析,我們知道主要用到了GDI+,同時支持滑鼠點擊、拖動,所以我們需要重寫以下這些方法,

其中,OnPaint事件是用來畫顯示界面的,Mouse相關的事件是與實作滑鼠操作相關的,
為了知道當前滑鼠的狀態(進入、離開、按下、松開),需要定義一個列舉:

下面是每個重寫方法的具體說明:
(1)OnMouseEnter方法
標識著滑鼠進入,只需要設定一下滑鼠狀態即可,

(2)OnMouseLeave方法
同上

(3)OnMouseUp方法
同上

(4)OnMouseDown方法
當滑鼠點擊了控制元件時會觸發本事件,在滑鼠點擊后,控制元件應該重繪界面,主要是滑塊(Slider)的變化,同時滑塊(Slider)所代表的值也應該發生變化,同時引發LValueChanged事件,

(5)OnMouseMove方法
當滑鼠在控制元件上移動時觸發本事件,在實際操作時都是在在按著滑鼠左鍵并拖動,所以要判斷滑鼠的狀態(mouseStatus)是否是按下(Down),其他同上,

在OnMouseDown和OnMouseMove中,有一個方法:pPointToValue(),其作用便是將滑鼠的坐標值轉換為對應代表的值,其代碼如下:

其代碼很簡單,就是計算滑鼠落點占控制元件寬度/高度的比例,再乘以值的范圍就得到了代表的值,在下文中有示意圖講解,本處不再贅述,
(6)OnPaint方法
本方法是控制元件實作的核心,幾乎只要涉及控制元件重繪和自定義控制元件,都兔不了要重寫OnPaint方法,
在OnPaint方法中,我們主要完成兩部分的操作:
1)畫背景條(Bar)
2)畫滑塊(Slider)
這便是OnPaint方法的完整代碼:
protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); pValueToPoint(); e.Graphics.SmoothingMode = SmoothingMode.HighQuality; Pen penBarBack = new Pen(_BarColor, _BarSize); Pen penBarFore = new Pen(_SliderColor, _BarSize); float fCapHalfWidth = 0; float fCapWidth = 0; if (_IsRound) { fCapWidth = _BarSize; fCapHalfWidth = _BarSize / 2.0f; penBarBack.StartCap = LineCap.Round; penBarBack.EndCap = LineCap.Round; penBarFore.StartCap = LineCap.Round; penBarFore.EndCap = LineCap.Round; } float fPointValue = https://www.cnblogs.com/lesliexin/p/0; if (_Orientation == Orientation.Horizontal_LR || _Orientation == Orientation.Horizontal_RL) { e.Graphics.DrawLine(penBarBack, fCapHalfWidth, Height / 2f, Width - fCapHalfWidth, Height / 2f); fPointValue = mousePoint.X; if (fPointValue < fCapHalfWidth) fPointValue =https://www.cnblogs.com/lesliexin/p/ fCapHalfWidth; if (fPointValue > Width - fCapHalfWidth) fPointValue = https://www.cnblogs.com/lesliexin/p/Width - fCapHalfWidth; } else { e.Graphics.DrawLine(penBarBack, Width / 2f, fCapHalfWidth, Width / 2f, Height - fCapHalfWidth); fPointValue = mousePoint.Y; if (fPointValue < fCapHalfWidth) fPointValue =https://www.cnblogs.com/lesliexin/p/ fCapHalfWidth; if (fPointValue > Height - fCapHalfWidth) fPointValue = https://www.cnblogs.com/lesliexin/p/Height - fCapHalfWidth; } if (_Orientation == Orientation.Horizontal_LR) { e.Graphics.DrawLine(penBarFore, fCapHalfWidth, Height / 2f, fPointValue, Height / 2f); } else if (_Orientation == Orientation.Horizontal_RL) { e.Graphics.DrawLine(penBarFore, fPointValue, Height / 2f, Width - fCapHalfWidth, Height / 2f); } else if (_Orientation == Orientation.Vertical_TB) { e.Graphics.DrawLine(penBarFore, Width / 2f, fCapHalfWidth, Width / 2f, fPointValue); } else { e.Graphics.DrawLine(penBarFore, Width / 2f, fPointValue, Width / 2f, Height - fCapHalfWidth); } }OnPaint
在OnPain方法用到了一個方法:pValueToPoint(),其作用是將值轉換為相應坐標,代碼如下:
private void pValueToPoint() { float fCapHalfWidth = 0; float fCapWidth = 0; if (_IsRound) { fCapWidth = _BarSize; fCapHalfWidth = _BarSize / 2.0f; } float fRatio = Convert.ToSingle(_Value-_Minimum) / (_Maximum - _Minimum); if (_Orientation == Orientation.Horizontal_LR) { float fPointValue = https://www.cnblogs.com/lesliexin/p/fRatio * (Width - fCapWidth) + fCapHalfWidth; mousePoint = new PointF(fPointValue, fCapHalfWidth); } else if (_Orientation == Orientation.Horizontal_RL) { float fPointValue = https://www.cnblogs.com/lesliexin/p/Width - fCapHalfWidth - fRatio * (Width - fCapWidth); mousePoint = new PointF(fPointValue, fCapHalfWidth); } else if (_Orientation == Orientation.Vertical_TB) { float fPointValue = https://www.cnblogs.com/lesliexin/p/fRatio * (Height - fCapWidth) + fCapHalfWidth; mousePoint = new PointF(fCapHalfWidth, fPointValue); } else { float fPointValue = https://www.cnblogs.com/lesliexin/p/Height - fCapHalfWidth - fRatio * (Height - fCapWidth); mousePoint = new PointF(fCapHalfWidth, fPointValue); } }pValueToPoint
之所以沒有注釋,實在是太過淺顯無可注釋,單純的看代碼很難理解,下面我將通過示意圖的方法講解,其實只要看了示意圖,就會恍然大悟,會發現其實很簡單,
7,示意圖解
對于LTrackBar而言,有兩種樣式:直角和圓角,這兩種的實作并沒有太大不同,主要是Pen的LineCap屬性不同,LineCap說明見前文,
(以下將以橫向、從左到右的樣式(_Orientation = Orientation.Horizontal_LR)進行講解,其他類同,不多贅述,)
示意圖1:

我在圖中標注了一些點,主要用來詳解,
上圖中的B點(Rect.B、Round.B)即是當前滑鼠點擊的點,也是代表當前值的點,也是藍色條的寬度,
示意圖2:

在LineCap=Round時,其在繪制的線條兩端會各繪制一個半圓,如上圖中紫色所示,其半圓直徑等于線條寬度,
下面我會講解一下上面那些代碼中的那些算式是怎么來的,
(1)直角
1)計算
已知:
起始點:Rect.A;
結束點:Rect.C;
點Rect.A 對應的值為: L_Minimum;
點Rect.C 對應的值為: L_Maximum;
滑鼠可點擊范圍=控制元件寬度 = Bar.Width;
實際取值范圍 = (L_Maximum-L_Minimum);
滑鼠點擊處的X值=點Rect.B = Slider.Width;
滑鼠點擊處的X值與滑鼠可點擊范圍的比值=該點擊處對應的實際值與取值范圍的比值,即:
對應值/取值范圍=Slider.Width/Bar.Width;
所以:
對應值(_Value)=Slider.Width/Bar.Width*(L_Maximum-L_Minimum);
由于最左側的點Rect.A并不是0,而是對應著L_Minimum,所以,最后得到的真實值(L_Value)=_Value+L_Minimum;
2)繪制
設定Pen的寬度=Bar.Height
所以要從控制元件高度的中間開始繪制,其起終坐標如下:
起點:(Rect.A)=(0,Bar.Height/2);
終點:(Rect.C)=(Bar.Width,Bar.Height/2);
(2)圓角
1)計算
已知:
因為設定了圓角(LineCap=Round),所以線條兩端會各繪制一個半圓(示意圖中紫色半圓所示),其半圓直徑等于線條寬度,
那么其開始點便不再是點Round.A,而是點Round.D,同理,其結束點也不是點Round.C,而是點Round.E,
點Round.D 對應的值為: L_Minimum;
點Round.E 對應的值為: L_Maximum;
滑鼠可點擊范圍=控制元件寬度減去兩個半圓的寬度 = (Bar.Width-Bar.Height);
實際取值范圍 = (L_Maximum-L_Minimum);
滑鼠點擊處的X值 (點Round.B) = (Slider.Width-Bar.Height/2);(注意:此時滑鼠點擊處所產生的視覺效果范圍是(Round.A~Round.F),但其真正移動的范圍是(Round.D~Round.B),)
滑鼠點擊處的X值與滑鼠可點擊范圍的比值=該點擊處對應的實際值與取值范圍的比值,即:
對應值/取值范圍= (Slider.Width-Bar.Height/2)/ (Bar.Width-Bar.Height);
所以:
對應值(_Value)= (Slider.Width-Bar.Height/2)/ (Bar.Width-Bar.Height)*(L_Maximum-L_Minimum);
由于可點擊的最左側的點Round.D對應著L_Minimum,所以,最后得到的真實值(L_Value)=_Value+L_Minimum;
2)繪制
設定Pen的寬度=Bar.Height,所以要從控制元件高度的中間開始繪制,
又因為設定LineCap=Round,導致兩端各繪制了一個半圓,所以其起點和終點的坐標也應減去相應的值:
起點:(Round.D)=(Bar.Height/2,Bar.Height/2);
終點:(Round.E)=(Bar.Width-Bar.Height/2,Bar.Height/2);
四,效果演示及調整優化
1,演示
我們在專案上右鍵,選擇生成,之后在同一解決方案下新建一WinForm專案,此時在工具箱的最上層會有我們的自定義控制元件——LTrackBar,
如圖:

我們選中并添加到主界面上,并設定相應的屬性,
同時添加一個label,用來顯示當前的值,
其實效果如下:

在實際運行時,我們會發現在點擊和拖動時,控制元件會有閃爍(由于GIF錄制幀率,所以上面的動圖不看不閃爍),
為了解決閃爍的問題,我們在LTrackBar的建構式上添加對雙緩沖的支持,

個人經驗之談
關于雙緩沖
一般而言,只要涉及到了GDI+,都會使用雙緩沖技術去減少閃爍,而且使用也很簡單,就兩行代碼而已:
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);當然,ControlStyles還有很多屬性,其作用也各有作用,在以后的文章中如果有用到我會再說明的,
2,默認事件
默認事件,顧名思義,就是雙擊控制元件時自動生成的事件,像雙擊Button時的Click事件,雙擊TextBox時的TextChanged事件等,
要實作這種效果,需要在代碼的最上面加上DefaultEvent事件,如下:

其中“LValueChanged”就是我們要設定的默認事件,這樣在我們雙擊LTrackBar時,便會自動生成該事件,
五、結束語
通篇下來,其實可以發現并沒有用到多深的知識,更多的是想像力,解放你的思想,不要被常規所束縛,
六、源代碼及工程下載
https://files.cnblogs.com/files/lesliexin/LTrackBar.7z
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/117.html
標籤:WinForm
上一篇:WinForm版 螢屏截圖





