• 写在前面:

      • 本系列随笔将作为我对于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. 10分钟搞定让你困惑的 Jenkins 环境变量

    前言 Jenkins, DevOps 技术栈的核心之一,CI/CD 离不开编写 Pipeline 脚本,上手 Jenkins ,简单查一下文档,你就应该不会被 agent,stages,step 这类 ...

  2. springboot源码解析-管中窥豹系列之项目类型(二)

    一.前言 Springboot源码解析是一件大工程,逐行逐句的去研究代码,会很枯燥,也不容易坚持下去. 我们不追求大而全,而是试着每次去研究一个小知识点,最终聚沙成塔,这就是我们的springboot ...

  3. 万万没想到,面试中,连 ClassLoader类加载器 也能问出这么多问题…..

    1.类加载过程 类加载时机 「加载」 将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在内存上创建一个java.lang.Class对象用来封装类在方法区内的数据 ...

  4. 数据库表空间收缩之pg_squeeze,pg_repack

    数据库表空间收缩之pg_squeeze,pg_repack 目录 数据库表空间收缩之pg_squeeze,pg_repack pg_squeeze1.2 原理 优点 安装 使用 pgstattuple ...

  5. Linux下最常用的10个文件压缩工具

    作者简介 李先生(Lemon),高级运维工程师(自称),SRE专家(目标),梦想在35岁买一辆保时捷.喜欢钻研底层技术,认为底层基础才是王道.一切新技术都离不开操作系统(CPU.内存.磁盘).网络等. ...

  6. ssh升级以及ssh: symbol lookup error: ssh: undefined symbol: EVP_aes_128_ctr错误处理

    1.解压安装openssl包:(不能卸载openssl,否则会影响系统的ssl加密库文件,除非你可以做两个软连接libcryto和libssl) # tar -zxvf openssl-1.0.1.t ...

  7. 浅谈JVM垃圾回收

    JVM内存区域 要想搞懂啊垃圾回收机制,首先就要知道垃圾回收主要回收的是哪些数据,这些数据主要在哪一块区域. Java8和Java8之前的相同点有很多. 都有虚拟机栈,本地方法栈,程序计数器,这三个是 ...

  8. If you see someone without smile

    If you see someone without smile, give them one of yours. 难怪我每次和不认识的人说话都放肆大笑.

  9. python中re模块的使用(正则表达式)

    一.什么是正则表达式? 正则表达式,又称规则表达式,通常被用来检索.替换那些符合某个模式(规则)的文本. 正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符.及这些特定字符的组合, ...

  10. git文件操作

    git下载地址: https://git-scm.com/download mac 直接使用brew下载brew install git 1Git一般工作流程: 1.在工作目录创建版本库 2.在工作目 ...