前言: CorePlex代碼庫 作為一個Visual Studio插件, 允許用戶通過VS直接訪問在線代碼庫,。開發(fā)過程中我翻閱了很多網(wǎng)上的資料,也總結(jié)了一些技術(shù)要點,,現(xiàn)寫成系列文章,,以饗讀者。同時,,里面某些技術(shù)也是我第一次使用,,如有不對的地方,還請行家狠拍,,歡迎大家指正~ 閑話休絮,,進(jìn)入正題。從本篇文章開始,,介紹 CorePlex 的窗體皮膚機(jī)制,以及簡單的換膚功能,。我們先來看看效果: 換一個皮膚看看: 需要實現(xiàn)的是圓角窗體+四周的陰影,要實現(xiàn)這個,,大致的思路是這樣的:先使用 Graphics 繪制一個 Bitmap,,將需要的皮膚繪制成一個內(nèi)存圖,然后使用 Win32的API:UpdateLayeredWindow 將這個構(gòu)造好的 Bitmap 繪制/更新到窗體上,。我們來看看具體的實現(xiàn)吧,。 第一部分,構(gòu)造皮膚背景,。為了實現(xiàn)圓角以及四周的陰影,,我將窗體背景劃分成了九宮格的形式: 主要思路是:除 5 之外的其他部分,都作為窗體的邊框和圓角來處理,。而5這個部分,,則作為圓角窗體的主體背景部分。1,、3,、7、9四個部分,,作為圓角,,我使用 PathGradientBrush 來繪制扇形漸變,而2,、4,、6,、8四個部分,作為邊框,,我使用 LinearGradientBrush 來繪制線性漸變,。 不多說,見代碼: /// <summary> /// 繪制四角的陰影 /// </summary> /// <param name="g"></param> /// <param name="corSize">圓角區(qū)域正方形的大小</param> /// <returns></returns> private void DrawCorners(Graphics g, Size corSize) { /* * 四個角,,每個角都是一個扇面 * 畫圖時扇面由外弧,、內(nèi)弧以及兩段的連接線構(gòu)成圖形 * 然后在內(nèi)弧中間附近向外做漸變 * * 陰影分為9宮格,5為內(nèi)部背景圖部分 * 1 2 3 * 4 5 6 * 7 8 9 */ Action<int> DrawCorenerN = (n) => { using (GraphicsPath gp = new GraphicsPath()) { // 扇面外沿,、內(nèi)沿曲線的尺寸 Size sizeOutSide = new Size(corSize.Width * 2, corSize.Height * 2); Size sizeInSide = new Size(this.SkinOptions.CornerRadius * 2, this.SkinOptions.CornerRadius * 2); // 扇面外沿,、內(nèi)沿曲線的位置 Point locationOutSide, locationInSide; // 當(dāng)圓角半徑小于MinCornerRadius時,內(nèi)沿不繪制曲線,,而以線段繪制近似值,。該線段繪制方向是從p1指向p2。 Point p1, p2; // 漸變起點位置 PointF brushCenter; // 扇面起始角度 float startAngle; // 根據(jù)四個方位不同,,確定扇面的位置,、角度及漸變起點位置 switch (n) { case 1: locationOutSide = new Point(0, 0); startAngle = 180; brushCenter = new PointF((float)sizeOutSide.Width - sizeInSide.Width * 0.5f, (float)sizeOutSide.Height - sizeInSide.Height * 0.5f); p1 = new Point(corSize.Width, this.SkinOptions.ShadowWidth); p2 = new Point(this.SkinOptions.ShadowWidth, corSize.Height); break; case 3: locationOutSide = new Point(this.Width - sizeOutSide.Width, 0); startAngle = 270; brushCenter = new PointF((float)locationOutSide.X + sizeInSide.Width * 0.5f, (float)sizeOutSide.Height - sizeInSide.Height * 0.5f); p1 = new Point(this.Width - this.SkinOptions.ShadowWidth, corSize.Height); p2 = new Point(this.Width - corSize.Width, this.SkinOptions.ShadowWidth); break; case 7: locationOutSide = new Point(0, this.Height - sizeOutSide.Height); startAngle = 90; brushCenter = new PointF((float)sizeOutSide.Width - sizeInSide.Width * 0.5f, (float)locationOutSide.Y + sizeInSide.Height * 0.5f); p1 = new Point(this.SkinOptions.ShadowWidth, this.Height - corSize.Height); p2 = new Point(corSize.Width, this.Height - this.SkinOptions.ShadowWidth); break; default: locationOutSide = new Point(this.Width - sizeOutSide.Width, this.Height - sizeOutSide.Height); startAngle = 0; brushCenter = new PointF((float)locationOutSide.X + sizeInSide.Width * 0.5f, (float)locationOutSide.Y + sizeInSide.Height * 0.5f); p1 = new Point(this.Width - corSize.Width, this.Height - this.SkinOptions.ShadowWidth); p2 = new Point(this.Width - this.SkinOptions.ShadowWidth, this.Height - corSize.Height); break; } // 扇面外沿曲線 Rectangle recOutSide = new Rectangle(locationOutSide, sizeOutSide); // 扇面內(nèi)沿曲線的位置 locationInSide = new Point(locationOutSide.X + (sizeOutSide.Width - sizeInSide.Width) / 2, locationOutSide.Y + (sizeOutSide.Height - sizeInSide.Height) / 2); // 扇面內(nèi)沿曲線 Rectangle recInSide = new Rectangle(locationInSide, sizeInSide); // 將扇面添加到形狀,以備繪制 gp.AddArc(recOutSide, startAngle, 91); if (this.SkinOptions.CornerRadius > MinCornerRadius) gp.AddArc(recInSide, startAngle + 90, -91); else gp.AddLine(p1, p2); // 使用漸變筆刷 using (PathGradientBrush shadowBrush = new PathGradientBrush(gp)) { Color[] colors = new Color[2]; float[] positions = new float[2]; ColorBlend sBlend = new ColorBlend(); // 扇面外沿色 colors[0] = this.SkinOptions.CornerColor[1]; // 扇面內(nèi)沿色 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); } /// <summary> /// 繪制上下左右四邊的陰影 /// </summary> /// <param name="g"></param> /// <param name="corSize"></param> /// <param name="gradientSize_LR"></param> /// <param name="gradientSize_TB"></param> private void DrawLines(Graphics g, Size corSize, Size gradientSize_LR, Size gradientSize_TB) { Rectangle rect2 = new Rectangle(new Point(corSize.Width, 0), gradientSize_TB); Rectangle rect4 = new Rectangle(new Point(0, corSize.Width), gradientSize_LR); Rectangle rect6 = new Rectangle(new Point(this.Size.Width - this.SkinOptions.ShadowWidth, corSize.Width), gradientSize_LR); Rectangle rect8 = new Rectangle(new Point(corSize.Width, this.Size.Height - this.SkinOptions.ShadowWidth), gradientSize_TB); using ( LinearGradientBrush brush2 = new LinearGradientBrush(rect2, this.SkinOptions.ShadowColor[1], /// <summary> /// 繪制主背景圖 /// </summary> /// <param name="g"></param> private void DrawMain(Graphics g) { // 要顯示的區(qū)域圖像的大小 Rectangle destRect = new Rectangle(0, 0, /// <summary> /// 構(gòu)造圓角路徑 /// </summary> /// <param name="rect"></param> /// <param name="radius"></param> /// <returns></returns> private GraphicsPath CreateRoundRect(Rectangle rect, int radius) { GraphicsPath gp = new GraphicsPath(); int x = rect.X; int y = rect.Y; int width = rect.Width; int height = 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(); } return gp; }好了,,帶圓角、漸變陰影的窗體背景算是構(gòu)造好了,,但它現(xiàn)在僅僅是一張內(nèi)存里的 bitmap 圖片,,我們要如何才能將它顯示到窗體上呢? 第二部分:使用 Win32的API將 bitmap 更新到窗體這個操作使用 UpdateLayeredWindow 來進(jìn)行,,MSDN上是這樣描述的:Updates the position, size, shape, content, and translucency of a layered window. MSDN:ms-help://MS.MSDNQTR.v90.chs/dv_vclib/html/9035dce1-8560-4ea4-94a8-f6e0ba2b2021.htm 現(xiàn)在我們只是用它來將我們繪制好的內(nèi)存圖Update到一個窗體上,。你應(yīng)該已經(jīng)注意到MSDN的說明以及這個方法的名字了,單詞Window前有一 個限定詞,,Layered,。那么我們怎么構(gòu)造一個Layered的Window呢?或者說,,怎么才能使我們的窗體成為Layered的呢,?很簡單,你可以 這樣: protected override CreateParams CreateParams { get { CreateParams cp = base.CreateParams; // 繪制背景必須針對具有 WS_EX_LAYERED 擴(kuò)展風(fēng)格的窗體進(jìn)行 if (!this.DesignMode) { cp.ExStyle |= Win32.WS_EX_LAYERED; // WS_EX_LAYERED } return cp; } }好了,,我們有一個合適的Window了,,下面我們使用 UpdateLayeredWindow 來講繪制的內(nèi)存圖更新到窗體上:(老實說,,這段代碼是從網(wǎng)上Copy的,嘿嘿~) /// <summary> /// 繪制已構(gòu)造好的位圖 /// </summary> /// <param name="bitmap"></param> /// <param name="opacity"></param> private void SetBitmap(Bitmap bitmap, byte opacity = 255) { if (bitmap.PixelFormat != PixelFormat.Format32bppArgb) throw new Exception("窗體背景圖必須是 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. IntPtr screenDc = Win32.GetDC(IntPtr.Zero); IntPtr memDc = Win32.CreateCompatibleDC(screenDc); IntPtr hBitmap = IntPtr.Zero; IntPtr oldBitmap = IntPtr.Zero; try { hBitmap = bitmap.GetHbitmap(Color.FromArgb(0)); // grab a GDI handle from this GDI+ bitmap oldBitmap = Win32.SelectObject(memDc, hBitmap); Win32.Size size = new Win32.Size(bitmap.Width, bitmap.Height); Win32.Point pointSource = new Win32.Point(0, 0); Win32.Point topPos = new Win32.Point(Left, Top); Win32.BLENDFUNCTION blend = new Win32.BLENDFUNCTION(); blend.BlendOp = Win32.AC_SRC_OVER; blend.BlendFlags = 0; blend.SourceConstantAlpha = opacity; blend.AlphaFormat = Win32.AC_SRC_ALPHA; Win32.UpdateLayeredWindow(Handle, screenDc, ref topPos, ref size, OK,,到此為止,我們需要的圓角,、漸變的陰影,,都有了! 第三部分:簡單的換膚機(jī)制大家可能已經(jīng)注意到,,上面的代碼有一個名為 SkinOptions 的東西,。為了實現(xiàn)皮膚參數(shù)的統(tǒng)一傳遞和包裝,我實現(xiàn)了 SkinOptions 這個類,,用于保存當(dāng)前皮膚的所有參數(shù)信息。 /// <summary> /// 皮膚風(fēng)格參數(shù) /// </summary> public class SkinOptions : ICloneable { /// <summary> /// 四邊陰影的寬度,。默認(rèn)為 6,。 /// </summary> public int ShadowWidth = 6; /// <summary> /// 主背景圓角半徑。最小值為 1,,默認(rèn)為 4,。 /// </summary> public int CornerRadius = 4; /// <summary> /// 界面整體透明度。值范圍 0~255,。 /// </summary> public byte Opacity = 255; /// <summary> /// 邊框?qū)挾?。默認(rèn)為 1。 /// </summary> public int BorderWidth = 1; /// <summary> /// 邊框顏色 /// </summary> public Color BorderColor = Color.FromArgb(255, 128, 128, 128); /// <summary> /// 四邊陰影的顏色,。[0]為陰影內(nèi)沿顏色,,[1]為陰影外沿顏色 /// </summary> public Color[] ShadowColor = { Color.FromArgb(60, 0, 0, 0), Color.FromArgb(0, 0, 0, 0) }; /// <summary> /// 圓角陰影的顏色。[0]為陰影內(nèi)沿顏色,,[1]為陰影外沿顏色,。 /// 注:一般來講,圓角陰影內(nèi)沿的顏色應(yīng)當(dāng)比四邊陰影內(nèi)沿的顏色更深,,才會有更好的顯示效果,。此值應(yīng)當(dāng)根據(jù)您的實際情況而定。 /// </summary> /// <remarks>由于給扇面上漸變時,,起點并不是準(zhǔn)確的扇面內(nèi)弧,,因此扇面的內(nèi)沿顏色可能應(yīng)比四邊的內(nèi)沿顏色深</remarks> public Color[] CornerColor = { Color.FromArgb(180, 0, 0, 0), Color.FromArgb(0, 0, 0, 0) }; /// <summary> /// 高光顏色。[0]為高光邊框左上角點的顏色,,[1]為高光邊框右下角的顏色 /// </summary> public Color[] BorderHighlightColor = { Color.FromArgb(200, 255, 255, 255), Color.FromArgb(200, 255, 255, 255) }; /// <summary> /// 高光過渡照射的角度,。默認(rèn)為“左下角到右上角對角線”的法線方向。 /// </summary> public float BorderHighlightAngle = 45f; /// <summary> /// 背景高光,。[0]為上下兩端的顏色,,[1]為中間高光的顏色 /// </summary> public Color[] BackgroundHighlightColor = { Color.FromArgb(0, 255, 255, 255), Color.FromArgb(200, 255, 255, 255) }; /// <summary> /// 窗體背景圖 /// </summary> public Image BackgroundImage { get { if (bgImg == null) { Bitmap defaultBmp = new Bitmap(200, 100); using (Graphics g = Graphics.FromImage(defaultBmp)) { g.FillRectangle(SystemBrushes.Control, 0, 0, 200, 100); g.DrawString("未設(shè)置背景圖", new Font(new FontFamily("宋體"), 9), SystemBrushes.GrayText, 50, 45); bgImg = defaultBmp; } this.BackgroundLayout = ImageLayout.Tile; } return bgImg; } set { //if (null == value) throw new Exception("窗體背景圖不能為 null 值,。"); bgImg = value; } } private Image bgImg; /// <summary> /// 窗體背景圖的顯示方式 /// </summary> public ImageLayout BackgroundLayout = ImageLayout.None; public static SkinOptions NewOne { get { return new SkinOptions(); } } #region ICloneable 成員 public object Clone() { SkinOptions result = 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; return result; } #endregion } 有了這個類,我就可以很簡單地通過切換這個類的實例,,從而達(dá)到換膚的目的: /// <summary> /// 應(yīng)用新的皮膚風(fēng)格(淡入淡出) /// </summary> /// <param name="newSkin">新皮膚風(fēng)格,。為 null 則取消皮膚</param> public void ApplySkin(SkinOptions newSkin) { if (_isSkinChanging) return; this._isSkinChanging = true; Cursor oldCursor = this.Cursor; this.Cursor = Cursors.WaitCursor; SkinEventArgs e = new SkinEventArgs() { Old = this.SkinOptions == null ? null : (SkinOptions)this.SkinOptions.Clone(), New = newSkin == null ? null : (SkinOptions)newSkin.Clone() }; // 觸發(fā)皮膚切換之前的事件 if (OnSkinPreChange != null) { OnSkinPreChange(this, e); } if (newSkin == null) { newSkin = SkinOptions.NewOne; } this.SkinOptions = newSkin; SynchronizationContext context = SynchronizationContext.Current; SendOrPostCallback check = (a) => { _isSkinChanging = false; this.Cursor = oldCursor; // 觸發(fā)皮膚切換之后的事件 if (OnSkinChanged != null) { OnSkinChanged(this, e); } }; // 淡入操作 SendOrPostCallback show = (a) => { byte tmp = 0; byte currentOpacity = newSkin.Opacity; bufferdBackgroundImage = CreateBackground(); while (tmp < currentOpacity) { SetBitmap(bufferdBackgroundImage, tmp); tmp += 5; Thread.Sleep(5); } new Thread((sc) => { ((SynchronizationContext)sc).Post(check, null); }) { Name = "檢查切換操作是否完畢" }.Start(context); }; // 淡出操作 SendOrPostCallback hide = (a) => { // 漸變因子 byte step = 10; SkinOptions oldSkin = (SkinOptions)this.SkinOptions.Clone(); Bitmap oldbg = (Bitmap)bufferdBackgroundImage.Clone(); int currentOpacity = oldSkin.Opacity; if (oldbg != null) { while (currentOpacity > step) { if (currentOpacity - (int)step <= 0) currentOpacity = step; SetBitmap(oldbg, (byte)currentOpacity); currentOpacity -= step; Thread.Sleep(5); } } new Thread((sc) => { ((SynchronizationContext)sc).Post(show, null); }) { Name = "淡入新皮膚" }.Start(context); }; new Thread((sc) => { ((SynchronizationContext)sc).Post(hide, null); }) { Name = "淡出舊皮膚" }.Start(context); } private bool _isSkinChanging = false;
OK,到此為止,,簡單的換膚機(jī)制也完成了,。 |
|
來自: 唐伯龍 > 《CodePlex》