2020-11-05

[C#] (原创)一步一步教你自定义控件——04,ProgressBar(进度条)

一、前言

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

本篇的自定义控件是:进度条(ProgressBar)。

进度条的实现方式多种多样,主流的方式有:使用多张图片去实现、使用1个或2个Panel放到UserControl上去实现、重载系统进度条去实现等等。

本次所实现的进度条仍是使用GDI+去实现。当然,如果只是实现最基本的扁平化的进度条,那完全不需要再写本篇文章,因为直接小改下第一篇的LTrackBar就行了。

既然写这篇文章,就是实现不一样的、比较好玩和好看的进度条,如环形进度条、饼形进度条等等。

本篇使用的知识都是前几篇中已经讲过的,并没有新的技术。但是却附加了一些想像力,一些不拘一格、稍稍突破常规的想像力。

 

相信看完的你,一定会有所收获。

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


 

二、前期分析

(一)为什么需要自定义进度条?

系统自带的滚动条样式太过单调,同时不够"扁平化",所以便去实现自己的滚动条。

 

(二)实现目标

1,支持三种样式

(1)横条(Bar)

 

(2)圆饼(Pie)

 

(3)圆弧(Arc)

 

 

2,支持显示和隐藏百分比

(1)横条(Bar)

 

(2)圆饼(Pie)

 

(3)圆弧(Arc)

 

 

3,支持多种进度形状

(1)连续

 

(2)分段块

 

(3)两段式:先分段块再连续

 

(4)螺旋

(注:只有"横条"样式时才支持螺旋形状)

 

 

4,支持Marquee样式

当进度无法确定时,便需要使用Marquee样式,同系统进度条的"Marquee"样式类似。

不过支持更多的Marquee样式。

(1)左右摆动

 

(2)循环穿过

 

(3)往复(连续)

 

(4)划过(连续)

 

(5)往复(分段块)

 

(6)划过(分段块)

 

(7)螺旋

 

 

5,支持调整进度条各元素尺寸和颜色

(1)设置边框厚度和颜色

 

(2)设置背景大小和颜色

 

(3)设置进度绘制位置和颜色

 

(4)设置进度文本颜色

 

(5)设置弧线厚度(仅样式为"圆弧(Arc)"时有效)

 

(6)设置"分段块"厚度(仅进度条形状为"分段块"时有效)

 

 


 

三、进度条拆解

看了上面的实现目标,是不是感觉眼花缭乱、无从下手?

下面我就对进度条进行拆分讲解,一步一步来看上面的效果是怎么实现的。

 

(一)组成元素

进度条由三个部分组成,分别是:进度边框、背景

下面是各种样式下,三个组成部分的拆分。

1,横条(Bar)

 

2,圆饼(Pie)

 

3,圆弧(Arc)

 

 

(二)元素属性

进度条的三个组成元素,都有着各自相关的属性。

(注:因为"颜色"属性是必备的,所以不再赘述。)

1,边框

其相关属性为:边框的厚度

当"厚度"为0时,看起来进度条就没有边框;

当"厚度"超过控件高度的二分之一时,整个进度条就变成了边框的颜色。

(注:因为是先绘制"背景",再绘制"边框",所以是"整个进度条变成边框的颜色")

A,对于 "橫条(Bar)",其边框厚度如下图所示:

B,对于"圆饼(Pie)"、"圆弧(Arc)",其边框厚度如下图所示:

2,背景

其相关属性为:背景的大小

背景的大小就是背景绘制的范围,这里用一个自造词"收缩宽度"来进行描述。

当"收缩宽度"为0时,背景大小=控件本身大小;

当"收缩宽度"为x时,背景宽度=控件宽度-2*x,背景高度=控件高度-2*x;

当"收缩宽度"超过控件高度的二分之一时,背景高度=控件高度-2*(控件高度/2)=0,此时进度条将没有了背景。

A,对于 "橫条(Bar)",其收缩宽度如下图所示:

B,对于"圆饼(Pie)"、"圆弧(Arc)",其收缩宽度如下图所示:

3,进度

其相关属性有:进度绘制范围、进度样式

A,进度绘制范围

顾名思义,就是进度可绘制的范围,这里用"绘制边框距离"来进行描述。

当"绘制边框距离"为0时,进度的绘制范围=控件本身大小;

当"绘制边框距离"为x时,进度的绘制起点:(x,x),绘制终点:(控件宽度-x,控件高度-x),所以绘制范围=(控件宽度-2*x,控件高度-2*x);

当"绘制边框距离"超过控件高度的二分之一时,进度的绘制高度将为0,也代表着没有进度。

A,对于 "橫条(Bar)",其绘制边框距离如下图所示:

B,对于"圆饼(Pie)",其绘制边框距离如下图所示:

C,对于"圆弧(Arc)",其绘制边框距离如下图所示:

 

B,进度样式

进度样式就是前节实现目标中各种进度的样式,有连续的、有分段块的、有螺旋的等等。

这此样式大部分一看都知道如何实现的。

比如"连续",在"橫条(Bar)"样式中,就是填充一个矩形;在"圆饼(Pie)"中,就是填充一个扇形;在"圆弧(Arc)"中,就是画一段弧线。

比如"分段块",在"橫条(Bar)"样式中,就是等间隔填充多个矩形;在"圆饼(Pie)"中,就是等间隔填充一个扇形;在"圆弧(Arc)"中,就是等间隔画一段弧线。

在这里,我将详细讲解一个特殊的形状:"螺旋",因为"螺旋"样式除了不太能直观想像出来之外,还有不少细节需要处理。

因为只有"橫条(Bar)"样式进度条有"螺旋"样式,所以接下来的讲解都是以"橫条(Bar)"为基础进行讲解的。

同时,为了避免干扰,前面的两个属性"边框"和"背景"都将保持默认值,即:没有边框,背景大小=控件大小。"进度绘制范围"也是默认值,即绘制范围=控件大小。

(1)"螺旋"样式实现讲解

A,进度明确时

即进度是已知且确定的,比如5%,33%等等。其外观样式如下:

 

其示意图如下:

 

其中蓝色的平行四边形就是"螺旋",其本身是一个背景透明、上有多个蓝色平行四边形的图片。

进度的增减实质上就是这个图片在控件上的左右移动。

那么,这个图片要和控件大小相等,特别是宽度相等。但是在使用GDI+去生成这个图片时,却不能让图片宽度与控件宽度相等。

我们在往图片绘制平行四边行时,是从右往左依次绘制的,之所以从右侧开始绘制,是为了保证进度条的右侧样式是固定的,固定在一个比较美观的状态。因为在进度变化时,即图片左右移动时,我们目光的焦点是在右侧,所以一个固定的右侧样式就比较重要,否则当控件宽度变化时,右侧的样式就随之变化。

当图片宽度与控件宽度相等时,会出现下面这种情况,即进度条的最右侧下方有空白。如下图所示:

 (其中上面图形是实际绘制图片,下面图形是将图片截取,和进度条宽度相等后样式)

所以我们令图片的宽度=控件的宽度+1个平行四边行的宽度。在绘制完图片后,我们从中截取出一个和控件宽度一样的图片,这样,整体的样式就比较好看。如下图所示:
 (其中上面图形是实际绘制图片,下面图形是将图片截取,和进度条宽度相等后样式)

 

综上,就是按照下图所示的5步依次实现:

 

 

B,进度未知时

即进度是不确定的,就像系统进度条的Marquee样式那样。其外观样式如下:

 

同上,上方仍是一个背景透明、上有多个蓝色平行四边形的图片。上图的效果就是这个图片在不段的循环移动。

具体示意如下,图片不断向右侧移动,当右侧超过一个平等四边行时,图片恢复原位,然后不断循环。

 

通过上方的示意图,我们发现一个特点,就是图片的宽度不再等于(控件的宽度+1个平行四边行的宽度),而是等于(控件的宽度+2个平行四边行的宽度)。原因如下:

在画示意图时,为了方便直观查看,平行四边形正好是两个相对的直角三角行,而实际绘制时,很少会有这种样式,大多都是两个相对的钝角三角行组成的平等四边形的样子,如下图所示:

 

这种情况下,如果图片的宽度=控件的宽度+1个平行四边行的宽度,那么在移动到最右侧时和复位时都会出现额外的空白,如下图所示:

 

所以,才会令:图片的宽度=控件的宽度+2个平行四边行的宽度。

 

(三)其它属性

除了前面的与进度条元素直接相关的属性外,还有一些其他属性,这些属性都是在某种特定样式下才起有作用。

1,弧线宽度

在"圆弧(Arc)"样式的进度条中,"进度"就是一段圆弧,所以在其他属性外,还要有"弧线本身宽度"这样一个属性。

在默认情况下,弧线宽度=控件宽度/10,因为当进度达到100%时,弧线就变成了圆环,此时看起来有弧线的地方占控件宽度的五分之一,是一个比较常规的宽度。

通过调用弧线宽度,可以实现一些特殊的效果。

 

 

2,分段块宽度

在进度样式为"分段块"时,为了支持不同的分段宽度,所以要有本属性。

为了绘制出更好的效果,"分段块宽度"不仅仅是有颜色的那部分的宽度,而是为:有颜色部分的宽度+两个颜色间隔。

经过调试,发现当(颜色:间隔=2:1)时,外观比较好看和自然。

所以,一个"分段块宽度"=2/3有颜色宽度+1/3无颜色宽度。

因为"橫条(Bar)"、"圆饼(Pie)"、"圆弧(Arc)"都支持"分段块"。所以在"橫条(Bar)"中,"分段块宽度"指的就是宽度;而在"圆饼(Pie)"、"圆弧(Arc)"中,"分段块宽度"指的是角度。

 


 

四、开始实现

(一)前期准备。

此处仅作提纲,具体操作见前篇。

新建类:LProgressBar.cs

添加继承:Control(需要添加引用:System.Windows.Forms.dll)

修改可访问性为:public

(二)添加属性

1,进度值

进度值指的是当前的进度,这里将进度范围固定为:0~100

这样的好处是方便计算和绘制,使用时也更加直观。

2,边框厚度

详细见前节中的讲解。

(注:我是先写完的代码,然后再写文章,有时在写文章时会发现属性的命名不太正规,比如"边框厚度"这个属性,用"BorderSize"更好,但是我在写代码时我想的是"环绕一圈的线",所以就成了下面图中的"SurroundLineSize",但是在写文章时发现不好描述,就改成了"边框厚度",不过再去修改代码意义不大,重要的是技术思想本身。下同。)

3,边框颜色

顾名思义,就是边框的颜色。

4,背景收缩宽度

详细见前节中的讲解。

5,背景颜色

6,进度绘制范围边缘

详细见前节中的讲解。

7,弧线厚度

详细见前节中的讲解。

8, 进度颜色

进度颜色是进度本身的颜色。

9,分段块宽度

详细见前节中的讲解。

10,进度样式

进度样式描述的是进度本身的外观,如连续、分段块、螺旋等等。

进度样式是一个枚举,明细如下:

11,进度条类型

进度条类型指的是进度条的样式外观,如横条、圆饼、圆弧等。

进度条类型是一个枚举,明细如下:

12,进度条模式

进度模式指的进度是明确的还是不明确的。

进度明确,比如当前进度是1%、45%等等。

进度不明确,比如加载某个文件时一直在加载,但不知道加载了多少。也就是系统进度条的Marquee样式。

进度条模式是一个枚举,明细如下:

13,Marquee循环一遍时间

 Marquee样式时动画的快慢。

 

14,Marquees样式

Marquee样式动画。

 

该属性是一个枚举,明细如下:

 

15,Marquee类型

Marquee样式同样是动画,所以支持匀速和缓入缓出效果。

 

该属性是一个枚举,明细如下:

 

16,进度文本类型

可以设置进度条是否显示文本,以及是否显示百分比。

 

该属性是一个枚举,明细如下:

 

17,进度文本颜色

18,构造函数

这里的构造函数中,除了使用了之前文章中固定的"双缓冲"外,还额外设置了几个属性,这几个属性是为了让控件支持透明,所以下面才可以令背景色和前景色为透明色。

这种情况下,整个控件就相当于一个完全透明的画布,可以任由我们绘制。

(三)重写方法

对于本进度条而言,只是用来指示进度信息,所以不需要事件。只需要重写最基本的OnPaint方法就行了。

不过,由于在OnPaint中要绘制的样式以及类型太多,所以代码量较大,但是代码难度不大。我会先将OnPaint代码全部放上,然后着重讲一下实现的原理。

而Marquee样式本质上就是一个动画效果,循环的绘制一些图形或改变图形的大小位置,所以我们使用一个定时器来进行触发,定时的改变图形或图形大小位置。 

protected override void OnPaint(PaintEventArgs e){ base.OnPaint(e); e.Graphics.SmoothingMode = SmoothingMode.HighQuality; //进度实际绘制宽度、高度 float fRealWidth = Width - _DrawMargin * 2; float fRealHeight = Height - _DrawMargin * 2; float fRealLeftTop = _DrawMargin - 0.5f; if (fRealLeftTop < 0) fRealLeftTop = 0; //上下左右各留0.5个像素,防止切边 bool bDrawSurroundLine = _SurroundLineSize > 0 ? true : false; bool bDrawInner = _ShrinkSize * 2 < Math.Min(Width, Height) ? true : false; float fSurroundLineSizeF = Convert.ToSingle(_SurroundLineSize); RectangleF rectInner = new RectangleF(_ShrinkSize, _ShrinkSize, (Width - 1) - _ShrinkSize * 2, (Height - 1) - _ShrinkSize * 2); RectangleF retcSurrondLine = new RectangleF(fSurroundLineSizeF / 2, fSurroundLineSizeF / 2, (Width - 1) - fSurroundLineSizeF, (Height - 1) - fSurroundLineSizeF); //弧线宽度为总宽度的十分之一,因为是圆弧,所以最终看起来是五分之一 float fSurroundArcSize = _ArcSize > 0 ? _ArcSize : Width / 10; RectangleF retcSurrondArc = new RectangleF(fSurroundArcSize / 2 + fRealLeftTop, fSurroundArcSize / 2 + fRealLeftTop, (fRealWidth - 1) - fSurroundArcSize, (fRealHeight - 1) - fSurroundArcSize); SolidBrush sbInner = new SolidBrush(_ShrinkColor); SolidBrush sbFore = new SolidBrush(L_SliderColor); SolidBrush sbText = new SolidBrush(_TextColor); Pen penSurrondLine = new Pen(_SurroundLineColor, fSurroundLineSizeF); Pen penSurroundArc = new Pen(L_SliderColor, fSurroundArcSize); //滑块宽度为总宽度的五分之一 float fSlider = Convert.ToSingle(fRealWidth) / 5; if (_ProgressType != ProgressType.Bar) {  //360/5  fSlider = 72; } float fRange = 0; if (L_MarqueeStyle == MarqueeStyle.Swing) fRange = fRealWidth - fSlider; else if (L_MarqueeStyle == MarqueeStyle.Cross) fRange = fRealWidth + fSlider; else fRange = fRealWidth; float fBlockSize = _BlockSize; if (fBlockSize < 3) fBlockSize = 3.0f; if (_ProgressType == ProgressType.Bar) {  if (fBlockSize > fRealWidth) fBlockSize = fRealWidth; } else {  if (fBlockSize > 360.0f) fBlockSize = 360.0f; } #region Bar if (_ProgressType == ProgressType.Bar) {  //画内部  if (bDrawInner)  {   e.Graphics.FillRectangle(sbInner, rectInner);  }  //画线  if (bDrawSurroundLine)  {   e.Graphics.DrawRectangle(penSurrondLine, retcSurrondLine.X, retcSurrondLine.Y, retcSurrondLine.Width, retcSurrondLine.Height);  }  if (_ProgressMode == ProgressMode.Known)  {   #region Continuous   if (_ProgressStyle == ProgressStyle.Continuous)   {    //画进度    float fProgress = Convert.ToSingle(_Value) * fRealWidth / 100;    e.Graphics.FillRectangle(sbFore, _DrawMargin - 0.5f, fRealLeftTop, fProgress, fRealHeight);   }   

No comments:

Post a Comment