这次实现的换肤都是基于贴图换肤的,并不可以像QQ那样还可以调整色调甚至自定义图片为背景。如果您已经有过这方面的经验,下面的内容或许不一定适合你。
贴图换肤就是用不同的图片去画不同的地方的背景,最后形成了界面的一个整体样式外观。只要我们将每个背景图片的位置以及大小信息记录下来,并在换肤的时候加载这些图片和信息并将它们画到背景上去就能实现换肤了。很简单吧~~
最终的效果图:
换肤实现:
上面只是简单说了一下换肤的“原理”,下面这个换肤流程图或许能够帮助您更好理解它:
上面的这四个过程就对应了实际类中的四个主要方法:ReadIniFile,CaculatePartLocation,ReadBitmap和DrawBackground。我们一个一个来看:
ReadIniFile
这个方法主要用来读取皮肤配置信息。 什么?不知道配置信息长啥样?得了,那就给你看一眼吧。
这是个INI的配置文件,网上有很多方法教你怎么读取和写入INI了。我将它们封装成了ReadIniValue和WriteIniValue方法。读取出来的这些信息都放在各自的背景块变量中。这里所说的背景块就是前面一篇文章介绍到的将界面划分的九个区域,如果您不了解可以看看这个系列的前一篇文章。一个背景块就是一个类,这些类都继承于partbase基类。partbase类中就定义了配置文件中对应的几个变量和几个方法,具体的可以到源代码中查看这个类,不复杂。
private bool ReadIniFile(string skinFolder) { try { string filePath = skinFolder + "\\config.ini"; //顶部 _topLeft.Height = _topMiddle.Height = _topRight.Height = int.Parse(IniHelper.ReadIniValue(filePath, "Main", "Top_Height")); _topLeft.Width = int.Parse(IniHelper.ReadIniValue(filePath, "Main", "TopLeft_Width")); _topRight.Width = int.Parse(IniHelper.ReadIniValue(filePath, "Main", "TopRight_Width")); //底部 _bottomLeft.Height = _bottomMiddle.Height = _bottomRight.Height = int.Parse(IniHelper.ReadIniValue(filePath, "Main", "Bottom_Height")); _bottomLeft.Width = int.Parse(IniHelper.ReadIniValue(filePath, "Main", "BottomLeft_Width")); _bottomRight.Width = int.Parse(IniHelper.ReadIniValue(filePath, "Main", "BottomRight_Width")); //中部 _centerLeft.Width = int.Parse(IniHelper.ReadIniValue(filePath, "Main", "MiddleLeft_Width")); _centerRight.Width = int.Parse(IniHelper.ReadIniValue(filePath, "Main", "MiddleRight_Width")); minButton.Width = int.Parse(IniHelper.ReadIniValue(filePath, "Main", "MinButton_Width")); minButton.Height = int.Parse(IniHelper.ReadIniValue(filePath, "Main", "MinButton_Height")); minButton.XOffset = int.Parse(IniHelper.ReadIniValue(filePath, "Main", "MinButton_X")); minButton.Top = int.Parse(IniHelper.ReadIniValue(filePath, "Main", "MinButton_Y")); maxButton.Width = int.Parse(IniHelper.ReadIniValue(filePath, "Main", "MaxButton_Width")); maxButton.Height = int.Parse(IniHelper.ReadIniValue(filePath, "Main", "MaxButton_Height")); maxButton.XOffset = int.Parse(IniHelper.ReadIniValue(filePath, "Main", "MaxButton_X")); maxButton.Top = int.Parse(IniHelper.ReadIniValue(filePath, "Main", "MaxButton_Y")); closeButton.Width = int.Parse(IniHelper.ReadIniValue(filePath, "Main", "CloseButton_Width")); closeButton.Height = int.Parse(IniHelper.ReadIniValue(filePath, "Main", "CloseButton_Height")); closeButton.XOffset = int.Parse(IniHelper.ReadIniValue(filePath, "Main", "CloseButton_X")); closeButton.Top = int.Parse(IniHelper.ReadIniValue(filePath, "Main", "CloseButton_Y")); selectSkinButton.Width = int.Parse(IniHelper.ReadIniValue(filePath, "Main", "selectSkinButton_Width")); selectSkinButton.Height = int.Parse(IniHelper.ReadIniValue(filePath, "Main", "selectSkinButton_Height")); selectSkinButton.XOffset = int.Parse(IniHelper.ReadIniValue(filePath, "Main", "selectSkinButton_X")); selectSkinButton.Top = int.Parse(IniHelper.ReadIniValue(filePath, "Main", "selectSkinButton_Y")); return true; } catch { return false; } }
CaculatePartLocation
在这个方法中就是根据当前窗体的width和height,再加上读取到的配置信息计算各个背景块的大小和位置。请注意方法中计算的顺序,先是左上角然后右上角最后中间。因为为了实现窗体的可缩放,中间的宽度是可变的。然后是左下方右下方,最后才是中间。中间的内容区域被我放了一个panel,其大小是可以变化的,具体的大小位置要看计算的结果。panel的另一个作用就是实现放在里面的控件的布局更好设置而不必关心上下两个边框。
private void CaculatePartLocation() { //顶部 _topLeft.X = 0; _topLeft.Y = 0; _topRight.X = Width - _topRight.Width; _topRight.Y = 0; _topMiddle.X = _topLeft.Width; _topMiddle.Y = 0; _topMiddle.Width = Width - _topLeft.Width - _topRight.Width; //中间部分 _centerLeft.X = 0; _centerLeft.Y = _topLeft.Height; _centerLeft.Height = Height - _topLeft.Height - _bottomLeft.Height; _centerRight.X = Width - _centerRight.Width; _centerRight.Y = _topRight.Height; _centerRight.Height = Height - _topLeft.Height - _bottomLeft.Height; _centerMiddle.X = _centerLeft.Width; _centerMiddle.Y = _topMiddle.Height; _centerMiddle.Width = Width - _centerLeft.Width - _centerRight.Width; _centerMiddle.Height = Height - _topMiddle.Height - _bottomMiddle.Height; //底部 _bottomLeft.X = 0; _bottomLeft.Y = Height - _bottomLeft.Height; _bottomRight.X = Width - _bottomRight.Width; _bottomRight.Y = Height - _bottomRight.Height; _bottomMiddle.X = _bottomLeft.Width; _bottomMiddle.Y = Height - _bottomMiddle.Height; _bottomMiddle.Width = Width - _bottomLeft.Width - _bottomRight.Width; //按钮位置 if (MaximizeBox && MinimizeBox) // 允许最大化,最小化 { maxButton.Left = Width - maxButton.Width - maxButton.XOffset; minButton.Left = Width - minButton.Width - minButton.XOffset; selectSkinButton.Left = Width - selectSkinButton.Width - selectSkinButton.XOffset; } if (MaximizeBox && !MinimizeBox) //不允许最小化 { maxButton.Left = Width - maxButton.Width - maxButton.XOffset; selectSkinButton.Left = Width - selectSkinButton.Width - minButton.XOffset; minButton.Top = -60; } if (!MaximizeBox && MinimizeBox) //不允许最大化 { maxButton.Top = -60; minButton.Left = Width - maxButton.XOffset - minButton.Width; selectSkinButton.Left = Width - selectSkinButton.Width - minButton.XOffset; } if (!MaximizeBox && !MinimizeBox) //不允许最大化,最小化 { minButton.Top = -60; maxButton.Top = -60; selectSkinButton.Left = Width - selectSkinButton.Width - maxButton.XOffset; } if (!_showSelectSkinButton) { selectSkinButton.Top = -60; } closeButton.Left = Width - closeButton.Width - closeButton.XOffset; //内容panel位置大小 contentPanel.Top = _centerMiddle.Y; contentPanel.Left = _centerMiddle.X; contentPanel.Width = _centerMiddle.Width; contentPanel.Height = _centerMiddle.Height; }
ReadBitmap
这个方法是用来加载要使用皮肤的各个背景图片的,大家看代码就明白了,没什么好讲的。
private void ReadBitmap(string skinFolder) { //读取需要透明的颜色值 int r = int.Parse(IniHelper.ReadIniValue(skinFolder + "\\config.ini", "Main", "TransparentColorR")); int g = int.Parse(IniHelper.ReadIniValue(skinFolder + "\\config.ini", "Main", "TransparentColorG")); int b = int.Parse(IniHelper.ReadIniValue(skinFolder + "\\config.ini", "Main", "TransparentColorB")); Color trans = Color.FromArgb(r, g, b); TransparencyKey = trans; //透明处理 _topLeft.BackgroundBitmap = Image.FromFile(skinFolder + "\\TopLeft.bmp") as Bitmap; _topMiddle.BackgroundBitmap = Image.FromFile(skinFolder + "\\TopMiddle.bmp") as Bitmap; _topRight.BackgroundBitmap = Image.FromFile(skinFolder + "\\TopRight.bmp") as Bitmap; _centerLeft.BackgroundBitmap = Image.FromFile(skinFolder + "\\MiddleLeft.bmp") as Bitmap; _centerMiddle.BackgroundBitmap = Image.FromFile(skinFolder + "\\Middle.bmp") as Bitmap; _centerRight.BackgroundBitmap = Image.FromFile(skinFolder + "\\MiddleRight.bmp") as Bitmap; _bottomLeft.BackgroundBitmap = Image.FromFile(skinFolder + "\\BottomLeft.bmp") as Bitmap; _bottomMiddle.BackgroundBitmap = Image.FromFile(skinFolder + "\\BottomMiddle.bmp") as Bitmap; _bottomRight.BackgroundBitmap = Image.FromFile(skinFolder + "\\BottomRight.bmp") as Bitmap; minButton.ReadButtonImage(skinFolder + "\\MinNormal.bmp", skinFolder + "\\MinMove.bmp", skinFolder + "\\MinDown.bmp"); maxButton.ReadButtonImage(skinFolder + "\\MaxNormal.bmp", skinFolder + "\\MaxMove.bmp", skinFolder + "\\MaxDown.bmp"); closeButton.ReadButtonImage(skinFolder + "\\CloseNormal.bmp", skinFolder + "\\CloseMove.bmp", skinFolder + "\\CloseDown.bmp"); selectSkinButton.ReadButtonImage(skinFolder + "\\SelectSkinNormal.bmp", skinFolder + "\\SelectSkinMove.bmp", skinFolder + "\\SelectSkinDown.bmp"); }
DrawBackground
前面所有的东西都准备好了,现在就可以画背景了。在哪里调用?当然在OnPaint里面。每一次窗体变化都会调用这个函数。(不知道这种方式和直接拉个picturebox然后设置背景哪个好?这种直接画的方式会不会因为onpaint的频繁调用而受到影响?)
因为原来左上方的图标被背景图片遮住了,所以在这个方法中也就顺便将图标和标题画上去了。
private void DrawBackground(Graphics g) { if (_topLeft.BackgroundBitmap == null) //确认已经读取图片 { return; } #region 绘制背景 ImageAttributes attribute = new ImageAttributes(); attribute.SetWrapMode(WrapMode.TileFlipXY); _topLeft.DrawSelf(g, null); _topMiddle.DrawSelf(g, attribute); _topRight.DrawSelf(g, null); _centerLeft.DrawSelf(g, attribute); contentPanel.BackgroundImage = _centerMiddle.BackgroundBitmap; //中间的背景色用内容panel背景代替 _centerRight.DrawSelf(g, attribute); _bottomLeft.DrawSelf(g, null); _bottomMiddle.DrawSelf(g, attribute); _bottomRight.DrawSelf(g, null); attribute.Dispose(); //释放资源 #endregion #region 绘制标题和LOGO //绘制标题 if (!string.IsNullOrEmpty(Text)) { g.DrawString(Text, Font, new SolidBrush(ForeColor), ShowIcon ? new Point(_titlePoint.X + 18, _titlePoint.Y) : _titlePoint); } //绘制图标 if (ShowIcon) { g.DrawIcon(Icon, new Rectangle(4, 4, 18, 18)); } #endregion }
说完了主要方法,下面看看提供的几个属性:
这里想提的就是skinfolder这个属性。按照理想的样子这里选择的时候应该直接弹出已有皮肤的选项直接选择。但是问题是我没有找到在设计模式下读取程序所在目录的方法(设计模式下Application.StartupPath读取到的是vs的目录),所以只好采取这种方法让设计者选择皮肤目录。在设计的时候程序到这个目录下读取配置信息,实际运行的时候程序自动截取skinfolder这个属性中的皮肤名字,再通过application.startuppath读取皮肤。
一些细节
1.该窗体默认已经嵌入了一套皮肤(春色),所以即使您没有皮肤文件夹也能照样显示,只不过就一套皮肤罢了。
2.使用方法:项目中引用QLFUI.DLL,然后在要使用的类中将继承类由Form类改为QLFUI.Mainfrm即可。
3.因为前面的系列已经有了窗体详细的实现,所以换肤这里我只主要讲了下换肤的部分。窗体制作的细节就不再赘述了。