- 寫在前面:
- 本系列隨筆將作為我對於winform控制元件開發的心得總結,方便對一些讀者在GDI+、winform等技術方面進行一個入門級的講解,拋磚引玉。
- 別問為什麼不用WPF,為什麼不用QT。問就是懶,不想學。
- 本專案所有程式碼均開源在https://github.com/muxiang/PowerControl
- 效果預覽:(gif,3.4MB)
- 本系列第一篇內容將僅包含對於Winform基礎視窗也就是System.Windows.Forms.Form的美化,後續將對一些常用控制元件如Button、ComboBox、CheckBox、TextBox等進行修改,並提供一些其他如Loading遮罩層等常見控制元件。
- 對於基礎視窗的美化,首要的任務就是先把基礎標題欄干掉。這個過程中會涉及一些Windows訊息機制。
- 首先,我們新建一個類XForm,派生自System.Windows.Forms.Form。
1 /// <summary> 2 /// 表示組成應用程式的使用者介面的視窗或對話方塊。 3 /// </summary> 4 [ToolboxItem(false)] 5 public class XForm : Form 6 ...
隨後,我們定義一些常量
1 /// <summary> 2 /// 標題欄高度 3 /// </summary> 4 public const int TitleBarHeight = 30; 5 6 // 邊框寬度 7 private const int BorderWidth = 4; 8 // 標題欄圖示大小 9 private const int IconSize = 16; 10 // 標題欄按鈕大小 11 private const int ButtonWidth = 30; 12 private const int ButtonHeight = 30;
覆蓋基類屬性FormBorderStyle使base.FormBorderStyle保持None,覆蓋基類屬性Padding返回或設定正確的內邊距
1 /// <summary> 2 /// 獲取或設定窗體的邊框樣式。 3 /// </summary> 4 [Browsable(true)] 5 [Category("Appearance")] 6 [Description("獲取或設定窗體的邊框樣式。")] 7 [DefaultValue(FormBorderStyle.Sizable)] 8 public new FormBorderStyle FormBorderStyle 9 { 10 get => _formBorderStyle; 11 set 12 { 13 _formBorderStyle = value; 14 UpdateStyles(); 15 DrawTitleBar(); 16 } 17 } 18 19 /// <summary> 20 /// 獲取或設定窗體的內邊距。 21 /// </summary> 22 [Browsable(true)] 23 [Category("Appearance")] 24 [Description("獲取或設定窗體的內邊距。")] 25 public new Padding Padding 26 { 27 get => new Padding(base.Padding.Left, base.Padding.Top, base.Padding.Right, base.Padding.Bottom - TitleBarHeight); 28 set => base.Padding = new Padding(value.Left, value.Top, value.Right, value.Bottom + TitleBarHeight); 29 }
※最後一步也是最關鍵的一步:重新定義視窗客戶區邊界。重寫WndProc並處理WM_NCCALCSIZE訊息。
1 protected override void WndProc(ref Message m) 2 { 3 switch (m.Msg) 4 { 5 case WM_NCCALCSIZE: 6 { 7 // 自定義客戶區 8 if (m.WParam != IntPtr.Zero && _formBorderStyle != FormBorderStyle.None) 9 { 10 NCCALCSIZE_PARAMS @params = (NCCALCSIZE_PARAMS) 11 Marshal.PtrToStructure(m.LParam, typeof(NCCALCSIZE_PARAMS)); 12 @params.rgrc[0].Top += TitleBarHeight; 13 @params.rgrc[0].Bottom += TitleBarHeight; 14 Marshal.StructureToPtr(@params, m.LParam, false); 15 m.Result = (IntPtr)(WVR_ALIGNTOP | WVR_ALIGNBOTTOM | WVR_REDRAW); 16 } 17 18 base.WndProc(ref m); 19 break; 20 } 21 ……
同樣在WndProc中處理WM_NCPAINT訊息1 case WM_NCPAINT: 2 { 3 DrawTitleBar(); 4 m.Result = (IntPtr)1; 5 break; 6 }
DrawTitleBar()方法定義如下:
1 /// <summary> 2 /// 繪製標題欄 3 /// </summary> 4 private void DrawTitleBar() 5 { 6 if (_formBorderStyle == FormBorderStyle.None) 7 return; 8 9 DrawTitleBackgroundTextIcon(); 10 CreateButtonImages(); 11 DrawTitleButtons(); 12 }
首先使用線性漸變畫刷繪製標題欄背景、圖示、標題文字:
1 /// <summary> 2 /// 繪製標題欄背景、文字、圖示 3 /// </summary> 4 private void DrawTitleBackgroundTextIcon() 5 { 6 IntPtr hdc = GetWindowDC(Handle); 7 Graphics g = Graphics.FromHdc(hdc); 8 9 // 標題欄背景 10 using (Brush brsTitleBar = new LinearGradientBrush(TitleBarRectangle, 11 _titleBarStartColor, _titleBarEndColor, LinearGradientMode.Horizontal)) 12 g.FillRectangle(brsTitleBar, TitleBarRectangle); 13 14 // 標題欄圖示 15 if (ShowIcon) 16 g.DrawIcon(Icon, new Rectangle( 17 BorderWidth, TitleBarRectangle.Top + (TitleBarRectangle.Height - IconSize) / 2, 18 IconSize, IconSize)); 19 20 // 標題文字 21 const int txtX = BorderWidth + IconSize; 22 SizeF szText = g.MeasureString(Text, SystemFonts.CaptionFont, Width, StringFormat.GenericDefault); 23 using Brush brsText = new SolidBrush(_titleBarForeColor); 24 g.DrawString(Text, 25 SystemFonts.CaptionFont, 26 brsText, 27 new RectangleF(txtX, 28 TitleBarRectangle.Top + (TitleBarRectangle.Bottom - szText.Height) / 2, 29 Width - BorderWidth * 2, 30 TitleBarHeight), 31 StringFormat.GenericDefault); 32 33 g.Dispose(); 34 ReleaseDC(Handle, hdc); 35 }
隨後繪製標題欄按鈕,猶豫篇幅限制,在此不多贅述,詳見原始碼中CreateButtonImages()與DrawTitleButtons()。
至此,表面工作基本做完了,但這個視窗還不像個視窗,因為最小化、最大化、關閉以及調整視窗大小都不好用。
為什麼?因為還有很多工作要做,首先,同樣在WndProc中處理WM_NCHITTEST訊息,通過m.Result指定當前滑鼠位置位於標題欄、最小化按鈕、最大化按鈕、關閉按鈕或上下左右邊框
1 case WM_NCHITTEST: 2 { 3 base.WndProc(ref m); 4 5 Point pt = PointToClient(new Point((int)m.LParam & 0xFFFF, (int)m.LParam >> 16 & 0xFFFF)); 6 7 _userSizedOrMoved = true; 8 9 switch (_formBorderStyle) 10 { 11 case FormBorderStyle.None: 12 break; 13 case FormBorderStyle.FixedSingle: 14 case FormBorderStyle.Fixed3D: 15 case FormBorderStyle.FixedDialog: 16 case FormBorderStyle.FixedToolWindow: 17 if (pt.Y < 0) 18 { 19 _userSizedOrMoved = false; 20 m.Result = (IntPtr)HTCAPTION; 21 } 22 23 if (CorrectToLogical(CloseButtonRectangle).Contains(pt)) 24 m.Result = (IntPtr)HTCLOSE; 25 if (CorrectToLogical(MaximizeButtonRectangle).Contains(pt)) 26 m.Result = (IntPtr)HTMAXBUTTON; 27 if (CorrectToLogical(MinimizeButtonRectangle).Contains(pt)) 28 m.Result = (IntPtr)HTMINBUTTON; 29 30 break; 31 case FormBorderStyle.Sizable: 32 case FormBorderStyle.SizableToolWindow: 33 if (pt.Y < 0) 34 { 35 _userSizedOrMoved = false; 36 m.Result = (IntPtr)HTCAPTION; 37 } 38 39 if (CorrectToLogical(CloseButtonRectangle).Contains(pt)) 40 m.Result = (IntPtr)HTCLOSE; 41 if (CorrectToLogical(MaximizeButtonRectangle).Contains(pt)) 42 m.Result = (IntPtr)HTMAXBUTTON; 43 if (CorrectToLogical(MinimizeButtonRectangle).Contains(pt)) 44 m.Result = (IntPtr)HTMINBUTTON; 45 46 if (WindowState == FormWindowState.Maximized) 47 break; 48 49 bool bTop = pt.Y <= -TitleBarHeight + BorderWidth; 50 bool bBottom = pt.Y >= Height - TitleBarHeight - BorderWidth; 51 bool bLeft = pt.X <= BorderWidth; 52 bool bRight = pt.X >= Width - BorderWidth; 53 54 if (bLeft) 55 { 56 _userSizedOrMoved = true; 57 if (bTop) 58 m.Result = (IntPtr)HTTOPLEFT; 59 else if (bBottom) 60 m.Result = (IntPtr)HTBOTTOMLEFT; 61 else 62 m.Result = (IntPtr)HTLEFT; 63 } 64 else if (bRight) 65 { 66 _userSizedOrMoved = true; 67 if (bTop) 68 m.Result = (IntPtr)HTTOPRIGHT; 69 else if (bBottom) 70 m.Result = (IntPtr)HTBOTTOMRIGHT; 71 else 72 m.Result = (IntPtr)HTRIGHT; 73 } 74 else if (bTop) 75 { 76 _userSizedOrMoved = true; 77 m.Result = (IntPtr)HTTOP; 78 } 79 else if (bBottom) 80 { 81 _userSizedOrMoved = true; 82 m.Result = (IntPtr)HTBOTTOM; 83 } 84 break; 85 default: 86 throw new ArgumentOutOfRangeException(); 87 } 88 break; 89 }
隨後以同樣的方式處理WM_NCLBUTTONDBLCLK、WM_NCLBUTTONDOWN、WM_NCLBUTTONUP、WM_NCMOUSEMOVE等訊息,進行標題欄按鈕等元素重繪,不多贅述。
現在視窗進行正常的單擊、雙擊、調整尺寸,我們在最後為視窗新增陰影
首先定義一個可以承載32位點陣圖的分層視窗(Layered Window)來負責主視窗陰影的呈現,詳見原始碼中XFormShadow類,此處僅列出用於建立分層視窗的核心程式碼:
1 private void UpdateBmp(Bitmap bmp) 2 { 3 if (!IsHandleCreated) return; 4 5 if (!Image.IsCanonicalPixelFormat(bmp.PixelFormat) || !Image.IsAlphaPixelFormat(bmp.PixelFormat)) 6 throw new ArgumentException(@"點陣圖格式不正確", nameof(bmp)); 7 8 IntPtr oldBits = IntPtr.Zero; 9 IntPtr screenDC = GetDC(IntPtr.Zero); 10 IntPtr hBmp = IntPtr.Zero; 11 IntPtr memDc = CreateCompatibleDC(screenDC); 12 13 try 14 { 15 POINT formLocation = new POINT(Left, Top); 16 SIZE bitmapSize = new SIZE(bmp.Width, bmp.Height); 17 BLENDFUNCTION blendFunc = new BLENDFUNCTION( 18 AC_SRC_OVER, 19 0, 20 255, 21 AC_SRC_ALPHA); 22 23 POINT srcLoc = new POINT(0, 0); 24 25 hBmp = bmp.GetHbitmap(Color.FromArgb(0)); 26 oldBits = SelectObject(memDc, hBmp); 27 28 UpdateLayeredWindow( 29 Handle, 30 screenDC, 31 ref formLocation, 32 ref bitmapSize, 33 memDc, 34 ref srcLoc, 35 0, 36 ref blendFunc, 37 ULW_ALPHA); 38 } 39 finally 40 { 41 if (hBmp != IntPtr.Zero) 42 { 43 SelectObject(memDc, oldBits); 44 DeleteObject(hBmp); 45 } 46 47 ReleaseDC(IntPtr.Zero, screenDC); 48 DeleteDC(memDc); 49 } 50 }
最後通過路徑漸變畫刷建立陰影點陣圖,通過點陣圖構建分層視窗,並與主視窗建立父子關係:
1 /// <summary> 2 /// 構建陰影 3 /// </summary> 4 private void BuildShadow() 5 { 6 lock (this) 7 { 8 _buildingShadow = true; 9 10 if (_shadow != null && !_shadow.IsDisposed && !_shadow.Disposing) 11 { 12 // 解除父子視窗關係 13 SetWindowLong( 14 Handle, 15 GWL_HWNDPARENT, 16 0); 17 18 _shadow.Dispose(); 19 } 20 21 Bitmap bmpBackground = new Bitmap(Width + BorderWidth * 4, Height + BorderWidth * 4); 22 23 GraphicsPath gp = new GraphicsPath(); 24 gp.AddRectangle(new Rectangle(0, 0, bmpBackground.Width, bmpBackground.Height)); 25 26 using (Graphics g = Graphics.FromImage(bmpBackground)) 27 using (PathGradientBrush brs = new PathGradientBrush(gp)) 28 { 29 g.CompositingMode = CompositingMode.SourceCopy; 30 g.InterpolationMode = InterpolationMode.HighQualityBicubic; 31 g.PixelOffsetMode = PixelOffsetMode.HighQuality; 32 g.SmoothingMode = SmoothingMode.AntiAlias; 33 34 // 中心顏色 35 brs.CenterColor = Color.FromArgb(100, Color.Black); 36 // 指定從實際陰影邊界到視窗邊框邊界的漸變 37 brs.FocusScales = new PointF(1 - BorderWidth * 4F / Width, 1 - BorderWidth * 4F / Height); 38 // 邊框環繞顏色 39 brs.SurroundColors = new[] { Color.FromArgb(0, 0, 0, 0) }; 40 // 掏空視窗實際區域 41 gp.AddRectangle(new Rectangle(BorderWidth * 2, BorderWidth * 2, Width, Height)); 42 g.FillPath(brs, gp); 43 } 44 45 gp.Dispose(); 46 47 _shadow = new XFormShadow(bmpBackground); 48 49 _buildingShadow = false; 50 51 AlignShadow(); 52 _shadow.Show(); 53 54 // 設定父子視窗關係 55 SetWindowLong( 56 Handle, 57 GWL_HWNDPARENT, 58 _shadow.Handle.ToInt32()); 59 60 Activate(); 61 }//end of lock(this) 62 }
感謝大家能讀到這裡,程式碼中如有錯誤,或存在其它建議,歡迎在評論區或Github指正。
如果覺得本文對你有幫助,還請點個推薦或Github上點個星星,謝謝大家。
轉載請註明原作者,謝謝。