C# 窗体和消息处理

来源:互联网 发布:1password怎么用 mac 编辑:程序博客网 时间:2024/05/21 17:27

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); // 实例化字体对象// 使用窗体默认字体, 字号30Font 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) {// 实例化字体对象// 使用窗体默认字体, 字号30Font 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对象和它的各类事件属性,执行结果和上一节代码完全相同。

  对于一般性的编程,我们可以灵活的使用上述的两种方式,继承或使用事件属性,来为窗体添加我们感兴趣的事件处理方法,让窗体具有更好的功能。

原创粉丝点击