C#发现之旅第五讲 图形开发基础篇

C#发现之旅第五讲 图形开发基础篇

袁永福 2008-5-15

系列课程说明

    为了让大家更深入的了解和使用C#,我们将开始这一系列的主题为“C#发现之旅”的技术讲座。考虑到各位大多是进行WEB数据库开发的,而所谓发现就是发现我们所不熟悉的领域,因此本系列讲座内容将是C#在WEB数据库开发以外的应用。目前规划的主要内容是图形开发和XML开发,并计划编排了多个课程。在未来的C#发现之旅中,我们按照由浅入深,循序渐进的步骤,一起探索和发现C#的其他未知的领域,更深入的理解和掌握使用C#进行软件开发,拓宽我们的视野,增强我们的软件开发综合能力。

本系列课程配套的演示代码下载地址为 http://files.cnblogs.com/xdesigner/cs_discovery.zip 。其中的EllipseButtonLib.zip 就是本课程的演示代码。

本系列课程已发布的文章有
C#发现之旅第一讲 C#-XML开发
C#发现之旅第二讲 C#-XSLT开发
C#发现之旅第三讲 使用C#开发基于XSLT的代码生成器
C#发现之旅第四讲 Windows图形开发入门
C#发现之旅第五讲 图形开发基础篇
C#发现之旅第六讲 C#图形开发中级篇
C#发现之旅第七讲 C#图形开发高级篇
C#发现之旅第八讲 ASP.NET图形开发带超链接的饼图
C#发现之旅第九讲 ASP.NET验证码技术
C#发现之旅第十讲 文档对象模型

课程说明

    经过上次Windows图形开发基本原理的课程,大家对Windows图形开发有着一些感性的认识,但还可能对此不甚了解,还有一些迷茫,在本次课程中,我们将用C#从零开始开发一个比较简单的椭圆形按钮的图形软件,和大家一起开始探索C#图形开发。

功能需求

    在本次快速软件开发中,首先是确定软件功能需求。

    现有一个客户,需要一个软件,其功能要求如下

  1. 实现一个椭圆形的按钮。可居中显示一段单行文本。
  2. 鼠标离开按钮和进入这个按钮时,按钮边框和背景色需要变化。
  3. 鼠标点击按钮会触发一个 Click 事件。

    最后生成的软件的用户界面如图所示

C#发现之旅第五讲 图形开发基础篇

软件设计

根据功能需求,本软件设计如下

  1. 椭圆形按钮是从UserControl 派生的一种自定义控件。
  2. 控件内部重写OnPaint事件来绘制按钮界面。
  3. 重写OnMouseMoveOnMouseEnter,OnMouseLeave事件来实现按钮的动态效果。
  4. 重写OnClick事件来触发 Click 事件。
软件开发过程

    经过简单的设计,我们开始来开发这个软件了。

新建C# WinForm.NET工程

    打开VS.NET2003集成开发环境。新建立一个C#WinForm.NET程序。客户最终需要一个组件,但此处为了调试方便,开始使用WinForm.NET应用程序工程模式,开发完毕后可以设置它为DLL工程模式提交给客户。
    要进行图形开发,C#工程必须引用 System.Drawing.dll,在新增WinForm.NET过程时,会自动添加该引用,而新增其他类型的工程时可能不会默认添加该引用,此时需要手动添加该引用。图形编程需要频繁引用System.Drawing名称空间中的类型,因此在代码的开头需要添加 using System.Drawing ; 不过很多时候VS.NET会自动添加这个代码,若不自动添加则需要手动添加。

新增控件

    新增一个名称为EllipseButton 的用户控件。

    首先是定义控件的一些属性,主要有边框色,按钮背景色,鼠标悬浮时边框色和按钮背景色。

    定义一个鼠标悬停标志变量。 bool bolMouseHoverFlag = false ;

