赞
踩
前言: CorePlex代码库 作为一个Visual Studio插件, 允许用户通过VS直接访问在线代码库。开发过程中我翻阅了很多网上的资料,也总结了一些技术要点,现写成系列文章,以飨读者。同时,里面某些技术也是我第一次使用,如有不对的地方,还请行家狠拍,欢迎大家指正~
闲话休絮,进入正题。从本篇文章开始,介绍 CorePlex 的窗体皮肤机制,以及简单的换肤功能。我们先来看看效果:
换一个皮肤看看:
需要实现的是圆角窗体+四周的阴影,要实现这个,大致的思路是这样的:先使用 Graphics 绘制一个 Bitmap,将需要的皮肤绘制成一个内存图,然后使用 Win32的API:UpdateLayeredWindow 将这个构造好的 Bitmap 绘制/更新到窗体上。我们来看看具体的实现吧。
第一部分,构造皮肤背景。
为了实现圆角以及四周的阴影,我将窗体背景划分成了九宫格的形式:
主要思路是:除 5 之外的其他部分,都作为窗体的边框和圆角来处理。而5这个部分,则作为圆角窗体的主体背景部分。1、3、7、9四个部分,作为圆角,我使用 PathGradientBrush来绘制扇形渐变,而2、4、6、8四个部分,作为边框,我使用 LinearGradientBrush来绘制线性渐变。
不多说,见代码:
///
///绘制四角的阴影///
///
/// 圆角区域正方形的大小
/// private voidDrawCorners(Graphicsg, SizecorSize)
{
/*
* 四个角,每个角都是一个扇面
* 画图时扇面由外弧、内弧以及两段的连接线构成图形
* 然后在内弧中间附近向外做渐变
*
* 阴影分为9宫格,5为内部背景图部分
* 1 2 3
* 4 5 6
* 7 8 9
*/Action DrawCorenerN = (n) =>
{
using(GraphicsPathgp = newGraphicsPath())
{
// 扇面外沿、内沿曲线的尺寸SizesizeOutSide = newSize(corSize.Width * 2, corSize.Height * 2);
SizesizeInSide = newSize(this.SkinOptions.CornerRadius * 2, this.SkinOptions.CornerRadius * 2);
// 扇面外沿、内沿曲线的位置PointlocationOutSide, locationInSide;
// 当圆角半径小于MinCornerRadius时,内沿不绘制曲线,而以线段绘制近似值。该线段绘制方向是从p1指向p2。Pointp1, p2;
// 渐变起点位置PointFbrushCenter;
// 扇面起始角度floatstartAngle;
// 根据四个方位不同,确定扇面的位置、角度及渐变起点位置switch(n)
{
case1:
locationOutSide = newPoint(0, 0);
startAngle = 180;
brushCenter = newPointF((float)sizeOutSide.Width - sizeInSide.Width * 0.5f,
(float)sizeOutSide.Height - sizeInSide.Height * 0.5f);
p1 = newPoint(corSize.Width, this.SkinOptions.ShadowWidth);
p2 = newPoint(this.SkinOptions.ShadowWidth, corSize.Height);
break;
case3:
locationOutSide = newPoint(this.Width - sizeOutSide.Width, 0);
startAngle = 270;
brushCenter = newPointF((float)locationOutSide.X + sizeInSide.Width * 0.5f,
(float)sizeOutSide.Height - sizeInSide.Height * 0.5f);
p1 = newPoint(this.Width - this.SkinOptions.ShadowWidth, corSize.Height);
p2 = newPoint(this.Width - corSize.Width, this.SkinOptions.ShadowWidth);
break;
case7:
locationOutSide = newPoint(0, this.Height - sizeOutSide.Height);
startAngle = 90;
brushCenter = newPointF((float)sizeOutSide.Width - sizeInSide.Width * 0.5f,
(float)locationOutSide.Y + sizeInSide.Height * 0.5f);
p1 = newPoint(this.SkinOptions.ShadowWidth, this.Height - corSize.Height);
p2 = newPoint(corSize.Width, this.Height - this.SkinOptions.ShadowWidth);
break;
default:
locationOutSide = newPoint(this.Width - sizeOutSide.Width, this.Height - sizeOutSide.Height);
startAngle = 0;
brushCenter = newPointF((float)locationOutSide.X + sizeInSide.Width * 0.5f,
(float)locationOutSide.Y + sizeInSide.Height * 0.5f);
p1 = newPoint(this.Width - corSize.Width, this.Height - this.SkinOptions.ShadowWidth);
p2 = newPoint(this.Width - this.SkinOptions.ShadowWidth, this.Height - corSize.Height);
break;
}
// 扇面外沿曲线RectanglerecOutSide = newRectangle(locationOutSide, sizeOutSide);
// 扇面内沿曲线的位置locationInSide = newPoint(locationOutSide.X + (sizeOutSide.Width - sizeInSide.Width) / 2,
locationOutSide.Y + (sizeOutSide.Height - sizeInSide.Height) / 2);
// 扇面内沿曲线RectanglerecInSide = newRectangle(locationInSide, sizeInSide);
// 将扇面添加到形状,以备绘制gp.AddArc(recOutSide, startAngle, 91);
if(this.SkinOptions.CornerRadius > MinCornerRadius)
gp.AddArc(recInSide, startAngle + 90, -91);
elsegp.AddLine(p1, p2);
// 使用渐变笔刷using(PathGradientBrushshadowBrush = newPathGradientBrush(gp))
{
Color[] colors = newColor[2];
float[] positions = new float[2];
ColorBlendsBlend = newColorBlend();
// 扇面外沿色colors[0] = this.SkinOptions.CornerColor[1];
// 扇面内沿色colors[1] = this.SkinOptions.CornerColor[0];
positions[0] = 0.0f;
positions[1] = 1.0f;
sBlend.Colors = colors;
sBlend.Positions = positions;
shadowBrush.InterpolationColors = sBlend;
// 上色中心点shadowBrush.CenterPoint = brushCenter;
g.FillPath(shadowBrush, gp);
}
}
};
DrawCorenerN(1);
DrawCorenerN(3);
DrawCorenerN(7);
DrawCorenerN(9);
}
///
///绘制上下左右四边的阴影///
///
///
///
/// private voidDrawLines(Graphicsg, SizecorSize, SizegradientSize_LR, SizegradientSize_TB)
{
Rectanglerect2 = newRectangle(newPoint(corSize.Width, 0), gradientSize_TB);
Rectanglerect4 = newRectangle(newPoint(0, corSize.Width), gradientSize_LR);
Rectanglerect6 = newRectangle(newPoint(this.Size.Width - this.SkinOptions.ShadowWidth, corSize.Width), gradientSize_LR);
Rectanglerect8 = newRectangle(newPoint(corSize.Width, this.Size.Height - this.SkinOptions.ShadowWidth), gradientSize_TB);
using(
LinearGradientBrushbrush2 = newLinearGradientBrush(rect2, this.SkinOptions.ShadowColor[1],
this.SkinOptions.ShadowColor[0], LinearGradientMode.Vertical),
brush4 = newLinearGradientBrush(rect4, this.SkinOptions.ShadowColor[1],
this.SkinOptions.ShadowColor[0], LinearGradientMode.Horizontal),
brush6 = newLinearGradientBrush(rect6, this.SkinOptions.ShadowColor[0],
this.SkinOptions.ShadowColor[1], LinearGradientMode.Horizontal),
brush8 = newLinearGradientBrush(rect8, this.SkinOptions.ShadowColor[0],
this.SkinOptions.ShadowColor[1], LinearGradientMode.Vertical)
)
{
g.FillRectangle(brush2, rect2);
g.FillRectangle(brush4, rect4);
g.FillRectangle(brush6, rect6);
g.FillRectangle(brush8, rect8);
}
}
好,到此为止,四周的圆角和渐变边框算是模拟出来了,然后就是将 5号为止的主背景绘制上去,以及添加一些高光、暗部等线条,突出质感:
///
///绘制主背景图///
/// private voidDrawMain(Graphicsg)
{
// 要显示的区域图像的大小RectangledestRect = newRectangle(0, 0,
this.Width - this.SkinOptions.ShadowWidth * 2 + 1,
this.Height - this.SkinOptions.ShadowWidth * 2 + 1);
// 建立一个临时的 bitmap,用于存放被圆角化的图像using(BitmapcorBg = newBitmap(destRect.Width, destRect.Height))
{
using(GraphicscorG = Graphics.FromImage(corBg))
{
corG.SmoothingMode = SmoothingMode.HighQuality;
// 创建圆角区域using(GraphicsPathgp = CreateRoundRect(destRect, this.SkinOptions.CornerRadius))
{
Regionre = newSystem.Drawing.Region(gp);
// 设置画布区域为圆角区域corG.IntersectClip(re);
Penp = newPen(this.SkinOptions.BorderColor);
p.Width = this.SkinOptions.BorderWidth;
p.Alignment = PenAlignment.Inset;
switch(this.SkinOptions.BackgroundLayout)
{
caseImageLayout.Center:
{
// 创建源图上的截取区域RectanglesrcRect = newRectangle(newPoint(0, 0), this.SkinOptions.BackgroundImage.Size);
// 绘制背景图corG.DrawImage(this.SkinOptions.BackgroundImage,
(this.Width - this.SkinOptions.BackgroundImage.Width) / 2,
(this.Height - this.SkinOptions.BackgroundImage.Height) / 2,
srcRect, GraphicsUnit.Pixel);
}
break;
caseImageLayout.Stretch:
{
// 创建源图上的截取区域RectanglesrcRect = newRectangle(newPoint(0, 0), this.SkinOptions.BackgroundImage.Size);
// 绘制背景图corG.DrawImage(this.SkinOptions.BackgroundImage, p.Width, p.Width, srcRect, GraphicsUnit.Pixel);
}
break;
caseImageLayout.Tile:
{
// 创建源图上的截取区域TextureBrushtb = newTextureBrush(this.SkinOptions.BackgroundImage);
corG.FillRectangle(tb, destRect);
}
break;
caseImageLayout.Zoom:
{
// 创′建¨源′图?上?的?截?取?区?域òRectanglesrcRect = newRectangle(newPoint(0, 0), this.SkinOptions.BackgroundImage.Size);
// 绘制背景图corG.DrawImage(this.SkinOptions.BackgroundImage,
(this.Width - this.SkinOptions.BackgroundImage.Width) / 2,
(this.Height - this.SkinOptions.BackgroundImage.Height) / 2,
srcRect, GraphicsUnit.Pixel);
}
break;
caseImageLayout.None:
default:
corG.DrawImage(this.SkinOptions.BackgroundImage, p.Width, p.Width);
break;
}
// 构造外边框RectangleborderOut = newRectangle(0, 0, destRect.Width - 1, destRect.Height - 1);
// 绘制外边框corG.DrawPath(p, CreateRoundRect(borderOut, this.SkinOptions.CornerRadius));
// 构造内边框RectangleborderIn = newRectangle(1, 1, borderOut.Width - 2, borderOut.Height - 2);
using(LinearGradientBrushb = newLinearGradientBrush(borderIn,
this.SkinOptions.BorderHighlightColor[0],
this.SkinOptions.BorderHighlightColor[1],
this.SkinOptions.BorderHighlightAngle))
{
// 绘制内边框高光using(PenlightPen = newPen(b))
{
// 绘制内边框corG.DrawPath(lightPen, CreateRoundRect(borderIn, this.SkinOptions.CornerRadius));
}
}
// 将圆角图绘制到主画布g.DrawImage(corBg, this.SkinOptions.ShadowWidth, this.SkinOptions.ShadowWidth);
#region绘制Logo
using(Bitmaplogo = Resources.Default.formlogo)
{
RectanglepaintTo = newRectangle(25, 20, logo.Width, logo.Height);
RectanglesourceRec = newRectangle(0, 0, logo.Width, logo.Height);
g.DrawImage(logo, paintTo, sourceRec, GraphicsUnit.Pixel);
}
#endregion
#region绘制控制按钮
using(Bitmapcb = Resources.Default.controlboxes)
{
RectanglepaintTo = newRectangle(this.Width - this.SkinOptions.ShadowWidth - cb.Width + 3,
this.SkinOptions.ShadowWidth - 1, cb.Width, ControlBoxHeight);
RectanglesourceRec = newRectangle(0, this._currentControlBoxImgY, cb.Width, ControlBoxHeight);
g.DrawImage(cb, paintTo, sourceRec, GraphicsUnit.Pixel);
}
#endregion}
}
}
}
///
///构造圆角路径///
///
///
/// privateGraphicsPathCreateRoundRect(Rectanglerect, intradius)
{
GraphicsPathgp = newGraphicsPath();
intx = rect.X;
inty = rect.Y;
intwidth = rect.Width;
intheight = rect.Height;
if(width > 0 && height > 0)
{
// 半径大才做圆角if(radius > MinCornerRadius)
{
radius = Math.Min(radius, height / 2 - 1);
radius = Math.Min(radius, width / 2 - 1);
gp.AddArc(x + width - (radius * 2), y, radius * 2, radius * 2, 270, 90);
gp.AddLine(x + width, y + radius, x + width, y + height - (radius * 2));
gp.AddArc(x + width - (radius * 2), y + height - (radius * 2), radius * 2, radius * 2, 0, 90);
gp.AddLine(x + width - (radius * 2), y + height, x + radius, y + height);
gp.AddArc(x, y + height - (radius * 2), radius * 2, radius * 2, 90, 90);
gp.AddLine(x, y + height - (radius * 2), x, y + radius);
gp.AddArc(x, y, radius * 2, radius * 2, 180, 90);
}
else{
gp.AddLine(x + width - radius, y, x + width, y + radius);
gp.AddLine(x + width, y + radius, x + width, y + height - radius);
gp.AddLine(x + width, y + height - radius, x + width - radius, y + height);
gp.AddLine(x + width - radius, y + height, x + radius, y + height);
gp.AddLine(x + radius, y + height, x, y + height - radius);
gp.AddLine(x, y + height - radius, x, y + radius);
gp.AddLine(x, y + radius, x + radius, y);
}
gp.CloseFigure();
}
returngp;
}
好了,带圆角、渐变阴影的窗体背景算是构造好了,但它现在仅仅是一张内存里的 bitmap 图片,我们要如何才能将它显示到窗体上呢?
第二部分:使用 Win32的API将 bitmap 更新到窗体
这个操作使用 UpdateLayeredWindow 来进行,MSDN上是这样描述的:Updates the position, size, shape, content, and translucency of a layered window.
现在我们只是用它来将我们绘制好的内存图Update到一个窗体上。你应该已经注意到MSDN的说明以及这个方法的名字了,单词Window前有一个限定词,Layered。那么我们怎么构造一个Layered的Window呢?或者说,怎么才能使我们的窗体成为Layered的呢?很简单,你可以这样:
protected overrideCreateParamsCreateParams
{
get{
CreateParamscp = base.CreateParams;
// 绘制背景必须针对具有 WS_EX_LAYERED 扩展风格的窗体进行if(!this.DesignMode)
{
cp.ExStyle |= Win32.WS_EX_LAYERED; // WS_EX_LAYERED}
returncp;
}
}
好了,我们有一个合适的Window了,下面我们使用 UpdateLayeredWindow 来讲绘制的内存图更新到窗体上:(老实说,这段代码是从网上Copy的,嘿嘿~)
///
///绘制已构造好的位图///
///
/// private voidSetBitmap(Bitmapbitmap, byteopacity = 255)
{
if(bitmap.PixelFormat != PixelFormat.Format32bppArgb)
throw newException("窗体背景图必须是 8 位/通道 RGB 颜色的图。");
// The ideia of this is very simple,
// 1. Create a compatible DC with screen;
// 2. Select the bitmap with 32bpp with alpha-channel in the compatible DC;
// 3. Call the UpdateLayeredWindow.IntPtrscreenDc = Win32.GetDC(IntPtr.Zero);
IntPtrmemDc = Win32.CreateCompatibleDC(screenDc);
IntPtrhBitmap = IntPtr.Zero;
IntPtroldBitmap = IntPtr.Zero;
try{
hBitmap = bitmap.GetHbitmap(Color.FromArgb(0)); // grab a GDI handle from this GDI+ bitmapoldBitmap = Win32.SelectObject(memDc, hBitmap);
Win32.Sizesize = newWin32.Size(bitmap.Width, bitmap.Height);
Win32.PointpointSource = newWin32.Point(0, 0);
Win32.PointtopPos = newWin32.Point(Left, Top);
Win32.BLENDFUNCTIONblend = newWin32.BLENDFUNCTION();
blend.BlendOp = Win32.AC_SRC_OVER;
blend.BlendFlags = 0;
blend.SourceConstantAlpha = opacity;
blend.AlphaFormat = Win32.AC_SRC_ALPHA;
Win32.UpdateLayeredWindow(Handle, screenDc, reftopPos, refsize,
memDc, refpointSource, 0, refblend, Win32.ULW_ALPHA);
}
finally{
Win32.ReleaseDC(IntPtr.Zero, screenDc);
if(hBitmap != IntPtr.Zero)
{
Win32.SelectObject(memDc, oldBitmap);
Win32.DeleteObject(hBitmap);
}
Win32.DeleteDC(memDc);
}
}
OK,到此为止,我们需要的圆角、渐变的阴影,都有了!
第三部分:简单的换肤机制
大家可能已经注意到,上面的代码有一个名为 SkinOptions 的东西。为了实现皮肤参数的统一传递和包装,我实现了 SkinOptions 这个类,用于保存当前皮肤的所有参数信息。
///
///皮肤风格参数/// public classSkinOptions: ICloneable{
///
///四边阴影的宽度。默认为 6。/// public intShadowWidth = 6;
///
///主背景圆角半径。最小值为 1,默认为 4。/// public intCornerRadius = 4;
///
///界面整体透明度。值范围 0~255。/// public byteOpacity = 255;
///
///边框宽度。默认为 1。/// public intBorderWidth = 1;
///
///边框颜色/// publicColorBorderColor = Color.FromArgb(255, 128, 128, 128);
///
///四边阴影的颜色。[0]为阴影内沿颜色,[1]为阴影外沿颜色/// publicColor[] ShadowColor = { Color.FromArgb(60, 0, 0, 0), Color.FromArgb(0, 0, 0, 0) };
///
///圆角阴影的颜色。[0]为阴影内沿颜色,[1]为阴影外沿颜色。///注:一般来讲,圆角阴影内沿的颜色应当比四边阴影内沿的颜色更深,才会有更好的显示效果。此值应当根据您的实际情况而定。///
/// 由于给扇面上渐变时,起点并不是准确的扇面内弧,因此扇面的内沿颜色可能应比四边的内沿颜色深publicColor[] CornerColor = { Color.FromArgb(180, 0, 0, 0), Color.FromArgb(0, 0, 0, 0) };
///
///高光颜色。[0]为高光边框左上角点的颜色,[1]为高光边框右下角的颜色/// publicColor[] BorderHighlightColor = { Color.FromArgb(200, 255, 255, 255), Color.FromArgb(200, 255, 255, 255) };
///
///高光过渡照射的角度。默认为“左下角到右上角对角线”的法线方向。/// public floatBorderHighlightAngle = 45f;
///
///背景高光。[0]为上下两端的颜色,[1]为中间高光的颜色/// publicColor[] BackgroundHighlightColor = { Color.FromArgb(0, 255, 255, 255), Color.FromArgb(200, 255, 255, 255) };
///
///窗体背景图/// publicImageBackgroundImage
{
get{
if(bgImg == null)
{
BitmapdefaultBmp = newBitmap(200, 100);
using(Graphicsg = Graphics.FromImage(defaultBmp))
{
g.FillRectangle(SystemBrushes.Control, 0, 0, 200, 100);
g.DrawString("未设置背景图", newFont(newFontFamily("宋体"), 9), SystemBrushes.GrayText, 50, 45);
bgImg = defaultBmp;
}
this.BackgroundLayout = ImageLayout.Tile;
}
returnbgImg;
}
set{
//if (null == value) throw new Exception("窗体背景图不能为 null 值。");bgImg = value;
}
}
privateImagebgImg;
///
///窗体背景图的显示方式/// publicImageLayoutBackgroundLayout = ImageLayout.None;
public staticSkinOptionsNewOne
{
get{
return newSkinOptions();
}
}
#regionICloneable 成员
public objectClone()
{
SkinOptionsresult = SkinOptions.NewOne;
result.BackgroundImage = (Image)this.BackgroundImage.Clone();
result.BackgroundLayout = this.BackgroundLayout;
result.BorderColor = this.BorderColor;
result.BorderWidth = this.BorderWidth;
result.CornerColor = (Color[])this.CornerColor.Clone();
result.CornerRadius = this.CornerRadius;
result.BorderHighlightAngle = this.BorderHighlightAngle;
result.BorderHighlightColor = (Color[])this.BorderHighlightColor.Clone();
result.BackgroundHighlightColor = (Color[])this.BackgroundHighlightColor.Clone();
result.Opacity = this.Opacity;
result.ShadowColor = (Color[])this.ShadowColor.Clone();
result.ShadowWidth = this.ShadowWidth;
returnresult;
}
#endregion}
有了这个类,我就可以很简单地通过切换这个类的实例,从而达到换肤的目的:
///
///应用新的皮肤风格(淡入淡出)///
/// 新皮肤风格。为 null 则取消皮肤public voidApplySkin(SkinOptionsnewSkin)
{
if(_isSkinChanging) return;
this._isSkinChanging = true;
CursoroldCursor = this.Cursor;
this.Cursor = Cursors.WaitCursor;
SkinEventArgse = newSkinEventArgs() {
Old = this.SkinOptions == null? null: (SkinOptions)this.SkinOptions.Clone(),
New = newSkin == null? null: (SkinOptions)newSkin.Clone()
};
// 触发皮肤切换之前的事件if(OnSkinPreChange != null) { OnSkinPreChange(this, e); }
if(newSkin == null) { newSkin = SkinOptions.NewOne; }
this.SkinOptions = newSkin;
SynchronizationContextcontext = SynchronizationContext.Current;
SendOrPostCallbackcheck = (a) =>
{
_isSkinChanging = false;
this.Cursor = oldCursor;
// 触发皮肤切换之后的事件if(OnSkinChanged != null) { OnSkinChanged(this, e); }
};
// 淡入操作SendOrPostCallbackshow = (a) =>
{
bytetmp = 0;
bytecurrentOpacity = newSkin.Opacity;
bufferdBackgroundImage = CreateBackground();
while(tmp < currentOpacity)
{
SetBitmap(bufferdBackgroundImage, tmp);
tmp += 5;
Thread.Sleep(5);
}
newThread((sc) => { ((SynchronizationContext)sc).Post(check, null); }) { Name = "检查切换操作是否完毕"}.Start(context);
};
// 淡出操作SendOrPostCallbackhide = (a) =>
{
// 渐变因子bytestep = 10;
SkinOptionsoldSkin = (SkinOptions)this.SkinOptions.Clone();
Bitmapoldbg = (Bitmap)bufferdBackgroundImage.Clone();
intcurrentOpacity = oldSkin.Opacity;
if(oldbg != null)
{
while(currentOpacity > step)
{
if(currentOpacity - (int)step <= 0) currentOpacity = step;
SetBitmap(oldbg, (byte)currentOpacity);
currentOpacity -= step;
Thread.Sleep(5);
}
}
newThread((sc) => { ((SynchronizationContext)sc).Post(show, null); }) { Name = "淡入新皮肤"}.Start(context);
};
newThread((sc) => { ((SynchronizationContext)sc).Post(hide, null); }) { Name = "淡出旧皮肤"}.Start(context);
}
private bool_isSkinChanging = false;
OK,到此为止,简单的换肤机制也完成了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。