浅谈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 : 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+美化基础窗口的更多相关文章
- WinForm控件开发总结目录
WinForm控件开发总结(一)------开篇 WinForm控件开发总结(二)------使用和调试自定义控件 WinForm控件开发总结(三)------认识WinForm控件常用的Attrib ...
- 浅谈MapControl控件和PageLayoutControl控件
1.MapControl控件是ArcObject(ArcEngine)中使用非常普遍的一个控件,它对应ArcMap中的DataView视图.MapControl控件实现的功能: 1)管理控件的外观.显 ...
- 浅谈ListBox控件,将对象封装在listBox中,在ListBox中显示对象中某个属性,在ListBox中移除和移动信息
大家好,俗称万事开头难,不经历风雨,怎能见彩虹.在此小编给大家带来一个自己练习的小实例,希望与大家一起分享与交流.下面进入应用场景,从SQL2008数据库取出数据,给ListBox赋值在界面并显示出来 ...
- 浅谈 WPF控件
首先我们必须知道在WPF中,控件通常被描述为和用户交互的元素,也就是能够接收焦点并响应键盘.鼠标输入的元素.我们可以把控件想象成一个容器,容器里装的东西就是它的内容.控件的内容可以是数据,也可以是控件 ...
- 浅谈XAML控件
在win10系统内简单使用了XAML控件,由于本人英语水平有限,在自己的摸索使用.分析代码以及翻译软件.搜索引擎.室友情的帮助下了解了控件的相关功能,下面简要对XAML控件提出几点建议: 1.Cale ...
- winform 控件开发1——复合控件
哈哈是不是丑死了? 做了一个不停变色的按钮,可以通过勾选checkbox停下来,代码如下: 复合控件果然简单呀,我都能学会~ using System; using System.Collection ...
- 浅谈EditText控件的inputType类型
android:inputType="none"--默认 android:inputType="text"--输入文本字符 android:inputType= ...
- [C#开发小技巧]解决WinForm控件TabControl闪烁问题
在用C#开发WinForm程序时,常发现TabControl出现严重的闪烁问题,这主要是由于TabControl控件在实现时会绘制默认的窗口背景.其实以下一段简单的代码可以有效的缓解该问题的发生.这就 ...
- C# Winform开发以及控件开发的需要注意的,被人问怕了,都是基础常识
我是搞控件开发的,经常被人问,所以把一些问题记录了下来!如果有人再问,直接把地址丢给他看. 一. 经常会有人抱怨Winform界面闪烁,下面有几个方法可以尽可能的避免出现闪烁 1.控件的使用尽量以纯色 ...
随机推荐
- 【python接口自动化】- DDT数据驱动测试
简单介绍 DDT(Date Driver Test),所谓数据驱动测试,简单来说就是由数据的改变从而驱动自动化测试的执行,最终引起测试结果的改变.通过使用数据驱动测试的方法,可以在需要验证多组数据 ...
- Facetoprocess_program_design
面向过程程序设计 程序:计算机用可理解可执行的命令的集合. 过程:问题解决的步骤. 方法(函数) 结构化程序设计的基础 一.方法三要素 1 功能: 实现的功能(单一).简单.易维护 2 参数: (传入 ...
- Python requirements.txt 语法
前言 之前一直苦于一个问题,比如一些包在Win上安装不了,比如 uvloop 但是为了提高效率,代码中必须有这个模块 在运行中可以通过 os 模块判断是否使用, 那依赖文件呢? requirement ...
- LeetCode94 二叉树的中序遍历
给定一个二叉树,返回它的中序 遍历. 示例: 输入: [1,null,2,3] 1 \ 2 / 3 输出: [1,3,2] 进阶: 递归算法很简单,你可以通过迭代算法完成吗? /** * ...
- Centos 6.5 Rabbitmq 安装和集群,镜像部署
centos 6.5 rabbitmq 安装和集群,镜像部署 安装erlang: yum install gcc glibc-devel make ncurses-devel openssl-deve ...
- 18.java设计模式之中介者模式
基本需求 智能家庭包括各种设备,闹钟.咖啡机.电视机.窗帘等 要看电视时,各个设备可以协同工作,自动完成看电视的准备工作,比如流程为:闹铃响起->咖啡机开始做咖啡->窗帘自动落下-> ...
- 【Linux】NFS相关小问题
NFS一些小问题: 1. 由于实验环境中,给了rw权限,但是执行的时候,还是提示Permission denied 于是查看nfs服务端,查看/etc/exports文件是否配置有问题 网上很多人配置 ...
- CS_WHERE_USED_MAT 反查BOM的成品CS15
可能很多人都用过BOM展开的函数,但是有的时候,需要通过组件去反查BOM的成品,而这时候就需要用到函数 CS_WHERE_USED_MAT来实现,而对于CS_WHERE_USED_MAT只能反查到上一 ...
- 算法模板 - C++ 高精度运算
C++算法板子 高精度 高精度推荐用python来写,python有大整数,这里写的是关于C++的高精度运算模板 1.高精 * 低精 #include <iostream> #includ ...
- 前端知识(二)08-Vue.js的路由-谷粒学院
目录 一.锚点的概念 二.路由的作用 三.路由实例 1.复制js资源 2.创建 路由.html 3.引入js 4.编写html 5.编写js 一.锚点的概念 案例:百度百科 特点:单页Web应用,预先 ...