[C#] (原創)一步一步教你自定義控制元件——02,ScrollBar(滾動條)

leslie_xin發表於2020-08-14

一、前言

技術沒有先進與落後,只有合適與不合適。

本篇的自定義控制元件是:滾動條(ScollBar)。

我們可以在網上看到很多自定義的滾動條控制元件,它們大都是使用UserControl去做,即至少使用一個Panel或其它控制元件作滑塊,使用UserControl本身或另一個控制元件作為背景條,而有的複雜的還會加上頂端和底端的按鈕。這樣作的好處有很多,最主要的是支援承載更加複雜的視覺和動作效果,比如使用一系列圖片來實現非常炫麗的動畫效果等。

不過本次所實現的滾動條並不需要太多複雜的效果,扁平化樣式即可,所以就不需要使用UserControl,所以直接參照上一篇的LTrackBar,實現一個類似樣式的滾動條,這也是在上一篇之後緊接本篇的原因之一。

就像實現上篇的LTrackBar時的圓角的座標計算是個難點,然後著重結合示意圖去講解一樣,在本篇,關於滾動條中比較反直覺的、不太好直觀想像的,我仍會結合示意圖去詳細講解。

相信看完的你,一定會有所收穫。

本文地址:https://www.cnblogs.com/lesliexin/p/13440927.html

 


 

二、前期分析

(一)為什麼需要自定義滾動條?

滾動條(ScrollBar)控制元件是一個常常被忽略,但是應用卻極其頻繁的控制元件。由於很多控制元件都自帶滾動條,所以往往令人忽略滾動條的存在。

但是這些控制元件自帶的滾動條有個非常大的缺點,那就是幾乎不可以單獨對滾動條進行重繪,甚至連調整控制元件自帶滾動條的寬度這種”看起來很簡單“的操作,真正實現起來的工作量簡直令人”懷疑人生“。

而且,系統的滾動條控制元件:VScrollBar和HScrollBar,其樣式仍是最基本的那樣,其可重繪性非常小。

 

系統控制元件自帶的滾動條,在一般的WinForm程式中,看著還挺不錯的,但是一旦你的WinForm程式中使用了大比例的自定義控制元件後,系統的滾動條就有點”刺眼“了。所以,便需要自己的滾動條控制元件。

(二)實現目標

1,外觀

參考上一篇的 LTrackBar控制元件,我想實現的滾動條的外觀樣式如下:

2,功能

(1)支援滑鼠拖動。

(2)支援點選非”滑塊“區,改變滑塊位置。

(3)支援滑鼠進入時改變滑塊顏色。

(4)支援修改各部分顏色。

3,特點

(1)支援改變滾動條方向:垂直或橫向。

(2)支援改變顏色。

(3)支援圓角、直角顯示。

 

(三)技術分析

滾動條控制元件LScrollBar與LTrackBar類似,同樣是分為背景條(Bar)和滑塊(Slider)。所使用的核心技術仍是GDI+。

(四)滾動條講解

在這裡,我會對滾動條進行詳細的講解,包括滾動條的效果、比例的計算等。

在本節的講解中,我將以”直角“、”垂直“樣式的滾動條進行講解說明。

1,滾動條的工作方式。

如下圖所示,我們將”滾動條“和”顯示內容“拆分成兩部分。


其中,半透明的黑色部分是”螢幕“,也就是我們實際可以看到的範圍。而“文件”的實際長度要遠遠超過螢幕可顯示部分,所以就需要通過“滾動條”來上下“滾動”來看文件的其他部分。

那麼,在直觀的想像中,”滾動“應該如下面的動圖演示那樣:

但是實現情況卻不是如此,因為現在中”螢幕“是不會動的,那麼”滾動“的只能是”文件“,所以真實的”滾動“應該像下面這樣:

 

也就是說,我們在將滾動條的滑塊”向下“拖動時,”文件“實際是”向上“拖動的。

在具體使用程式實現時,也就是改變”文件“的”Y“座標值。

2,比值計算。

在知道了滾動條的內部原理後,就需要一些比值的計算了,比如滾動條“滑塊”的長度、“滑塊”的拖動距離與“文件”位置改變的距離的比值等。

首先,如下圖所未,我加上了一些標識,以方便描述。

 

其中:

(1)

文件的長度=A。

螢幕的長度=可見文件部分的長度=B。

那麼,不可見的文件長度=A-B=C。

(2)

滾動條的長度=D。

滑塊的長度=E。

滑塊可拖動的範圍=D-E=F。

(3)

在實現的時候,我們已經的量有:A(文件長度)、B(螢幕長度)、D(滾動條長度),而E(滑塊長度)是不知道的,是計算出來的。

在日常使用過程中我們也會發現,如果要顯示的內容越多,滑塊的長度越短。

其中的比例關係如下:

B/A=E/D

可得:E=D*B/A。

此時我們已經得到了滑塊的長度,這是一個非常重要的基本量。

得到E(滑塊長度)後,F(滑塊可移動範圍)也可以得到:

F=D-E。

(4)

因為“螢幕”已經顯示了一部分的“文件”,所以真正需要通過滾動條去檢視的“文件”部分是C(不可見文件長度):

C=A-B。

由此,我們可以得到一個重要的比值關係:

n*F(滑塊拖動範圍)=m*C(不可見文件長度)

(注:加上n、m是因為F與C並不是1:1的關係)

n/m=C/F

這個比值我們使用X代替,即:

X=C/F=(A-B)/(D-E)。

(5)

為什麼要計算這個比值,因為我們在使用滾動條時,並不是去考慮滑鼠是向下拖動還是向上拖動,以及拖動了多少距離。而是隻需要知道滾動條的“滑塊”的位置,更準確的說是“滑塊”頂端距滾動條頂端的距離,如下圖所示,我們所需要知道的只是下圖中F(注意:此F不是上面中的F)的值而已。

通過F的值,我們計算出B的值,然後再使“文件”向上移動B的距離,這就是滾動條的使用方法。

那麼,怎麼通過F來得到B就非常重要了,這就需要一個比值,通過這個比值將F轉換為B,這個比值就是上面的X。

通過X,我們可以得到以下資訊:

a,拖動滾動條時,文件應該“上移”的距離B:

B=F*X。

b,文件位置改變後,滾動條所處的位置F:

F=B/X。


三、開始實現

(一)前期準備。

由於本篇實現的滾動條控制元件(LScrollBar)是參考前篇的LTrackBar來實現的,所以此處僅作提綱用,具體操作見前篇。

新建類:LScrollBar.cs

新增繼承:Control(需要新增引用:System.Windows.Forms.dll)

修改可訪問性為:public

(二)新增控制元件屬性

由於本控制元件和前篇的LTrackBar有極大的相似性,所以一些屬性也可以直接拿來使用。這也是“複用”的一種。

1,滾動條背景顏色

 

2,滑塊顏色

 

3,滑鼠進入滾動條後滑塊顏色

這是一種提示顏色,像一些軟體、網頁的滾動條在平時是一種顏色,在滑鼠處於滾動條上方時又是一種顏色,本屬性就是實現的這種效果。

 

4,滾動條是圓角還是直角

 

5,滾動條方向

此處和LTrackBar不一樣,對於滾動條(LScrollBar)而言,只有兩種方向:水平、垂直。所以我們需要新建一個方向列舉,為了避免與LTrackBar方向列舉衝突,我們將列舉命名為:OrientationScrollBar

在改變滾動條的方向時,下面的程式碼會自動交換滾動條的寬和高。

6,滾動條尺寸

這裡的尺寸是指滾動條(LScrollBar)的寬度(在垂直方向時)或高度(在水平方向時)。

為了支援此屬性,還需要設定控制元件使之只能修改高度(在垂直方向時)或寬度(在水平方向時),就需要重寫SetBoundsCore。

在重寫了SetBoundsCore後,在設計器介面我們便只能調整控制元件的寬或高。

7,“文件”長度

這裡使用“文件”這個說法是源自MFC,本處仍沿用其說法。其指的是所要顯示的總長度。

這裡用“長度”是方便說明,不然就要在垂直狀態時說“高度”,水平狀態時說“寬度”,太過繁瑣。

 

在設定了“文件”長度時,我們呼叫了兩個方法:pInit()和pChangeSliderLocation()。

其中pInit()的作用是初始化各個比例、計算滑塊長等。pChangeSliderLocation()的作用是改變滑塊的位置。其程式碼如下:

8,“頁面”長度

指示視窗可顯示的長度。

 

9,滑塊位置

這裡滑塊的位置指的時滑塊的頂端相對於滾動條頂端的距離。

這裡我將其設定為公共只讀,是因為這個屬性更多的是用來檢視,如果直接修改還需要計算比例什麼的,而為了計算比例,就需要額外提供一些額外的值以支援其計算,很麻煩,所以就不讓使用者去直接修改。如果想修改滑塊的位置,則使用下面的“顯示位置”屬性去設定。

 

10,顯示位置(正數)

這裡的顯示位置就是“文件”頂端與顯示視窗的距離。

以垂直狀態為例,就是其Y座標,因為預設情況下,系統的座標方向是向右為正、向下為正。而其原點就是顯示視窗的左上角,所以“文件”的Y座標值便是個負數,為了方便計算和處理,我們將以正數的方式進行處理。

在使用者設定了此屬性時,我們會自動進行相應的計算,並改變滑塊的位置(即上面的“滑塊位置”屬性的值),這樣對使用者而言,只需要知道“文件”的位置就行了,這樣當其設定了文件的位置後,滑塊的位置會自動改變。

11,滑塊可移動距離

本屬性是公共只讀,以備某些情況下使用。

 

12,滑塊長度

本屬性是公共只讀,以備某些情況下使用。

 

13,滑塊最小長度

平常狀態下,滾動條是支援滑鼠按在滑塊上拖動的,所以滑塊的長度就不能太短。當然如果使用滾動條時僅是作為顯示用,而不需要操作,則可以將滑塊的最小長度設定為0。

 

14,滾動間隔距離

本屬性是為了在使用滑鼠滾輪上下滾動時,和按上下左右鍵時,滑塊每次移動的距離。

這個距離是“文件”角度下的距離,不是滑塊真實移動的距離,這樣設定的原因是方便使用者按需要設定,比如其想實現一個ListBox,每次按鍵都是一行的高度,此時就可以將本屬性設定為那一行的高度值。

 

(三)新增事件

對於本滾動條(LScrollBar)而言,只需要一個事件,那就是滑塊發生了滾動時的事件。

當然,估計很多人在初次實現時,會想到有很多事件,像點選了滑塊事件、點選了滑塊上面空白部分事件、拖動事件等等。但是我們要記得之前所說的滾動條的工作方式:只需要知道滑塊的位置即可。前面事件的結果都是滑塊的位置發生了改變,所以可以歸到一個事件裡面。

 

(四)重寫方法

1,OnPaint

OnPaint是實現效果的根本,不過本次的實現內容比LtrackBar要簡單很多,總的來說就是先畫一個背景條,再畫一個滑塊。

 

2,OnMouseEnter

因為我們要實現滑鼠處於滾動條上方時滑塊變色,所以我們要將滑塊的顏色設定為屬性:“滑鼠進入滾動條後滑塊顏色”的值,並使控制元件發生重繪。

 

3,OnMouseLeave

同上,我們在滑鼠離開滾動條後,將滑塊的顏色設定為屬性:“滑塊顏色”的值。

 

4,OnMouseUp

 

5,OnMouseDown

在這裡,我們需要確定下滑鼠點選處的位置:點在了滑塊上、點在了滑塊上方、點在了滑塊下方。

根據日常使用經驗,在點選非滑塊上時,是有相應的效果的,一般而方點選滑塊上方空白處則代表“上一頁”,就是減去屬性“頁面長度”的值。同理,點選滑塊下方的空白處則代表“下一頁”,就是加上屬性“頁面長度”的值。

 

6,OnMouseMove

這裡實現的是當滑鼠點在了滑塊上時,按住滑鼠拖動。

在上面的OnMouseDown中,我們額外計算了兩個值:fAbove、fBelow,其作用便是為了在此時計算滑塊的位置。

具體如下圖演示:

7,OnMouseWheel

我們想實現滾動滑鼠滾輪鍵(中鍵)時滑塊跟著上下滾動,便需要重寫本方法。

其中需要用到事件的一個屬性:Delta,其MSDN的說明如下:

 

在滾輪向下滾動時,Delta的值為負數;滾輪向上滾動時,Delta的值為正數。

根據上面MSDN的解釋,我們只需要知道滾輪是向上滾動還是向下滾動就行了。根據滾輪是向上滾動還是向下滾動,將顯示位置的值加上或減去屬性:“滾動間隔距離”的值。

 

8,ProcessDialogKey

本方法中,主要是為了實現按滑鼠箭頭鍵時執行相當的操作。

在滾動條是垂直狀態時,按上箭頭鍵,滑塊向上滾動;按下箭頭鍵,滑塊向下滾動。

在滾動條是水平狀態時,按左箭頭鍵,滑塊向左滾動;按右箭頭鍵,滑塊向右滾動。

在按鍵時,是將顯示位置的值加上或減去屬性“滾動間隔距離”的值。

 

(五)新增雙緩衝

為了避免拖動滑塊時、改變滾動條尺寸時滾動條閃爍,故在其建構函式中加上對雙緩衝的支援。

 

(六)新增預設事件

為了達到雙擊控制元件就自動實現僅有的一個事件:L_Scrolled,所以在類的最上方加上預設事件支援。

(七)其它說明

1,在”屬性“視窗中隱藏某屬性

在前面的屬性中,有不少屬性我除了加了Category和Description——這兩個的含意我在LTrackBar那篇講過,分別是分類和描述,還有一個“Browsable(false)”,如下圖所示,其作用是不在設計介面的“屬性”視窗中顯示。

在一些不可設定的屬性或者不想讓使用者直接通過屬性視窗設定的屬性,可以新增Browsable(false)以達到此目的。

當然,在程式碼介面,還是可以看到該屬性提示的。

 2,寫程式碼時方法、屬性顯示提示

如下圖那樣當滑鼠放上去時彈出相應的中文提示,寫程式碼時的智慧提示中顯示中文提示。

要想實現該效果,首先要在屬性或方法上輸入”///“,此時VS會自動補全,之後便可以新增想要的提示了。像上面的屬性都是這樣寫的。

不過只這樣寫的話在同一個解決方案中是可以顯示中文提示的,如果單獨引用生成的dll就沒有提示了,這時需要生成對應的xml幫助文件,這樣在引用該dll時,VS會自動載入對應的xml檔案,也就會有對應的提示了。

生成xml方法:選擇控制元件類庫屬性,在”生成“標籤頁中,勾選“XML檔案檔案”,VS會自動填入生成路徑,如果想生成到其他地方可以自行修改。

不過在單獨引用dll時要保證dll和xml檔案在同一目錄下。


 

四、效果演示

 在本節中,我們不止要演示滾動條LScrollBar的各種效果、特點,最主要的是演示一下怎麼如何使用LScrollBar。

我們會在一個panel中新增50個按鈕,然後通過操作滾動條還使這些按鈕上下移動。

首先,我們新建一WinForm程式,在上面新增如下幾個控制元件,其控制元件名如下:

接著,我們雙擊“載入列表”按鈕,在其方法中寫入以下程式碼。程式碼功能是往panel1中新增50個按鈕。

最後,我們雙擊滾動條lScrollBar1,在其方法中寫入以下程式碼。程式碼功能一是顯示當前滑塊的位置,以及當前所有按鈕距初始位置的距離;二是改變所有按鈕的位置,以實現滾動效果。

之後編譯並執行程式,其執行效果如下:


 

五、調整優化

 LScrollBar實現到當前這種程度,對我目前而言,以及對大多數需要使用自定義滾動條的地方而言都是足夠了的。但是,並不是沒有缺點或可優化的地方。

其中最主要的一點,就是無法直接代替系統控制元件自帶的滾動條,比如替換ListBox自帶的滾動條,替換TextBox自帶的滾動條等等。

之所以無法替換,是因為對ListBox、TextBox等自帶滾動條的控制元件而言,其控制滾動條的滾動和處理滾動條的滾動是通過Windows訊息去處理的,而LScrollBar並沒有攔截和處理這些滾動條訊息。

如果想使用LScrollBar替換ListBox、TextBox等控制元件自帶的滾動條,可以參考下面的思路:

首先要在LScrollBar中增加對滾動條訊息的處理,包括攔截和傳送。

然後,可以直接將LScrollBar的寬度調成和ListBox、TextBox自帶滾動條同樣的寬度,然後覆蓋上;

或者將ListBox、TextBox自帶滾動條隱藏掉,在旁邊放上LScrollBar。

通過上面的方法,應該就可以達到”自定義ListBox、TextBox控制元件的滾動條“的效果了。

注:鑑於篇幅及個人需要和使用場景,我並沒去實現上面的替換ListBox、TextBox滾動條的效果,只是從邏輯層面上驗證可以達到預期效果。

後續如果有需要,我會考慮寫一篇文章去實現一下這種效果。

 


 

六、結束語

通篇下來,會發現技術層面上算不上太難,難點在於突破常規的思維束縛。

可以看到,WinForm並非不能實現炫麗、更加現代化的效果,不過需要一些想像力支撐、並多付出一些的努力罷了。同樣的,像WPF、像Electron等WebUI,雖然本身就更加現代化,但是想到達到一定的炫麗效果、比較人性化介面,也是需要花費不小的精力的。並不是說使用了新的語言、框架就可以很輕鬆的、或者自動的實現想要的效果。

技術並沒有先進和落後,只有合適與不合適,因為各有優點與缺點、各有擅長與不擅長。

所以,對自己掌握的知識多抱有一些信心,釋放自己的想像力,在實踐中提升自己。


 

七、原始碼及工程下載

 https://files.cnblogs.com/files/lesliexin/02,LScrollBar.7z

 

相關文章