本文转载自:http://www.cnblogs.com/encoding/p/5603080.html

按照惯例,先来几张样例图(注:为了展示窗口阴影效果,截图范围向外扩展了些,各位凭想象吧)。

还要来个序

其实,很多年没写过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)
{
     + 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())
        {
            ) / , , );

            g.DrawImage(image, rec, , , 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 = (;
        }
        else if (this.ShowLogo)
        {
            x += ;
        }

        using (var brush = new SolidBrush(this.CaptionForeColor))
        {
            g.DrawString( + this.GetBorderWidth());
        }
    }
}

绘制最小化、最大化、关闭、帮助按钮

private void DrawControlBox(Graphics g)
{
    if (this.ControlBox)
    {
        ImageAttributes ImgAtt = new ImageAttributes();
        ImgAtt.SetWrapMode(System.Drawing.Drawing2D.WrapMode.TileFlipXY);
        ;
        //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 = , , );
            using (var brush = new SolidBrush(closeHover ? this.ControlActivedColor : this.ControlBackColor))
            {
                g.FillRectangle(brush, closeRect);
            }

            g.DrawImage(, , this.CloseButtonImage.Width, this.CloseButtonImage.Height, GraphicsUnit.Pixel, ImgAtt);
            x -= ;
        }

        if (this.MaximizeBox && this.WindowState == FormWindowState.Maximized && this.MaximumNormalButtonImage != null)
        {
            maxRect = , , );

            using (var brush = new SolidBrush(maxHover ? this.ControlActivedColor : this.ControlBackColor))
            {
                g.FillRectangle(brush, maxRect);
            }

            g.DrawImage(, , this.MaximumNormalButtonImage.Width, this.MaximumNormalButtonImage.Height, GraphicsUnit.Pixel, ImgAtt);
            x -= ;
        }
        else if (this.MaximizeBox && this.WindowState != FormWindowState.Maximized && this.MaximumButtonImage != null)
        {
            maxRect = , , );
            using (var brush = new SolidBrush(maxHover ? this.ControlActivedColor : this.ControlBackColor))
            {
                g.FillRectangle(brush, maxRect);
            }
            g.DrawImage(, , this.MaximumButtonImage.Width, this.MaximumButtonImage.Height, GraphicsUnit.Pixel, ImgAtt);
            x -= ;
        }

        if (this.MinimizeBox && this.MinimumButtonImage != null)
        {
            minRect = , , );

            using (var brush = new SolidBrush(minHover ? this.ControlActivedColor : this.ControlBackColor))
            {
                g.FillRectangle(brush, minRect);
            }
            g.DrawImage(, , this.MinimumButtonImage.Width, this.MinimumButtonImage.Height, GraphicsUnit.Pixel, ImgAtt);
            x -= ;
        }

        if (base.HelpButton && this.HelpButtonImage != null)
        {
            helpRect = , , );
            using (var brush = new SolidBrush(helpHover ? this.ControlActivedColor : this.ControlBackColor))
            {
                g.FillRectangle(brush, helpRect);
            }
            g.DrawImage(, , this.HelpButtonImage.Width, this.HelpButtonImage.Height, GraphicsUnit.Pixel, ImgAtt);
            x -= ;
        }
    }
}

窗体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(")]
public int CaptionHeight { get { return this.captionHeight; } set { this.captionHeight = value; this.SetPadding(); } }

每次修改边框宽度时,需要重新生成窗体的Padding属性

private int borderWidth;
[Category(")]
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_HTBOTTOMLEFT = 0x10;
        ;

        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,
                        ( & 0xFFFF);
                    vPoint = PointToClient(vPoint);
                    )
                        )
                            m.Result = (IntPtr)Guying_HTTOPLEFT;
                        )
                            m.Result = (IntPtr)Guying_HTBOTTOMLEFT;
                        else m.Result = (IntPtr)Guying_HTLEFT;
                    )
                        )
                            m.Result = (IntPtr)Guying_HTTOPRIGHT;
                        )
                            m.Result = (IntPtr)Guying_HTBOTTOMRIGHT;
                        else m.Result = (IntPtr)Guying_HTRIGHT;
                    )
                        m.Result = (IntPtr)Guying_HTTOP;
                    )
                        m.Result = (IntPtr)Guying_HTBOTTOM;
                    break;
                case 0x0201:                //鼠标左键按下的消息
                    m.Msg = 0x00A1;         //更改消息为非客户区按下鼠标
                    m.LParam = IntPtr.Zero; //默认值
                    m.WParam = );//鼠标放在标题栏内
                    base.WndProc(ref m);
                    break;
                default:
                    base.WndProc(ref m);
                    break;
            }
        }
        #endregion

全类文件,不晓得咋上传附件,所以没传,要的可以找我QQ。

2016年6月22日编辑添加:

  由于本人在北京出差,昨晚上飞机航班延误,根本没想到突然这么多人要源码,无法做到一一回应,请大家谅解,我已经将DForm类上传,感谢“大萝卜控”给我提示。

  请大家点击这里下载。

  里面有几张图片,大家可以随便弄下,我出差比较忙,回去之后,再放github,到时再开个单章通知大家。

  关于阴影的部分,大家可以先注释掉代码,完整源码放出之后,就可以了。

  还有哟,我QQ在文章某个地方有显示,没加码。

