[DForm]我也来做自定义Winform之另类标题栏重绘
据说得有楔子
按照惯例,先来几张样例图(注:为了展示窗口阴影效果,截图范围向外扩展了些,各位凭想象吧)。

还要来个序
其实,很多年没写过Winform了,前端时间在重构我们公司自己的呼叫中心系统,突然就觉得客户端好丑好丑,对于我这种强迫症晚期患者来说,界面不好看都不知道怎么写代码的,简直就是种折磨,还是满清十大酷刑级别那种。
很多人推荐WPF,不过个人对WPF没啥感觉,而且据说也无法支持2.0,还是采用Winform技术来搞吧。
终于第一节
做Winform皮肤,我了解的无非有2种方式。
1.采用纯图片模式:由专业美工设计各种样式的图片,进行窗口背景图片设置
2.采用GDI+纯代码绘制:参照美工设计的各种样式的图片,使用C#代码绘制出来
第一种方式很简单,但是可复用性不高,性能上面应该也会有一些影响,如果图片太大,窗口拖动等引起的重绘时,会明显有闪烁等情况。
第二种方式很复杂,但是效率和复用性高,开放各种扩张属性之后,可以适用于大部分场景。
以前用了很多次第一种方案,每次新做APP,都得重新设计界面,很不便利。这次,我准备采用第二种方案来做一个公用的皮肤。
关于GDI+,我只能算一个新人,不做具体的介绍,这里只讲我的一些实现方式,计划项目完成后,开源到github。
绘制标题栏
做自定义界面,绕不开一个问题就是绘制标题栏。
每个Winform窗体,可以分为两个部分:非客户区域和客户区域。
非客户区域:表示无法由我们程序猿绘制的部分,具体包括:窗口标题栏,边框
客户区域:表示由我们程序猿绘制的部分,也就是窗体内容,平时我们拖控件都是拖到客户区域
一般自定义窗口的实现方式无非以下种
1.设置窗口为无边框窗口,顶部放一个Panel,设置Panel.Dock=Top,然后在Panel里面绘制logo、标题、按钮等元素。
2.拦截窗口消息,重写WndProc方法,拦截窗口标题绘制消息,由自己手工绘制
很多人会为了简便,采用第一种方式,不过缺点比较明显,对于我来说,最主要的一点就是真正的实现界面,里面的控件元素Dock会受到影响,不利于客户区域布局。
高手牛人会采用第二种方式,不是我这种Winform小白的菜,所以,我采用第三种方式,也是本篇文章的核心思想。
采用无边框窗口,设置窗口Padding.Top为标题栏高度,采用GDI+绘制标题栏元素。
这种方式的好处显而易见
具体实现窗体子控件Dock不受影响
无边框之后,重写窗体拖动事件不需要对标题栏每一个元素进行事件处理
标题栏高度可随时自定义
本文开头的几个截图,标题栏绘制代码如下
绘制标题文字、Logo图片
private void DrawTitle(Graphics g)
{
var x = 6 + this.GetBorderWidth();
if (this.ShowLogo)
{
g.SmoothingMode = SmoothingMode.AntiAlias;
ImageAttributes imgAtt = new ImageAttributes();
imgAtt.SetWrapMode(System.Drawing.Drawing2D.WrapMode.TileFlipXY);
using (var image = this.Icon.ToBitmap())
{
var rec = new Rectangle(x, (this.captionHeight - 24) / 2, 24, 24); g.DrawImage(image, rec, 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, imgAtt);
} } if (this.ShowTitle)
{
var font = this.titleFont == null ? this.Font : this.titleFont;
var fontSize = Size.Ceiling(g.MeasureString(this.Text, font));
if (this.CenterTitle)
{
x = (this.Width - fontSize.Width) / 2;
}
else if (this.ShowLogo)
{
x += 30;
} using (var brush = new SolidBrush(this.CaptionForeColor))
{
g.DrawString(this.Text, font, brush, x, (this.CaptionHeight - fontSize.Height) / 2 + this.GetBorderWidth());
}
}
}
绘制最小化、最大化、关闭、帮助按钮
private void DrawControlBox(Graphics g)
{
if (this.ControlBox)
{
ImageAttributes ImgAtt = new ImageAttributes();
ImgAtt.SetWrapMode(System.Drawing.Drawing2D.WrapMode.TileFlipXY);
var x = this.Width - 32;
//var rec = new Rectangle(this.Width - 32, (this.CaptionHeight - 32) / 2 + this.BorderWidth, 32, 32);
//var rec = new Rectangle(x, this.BorderWidth, 32, 32);
if (this.CloseButtonImage != null)
{
closeRect = new Rectangle(x, 0, 32, 32);
using (var brush = new SolidBrush(closeHover ? this.ControlActivedColor : this.ControlBackColor))
{
g.FillRectangle(brush, closeRect);
} g.DrawImage(this.CloseButtonImage, closeRect, 0, 0, this.CloseButtonImage.Width, this.CloseButtonImage.Height, GraphicsUnit.Pixel, ImgAtt);
x -= 32;
} if (this.MaximizeBox && this.WindowState == FormWindowState.Maximized && this.MaximumNormalButtonImage != null)
{
maxRect = new Rectangle(x, 0, 32, 32); using (var brush = new SolidBrush(maxHover ? this.ControlActivedColor : this.ControlBackColor))
{
g.FillRectangle(brush, maxRect);
} g.DrawImage(this.MaximumNormalButtonImage, maxRect, 0, 0, this.MaximumNormalButtonImage.Width, this.MaximumNormalButtonImage.Height, GraphicsUnit.Pixel, ImgAtt);
x -= 32;
}
else if (this.MaximizeBox && this.WindowState != FormWindowState.Maximized && this.MaximumButtonImage != null)
{
maxRect = new Rectangle(x, 0, 32, 32);
using (var brush = new SolidBrush(maxHover ? this.ControlActivedColor : this.ControlBackColor))
{
g.FillRectangle(brush, maxRect);
}
g.DrawImage(this.MaximumButtonImage, maxRect, 0, 0, this.MaximumButtonImage.Width, this.MaximumButtonImage.Height, GraphicsUnit.Pixel, ImgAtt);
x -= 32;
} if (this.MinimizeBox && this.MinimumButtonImage != null)
{
minRect = new Rectangle(x, 0, 32, 32); using (var brush = new SolidBrush(minHover ? this.ControlActivedColor : this.ControlBackColor))
{
g.FillRectangle(brush, minRect);
}
g.DrawImage(this.MinimumButtonImage, minRect, 0, 0, this.MinimumButtonImage.Width, this.MinimumButtonImage.Height, GraphicsUnit.Pixel, ImgAtt);
x -= 32;
} if (base.HelpButton && this.HelpButtonImage != null)
{
helpRect = new Rectangle(x, 0, 32, 32);
using (var brush = new SolidBrush(helpHover ? this.ControlActivedColor : this.ControlBackColor))
{
g.FillRectangle(brush, helpRect);
}
g.DrawImage(this.HelpButtonImage, helpRect, 0, 0, this.HelpButtonImage.Width, this.HelpButtonImage.Height, GraphicsUnit.Pixel, ImgAtt);
x -= 32;
}
}
}
窗体OnPaint事件,自绘标题栏
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
#region draw caption
using (var brush = new SolidBrush(this.CaptionBackgroundColor))
{
e.Graphics.FillRectangle(brush, captionRect);
} this.DrawTitle(e.Graphics);
this.DrawControlBox(e.Graphics);
#endregion #region draw border
ControlPaint.DrawBorder(e.Graphics, this.ClientRectangle, borderColor, ButtonBorderStyle.Solid);
#endregion
}
采用Padding来约束子实现界面的元素布局位置
当我采用了无边框窗体来做自定义皮肤之后,由于去除了非客户区域(标题栏、边框),子实现窗体的坐标位置(0,0)实际上应该会覆盖我的标题栏,不过,反编译.NET源码之后,我发现Form类有一个Padding属性,这个属性继承自Control类,它的作用与CSS中的padding相同。所以,我决定使用这个技术来约束子实现界面的元素布局位置。
每次修改标题栏高度时,需要重新生成窗体的Padding属性
private int captionHeight;
[Category("标题栏"), Description("标题栏高度"), DefaultValue(typeof(int), "40")]
public int CaptionHeight { get { return this.captionHeight; } set { this.captionHeight = value; this.SetPadding(); } }
每次修改边框宽度时,需要重新生成窗体的Padding属性
private int borderWidth;
[Category("边框"), Description("边框宽度"), DefaultValue(typeof(int), "1")]
public int BorderWidth { get { return this.borderWidth; } set { this.borderWidth = value; this.SetPadding(); } }
最后,隐藏掉Padding属性,外部修改无效
public new Padding Padding { get; set; }
附加1:标题栏自绘按钮悬浮背景色修改和单击事件处理
protected override void OnMouseMove(MouseEventArgs e)
{
Point p = new Point(e.X, e.Y);
captionHover = captionRect.Contains(p);
if (captionHover)
{
closeHover = closeRect != Rectangle.Empty && closeRect.Contains(p);
minHover = minRect != Rectangle.Empty && minRect.Contains(p);
maxHover = maxRect != Rectangle.Empty && maxRect.Contains(p);
helpHover = helpRect != Rectangle.Empty && helpRect.Contains(p);
this.Invalidate(captionRect);
this.Cursor = (closeHover || minHover || maxHover || helpHover) ? Cursors.Hand : Cursors.Default;
}
else
{
if (closeHover || minHover || maxHover || helpHover)
{
this.Invalidate(captionRect);
closeHover = minHover = maxHover = helpHover = false;
} this.Cursor = Cursors.Default;
} base.OnMouseMove(e);
}
protected override void OnMouseClick(MouseEventArgs e)
{
var point = new Point(e.X, e.Y);
if (this.closeRect != Rectangle.Empty && this.closeRect.Contains(point))
{
this.Close();
return;
}
if (!this.maxRect.IsEmpty && this.maxRect.Contains(point))
{
if (this.WindowState == FormWindowState.Maximized)
{
this.WindowState = FormWindowState.Normal;
}
else
{
this.WindowState = FormWindowState.Maximized;
}
this.maxHover = false;
return;
}
if (!this.minRect.IsEmpty && this.minRect.Contains(point))
{
this.WindowState = FormWindowState.Minimized;
this.minHover = false;
return;
}
if (!this.helpRect.IsEmpty && this.helpRect.Contains(point))
{
this.helpHover = false;
this.Invalidate(this.captionRect);
CancelEventArgs ce = new CancelEventArgs();
base.OnHelpButtonClicked(ce);
return;
}
base.OnMouseClick(e);
}
附加2:处理无边框窗体用户调整大小
#region 调整窗口大小
const int Guying_HTLEFT = 10;
const int Guying_HTRIGHT = 11;
const int Guying_HTTOP = 12;
const int Guying_HTTOPLEFT = 13;
const int Guying_HTTOPRIGHT = 14;
const int Guying_HTBOTTOM = 15;
const int Guying_HTBOTTOMLEFT = 0x10;
const int Guying_HTBOTTOMRIGHT = 17; protected override void WndProc(ref Message m)
{
if (this.closeHover || this.minHover || this.maxHover || this.helpHover)
{
base.WndProc(ref m);
return;
} if (!this.CustomResizeable)
{
base.WndProc(ref m);
return;
}
switch (m.Msg)
{
case 0x0084:
base.WndProc(ref m);
Point vPoint = new Point((int)m.LParam & 0xFFFF,
(int)m.LParam >> 16 & 0xFFFF);
vPoint = PointToClient(vPoint);
if (vPoint.X <= 5)
if (vPoint.Y <= 5)
m.Result = (IntPtr)Guying_HTTOPLEFT;
else if (vPoint.Y >= ClientSize.Height - 5)
m.Result = (IntPtr)Guying_HTBOTTOMLEFT;
else m.Result = (IntPtr)Guying_HTLEFT;
else if (vPoint.X >= ClientSize.Width - 5)
if (vPoint.Y <= 5)
m.Result = (IntPtr)Guying_HTTOPRIGHT;
else if (vPoint.Y >= ClientSize.Height - 5)
m.Result = (IntPtr)Guying_HTBOTTOMRIGHT;
else m.Result = (IntPtr)Guying_HTRIGHT;
else if (vPoint.Y <= 5)
m.Result = (IntPtr)Guying_HTTOP;
else if (vPoint.Y >= ClientSize.Height - 5)
m.Result = (IntPtr)Guying_HTBOTTOM;
break;
case 0x0201: //鼠标左键按下的消息
m.Msg = 0x00A1; //更改消息为非客户区按下鼠标
m.LParam = IntPtr.Zero; //默认值
m.WParam = new IntPtr(2);//鼠标放在标题栏内
base.WndProc(ref m);
break;
default:
base.WndProc(ref m);
break;
}
}
#endregion
全类文件,不晓得咋上传附件,所以没传,要的可以找我QQ。
2016年6月22日编辑添加:
由于本人在北京出差,昨晚上飞机航班延误,根本没想到突然这么多人要源码,无法做到一一回应,请大家谅解,我已经将DForm类上传,感谢“大萝卜控”给我提示。
请大家点击这里下载。
里面有几张图片,大家可以随便弄下,我出差比较忙,回去之后,再放github,到时再开个单章通知大家。
关于阴影的部分,大家可以先注释掉代码,完整源码放出之后,就可以了。
还有哟,我QQ在文章某个地方有显示,没加码。
很多人要源码,以前做的找不到了,新发一个,用的东西差不多。拿去吧。
https://github.com/dongger/MomoForm
[DForm]我也来做自定义Winform之另类标题栏重绘的更多相关文章
- 『转载』C# winform 中dataGridView的重绘(进度条,虚线,单元格合并等)
原文转载自:http://hi.baidu.com/suming/item/81e45b1ab9b4585f2a3e2243 最近比较浅的研究了一下dataGridView的重绘,发现里面还是有很多东 ...
- WinForm GroupBox控件重绘外观
private void groupBoxFun_Paint(PaintEventArgs e, GroupBox groupBox){ e.Graphics.Clear(groupBox.BackC ...
- c#winform自定义窗体,重绘标题栏,自定义控件学习
c#winform自定义窗体,重绘标题栏 虽然现在都在说winform窗体太丑了,但是我也能尽量让桌面应用程序漂亮那么一点点话不多说,先上图 重绘标题栏先将原生窗体设置成无边框,FormBoderSt ...
- 背水一战 Windows 10 (20) - 绑定: DataContextChanged, UpdateSourceTrigger, 对绑定的数据做自定义转换
[源码下载] 背水一战 Windows 10 (20) - 绑定: DataContextChanged, UpdateSourceTrigger, 对绑定的数据做自定义转换 作者:webabcd 介 ...
- 绑定: DataContextChanged, UpdateSourceTrigger, 对绑定的数据做自定义转换
介绍背水一战 Windows 10 之 绑定 DataContextChanged - FrameworkElement 的 DataContext 发生变化时触发的事件 UpdateSourceTr ...
- C#自定义按钮、自定义WinForm无边框窗体、自定义MessageBox窗体
C#自定义按钮.自定义WinForm无边框窗体.自定义MessageBox窗体 C#自定义Button按钮控件 效果展示 C#自定义Winform无边框窗体 效果展示 C#自定义无边框MessageB ...
- C#自定义Winform无边框窗体
C#自定义Winform无边框窗体 在实际项目中,WinForm窗体或者控件不能满足要求,所以就需要自己设计窗体等,当然设计界面可以用的东西很多,例如WPF.或者一些第三方的库等.本例中将采用WinF ...
- 带入gRPC:对 RPC 方法做自定义认证
带入gRPC:对 RPC 方法做自定义认证 原文地址:带入gRPC:对 RPC 方法做自定义认证项目地址:https://github.com/EDDYCJY/go... 前言 在前面的章节中,我们介 ...
- 重绘Winform窗体
本文转载自:http://www.cnblogs.com/encoding/p/5603080.html 按照惯例,先来几张样例图(注:为了展示窗口阴影效果,截图范围向外扩展了些,各位凭想象吧). 还 ...
随机推荐
- Delphi XE8中开发DataSnap程序常见问题和解决方法 (二)想对DBExpress的TSQLDataSet写对数据库操作的SQL语句出错了!
当我们搞定DataSnap后,我们进入客户端程序开发阶段了,我们建立了客户端模块后,打算按照刚才开发服务器的步骤开发客户端程序,随后加入了DBExpress的TSQLDataSet,设定数据库连接后, ...
- Timer(定时器)
默认情况下,在每个采样器之前没有任何延时,这样不能很好的模拟现实生活中人们访问网页,因为现实生活中人们点击一个请求后,会有一定的时间,然后再点击下一个请求,JMeter提供了定时器来模拟这种行为. 定 ...
- 如何在SQLServer中处理每天四亿三千万记录的(数据库大数据处理)
首先声明,我只是个程序员,不是专业的DBA,以下这篇文章是从一个问题的解决过程去写的,而不是一开始就给大家一个正确的结果,如果文中有不对的地方,请各位数据库大牛给予指正,以便我能够更好的处理此次业务. ...
- 19年PDYZ冬令营游记
我和卓越的那些事 ——2019年平度一中卓越计划冬令营 题前记: 正月十三那天,刚看完<流浪地球>,便接到了一个电话,老妈告诉我竟然一中组织了一个冬令营,并且起了一个很好的名字“卓越计 ...
- FPGA基础学习(3) -- 跨时钟域处理方法
文章主要是基于学习后的总结. 1. 时钟域 假如设计中所有的触发器都使用一个全局网络,比如FPGA的主时钟输入,那么我们说这个设计只有一个时钟域.假如设计有两个输入时钟,如图1所示,一个时钟给接口1使 ...
- struts2学习笔记(四)——访问Servlet的API&结果跳转&数据封装
一.Struts2访问Servlet的API 前面已经对Struts2的流程执行完成了,但是如果表单中有参数如何进行接收?又或者我们需要向页面保存一些数据,又要如何完成呢?我们可以通过学习Struts ...
- ABP中文网入门篇教程中的一个bug
入门--从空项目开始--使用ASP.NET Core Web Application https://cn.abp.io/documents/abp/latest/Autofac-Integratio ...
- C#基础语法(二)
四.CTS类型 C#认可的基本预定义类型并没有内置于C#语言中,而是内置于.NET Framework中. 例如,在C#中声明一个int类型的数据时,声明的实际上是.NET结构System.Int32 ...
- springboot(五)-使用Redis
Redis服务器 springboot要使用redis,首先当然要确保redis服务器能够正常跑起来. pom.xml 这里添加redis的依赖,当然也是springboot集成好的. <!-- ...
- Win10安装MySQL5.7.22解压缩版的方法及手动配置讲解
1.先去MYSQL官网下载安装包,解压放到C盘 2.新建一个my.ini文件放到bin文件夹下面,内容如下,路径对应自己的安装目录: [mysql] # 设置mysql客户端默认字符集 default ...