• 浅谈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上点个星星,谢谢大家。

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

    Never give up!
  • 相关阅读:
    【2017-06-20】Linux应用开发工程师C/C++面试问题记录之一:Linux多线程程序的同步问题
    April 14 2017 Week 15 Friday
    April 13 2017 Week 15 Thursday
    April 12 2017 Week 15 Wednesday
    【2017-06-17】QtGui基础控件:QSpinBox及QDoubleSpinBox
    April 11 2017 Week 15 Tuesday
    April 10 2017 Week 15 Monday
    April 9 2017 Week 15 Sunday
    April 8 2017 Week 14 Saturday
    April 7 2017 Week 14 Friday
  • 原文地址:https://www.cnblogs.com/CoffeeMX/p/14368052.html
Copyright © 2020-2023  润新知