Loading

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

  •  写在前面:
      • 本系列随笔将作为我对于winform控件开发的心得总结,方便对一些读者在GDI+、winform等技术方面进行一个入门级的讲解,抛砖引玉。
      • 别问为什么不用WPF,为什么不用QT。问就是懒,不想学。
      • 本项目所有代码均开源在https://github.com/muxiang/PowerLib
      • 效果预览:(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 
    13                             @params.rgrc[0].Top += BorderWidth + TitleBarHeight;
    14                             @params.rgrc[0].Bottom -= BorderWidth;
    15                             @params.rgrc[0].Left += BorderWidth;
    16                             @params.rgrc[0].Right -= BorderWidth;
    17 
    18                             @params.rgrc[1] = @params.rgrc[0];
    19 
    20                             Marshal.StructureToPtr(@params, m.LParam, false);
    21 
    22                             m.Result = (IntPtr)WVR_VALIDRECTS;
    23                         }
    24 
    25                         base.WndProc(ref m);
    26                         break;
    27                     }
    28 ...

    相关常量以及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上点个星星,谢谢大家。

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

posted @ 2021-02-05 17:36  __Meow  阅读(4656)  评论(12编辑  收藏  举报