绘制控件用户界面

    重写控件的OnPaint方法,绘制椭圆形按钮,其代码在演示程序中可以看到。在开发自定义的控件时,可以相应控件的Paint事件,也可以重写OnPaint方法,这里为了代码结构简单,此处重写了OnPaint方法,在重写该方法时一定要调用基类的 base.OnPaint 方法。

    在重写的OnPaint 方法中,具有一个类型为 PaintEventArgs 的参数,该参数有若干个成员,其中最重要的就是Graphics成员和ClipRectangle成员,Graphics成员是图形绘制对象,可以看作一个空白的画布,可以任意绘制图形;ClipRectangle成员就是绘制区域剪切矩形。

    在C#图形开发中,Graphics类型是最重要的类型,它表示一个画布对象,任何图形操作都是输出到这个画布上。这个类型提供了很多属性和方法,可以设置某些图形输出质量,还提供了一系列的以Draw开头的方法来绘制图形,以Fill开头的方法来填充图形。此外还提供方法和属性进行坐标转换。

    ClipRectangle表示剪切矩形,一般情况下,控件重新绘制内容时是不需要重写所有的内容,而是绘制一部分内容,该参数就指明控件中那个部分是需要重新绘制的,该区域以外的界面是不需要绘制,因此该参数是优化图形界面软件性能的基础,在这里,由于椭圆形按钮绘制的内容少,界面结构简单,因此不需要优化,不需要使用ClipRectangle参数。

    我们重写的OnPaint函数代码如下

protected override void OnPaint(PaintEventArgs e)
{
    base.OnPaint (e);
    // 创建椭圆路径
    using( System.Drawing.Drawing2D.GraphicsPath path =
                new System.Drawing.Drawing2D.GraphicsPath())
    {
        path.AddEllipse( 0 , 0 , this.ClientSize.Width -1 , this.ClientSize.Height -1 );
        // 填充背景色
        using( SolidBrush b = new SolidBrush(
                    bolMouseHoverFlag ? this.HoverBackColor : this.ButtonBackColor ))
        {
            e.Graphics.FillPath( b , path );
        }
        // 绘制边框
        using( Pen p = new Pen(
                    bolMouseHoverFlag ? this.HoverBorderColor : this.BorderColor , 2 ))
        {
            e.Graphics.DrawPath( p , path );
        }
    }
    if( this.Caption != null )
    {
        // 绘制文本
        using( StringFormat f = new StringFormat())
        {
            // 水平居中对齐
            f.Alignment = System.Drawing.StringAlignment.Center ;
            // 垂直居中对齐
            f.LineAlignment = System.Drawing.StringAlignment.Center ;
            // 设置为单行文本
            f.FormatFlags = System.Drawing.StringFormatFlags.NoWrap ;
            // 绘制文本
            using( SolidBrush b = new SolidBrush( this.ForeColor ))
            {
                e.Graphics.DrawString(
                    this.Caption ,
                    this.Font ,
                    b ,
                    new System.Drawing.RectangleF(
                    0 ,
                    0 ,
                    this.ClientSize.Width ,
                    this.ClientSize.Height ) ,
                    f );
            }
        }
    }
}//protected override void OnPaint(PaintEventArgs e)

    在这个方法中,我们首先创建了一个 GraphicsPath 对象,这个对象表示一个路径,所谓路径就是若干个直线和曲线的组合。我们可以向路径对象中添加各种直线段或曲线。在这里我们调用它的 AddEllipse 方法向路径中添加了一个椭圆曲线,这是一个封闭曲线。AddEllipse 方法的参数表示一个椭圆的外切矩形。在这里外切矩形就是控件的客户区域。

    所谓客户区就是控件内部可以自定义绘制图形的区域。某些Windows控件具有边框,比如文本输入框,边框上面是不能绘制图形的,因此若控件有边框则它的客户区大小不等于控件大小,此时需要使用控件的 ClientSize 属性获得控件客户区大小,当然若控件没有边框,则它的客户区大小等于控件大小,为了编程方便,建议大家以后绘制控件内容时都使用 ClientSize 属性获得可绘制区域的大小。

    创建了一个椭圆路径后,我们可以使用绘制椭圆形了,首先是创建一个 SolidBrush 对象,然后调用图形绘制对象的FillPath方法来填充路径。然后创建 Pen 对象,使用GraphicsDrawPath方法来绘制路径。这里要注意顺序不能搞反。若先绘制边框然后填充椭圆,则会导致后面的操作覆盖掉前面的操作成果。

    图形编程有一个很明显的特点,那就是各种图形操作是要注意顺序的,因为后一个图形操作很容易覆盖掉前面的图形操作结果,这造成了图形开发中调试困难,很多时候需要对代码进行非常仔细的静态检查。

    很多图形编程对象,例如SolidBrushPenGraphisPath等等,都实现了System.IDisposable接口,其内部都使用了非托管资源,在不使用的时候要销毁这些对象,因此在代码中使用了 using 语法结构来处理这些对象。

    这里我们使用鼠标悬停标志变量 bolMouseHoverFlag ,使得鼠标悬停和不悬停时按钮的背景色和边框色有所不同。

    绘制出椭圆区域后,我们就可以绘制按钮文本。首先创建一个 StringFormat 对象,这个对象用于控制绘制文本时的样式。我们设置文本格式为水平居中对齐方式,垂直居中对齐样式,而且还不能换行,只能显示单行文本。

    我们根据文本颜色创建一个SolidBrush对象,然后绘制文本,然后调用图形绘制对象的 DrawString 方法来绘制字符串。这个函数第一个参数是文本内容,第二个是字体,第三个就是绘制文本使用的画刷对象,第四个就是包含文本显示区域的矩形区域,第5个就是文本格式控制。

    完成了OnPaint方法后,我们就获得了一个具有椭圆形外观的用户控件,我们编译程序,然后进入一个窗体设计器,在工具箱的“我的用户控件”栏目,上面可以看到已经有一个 EllipseButton 项目,按下这个项目就可以在窗体上放置一个椭圆形的按钮了,你可以在属性列表中设置它的文本。然后运行程序,可以看到运行的窗体上显示了一个椭圆形的按钮,但这个按钮就像图片一样,毫无生机,我们还需要改进这个控件来实现动态效果。

