最近到年底了,比较忙,C# WinForm控件开发和美化 也好长时间没有更新了。这段时间主要是在尝试着做一套自己的Winform皮肤控件。现在看到的客户端程序中,我发现的皮肤做得最牛的是迅雷7(只是说皮肤),再就是QQ了。看着迅雷7的界面思考了半天(真正的半天),我决定放弃模仿迅雷7的念头,难度太大了。看着QQ的界面,觉得我做聊天软件的可能性不大,即使我把这个皮肤做出来了(当然难度也很大),在实际的项目中很难用到。后来有一天去机房的时候看到一个管理人员在用Foxmail,我看到这个软件的界面就眼前一亮:界面效果还可以,控件效果比较简单,做换肤的话应该比较容易实现。回来后赶紧下了一个Foxmail装上,一边研究一边开发,花了2周的时间作出了一套仿Foxmail的皮肤控件,感觉相当的兴奋,今天事情不多,就发到园子里和大家分享一下。顺便也写一下设计思路,希望对有这方面需求的人有所帮助。
在看过这篇文章后,如果您有什么好的意见和建议,请在下面留言。
一、先看一下效果图:
二、特点介绍
1、实现了调色和更换底纹两种基本的换肤功能,具体操作方式在界面中应该很清楚的就能看出来了。
2、重新开发了几个具有换肤功能的控件,主要有Panel、Trackbar、TabControl和几个Strip控件。
3、在换肤和调整窗体大小的时候没有很明显的闪烁问题。(电脑的配置是奔四处理器,4G内存,集成显卡。)
三、思路说明
1、关于颜色的问题
皮肤控件要处理的颜色问题有两类,界面的颜色和图片的颜色。
先来说一下界面的颜色,在这套皮肤控件中,只使用了一个基准色BaseColor,其它的颜色都是通过这个BaseColor计算出来的。颜色的计算使用HSL颜色比较方便,所以需要
先把RGB颜色转换为HSL颜色进行处理,处理方式很简单,就是加大或减小颜色的L值,以实现加深或加浅颜色的目的,然后在将HSL颜色转换回RGB颜色在界面上使用。具体的颜色
转换类如下:
注:这个类有些误差,但不是很大。
public class HSLColor
{
private int _alpha = 255;
private double _hue = 0d;
private double _saturation = 1d;
private double _lightness = 1d;
public HSLColor()
{
}
/// <summary>
/// 用一个RGB颜色构造HSLColor。
/// </summary>
/// <param name="color"></param>
public HSLColor(Color color)
{
_alpha = color.A;
FromRGB(color);
}
/// <summary>
/// 用色彩、饱和度、亮度构造HSLColor。
/// </summary>
/// <param name="hue">色彩。</param>
/// <param name="saturation">饱和度。</param>
/// <param name="lightness">亮度。</param>
public HSLColor(
int hue,
double saturation,
double lightness)
{
Hue = hue;
Saturation = saturation;
Lightness = lightness;
}
public double Hue
{
get { return _hue; }
set
{
if (value < 0)
{
_hue = value + 360;
}
else if (_hue > 360)
{
_hue = value % 360;
}
else
{
_hue = value;
}
}
}
public double Saturation
{
get { return _saturation; }
set
{
if (_saturation < 0)
{
_saturation = 0;
}
else
{
_saturation = Math.Min(value, 1d);
}
}
}
public double Lightness
{
get { return _lightness; }
set
{
if (_lightness < 0)
{
_lightness = 0;
}
else
{
_lightness = Math.Min(value, 1d);
}
}
}
public Color Color
{
get { return ToRGB(); }
set { FromRGB(value); }
}
public static bool operator ==(HSLColor left, HSLColor right)
{
return (left.Hue == right.Hue &&
left.Lightness == right.Lightness &&
left.Saturation == right.Saturation);
}
public static bool operator !=(HSLColor left, HSLColor right)
{
return !(left == right);
}
public override bool Equals(object obj)
{
if (obj == null && !(obj is HSLColor))
{
return false;
}
HSLColor color = (HSLColor)obj;
return this == color;
}
public override int GetHashCode()
{
return Color.GetHashCode();
}
public override string ToString()
{
return string.Format(
"HSL({0:f2}, {1:f2}, {2:f2})",
Hue, Saturation, Lightness);
}
private void FromRGB(Color color)
{
double r = ((double)color.R) / 255;
double g = ((double)color.G) / 255;
double b = ((double)color.B) / 255;
double min = Math.Min(Math.Min(r, g), b);
double max = Math.Max(Math.Max(r, g), b);
double distance = max - min;
_lightness = (max + min) / 2;
if (distance == 0)
{
_hue = 0;
_saturation = 0;
}
else
{
double hueTmp;
_saturation =
(_lightness < 0.5) ?
(distance / (max + min)) : (distance / ((2 - max) - min));
double tempR = (((max - r) / 6) + (distance / 2)) / distance;
double tempG = (((max - g) / 6) + (distance / 2)) / distance;
double tempB = (((max - b) / 6) + (distance / 2)) / distance;
if (r == max)
{
hueTmp = tempB - tempG;
}
else if (g == max)
{
hueTmp = (0.33333333333333331 + tempR) - tempB;
}
else
{
hueTmp = (0.66666666666666663 + tempG) - tempR;
}
if (hueTmp < 0)
{
hueTmp += 1;
}
if (hueTmp > 1)
{
hueTmp -= 1;
}
_hue = (int)(hueTmp * 360);
}
}
private Color ToRGB()
{
byte r;
byte g;
byte b;
if (_saturation == 0)
{
r = (byte)(_lightness * 255);
g = r;
b = r;
}
else
{
double vH = ((double)_hue) / 360;
double v2 =
(_lightness < 0.5) ?
(_lightness * (1 + _saturation)) :
((_lightness + _saturation) - (_lightness * _saturation));
double v1 = (2 * _lightness) - v2;
r = (byte)(255 * HueToRGB(v1, v2, vH + 0.33333333333333331));
g = (byte)(255 * HueToRGB(v1, v2, vH));
b = (byte)(255 * HueToRGB(v1, v2, vH - 0.33333333333333331));
}
return Color.FromArgb(r, g, b);
}
private double HueToRGB(double v1, double v2, double vH)
{
if (vH < 0)
{
vH += 1;
}
if (vH > 1)
{
vH -= 1;
}
if ((6 * vH) < 1)
{
return v1 + (((v2 - v1) * 6) * vH);
}
if ((2 * vH) < 1)
{
return v2;
}
if ((3 * vH) < 2)
{
return v1 + (((v2 - v1) * (0.66666666666666663 - vH)) * 6);
}
return v1;
}
}
再 来说一下关于图片颜色的处理,图片颜色的处理过程主要是处理底纹和其它一些需要改变颜色的界面图片的颜色,图片颜色处理技术叫图片颜色校正。在.NET中有一个5*5的
调色矩阵ColorMatrix,可以使用这个矩阵很方便并且高效的校正图片的颜色。如果想进一步了解颜色校正和ColorMatrix,可以自己搜索一下,网上比我说得清楚。这里只提供一个简单的
使用ColorMatrix进行颜色校正的例子:
Color color = SkinDrawMethod.ColorManage.BaseColor;
float R = (float)Convert.ToDouble(color.R) / 255;
float G = (float)Convert.ToDouble(color.G) / 255;
float B = (float)Convert.ToDouble(color.B) / 255;
float[][] matrixItems ={
new float[] {R, 0, 0, 0, 0},
new float[] {0, G, 0, 0, 0},
new float[] {0, 0, B, 0, 0},
new float[] {0, 0, 0, 0.38f, 0},
new float[] {0, 0, 0, 0, 1}};
ColorMatrix colorMatrix = new ColorMatrix(matrixItems);
ImageAttributes imageAtt = new ImageAttributes();
imageAtt.SetColorMatrix(colorMatrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);
int iWidth = bitmap.Width;
int iHeight = bitmap.Height;
graphics.DrawImage(bitmap, rect, 0.0f, 0.0f, iWidth, iHeight, GraphicsUnit.Pixel, imageAtt);
用这段代码调整出来的颜色还是有点偏,效果没有Foxmaile那样完美,但基本上说得过去。如果大家有一个更好的矩阵参数设置方案,请麻烦告诉我一下。
2、关于控件的设计
为了实现换肤功能,就要设计一套支持换肤功能的控件。在这套控件里,我定义了一个换肤的接口,让需要实现换肤功能的控件都实现这个接口,在进行换肤操作的时候,调用
每个控件的换肤接口就可以实现对该控件的换肤。关于控件的设计,每个控件的换肤方式都不相同,这一部分的话可以查看我正在写的系列文章C# WinForm控件开发和美化。界面换肤
只是将这些控件设计和美化的方案综合起来,所以控件设计才是根本。在有时间的时候,我会继续更新这部分的文章。
3、关于界面闪烁的问题
.NET窗体的界面闪烁是一个老生常谈的问题,每个做控件开发的人都会遇到。除了要有高效的代码外还可以用下面这些方案来解决闪烁的问题。
a、使用双缓存,将窗体的DoubleBufferde设置为True。
b、在控件的构造函数中加入下面的代码:
base.SetStyle(ControlStyles.OptimizedDoubleBuffer |
ControlStyles.AllPaintingInWmPaint |
ControlStyles.ResizeRedraw |
ControlStyles.UserPaint, true);
base.UpdateStyles();
当然这段代码只能加在由自己绘制界面的控件中。
上面的两种方法我相信大家都用过,但会发现自己的界面还是闪得厉害,这是为什么呢?
c、将Main函数中的这行语句注释掉 Application.EnableVisualStyles();对于自绘控件,不需要启动可视样式。我的程序注释了这段代码后,闪烁的问题已经
明显降低了。
d、最重要的解决闪烁问题的方法,减少不必要的刷新。
这个可能说得太笼统了,我还是先举个例子来说明这个问题,比如说你设计的自定义控件有一个属性
private Color _backColor = Color.FromArgb(255, 255, 255);
你很可能设计这样的一个访问器来访问这个属性,这也是很多C#的书上教你的做法。
public Color BackColor
{
get { return _backColor; }
set
{
_backColor = value;
base.Invalidate();
}
}
在这样写的话就很有可能产生我所说的不必要的刷新,很简单,如果你写了下面的代码:控件名.BackColor = Color.FromArgb(255, 255, 255);控件是要进行一次刷新的,并且是刷新了整个
的控件区域,显然这是没用必要的。还有就在在拖动控件到窗体界面的时候,窗体的代码生成器会自动的生成一条语句 :控件名.BackColor = Color.FromArgb(255, 255, 255);在执行这条语句的时候
,显然又会刷新一次控件,这显然也是不必要的。正确的写法应该是这样的:
[DefaultValue(typeof(Color),"255,255,255")]
public Color BackColor
{
get { return _backColor; }
set
{
if (_backColor != value)
{
_backColor = value;
base.Invalidate(最好加一个Rectangle参数用来控制刷新的区域);
}
}
}
设置字段的默认值,这样在值不更改的时候,也就是_backColor = Color.FromArgb(255, 255, 255)的时候,代码生成器不会生成
控件名.BackColor = Color.FromArgb(255, 255, 255);这段代码,在给字段赋值前,先判断字段的值是否有变化,如果有变化的话在更新字段的值和刷新
控件。如果有必要的话,在base.Invalidate(最好加一个Rectangle参数用来控制刷新的区域)方法中最好指定刷新的区域,已减少不必要的刷新。
关于减少不必要的刷新还有一种方法就是将刷新过程统一到所有的变化处理之后来做,举个例子来说就是一个控件如果有多个字段的值需要改变的话,
就要在所有这几个字段的值改变后在刷新控件,而不是每改变一个值就刷新一次控件。当然,这个说起来简单,但实际中是不好实现的,但在编写代码的时候一定要
想想,那些刷新过程是可以合并的。
e、在更改界面大小和调整界面皮肤的时候强制关闭控件的刷新功能,直到所有的更改完成后,在重新刷新一次控件,这种方式对多控件窗体的闪烁问题很有效果。
具体的做法就是,在需要大量刷新控件前,对窗体发送一个SendMessage(toFreezeControl.Handle, WM_SETREDRAW, 0, 0)消息,这样就冻结了窗体和所有窗体
子控件的刷新功能。等刷新操作完成后,在对窗体发送一个SendMessage(toFreezeControl.Handle, WM_SETREDRAW, 1, 0);强制窗体和控件完成一次刷新。这样就实现
了在所有的更改完成后统一的进行刷新的要求。实践证明,这种方式能很有效的解决窗体在改变大小和换肤时是闪烁问题。
发送消息使用下面的Win32接口:
[DllImport("user32.dll")]
public static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam);
备注: 关于设计思路,就挑了几个我认为比较麻烦的地方说了一下,如果大家还有什么问题或好的建议,可以在下面直接留言!谢谢!