一、前言
技術沒有先進落后之分,只有合不合適。
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 = 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 = fCapHalfWidth; if (fPointValue > Width - fCapHalfWidth) fPointValue = Width - fCapHalfWidth; } else { e.Graphics.DrawLine(penBarBack, Width / 2f, fCapHalfWidth, Width / 2f, Height - fCapHalfWidth); fPointValue = mousePoint.Y; if (fPointValue < fCapHalfWidth) fPointValue = fCapHalfWidth; if (fPointValue > Height - fCapHalfWidth) fPointValue = 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); } }
在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 = fRatio * (Width - fCapWidth) + fCapHalfWidth; mousePoint = new PointF(fPointValue, fCapHalfWidth); } else if (_Orientation == Orientation.Horizontal_RL) { float fPointValue = Width - fCapHalfWidth - fRatio * (Width - fCapWidth); mousePoint = new PointF(fPointValue, fCapHalfWidth); } else if (_Orientation == Orientation.Vertical_TB) { float fPointValue = fRatio * (Height - fCapWidth) + fCapHalfWidth; mousePoint = new PointF(fCapHalfWidth, fPointValue); } else { float fPointValue = Height - fCapHalfWidth - fRatio * (Height - fCapWidth); mousePoint = new PointF(fCapHalfWidth, fPointValue); } }
之所以沒有注釋,實在是太過淺顯無可注釋,單純的看代碼很難理解,下面我將通過示意圖的方法講解,其實只要看了示意圖,就會恍然大悟,會發現其實很簡單。
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