- 写在前面:
- 本系列随笔将作为我对于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/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初级运营知识
没有评论:
发表评论