2021年2月5日星期五

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

  •  写在前面:
      • 本系列随笔将作为我对于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 : Form6 ...

     随后,我们定义一些常量

     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  set12  {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 Padding26 {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,也可
    同样在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      else62       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      else72       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  try14  {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  finally40  {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上点个星星,谢谢大家。

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









原文转载:http://www.shaoqun.com/a/529620.html

跨境电商:https://www.ikjzd.com/

环球市场:https://www.ikjzd.com/w/1762

易佰:https://www.ikjzd.com/w/1482


写在前面:本系列随笔将作为我对于winform控件开发的心得总结,方便对一些读者在GDI+、winform等技术方面进行一个入门级的讲解,抛砖引玉。别问为什么不用WPF,为什么不用QT。问就是懒,不想学。本项目所有代码均开源在https://github.com/muxiang/PowerControl效果预览:(gif,3.4MB)本系列第一篇内容将仅包含对于Winform基础窗口也就是Syst
蜜芽:蜜芽
转运中国:转运中国
纯干货!速卖通产品上新技巧与快速出单的方法:纯干货!速卖通产品上新技巧与快速出单的方法
利天下跨境:利天下跨境
shopee初级运营知识:shopee初级运营知识

没有评论:

发表评论