浅谈Winform控件开发(一):使用GDI+美化基础窗口

  • A+
所属分类:.NET技术
摘要

 随后,我们定义一些常量覆盖基类属性FormBorderStyle使base.FormBorderStyle保持None,覆盖基类属性Padding返回或设置正确的内边距

  •  写在前面:
      • 本系列随笔将作为我对于winform控件开发的心得总结,方便对一些读者在GDI+、winform等技术方面进行一个入门级的讲解,抛砖引玉。
      • 别问为什么不用WPF,为什么不用QT。问就是懒,不想学。
      • 本项目所有代码均开源在https://github.com/muxiang/PowerControl
      • 效果预览:(gif,3.4MB)

    浅谈Winform控件开发(一):使用GDI+美化基础窗口

  • 本系列第一篇内容将仅包含对于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 ……

    相关常量以及P/Invoke相关方法已在我的库中定义,详见MSDN,也可从http://pinvoke.net/查询。
    同样在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上点个星星,谢谢大家。

转载请注明原作者,谢谢。