據說得有楔子 按照慣例,先來幾張樣例圖(註:為了展示視窗陰影效果,截圖範圍向外擴展了些,各位憑想象吧)。 還要來個序 其實,很多年沒寫過Winform了,前端時間在重構我們公司自己的呼叫中心系統,突然就覺得客戶端好醜好醜,對於我這種強迫症晚期患者來說,界面不好看都不知道怎麼寫代碼的,簡直就是種折磨, ...
據說得有楔子
按照慣例,先來幾張樣例圖(註:為了展示視窗陰影效果,截圖範圍向外擴展了些,各位憑想象吧)。
還要來個序
其實,很多年沒寫過Winform了,前端時間在重構我們公司自己的呼叫中心系統,突然就覺得客戶端好醜好醜,對於我這種強迫症晚期患者來說,界面不好看都不知道怎麼寫代碼的,簡直就是種折磨,還是滿清十大酷刑級別那種。
很多人推薦WPF,不過個人對WPF沒啥感覺,而且據說也無法支持2.0,還是採用Winform技術來搞吧。
終於第一節
做Winform皮膚,我瞭解的無非有2種方式。
1.採用純圖片模式:由專業美工設計各種樣式的圖片,進行視窗背景圖片設置
2.採用GDI+純代碼繪製:參照美工設計的各種樣式的圖片,使用C#代碼繪製出來
第一種方式很簡單,但是可復用性不高,性能上面應該也會有一些影響,如果圖片太大,視窗拖動等引起的重繪時,會明顯有閃爍等情況。
第二種方式很複雜,但是效率和復用性高,開放各種擴張屬性之後,可以適用於大部分場景。
以前用了很多次第一種方案,每次新做APP,都得重新設計界面,很不便利。這次,我準備採用第二種方案來做一個公用的皮膚。
關於GDI+,我只能算一個新人,不做具體的介紹,這裡只講我的一些實現方式,計劃項目完成後,開源到github。
繪製標題欄
做自定義界面,繞不開一個問題就是繪製標題欄。
每個Winform窗體,可以分為兩個部分:非客戶區域和客戶區域。
非客戶區域:表示無法由我們程式猿繪製的部分,具體包括:視窗標題欄,邊框
客戶區域:表示由我們程式猿繪製的部分,也就是窗體內容,平時我們拖控制項都是拖到客戶區域
一般自定義視窗的實現方式無非以下種
1.設置視窗為無邊框視窗,頂部放一個Panel,設置Panel.Dock=Top,然後在Panel裡面繪製logo、標題、按鈕等元素。
2.攔截視窗消息,重寫WndProc方法,攔截視窗標題繪製消息,由自己手工繪製
很多人會為了簡便,採用第一種方式,不過缺點比較明顯,對於我來說,最主要的一點就是真正的實現界面,裡面的控制項元素Dock會受到影響,不利於客戶區域佈局。
高手牛人會採用第二種方式,不是我這種Winform小白的菜,所以,我採用第三種方式,也是本篇文章的核心思想。
採用無邊框視窗,設置視窗Padding.Top為標題欄高度,採用GDI+繪製標題欄元素。
這種方式的好處顯而易見
具體實現窗體子控制項Dock不受影響
無邊框之後,重寫窗體拖動事件不需要對標題欄每一個元素進行事件處理
標題欄高度可隨時自定義
本文開頭的幾個截圖,標題欄繪製代碼如下
繪製標題文字、Logo圖片
private void DrawTitle(Graphics g) { var x = 6 + this.GetBorderWidth(); if (this.ShowLogo) { g.SmoothingMode = SmoothingMode.AntiAlias; ImageAttributes imgAtt = new ImageAttributes(); imgAtt.SetWrapMode(System.Drawing.Drawing2D.WrapMode.TileFlipXY); using (var image = this.Icon.ToBitmap()) { var rec = new Rectangle(x, (this.captionHeight - 24) / 2, 24, 24); g.DrawImage(image, rec, 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, imgAtt); } } if (this.ShowTitle) { var font = this.titleFont == null ? this.Font : this.titleFont; var fontSize = Size.Ceiling(g.MeasureString(this.Text, font)); if (this.CenterTitle) { x = (this.Width - fontSize.Width) / 2; } else if (this.ShowLogo) { x += 30; } using (var brush = new SolidBrush(this.CaptionForeColor)) { g.DrawString(this.Text, font, brush, x, (this.CaptionHeight - fontSize.Height) / 2 + this.GetBorderWidth()); } } }
繪製最小化、最大化、關閉、幫助按鈕
private void DrawControlBox(Graphics g) { if (this.ControlBox) { ImageAttributes ImgAtt = new ImageAttributes(); ImgAtt.SetWrapMode(System.Drawing.Drawing2D.WrapMode.TileFlipXY); var x = this.Width - 32; //var rec = new Rectangle(this.Width - 32, (this.CaptionHeight - 32) / 2 + this.BorderWidth, 32, 32); //var rec = new Rectangle(x, this.BorderWidth, 32, 32); if (this.CloseButtonImage != null) { closeRect = new Rectangle(x, 0, 32, 32); using (var brush = new SolidBrush(closeHover ? this.ControlActivedColor : this.ControlBackColor)) { g.FillRectangle(brush, closeRect); } g.DrawImage(this.CloseButtonImage, closeRect, 0, 0, this.CloseButtonImage.Width, this.CloseButtonImage.Height, GraphicsUnit.Pixel, ImgAtt); x -= 32; } if (this.MaximizeBox && this.WindowState == FormWindowState.Maximized && this.MaximumNormalButtonImage != null) { maxRect = new Rectangle(x, 0, 32, 32); using (var brush = new SolidBrush(maxHover ? this.ControlActivedColor : this.ControlBackColor)) { g.FillRectangle(brush, maxRect); } g.DrawImage(this.MaximumNormalButtonImage, maxRect, 0, 0, this.MaximumNormalButtonImage.Width, this.MaximumNormalButtonImage.Height, GraphicsUnit.Pixel, ImgAtt); x -= 32; } else if (this.MaximizeBox && this.WindowState != FormWindowState.Maximized && this.MaximumButtonImage != null) { maxRect = new Rectangle(x, 0, 32, 32); using (var brush = new SolidBrush(maxHover ? this.ControlActivedColor : this.ControlBackColor)) { g.FillRectangle(brush, maxRect); } g.DrawImage(this.MaximumButtonImage, maxRect, 0, 0, this.MaximumButtonImage.Width, this.MaximumButtonImage.Height, GraphicsUnit.Pixel, ImgAtt); x -= 32; } if (this.MinimizeBox && this.MinimumButtonImage != null) { minRect = new Rectangle(x, 0, 32, 32); using (var brush = new SolidBrush(minHover ? this.ControlActivedColor : this.ControlBackColor)) { g.FillRectangle(brush, minRect); } g.DrawImage(this.MinimumButtonImage, minRect, 0, 0, this.MinimumButtonImage.Width, this.MinimumButtonImage.Height, GraphicsUnit.Pixel, ImgAtt); x -= 32; } if (base.HelpButton && this.HelpButtonImage != null) { helpRect = new Rectangle(x, 0, 32, 32); using (var brush = new SolidBrush(helpHover ? this.ControlActivedColor : this.ControlBackColor)) { g.FillRectangle(brush, helpRect); } g.DrawImage(this.HelpButtonImage, helpRect, 0, 0, this.HelpButtonImage.Width, this.HelpButtonImage.Height, GraphicsUnit.Pixel, ImgAtt); x -= 32; } } }
窗體OnPaint事件,自繪標題欄
protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); #region draw caption using (var brush = new SolidBrush(this.CaptionBackgroundColor)) { e.Graphics.FillRectangle(brush, captionRect); } this.DrawTitle(e.Graphics); this.DrawControlBox(e.Graphics); #endregion #region draw border ControlPaint.DrawBorder(e.Graphics, this.ClientRectangle, borderColor, ButtonBorderStyle.Solid); #endregion }
採用Padding來約束子實現界面的元素佈局位置
當我採用了無邊框窗體來做自定義皮膚之後,由於去除了非客戶區域(標題欄、邊框),子實現窗體的坐標位置(0,0)實際上應該會覆蓋我的標題欄,不過,反編譯.NET源碼之後,我發現Form類有一個Padding屬性,這個屬性繼承自Control類,它的作用與CSS中的padding相同。所以,我決定使用這個技術來約束子實現界面的元素佈局位置。
每次修改標題欄高度時,需要重新生成窗體的Padding屬性
private int captionHeight; [Category("標題欄"), Description("標題欄高度"), DefaultValue(typeof(int), "40")] public int CaptionHeight { get { return this.captionHeight; } set { this.captionHeight = value; this.SetPadding(); } }
每次修改邊框寬度時,需要重新生成窗體的Padding屬性
private int borderWidth; [Category("邊框"), Description("邊框寬度"), DefaultValue(typeof(int), "1")] public int BorderWidth { get { return this.borderWidth; } set { this.borderWidth = value; this.SetPadding(); } }
最後,隱藏掉Padding屬性,外部修改無效
public new Padding Padding { get; set; }
附加1:標題欄自繪按鈕懸浮背景色修改和單擊事件處理
protected override void OnMouseMove(MouseEventArgs e) { Point p = new Point(e.X, e.Y); captionHover = captionRect.Contains(p); if (captionHover) { closeHover = closeRect != Rectangle.Empty && closeRect.Contains(p); minHover = minRect != Rectangle.Empty && minRect.Contains(p); maxHover = maxRect != Rectangle.Empty && maxRect.Contains(p); helpHover = helpRect != Rectangle.Empty && helpRect.Contains(p); this.Invalidate(captionRect); this.Cursor = (closeHover || minHover || maxHover || helpHover) ? Cursors.Hand : Cursors.Default; } else { if (closeHover || minHover || maxHover || helpHover) { this.Invalidate(captionRect); closeHover = minHover = maxHover = helpHover = false; } this.Cursor = Cursors.Default; } base.OnMouseMove(e); }
protected override void OnMouseClick(MouseEventArgs e)
{
var point = new Point(e.X, e.Y);
if (this.closeRect != Rectangle.Empty && this.closeRect.Contains(point))
{
this.Close();
return;
}
if (!this.maxRect.IsEmpty && this.maxRect.Contains(point))
{
if (this.WindowState == FormWindowState.Maximized)
{
this.WindowState = FormWindowState.Normal;
}
else
{
this.WindowState = FormWindowState.Maximized;
}
this.maxHover = false;
return;
}
if (!this.minRect.IsEmpty && this.minRect.Contains(point))
{
this.WindowState = FormWindowState.Minimized;
this.minHover = false;
return;
}
if (!this.helpRect.IsEmpty && this.helpRect.Contains(point))
{
this.helpHover = false;
this.Invalidate(this.captionRect);
CancelEventArgs ce = new CancelEventArgs();
base.OnHelpButtonClicked(ce);
return;
}
base.OnMouseClick(e);
}
附加2:處理無邊框窗體用戶調整大小
#region 調整視窗大小 const int Guying_HTLEFT = 10; const int Guying_HTRIGHT = 11; const int Guying_HTTOP = 12; const int Guying_HTTOPLEFT = 13; const int Guying_HTTOPRIGHT = 14; const int Guying_HTBOTTOM = 15; const int Guying_HTBOTTOMLEFT = 0x10; const int Guying_HTBOTTOMRIGHT = 17; protected override void WndProc(ref Message m) { if (this.closeHover || this.minHover || this.maxHover || this.helpHover) { base.WndProc(ref m); return; } if (!this.CustomResizeable) { base.WndProc(ref m); return; } switch (m.Msg) { case 0x0084: base.WndProc(ref m); Point vPoint = new Point((int)m.LParam & 0xFFFF, (int)m.LParam >> 16 & 0xFFFF); vPoint = PointToClient(vPoint); if (vPoint.X <= 5) if (vPoint.Y <= 5) m.Result = (IntPtr)Guying_HTTOPLEFT; else if (vPoint.Y >= ClientSize.Height - 5) m.Result = (IntPtr)Guying_HTBOTTOMLEFT; else m.Result = (IntPtr)Guying_HTLEFT; else if (vPoint.X >= ClientSize.Width - 5) if (vPoint.Y <= 5) m.Result = (IntPtr)Guying_HTTOPRIGHT; else if (vPoint.Y >= ClientSize.Height - 5) m.Result = (IntPtr)Guying_HTBOTTOMRIGHT; else m.Result = (IntPtr)Guying_HTRIGHT; else if (vPoint.Y <= 5) m.Result = (IntPtr)Guying_HTTOP; else if (vPoint.Y >= ClientSize.Height - 5) m.Result = (IntPtr)Guying_HTBOTTOM; break; case 0x0201: //滑鼠左鍵按下的消息 m.Msg = 0x00A1; //更改消息為非客戶區按下滑鼠 m.LParam = IntPtr.Zero; //預設值 m.WParam = new IntPtr(2);//滑鼠放在標題欄內 base.WndProc(ref m); break; default: base.WndProc(ref m); break; } } #endregion
全類文件,不曉得咋上傳附件,所以沒傳,要的可以找我QQ。