技術沒有先進落後之分,只有合不合適。html
WinForm有着很是多的優勢,在使用WinForm久了以後,不免會以爲WinForm自帶的某些控件外觀上有些許樸素、或者功能上有些不如意,天然而然便想去美化這些控件,或者給控件添加一些額外功能,而這即是自定義控件的意義所在。框架
自定義控件的難度並不大,可是卻處在一個比較尷尬的位置:ide
1,通常的教材不會講——由於仍是有難度的,並且通常用不上;函數
2,而網上或書上所找到的自定義控件相關知識教程裏,大多都是給一個已完成的自定義控件,再附上源碼,只有了了註釋和說明。畢竟難度不大,懂的天然懂,並且對懂的人來講,看別人的自定義控件每每是爲了看一下實現的思路或某個點的實現方法,由於不少都是一點就透。工具
對於初學者而言,要想掌握自定義控件,就須要花費很多的時間去學習那些源代碼、去模仿、去練習、去摸索,最後一步步去概括總結出適合本身的一條路。當掌握了以後,回頭看去,會發現其實真的不難,耗費的時間與學習的難度並不成正比,這些額外的時間就花費在了摸索和總結上了。學習
我也是這樣一步步走來的,因此不想讓你們再花費這麼多的時間去掌握一項並不太難的知識,便有了這篇文章。優化
在本文中,我會從零開始,帶着你們一步一步去實現一個自定義控件,同時會分享一些個人經驗之談,相信看完的你,必定會有所收穫。spa
本篇的自定義控件是:TrackBar設計
本文地址:http://www.javashuo.com/article/p-qqasooqt-md.html3d
咱們來分析一下爲何要去自定義控件。
以本文要實現的TrackBar爲例,最主要的緣由便 是系統自帶的TrackBar太過樸素,因此須要一款比較好看的TrackBar控件。
系統自帶的TrackBar:
預想的TrackBar樣式:
在實現一個自定義控件前,咱們要肯定一下咱們要實現的目標,好比外觀、功能、特色等。
1,外觀
我的經驗之談
在設計預想樣式時,能夠何用任何方式,只要本身能夠看明白就行,可是仍是推薦使用繪圖軟件去作一個示意圖,主要是由於在自定義控件時,每每會須要用到一些座標、寬、高等值,特別是和GDI+有關時。使用繪圖軟件則能夠去準確和清晰的標註出來這些信息,並進行相關的計算。
我想實現的TrackBar的外觀樣式以下:
2,功能
參考系統的TrackBar,能夠將所須要的功能歸爲下面幾點:
(1)支持鼠標點擊。
(2)支持鼠標拖動。
(3)支持修改顏色。
3,特色
既然全實現本身的TrackBar,確定要有本身的特色。
(1)支持顏色調整,包括背景色和前景色。
(2)支持圓角顯示,和直角顯示。
在自定義控件的目標定好以後,接下來即是分析實現上述目標所須要的技術。
自定義的TrackBar從邏輯上能夠分爲兩層:背景條(Bar)和滑塊(Slider)。
在具體實現時也是按照這兩層的思路去分層實現。
經過上面的分析的示意,咱們發現GDI+能夠實現上述目標,因此咱們的主要技術即是——GDI+。
直角可使用GDI+中的Graphics.DrawLine去實現。那麼圓角怎麼實現呢?
其實也很簡單,仍然使用Graphics.DrawLine實現,不過在建立Pen時,須要設置一下LineCap,經過LineCap能夠實現多種樣式,除了圓角外,還有菱形、箭頭等等。
具體的設置後文會講解,此處再也不贅述。
MSDN中關於LineCap的說明以下:
指定可用線帽樣式,Pen 對象以該線帽結束一段直線。
我的經驗之談
建議建立自定義控件時,將自定義控件寫在一個單獨的類庫裏。主要的目的是提升複用性,同時也方便管理,以及方便控件間的相互調用。
關於控件間的相互調用:
由於控件除了單個的自定義控件外,還有用戶控件(UserControl)——實現某些複雜功能的時候,每每就須要用到用戶控件。用戶控件每每是多個控件的組合,因此將控件放到一個類庫中能夠方便的調用,修改也方便。
啓動VS(本文使用的VS2019),添加新的 類庫(.NET Framework)項目,起好項目名稱並選好位置,點擊建立。
我的經驗之談
關於框架的選擇。
在實際應用當中,框架版本要根據自定義控件所服務的項目去選擇。由於是自定義控件,因此兼容性很高,每每.Net 2.0就能夠實現絕大部分效果。因此,能夠根據具體的項目去選擇框架的版本,固然也能夠選一個.Net 2.0,而後在實現完成以後編譯成不一樣框架版本。
在項目名稱上右擊,選擇添加-類,輸入類名:LTrackBar.cs,肯定。
我的經驗之談
關於類名
在起自定義控件的名稱時,最好不要和系統控件名稱同樣,那樣會致使二義性,平白增長代碼量。
因此能夠統一加一個前綴或後綴,如:TextBoxEx,PanelPlus。本文即是統一加上前綴」L「——LTrackBar
在添加繼承時,根據具體的須要去選擇不一樣的繼承。好比要對ComboBox的一拉選項添加不一樣的顏色,就繼承ComboBox並進行重繪;好比要讓TextBox支持透明,就繼承TextBox進行重寫等等。
在本例的LTrackBar中,經過前文的分析發現很簡單,因此能夠繼承基礎的Control類。
(1)添加繼承
在類名後輸入」:Control「
(2)添加引用
上一步裏會發現」Control「顯示錶明錯誤的波浪線,咱們將鼠標懸浮在上面,在彈出的提示按鈕上點擊,選擇」將引用添加到System.Windows.Forms.dll",而後"Control"下面的波浪線將會消失,並變爲淺藍色。
↓
(3)修改可訪問性。
因爲是一個單獨的類庫,而且LTrackBar是一個獨立的控件,因此咱們須要將類的可訪問性修改成Public。
我的經驗之談
關於參數命名
對於公共參數,我的建議添加一個統一的前綴。主要緣由有兩點:
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(描述)。以後即可以在屬性窗口看到相應的分類和說明。
爲了獲取LTrackBar的當前值,以及在值改變時執行某些操做,因此須要增長一個事件。事件數據則爲當前值(L_Value)。
(1)新建類,繼承自EventArgs。
(2)新建委託和事件
經過前文的分析,咱們知道主要用到了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); } }
之因此沒有註釋,實在是太過淺顯無可註釋,單純的看代碼很難理解,下面我將經過示意圖的方法講解,其實只要看了示意圖,就會恍然大悟,會發現其實很簡單。
對於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);
咱們在項目上右鍵,選擇生成,以後在同一解決方案下新建一WinForm項目,此時在工具箱的最上層會有咱們的自定義控件——LTrackBar。
如圖:
咱們選中並添加到主界面上,並設置相應的屬性。
同時添加一個label,用來顯示當前的值。
其實效果以下:
在實際運行時,咱們會發如今點擊和拖動時,控件會有閃爍(因爲GIF錄製幀率,因此上面的動圖不看不閃爍)。
爲了解決閃爍的問題,咱們在LTrackBar的構造函數上添加對雙緩衝的支持。
我的經驗之談
關於雙緩衝
通常而言,只要涉及到了GDI+,都會使用雙緩衝技術去減小閃爍,並且使用也很簡單,就兩行代碼而已:
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);固然,ControlStyles還有不少屬性,其做用也各有做用,在之後的文章中若是有用到我會再說明的。
默認事件,顧名思義,就是雙擊控件時自動生成的事件,像雙擊Button時的Click事件,雙擊TextBox時的TextChanged事件等。
要實現這種效果,須要在代碼的最上面加上DefaultEvent事件,以下:
其中「LValueChanged」就是咱們要設置的默認事件。這樣在咱們雙擊LTrackBar時,便會自動生成該事件。
通篇下來,其實能夠發現並無用到多深的知識,更多的是想像力,解放你的思想,不要被常規所束縛。