窗体和消息处理

来源:互联网 发布:linux终端 编辑:程序博客网 时间:2024/06/06 02:03

点击打开链接


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中输入如下代码:

[csharp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. using System;  
  2. using System.Windows.Forms;  
  3.   
  4. namespace Edu.Study.Graphic.Win {  
  5.   
  6.     class Program {  
  7.         static void Main(string[] args) {  
  8.         // 实例化一个Form类的对象  
  9.             Form form = new Form();  
  10.   
  11.             // 设置Text属性, 字符串  
  12.             form.Text = "第一个窗体";  
  13.   
  14.             // 设置Width, Height属性, 宽度高度  
  15.             form.Width = 800;  
  16.             form.Height = 600;  
  17.   
  18.             // 启动消息循环  
  19.             Application.Run(form);  
  20.         }  
  21.     }  
  22. }  

  运行代码的结果是显示一个如下的窗体:

 
图3 窗体显示效果图


  好了,目前我们就拥有了一个空白的窗体,这个窗体就是我们整个应用程序的“主窗体”,它具有“标题栏”,“最大化、最小化和关闭按钮”以及“系统图标和系统菜单”,是一个标准的Windows窗体。

  从代码可以看到,创建一个窗体非常简单,生成一个Form类的对象即可。Form类对象的引用传入一个Application类的静态Run方法作为参数,窗口就可以显示出来了。

  Form类具有一些属性,可以设置窗口的标题、大小和位置等。

  这里面我们注意一点:我们在Main方法中创建了主窗体的对象,随机将这个对象的引用作为参数传递给了Application类的静态方法Run方法中。这个Run方法实际上启动了一段称为消息循环的代码,即一个while循环,这个循环首先保证了Main方法不会直接运行完毕退出(所以那个Form类的对象也不会被销毁,那么消息循环还干什么事儿呢?

  在一个窗体应用程序中,具备一个队列结构称为“消息队列”,队列里面存放者发送给这个窗体的消息,例如当我们操作窗体时(操作鼠标或者按下键盘),这些操作就会形成“消息”,存入到消息队列中。

  在Main方法中,Application.Run内部启动了一个循环,循环从消息队列里面一条一条的读取消息。如果没有消息,则这个循环就会被阻塞,直到有消息到来。读取到的消息,根据消息类型不同,交给窗体类中对应的方法去处理。直到消息队列中一条“关闭窗体”的消息被获取到后结束循环。

  所以一旦运行了Application.Run方法,除非该方法参数指定的窗口被关闭,否则Main方法一直都不会运行结束,而是在一个循环中不断重复读取消息的工作。

图4 消息队列、消息循环示意图


2 继承窗体,完成消息处理

  继续扩充上述代码

[csharp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. using System;  
  2. using System.Drawing;  
  3. using System.Windows.Forms;  
  4.   
  5. namespace Edu.Study.Graphics.ExtendsForm {  
  6.   
  7.     /// <summary>  
  8.     /// 继承Form类  
  9.     /// </summary>  
  10.     class MyForm : Form {  
  11.   
  12.         /// <summary>  
  13.         /// 保存键盘按键消息的按键值  
  14.         /// </summary>  
  15.         private Keys key = Keys.NoName;  
  16.   
  17.         /// <summary>  
  18.         /// 保存鼠标单击消息的坐标值  
  19.         /// </summary>  
  20.         private Point point = Point.Empty;  
  21.              
  22.         /// <summary>  
  23.         /// 构造器  
  24.         /// </summary>  
  25.         public MyForm() {  
  26.             this.Width = 800;  
  27.             this.Height = 600;  
  28.             this.Text = "继承窗体类";  
  29.   
  30.             // 开启双缓冲绘图模式  
  31.             this.DoubleBuffered = true;  
  32.         }  
  33.       
  34.         /// <summary>  
  35.         /// 鼠标单击消息处理方法  
  36.         /// </summary>  
  37.         protected override void OnMouseDown(MouseEventArgs e) {  
  38.             base.OnMouseDown(e);  
  39.   
  40.             // 将单击时的坐标保存  
  41.             this.point = new Point(e.X, e.Y);  
  42.             // 刷新窗口, 发出窗口重绘消息  
  43.             Refresh();  
  44.         }  
  45.       
  46.         /// <summary>  
  47.         /// 键盘按下消息处理方法  
  48.         /// </summary>  
  49.         protected override void OnKeyDown(KeyEventArgs e) {  
  50.             base.OnKeyDown(e);  
  51.       
  52.             // 将键盘按键值保存  
  53.             this.key = e.KeyCode;  
  54.             this.Refresh();  
  55.         }  
  56.       
  57.         /// <summary>  
  58.         /// 重绘窗口消息处理方法  
  59.         /// </summary>  
  60.         protected override void OnPaint(PaintEventArgs e) {  
  61.             base.OnPaint(e);  
  62.       
  63.             // 实例化字体对象  
  64.             // 使用窗体默认字体, 字号30  
  65.             Font font = new Font(this.Font.FontFamily, 30F);  
  66.       
  67.             // 将鼠标坐标绘制在界面上  
  68.             e.Graphics.DrawString(  
  69.                 String.Format("{0} : {1}"this.point.X, this.point.Y),     // 要绘制的字符串  
  70.                 font,   // 绘制使用的字体  
  71.                 new SolidBrush(Color.Red),  // 绘制使用的画刷  
  72.                 10, 5   // 绘制到屏幕的坐标  
  73.             );  
  74.       
  75.             // 将键盘按键值转换为字符串  
  76.             string showText = this.key.ToString();  
  77.       
  78.             // 实例化字体对象  
  79.             font = new Font(this.Font.FontFamily, 50F);  
  80.       
  81.             // 测量字符串大小(高度、宽度)  
  82.             SizeF size = e.Graphics.MeasureString(showText, font);  
  83.       
  84.             // 获取屏幕中央点  
  85.             PointF point = new PointF((float)this.Width / 2F, (float)this.Height / 2F);  
  86.       
  87.             // 计算字符串绘制起始位置  
  88.             point.X -= size.Width / 2;  
  89.             point.Y -= size.Height / 2;  
  90.       
  91.             // 绘制字符串  
  92.             e.Graphics.DrawString(showText, font, new SolidBrush(Color.Green), point);  
  93.         }  
  94.     }  
  95.          
  96.     static class Program {  
  97.         static void Main(string[] args) {  
  98.             Application.Run(new MyForm());  
  99.         }  
  100.     }  
  101. }  

  执行结果如下图:

 
图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委托的类型为:

[csharp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public delegate void EventHandler(object sender, EventArgs e);  

  其中,sender参数为引发事件的对象,例如本例中的窗体Form对象。e参数为消息参数,即引发该事件的消息的附加参数。EventArgs类型表示没有附加参数。

  其它XXXEventHandler委托大体上和EventHandler委托类似,只是参数不为EventArgs,例如处理鼠标消息的MouseEventHandler类型为:

[csharp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public delegate void MouseEventHandler(object sender, MouseEventArgs e);  

  重新改动上一节的代码

[csharp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. using System;  
  2. using System.Drawing;  
  3. using System.Windows.Forms;  
  4.   
  5. namespace Edu.Study.Graphics.EventHandler {  
  6.   
  7.     static class Program {  
  8.   
  9.         /// <summary>  
  10.         /// 保存键盘按键消息的按键值  
  11.         /// </summary>  
  12.         private static Keys key = Keys.NoName;  
  13.   
  14.         /// <summary>  
  15.         /// 保存鼠标单击消息的坐标值  
  16.         /// </summary>  
  17.         private static Point point = Point.Empty;  
  18.   
  19.         /// <summary>  
  20.         /// 窗体对象  
  21.         /// </summary>  
  22.         private static Form form = new Form();  
  23.   
  24.         /// <summary>  
  25.         /// 主方法  
  26.         /// </summary>  
  27.         static void Main(string[] args) {  
  28.             form.Height = 600;  
  29.             form.Width = 800;  
  30.             form.Text = "窗体事件";  
  31.   
  32.             // 将委托方法关联窗体到事件属性上  
  33.   
  34.             // 关联鼠标事件  
  35.             form.MouseDown += new MouseEventHandler(FormMouseDown);  
  36.             // 关联按键事件  
  37.             form.KeyDown += new KeyEventHandler(FormKeyDown);  
  38.             // 关联刷新事件  
  39.             form.Paint += new PaintEventHandler(FormPaint);  
  40.   
  41.             Application.Run(form);  
  42.         }  
  43.   
  44.         static void FormMouseDown(object sender, MouseEventArgs e) {  
  45.             // 将单击时的坐标保存  
  46.             point = new Point(e.X, e.Y);  
  47.               
  48.             // 刷新窗口, 发出窗口重绘消息  
  49.             form.Refresh();  
  50.         }  
  51.   
  52.         static void FormKeyDown(object sender, KeyEventArgs e) {  
  53.             // 将键盘按键值保存  
  54.             key = e.KeyCode;  
  55.             form.Refresh();  
  56.         }  
  57.   
  58.         static void FormPaint(object sender, PaintEventArgs e) {  
  59.             // 实例化字体对象  
  60.             // 使用窗体默认字体, 字号30  
  61.             Font font = new Font(form.Font.FontFamily, 30F);  
  62.   
  63.             // 将鼠标坐标绘制在界面上  
  64.             e.Graphics.DrawString(  
  65.                 string.Format("{0} : {1}", point.X, point.Y), // 要绘制的字符串  
  66.                 font, // 绘制使用的字体  
  67.                 new SolidBrush(Color.Red), // 绘制使用的画刷  
  68.                 10, 5 // 绘制到屏幕的坐标  
  69.             );  
  70.   
  71.             // 将键盘按键值转换为字符串  
  72.             string showText = key.ToString();  
  73.   
  74.             // 实例化字体对象  
  75.             font = new Font(form.Font.FontFamily, 50F);  
  76.   
  77.             // 测量字符串大小(高度、宽度)  
  78.             SizeF size = e.Graphics.MeasureString(showText, font);  
  79.   
  80.             // 获取屏幕中央点  
  81.             PointF pointf = new PointF((float)form.Width / 2F, (float)form.Height / 2F);  
  82.   
  83.             // 计算字符串绘制起始位置  
  84.             pointf.X -= size.Width / 2;  
  85.             pointf.Y -= size.Height / 2;  
  86.   
  87.             // 绘制字符串  
  88.             e.Graphics.DrawString(showText, font, new SolidBrush(Color.Green), pointf);  
  89.         }  
  90.     }  
  91. }  

  这一次我们使用了Form对象和它的各类事件属性,执行结果和上一节代码完全相同。

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


0 0
原创粉丝点击