• 写在前面:

      • 本系列随笔将作为我对于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 : 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上点个星星,谢谢大家。

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

浅谈Winform控件开发(一):使用GDI+美化基础窗口的更多相关文章

  1. WinForm控件开发总结目录

    WinForm控件开发总结(一)------开篇 WinForm控件开发总结(二)------使用和调试自定义控件 WinForm控件开发总结(三)------认识WinForm控件常用的Attrib ...

  2. 浅谈MapControl控件和PageLayoutControl控件

    1.MapControl控件是ArcObject(ArcEngine)中使用非常普遍的一个控件,它对应ArcMap中的DataView视图.MapControl控件实现的功能: 1)管理控件的外观.显 ...

  3. 浅谈ListBox控件,将对象封装在listBox中,在ListBox中显示对象中某个属性,在ListBox中移除和移动信息

    大家好,俗称万事开头难,不经历风雨,怎能见彩虹.在此小编给大家带来一个自己练习的小实例,希望与大家一起分享与交流.下面进入应用场景,从SQL2008数据库取出数据,给ListBox赋值在界面并显示出来 ...

  4. 浅谈 WPF控件

    首先我们必须知道在WPF中,控件通常被描述为和用户交互的元素,也就是能够接收焦点并响应键盘.鼠标输入的元素.我们可以把控件想象成一个容器,容器里装的东西就是它的内容.控件的内容可以是数据,也可以是控件 ...

  5. 浅谈XAML控件

    在win10系统内简单使用了XAML控件,由于本人英语水平有限,在自己的摸索使用.分析代码以及翻译软件.搜索引擎.室友情的帮助下了解了控件的相关功能,下面简要对XAML控件提出几点建议: 1.Cale ...

  6. winform 控件开发1——复合控件

    哈哈是不是丑死了? 做了一个不停变色的按钮,可以通过勾选checkbox停下来,代码如下: 复合控件果然简单呀,我都能学会~ using System; using System.Collection ...

  7. 浅谈EditText控件的inputType类型

    android:inputType="none"--默认 android:inputType="text"--输入文本字符 android:inputType= ...

  8. [C#开发小技巧]解决WinForm控件TabControl闪烁问题

    在用C#开发WinForm程序时,常发现TabControl出现严重的闪烁问题,这主要是由于TabControl控件在实现时会绘制默认的窗口背景.其实以下一段简单的代码可以有效的缓解该问题的发生.这就 ...

  9. C# Winform开发以及控件开发的需要注意的,被人问怕了,都是基础常识

    我是搞控件开发的,经常被人问,所以把一些问题记录了下来!如果有人再问,直接把地址丢给他看. 一. 经常会有人抱怨Winform界面闪烁,下面有几个方法可以尽可能的避免出现闪烁 1.控件的使用尽量以纯色 ...

随机推荐

  1. WDCP v3 安装

    ---已更新至3.0.3---经过近期的努力,wdCP_v3正式版终于可以和大家见面了v3功能预览1 底层完全重新架构,更安全稳定,省资源更高效2 安装更简单,快速与方便3 功能更强大和易扩展,且完美 ...

  2. LeetCode225 用队列实现栈

    使用队列实现栈的下列操作: push(x) -- 元素 x 入栈 pop() -- 移除栈顶元素 top() -- 获取栈顶元素 empty() -- 返回栈是否为空 注意: 你只能使用队列的基本操作 ...

  3. 十六:SQL注入之查询方式及报错盲注

    在很多注入时,有很多注入会出现无回显的情况,其中不回显的原因可能是SQL查询语句有问题,这时候我们需要用到相关的报错或者盲注进行后续操作,同时作为手工注入的时候,需要提前了解SQL语句能更好的选择对应 ...

  4. 在项目中应该使用Boolean还是使用boolean?

    起因 在公司看代码时,看到了使用Boolean对象来完成业务逻辑判断的操作.和我的习惯不一致,于是引起了一些反思. boolean和Boolean的差别咱就不说了,我们仅探讨使用boolean与Boo ...

  5. Gulp4.0入门和实战

    gulp4.0入门和实战 我最近遇到需要优化web的性能的任务,然后就捣鼓了一些对资源文件优化压缩的方案.由于之前的项目中有使用到gulp,所以在需要处理的web项目中也优先使用这个技术. 先聊聊gu ...

  6. Azure Terraform(六)Common Module

    一,引言 之前我们在使用 Terraform 构筑一下 Azure 云资源的时候,直接将所以需要创建的资源全面写在 main.tf 这个文件中,这样写主要是为了演示使用,但是在实际的 Terrafor ...

  7. Markdown里常用的HTML元素

    转义:\ 换行:<br/> 红色文字:<font color=#FF0000>字体改成红色了</font> A标签 新窗口:<a href="xxx ...

  8. Tensorflow-线性回归与手写数字分类

    线性回归 步骤 构造线性回归数据 定义输入层 设计神经网络中间层 定义神经网络输出层 计算二次代价函数,构建梯度下降 进行训练,获取预测值 画图展示 代码 import tensorflow as t ...

  9. cisco思科交换机终端远程ssh另一端报错:% ssh connections not permitted from this terminal

    故障现象: XSJ-GH10-C3750->ssh 58.64.xx.xx% ssh connections not permitted from this terminal 解决办法: 原因: ...

  10. ADB 基本命令

    ADB很强大,记住一些ADB命令有助于提高工作效率. 获取序列号: adb get-serialno 查看连接计算机的设备: adb devices 重启机器: adb reboot 重启到bootl ...