响应事件,实现动态效果

    打开这个按钮控件的代码,开始添加代码来实现鼠标悬停的动态效果。首先编写一个 CheckMouseHover 函数,该函数用于判断鼠标是否悬停到按钮上面,由于按钮是椭圆形,控件上有部分内容不属于按钮区域,因此即使鼠标在控件上面,也要判断鼠标光标是否在椭圆形区域中。CheckMouseHover函数代码如下

/// <summary>
/// 检测释放发生鼠标悬停状态发生改变,若发生改变则重写绘制控件
/// </summary>
/// <param name="x">测试点X坐标</param>
/// <param name="y">测试点Y坐标</param>
/// <returns>测试点是否在椭圆区域中</returns>
private bool CheckMouseHover( int x , int y )
{
    using( System.Drawing.Drawing2D.GraphicsPath path = new System.Drawing.Drawing2D.GraphicsPath())
    {
        path.AddEllipse( 0 , 0 , this.ClientSize.Width -1 , this.ClientSize.Height -1 );
        bool flag = path.IsVisible( x , y );
        if( flag != bolMouseHoverFlag )
        {
            bolMouseHoverFlag = flag ;
            // 控件整体无效,准备重新绘制,但不立即绘制用户界面.
            this.Invalidate();
            //this.Refresh(); // 强制立即绘制用户界面.
        }
        return flag ;
    }
}

    我们创建一个路径对象,向该路径添加椭圆区域,然后调用路径的 IsVisible 函数判断指定点是否包含在这个路径中,若不包含在路径中,则该点不在椭圆形按钮上面。若这次判断的结果和上次判断的结果不相同,则设置鼠标悬停状态变量,然后重新绘制按钮。

    代码中重新绘制控件具有两种选择,一个是调用控件的 Invalidate 方法,另外可调用 Refresh 方法。两者都能重新绘制用户界面,但是有差别的。Invalidate方法是声明控件用户界面一部分或全部无效,但不会导致立即重新绘制用户界面,而是延迟一段时间后才真正的重新绘制用户界面,可以看作是一种异步操作;而Refresh则是立即重新绘制用户界面,绘制完毕后才结束Refresh方法,是一种同步操作。

    在一般情况下Invalidate函数导致的延迟时间很短暂,人类无法察觉,此时应当调用Invalidate方法;但在少数情况下使用Invalidate会导致明显的可察觉的延迟,则需要使用 Refresh 方法。Invalidate导致的延迟时间的长短和Windows底层消息驱动机制有关,这里看出比较精细的图形编程和Windows底层是有关联的,Invalidate方法是Win32API函数InvalidateRect.NET封装,而Refresh方法是Win32API函数UpdateWindow的封装。查阅MSND中关于这两个API函数的说明就可以理解为什么会出现这种情况。

    微软提出.NET框架目的是让开发者脱离Windows底层API来进行快速软件开发,这个目标在ASP.NET中得到的相当好的实现,因此常规的Web数据库开发中是不会用到Win32API的。但在图形开发中,.NET框架仍然很大程度的依赖Win32API函数,.NET图形相关类库中有很多部分是Win32API的封装,这方面和VCMFC框架有点类似,VCMFC个人认为是傻大黑粗,功能是强大,可是使用很不方便,而.NET框架中包含了一个充满灵性的MFC,使用方便,功能也不弱,但仍然是基于Win32API的。因此要很深入的学习.NET图形编程,就要求对Win32API有所了解,这也加大了.NET图形编程的学习难度。当然比较简单的.NET图形编程是不需要了解Win32API的。

    在这里也反映出图形开发中对用户体验的一些特殊要求。图形软件需要在计算机屏幕上绘制图形,而人类由于其生理特点,各种感觉器官和运动器官的速度是不同的,大脑思维反应最迟钝,手操作键盘和鼠标速度一般,而人眼的反映速度是很快的,能感知屏幕上几十毫秒内发生的变化,由于人眼具有很高的反应速度,因此对图形软件的图形绘制代码运行速度有很高的要求。

    重写控件的OnMouseMove 方法,处理鼠标移动事件,该事件处理中,只是简单的调用CheckMouseHover 成员,参数就使用鼠标光标位置。

    控件提供了一系列的以OnMouse开头的方法都是处理鼠标事件的,该方法有一个类型为 MouseEventArgs 的参数,该参数具有一些属性,列出了发生鼠标事件时的鼠标按键状态,鼠标滚轮计数和鼠标光标在控件客户区中的位置。

    控件还重写 OnMouseLeave 方法,处理鼠标离开控件客户区的事件,取消控件的鼠标悬停状态。

