0 概述
Windows的界面顾名思义,由“窗体”来组成,窗体的概念:屏幕上特定的一块区域,具有绘图区域和剪裁边界,并具备响应用户输入设备操作能力。
- 绘图区域:每一个窗体都定义了一块区域,在这块区域里,可以进行绘图,绘制的图形将显示在窗体中。随着窗体位置的移动,绘图区域也在不断移动;
- 剪裁边界:绘图区域的四周,由剪裁边界包围,剪裁边界保证了绘图区域确定的大小,超出部分会被剪裁掉,不被显示;
- 事件响应:主要响应鼠标和键盘事件,当鼠标在窗体上发生点击,则一组鼠标事件会从操作系统反映给该窗体;当按下键盘时,一组键盘事件会从操作系统反映给“输入焦点”所在的窗体。
Windows的图形系统由如下几个部分组成:
- *窗体:Desktop或称为桌面,是Windows操作系统的最主要的窗体,其它窗体都是在其基础上建立的;
- 窗体:是由应用程序建立的窗体;
- 子窗体:是依附于主窗体或弹出窗体之上的窗体,俗称的“控件”就是子窗体的一种。
所以所谓的图形编程,就是为各种各样的窗体编程。
1 窗体和消息循环
先来看一段主窗体的代码,由于Visual Studio会自动为Windows应用程序项目建立主窗体,所以这里我们采用将控制台应用程序项目改造为Windows应用程序项目的方法来体现建立窗体的过程。
第一步:建立一个普通的“控制台应用程序”项目:
第二步:为项目添加支持窗体编程的程序集引用:
1、选择“项目 –> 添加引用”打开“添加引用对话框”,选中“.NET选项卡”(如下图):
![]() |
![]() |
图1 添加引用
2、按住Ctrl键,同时选中其中的“System.Drawing”和“System.Windows.Forms”两项,点击确定,完成这两个程序集引用的添加,其中:
- System.Drawing 命名空间包括了.net Framework图形绘制相关类;
- System.Windows.Forms 命名空间包括了.net Framework窗体相关类。
第三步:将项目输出类型改为“Windows应用程序”
选择“项目 –> 属性”,打开项目属窗口,选择“应用程序”选项卡,将其中输出类型改为“Windows应用程序”
![]() |
![]() |
图2 更改项目输出类型
至此,我们的项目就可以支持图形用户界面的开发了。现在在Program.cs中输入如下代码:
- using System;
- using System.Windows.Forms;
- namespace Edu.Study.Graphic.Win {
- class Program {
- static void Main(string[] args) {
- // 实例化一个Form类的对象
- Form form = new Form();
- // 设置Text属性, 字符串
- form.Text = "第一个窗体";
- // 设置Width, Height属性, 宽度高度
- form.Width = 800;
- form.Height = 600;
- // 启动消息循环
- Application.Run(form);
- }
- }
- }
运行代码的结果是显示一个如下的窗体:
图3 窗体显示效果图
好了,目前我们就拥有了一个空白的窗体,这个窗体就是我们整个应用程序的“主窗体”,它具有“标题栏”,“最大化、最小化和关闭按钮”以及“系统图标和系统菜单”,是一个标准的Windows窗体。
从代码可以看到,创建一个窗体非常简单,生成一个Form类的对象即可。Form类对象的引用传入一个Application类的静态Run方法作为参数,窗口就可以显示出来了。
Form类具有一些属性,可以设置窗口的标题、大小和位置等。
这里面我们注意一点:我们在Main方法中创建了主窗体的对象,随机将这个对象的引用作为参数传递给了Application类的静态方法Run方法中。这个Run方法实际上启动了一段称为消息循环的代码,即一个while循环,这个循环首先保证了Main方法不会直接运行完毕退出(所以那个Form类的对象也不会被销毁,那么消息循环还干什么事儿呢?
在一个窗体应用程序中,具备一个队列结构称为“消息队列”,队列里面存放者发送给这个窗体的消息,例如当我们操作窗体时(操作鼠标或者按下键盘),这些操作就会形成“消息”,存入到消息队列中。
在Main方法中,Application.Run内部启动了一个循环,循环从消息队列里面一条一条的读取消息。如果没有消息,则这个循环就会被阻塞,直到有消息到来。读取到的消息,根据消息类型不同,交给窗体类中对应的方法去处理。直到消息队列中一条“关闭窗体”的消息被获取到后结束循环。
所以一旦运行了Application.Run方法,除非该方法参数指定的窗口被关闭,否则Main方法一直都不会运行结束,而是在一个循环中不断重复读取消息的工作。
图4 消息队列、消息循环示意图
2 继承窗体,完成消息处理
继续扩充上述代码
- using System;
- using System.Drawing;
- using System.Windows.Forms;
- namespace Edu.Study.Graphics.ExtendsForm {
- /// <summary>
- /// 继承Form类
- /// </summary>
- class MyForm : Form {
- /// <summary>
- /// 保存键盘按键消息的按键值
- /// </summary>
- private Keys key = Keys.NoName;
- /// <summary>
- /// 保存鼠标单击消息的坐标值
- /// </summary>
- private Point point = Point.Empty;
- /// <summary>
- /// 构造器
- /// </summary>
- public MyForm() {
- this.Width = 800;
- this.Height = 600;
- this.Text = "继承窗体类";
- // 开启双缓冲绘图模式
- this.DoubleBuffered = true;
- }
- /// <summary>
- /// 鼠标单击消息处理方法
- /// </summary>
- protected override void OnMouseDown(MouseEventArgs e) {
- base.OnMouseDown(e);
- // 将单击时的坐标保存
- this.point = new Point(e.X, e.Y);
- // 刷新窗口, 发出窗口重绘消息
- Refresh();
- }
- /// <summary>
- /// 键盘按下消息处理方法
- /// </summary>
- protected override void OnKeyDown(KeyEventArgs e) {
- base.OnKeyDown(e);
- // 将键盘按键值保存
- this.key = e.KeyCode;
- this.Refresh();
- }
- /// <summary>
- /// 重绘窗口消息处理方法
- /// </summary>
- protected override void OnPaint(PaintEventArgs e) {
- base.OnPaint(e);
- // 实例化字体对象
- // 使用窗体默认字体, 字号30
- Font font = new Font(this.Font.FontFamily, 30F);
- // 将鼠标坐标绘制在界面上
- e.Graphics.DrawString(
- String.Format("{0} : {1}", this.point.X, this.point.Y), // 要绘制的字符串
- font, // 绘制使用的字体
- new SolidBrush(Color.Red), // 绘制使用的画刷
- 10, 5 // 绘制到屏幕的坐标
- );
- // 将键盘按键值转换为字符串
- string showText = this.key.ToString();
- // 实例化字体对象
- font = new Font(this.Font.FontFamily, 50F);
- // 测量字符串大小(高度、宽度)
- SizeF size = e.Graphics.MeasureString(showText, font);
- // 获取屏幕*点
- PointF point = new PointF((float)this.Width / 2F, (float)this.Height / 2F);
- // 计算字符串绘制起始位置
- point.X -= size.Width / 2;
- point.Y -= size.Height / 2;
- // 绘制字符串
- e.Graphics.DrawString(showText, font, new SolidBrush(Color.Green), point);
- }
- }
- static class Program {
- static void Main(string[] args) {
- Application.Run(new MyForm());
- }
- }
- }
执行结果如下图:
图5 项目运行结果
可以看到,Form类本身具备了处理各种消息的方法,并且每个方法和不同的消息一一对应。我们只需要覆盖其中的不同方法,就可以改变对某个消息的处理方式。
在Form类中,所有以On开头的方法都是处理各种消息的,这些方法都定义为virtual,都可以被子类的同样方法所覆盖。
消息都会附加一些参数,例如鼠标消息会附加鼠标指针的坐标,鼠标按键的类型;键盘消息会附加键盘按键的值,键盘组合键值等。这些消息参数被包装为EventArgs类的不同子类的对象,传递给消息处理方法。
图6 EventArgs类继承图
其中,EventArgs类不具有特殊的属性,仅具有一个静态的Empty属性,引用到了一个EventArgs对象上,表示消息没有额外的参数。也就是说,当我们看到一个消息处理方法的参数为EventArgs类的对象引用,那说明这个方法处理的消息没有特别的消息参数。
如果消息方法的参数是一个EventArgs类的子类对象引用,则说明这个消息附带一些特殊的参数,例如图中的MouseVentArgs类,表示鼠标消息参数,具有坐标属性,鼠标按键属性等。
上述响应窗体事件的方法:继承Form类,覆盖On打头的消息处理方法,根据参数获取消息附加参数。
注意:一般而言,我们再覆盖On系列方法时,都要在第一句使用base.OnXXX调用超类的被覆盖方法一次,因为超类定义的On系列方法是虚拟方法,本身具有方法体,对消息有一些基本的处理,我们需要调用一次。
3 利用EventHandler委托
一个窗体除了On系列方法处理消息外,还有一系列XXXEventHandler类型的事件属性,名称恰好是On系列方法的方法名去掉On。消息处理方法和事件属性之间的关系为:每一个事件处理方法在内部都访问了对应的事件属性,通过这些属性调用了代理的方法(如果有的话)。
EventHandle委托的类型为:
- public delegate void EventHandler(object sender, EventArgs e);
其中,sender参数为引发事件的对象,例如本例中的窗体Form对象。e参数为消息参数,即引发该事件的消息的附加参数。EventArgs类型表示没有附加参数。
其它XXXEventHandler委托大体上和EventHandler委托类似,只是参数不为EventArgs,例如处理鼠标消息的MouseEventHandler类型为:
- public delegate void MouseEventHandler(object sender, MouseEventArgs e);
重新改动上一节的代码
- using System;
- using System.Drawing;
- using System.Windows.Forms;
- namespace Edu.Study.Graphics.EventHandler {
- static class Program {
- /// <summary>
- /// 保存键盘按键消息的按键值
- /// </summary>
- private static Keys key = Keys.NoName;
- /// <summary>
- /// 保存鼠标单击消息的坐标值
- /// </summary>
- private static Point point = Point.Empty;
- /// <summary>
- /// 窗体对象
- /// </summary>
- private static Form form = new Form();
- /// <summary>
- /// 主方法
- /// </summary>
- static void Main(string[] args) {
- form.Height = 600;
- form.Width = 800;
- form.Text = "窗体事件";
- // 将委托方法关联窗体到事件属性上
- // 关联鼠标事件
- form.MouseDown += new MouseEventHandler(FormMouseDown);
- // 关联按键事件
- form.KeyDown += new KeyEventHandler(FormKeyDown);
- // 关联刷新事件
- form.Paint += new PaintEventHandler(FormPaint);
- Application.Run(form);
- }
- static void FormMouseDown(object sender, MouseEventArgs e) {
- // 将单击时的坐标保存
- point = new Point(e.X, e.Y);
- // 刷新窗口, 发出窗口重绘消息
- form.Refresh();
- }
- static void FormKeyDown(object sender, KeyEventArgs e) {
- // 将键盘按键值保存
- key = e.KeyCode;
- form.Refresh();
- }
- static void FormPaint(object sender, PaintEventArgs e) {
- // 实例化字体对象
- // 使用窗体默认字体, 字号30
- Font font = new Font(form.Font.FontFamily, 30F);
- // 将鼠标坐标绘制在界面上
- e.Graphics.DrawString(
- string.Format("{0} : {1}", point.X, point.Y), // 要绘制的字符串
- font, // 绘制使用的字体
- new SolidBrush(Color.Red), // 绘制使用的画刷
- 10, 5 // 绘制到屏幕的坐标
- );
- // 将键盘按键值转换为字符串
- string showText = key.ToString();
- // 实例化字体对象
- font = new Font(form.Font.FontFamily, 50F);
- // 测量字符串大小(高度、宽度)
- SizeF size = e.Graphics.MeasureString(showText, font);
- // 获取屏幕*点
- PointF pointf = new PointF((float)form.Width / 2F, (float)form.Height / 2F);
- // 计算字符串绘制起始位置
- pointf.X -= size.Width / 2;
- pointf.Y -= size.Height / 2;
- // 绘制字符串
- e.Graphics.DrawString(showText, font, new SolidBrush(Color.Green), pointf);
- }
- }
- }
这一次我们使用了Form对象和它的各类事件属性,执行结果和上一节代码完全相同。
对于一般性的编程,我们可以灵活的使用上述的两种方式,继承或使用事件属性,来为窗体添加我们感兴趣的事件处理方法,让窗体具有更好的功能。