[C#] (原创)一步一步教你自定义控件——02,ScrollBar(滚动条)

一、前言

技术没有先进与落后,只有合适与不合适。

本篇的自定义控件是:滚动条(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

 

posted @ 2020-08-14 13:19  leslie_xin  阅读(10796)  评论(13编辑  收藏  举报