触发Click事件

    客户要求鼠标按下这个椭圆形按钮需要触发一个事件,我们选择了控件本身具有的Click事件作为按钮点击事件,于是我们重写了OnClick函数,该函数代码为

/// <summary>
/// 处理鼠标单击事件
/// </summary>
/// <param name="e"></param>
protected override void OnClick(EventArgs e)
{
    //base.OnClick (e);
    Point p = System.Windows.Forms.Control.MousePosition ;
    p = base.PointToClient( p );
    if( CheckMouseHover( p.X , p.Y ))
    {
        base.OnClick( e );
    }
}

    由于按钮是椭圆形的,当用户鼠标点击控件时,要判断点击点是否在椭圆形区域中,从而要判断是否需要触发Click事件。因此我们重写 OnClick 方法来处理控件的 Click 事件。

    OnClick方法的参数没有指明鼠标光标位置,因此我们自己计算鼠标光标在客户区中的位置,我们使用Control类型的MousePosition静态属性,获得鼠标光标在计算机屏幕中的位置,然后使用控件的PointToClient函数将这个坐标从计算机屏幕坐标转换为控件客户区坐标,然后调用CheckMouseHover函数判断这个坐标是否在椭圆形区域中,若鼠标在椭圆形区域中,则调用base.OnClick方法,触发Click事件。

测试控件

    重新编译程序,新建一个窗体,打开窗体设计器,在工具箱的我的用户控件页面中可以看到有一个EllipiseButton的用户控件,若没有则鼠标右击工具箱,选择菜单项目“添加/移除项目”。在对话框中点击浏览选择刚刚编译生成的EXEDLL文件,然后选中EllipiseButton即可在工具箱上新增EllipseButton项目。选中椭圆形按钮,设置属性列表为显示控件事件,双击添加控件的Click事件,在该事件中显示一个消息框,然后编译运行即可看到一个具有动态效果的椭圆形按钮。如此这个按钮控件编写完毕。

    我们设置工程类型为DLL样式,重新编译,得到一个DLL文件,这个DLL文件就可以提交给客户使用了。

小结

    在本次课程中,我们使用了C#开发了一个很简单的具有动态效果的椭圆形按钮的小组件,演示了C#图形开发的基本过程,使得大家能对C#图形开发有一个初步的印象。从这个小程序可以看出,代码是不多的,但所需的基本知识是比较多的,软件的设计,开发和WEB数据库开发有着很大的不同。最后我希望大家能在今天的程序的基础上,实现一个三角型的按钮控件。

    在下一次课程中,我们继续使用C#开发一个稍微复杂的图形软件,从而更深入的进行C#图形开发的探索。

你可能感兴趣的:(C#)