懒惰,是一个通病。 努力,必会成为一种习惯。

重绘Winform窗体的更多相关文章

  1. 【原创】重绘winform的GroupBox

    功能:重绘winform的GroupBox,以便调整边框颜色和边框宽度 using System; using System.Collections.Generic; using System.Com ...

  2. 窗体背景的绘制(Windows窗体每次都会重绘其窗体背景,所以我们可以通过拦截窗体重绘背景的消息(WM_ERASEBKGND),并自定义方法来实现重绘窗体背景)

    核心思想:由于Windows窗体每次都会重绘其窗体背景,所以我们可以通过拦截窗体重绘背景的消息(WM_ERASEBKGND),并自定义方法来实现重绘窗体背景.通过TImage组件也可以实现,但是重写W ...

  3. 【转】【C#】C#重绘windows窗体标题栏和边框

    摘要 windows桌面应用程序都有标准的标题栏和边框,大部分程序也默认使用这些样式,一些对视觉效果要求较高的程序,如QQ, MSN,迅雷等聊天工具的样式则与传统的windows程序大不相同,其中迅雷 ...

  4. WinForm中重绘TabControl选项卡标题

    最近开发WinForm频繁使用了TabControl控件,这个控件的选项卡没有BackgroundImage这个属性,那么如何为其各个选项卡添加背景图片呢?(这里说的是每个TabPage的头部,也就是 ...

  5. 重写OnPaint事件对窗体重绘(显示gif动画) 实例2

    /// <summary> /// 可显示Gif 的窗体 /// </summary> public class WinGif : Form { private Image _ ...

  6. [DForm]我也来做自定义Winform之另类标题栏重绘

    据说得有楔子 按照惯例,先来几张样例图(注:为了展示窗口阴影效果,截图范围向外扩展了些,各位凭想象吧).                   还要来个序 其实,很多年没写过Winform了,前端时间在 ...

  7. 『转载』C# winform 中dataGridView的重绘(进度条,虚线,单元格合并等)

    原文转载自:http://hi.baidu.com/suming/item/81e45b1ab9b4585f2a3e2243 最近比较浅的研究了一下dataGridView的重绘,发现里面还是有很多东 ...

  8. 深入Windows窗体原理及控件重绘技巧

    之前有学MFC的同学告诉我觉得Windows的控件重绘难以理解,就算重绘成功了还是有些地方不明白,我觉得可能很多人都有这样的问题,在这里我从Windows窗体的最基本原理来讲解,如果你有类似的疑惑希望 ...

  9. 使用重绘项美化WinForm中的控件

    如果你觉得项目中的ComboBox.ListBox或其它的Winforms控件不能满足你的显示要求,包括窗体在内很多控件都支持重绘修改显示样式.下面的示例完成对ComBox数据项的重绘,希望能起到抛砖 ...

随机推荐

  1. GridView获取CheckBox的值及所在列的ID

    //根据GridView的列数进行循环 ; i < GridView1.Rows.Count; i++) { //检查是否有ID为CheckBox1的CheckBox控件,如果有就赋值给Chec ...

  2. logback + slf4j + jboss + spring mvc

    logback.log4j.log4j2 全是以同一个人为首的团伙搞出来的(日志专业户!),这几个各有所长,log4j性能相对最差,log4j2性能不错,但是目前跟mybatis有些犯冲(log4j2 ...

  3. Mysql常用命令行大全

    第一招.mysql服务的启动和停止 net stop mysql net start mysql 第二招.登陆mysql 语法如下: mysql -u用户名 -p用户密码 键入命令mysql -uro ...

  4. svn Q&amp;A

    Q1:在svn commit的时候,会出现某某文件 is missing.这是因为此次提交时:远程repository中并没有该文件,而且本地repository也没有该文件. 具体原因: 1.可能因 ...

  5. 《剑指offer-名企面试官精讲典型编程题》读后感

    首先,不得不说这是一本好书!!! 我接触这本书是在学长的推荐下去看的,而且口碑还是挺好的一本书,豆瓣的评分也比较高,当我刚看了它,我就深深的爱上了这本书,到现在为止,我已经看了三遍这本书了,平时无聊时 ...

  6. SQL中自定义拆分为新表的函数

    /*按照符号分割字符串*/ create function [dbo].[m_split](@c varchar(2000),@split varchar(2)) returns @t table(c ...

  7. MLE MAP EM

    1.最大似然估计 (MLE):  什么是最大似然估计?     问题:给定一组观察数据还有一个参数待定的模型,如何来估计这个未知参数呢? 观察数据(x1,y1)......(xn,yn)   待定模型 ...

  8. Nuget控制台 - 给你的快速添加缺少的包

    利用命令行安装包

  9. Android与PHP服务器交互

    转自:http://blog.csdn.net/ab_ba/article/details/7912424 服务器端:server.php 1 <?php 2         include(' ...

  10. python 中如何导入一个自己创建的模块

    导入模块的语句的三种方法: 1.import module 2.from module import name1,[name2,name3....] 3.from module import * 先看 ...