我们首先来看一下窗体类的继承关系:
图1 窗体类继承关系图
其实微软从一开始开发Windows的时候,就采用了面向对象的方式(在C语言上模拟的面向对象),所以到后来的C++,VB,Delphi,都可以比较容易的使用一门面向对象语言来对Windows窗体这部分代码进行封装。
.net也是如此,它在内部依然调用了Win32 SDK中的API函数来生成窗体,启动消息循环。所以.net窗体类是一种所谓的“重量级”组件。即这些类会和操作系统直接通信,调用操作系统提供的接口函数,而不仅仅依赖于虚拟机和运行时环境。
MarshalByRefObject类向窗体提供了远程代理的能力(即可以通过MarshalByRefObject类来跨越域的边界,从而使得Windows消息可以传送到.net环境中来,这些内容后面再详细叙述)。
Component类定义了一个可以接收消息的组件,并可以将接收到的消息重新封装为对象,分发到不同的方法中去。
Control类定义了组件的基本外观和行为(例如大小、位置、颜色、字体等以及基本的消息响应方法例如鼠标移动,鼠标按下,鼠标释放等),Control类保存这一个重要的属性——窗体句柄,这个句柄是通过调用Win32 SDK的API函数CreateWindowEx获得的。所以可以认为,Control类定义了一块区域,这块区域可以接收消息,并可以显示。
从Control这里产生了一个分支,ScrollableControl类指定了一种包含内容可以大于窗体边界的窗体类,这个类产生的窗体对象具有滚动条属性,可以通过拖动滚动条来改变内容在窗体中显示的位置。
无需滚动条的窗体则直接继承Control类,例如按钮(Button类),单行文本框(TextBox类),标签(Label类)。
ContainerControl类包含一个Controls集合属性,表现为一个“容器”,可以容纳其它从Control类继承的窗体类。
Form类定义了一个“顶级容器”,即这个窗体可以包含其它窗体,但自身不能包含自身。
出了Form类外的所有窗体,都必须包含在一个容器窗体内。所以Form是整个窗体应用程序中最基本的容器窗体。
通俗上,把Form类的对象叫“窗体”,其它窗体类的对象称为“子窗体”或“控件”。
控件的种类很多,大约上百种,在我们的课程中无法一一叙述。只能大致的讲解一下:
这里我们仅举一例,来介绍控件的基本使用方法。
Program.cs
1 | using System; |
2 | using System.Windows.Forms; |
3 | |
4 | namespace Edu.Study.Graphics.SubWindow { |
5 | |
6 | /// <summary> |
7 | /// 声明一个继承自Button的类, 表示一个按钮 |
8 | /// </summary> |
9 | public class MyButton : Button { |
10 | |
11 | // 一个文本框控件的引用 |
12 | private TextBox textBox; |
13 | |
14 | /// <summary> |
15 | /// 构造器 |
16 | /// </summary> |
17 | public MyButton(TextBox textBox) { |
18 | |
19 | // 设置按钮的宽度和高度 |
20 | this.Width = 120; |
21 | this.Height = 90; |
22 | |
23 | // 设置按钮上显示的文字 |
24 | this.Text = "Hello"; |
25 | |
26 | this.TextBox = textBox; |
27 | } |
28 | |
29 | /// <summary> |
30 | /// 按钮中包含的文本框控件的引用 |
31 | /// </summary> |
32 | public TextBox TextBox { |
33 | get { |
34 | return this.textBox; |
35 | } |
36 | set { |
37 | if (value == null) { |
38 | throw new ArgumentNullException("TextBox"); |
39 | } |
40 | this.textBox = value; |
41 | } |
42 | } |
43 | |
44 | /// <summary> |
45 | /// 覆盖超类中的CreateControl方法, 处理控件创建消息 |
46 | /// 当控件被创建的时候会执行这个方法 |
47 | /// </summary> |
48 | protected override void OnCreateControl() { |
49 | base.OnCreateControl(); |
50 | |
51 | // Parent属性表示包含该控件的容器对象, 这里为Form类型对象 |
52 | // 设置按钮的位置, 令其在父容器内居中 |
53 | this.Top = (this.Parent.Height - this.Height) / 2; |
54 | this.Left = (this.Parent.Width - this.Width) / 2; |
55 | } |
56 | |
57 | /// <summary> |
58 | /// 处理按钮的点击消息 |
59 | /// 当按钮被鼠标点击后, 会执行这个方法 |
60 | /// </summary> |
61 | /// <param name="e"></param> |
62 | protected override void OnClick(EventArgs e) { |
63 | base.OnClick(e); |
64 | |
65 | // 显示一个消息对话框 |
66 | MessageBox.Show("Hello"); |
67 | // 将文本框控件的内容该为Hello |
68 | this.TextBox.Text = "Hello"; |
69 | } |
70 | } |
71 | |
72 | |
73 | |
74 | static class Program { |
75 | /// <summary> |
76 | /// 应用程序的主入口点。 |
77 | /// </summary> |
78 | static void Main() { |
79 | |
80 | // 为应用程序启动高级外观样式(Windows XP以上有效) |
81 | Application.EnableVisualStyles(); |
82 | // 为应用程序内控件上文本绘制启动GDI+(如果参数为true, 则使用传统的GDI) |
83 | Application.SetCompatibleTextRenderingDefault(false); |
84 | |
85 | // 实例化一个窗体对象 |
86 | Form form = new Form(); |
87 | // 设置窗体的标题文本 |
88 | form.Text = "控件测试"; |
89 | |
90 | // 实例化一个文本框对象 |
91 | TextBox textBox = new TextBox(); |
92 | // 设置文本框的位置和宽度 |
93 | textBox.Top = 10; |
94 | textBox.Left = 20; |
95 | textBox.Width = 100; |
96 | |
97 | // 将文本框加入到窗体上 |
98 | form.Controls.Add(textBox); |
99 | // 实例化一个MyButton按钮对象并加入到窗体上 |
100 | form.Controls.Add(new MyButton(textBox)); |
101 | |
102 | Application.Run(form); |
103 | } |
104 | } |
105 | } |
本节代码下载
在上述代码中,我们从Button类继承了一个子类MyButton,在其构造器中完成了对其外观属性的设置;在其响应“创建完毕(ON_CREATE)消息”的方法中完成了对其位置的设置(通过覆盖OnCreateControl方法)。
通过覆盖OnClick方法,我们完成了对按钮鼠标点击消息的处理(ON_CLICK消息)。在这个方法内,通过一个文本框对象的引用,改变了文本框对象的样式(其Text属性为文本框内显示的文本)。
在Main方法中,我们创建了定义容器(Form类的对象),创建了一个文本框对象,通过设置其属性改变了其样式。使用Form类的Controls属性,将文本框和按钮(MyButton类对象)作为控件加入到容器中。
我们来谈谈窗体的“创建”这一概念:
无论是子窗体还是容器窗体,其“创建”这个概念并不仅仅是生成该类的对象(当然,实例化对象是第一步),实例化仅仅是生成了窗体的对象,还并没有对其进行“创建”,窗体创建的标志是:这个窗体对象已经被加入到窗体上下文中并且被赋予了窗体句柄。
什么是上下文?上下文是一个特殊的对象,我们可以把它理解成为一篇文章。一篇文章有不同的章节,每个章节有不同的段落。如果我们已经有了文章的章节和段落,但此时还没有将它们加入到文章整体,就还不算一篇完整的文章。对于窗体,我们生成了窗体类的对象,赋予属性值,覆盖其方法,但此时这个对象还没有加入到整个应用程序窗体界面的上下文中,也就是并没有创建称为真正的窗体。
当我们把所有的对象生成完毕后,将所有窗体对象的顶级容器Form类对象传入Application.Run方法中,这时候Application.Run才得到了完整的上下文,并开始根据上下文中保存的对象引用逐一创建窗体,每创建一个窗体,就将这个窗体增加到操作系统整个的窗体上下文中,并得到操作系统返回的窗体标示——一个窗体句柄,此时窗体才算真正创建完毕,操作系统会根据它掌握的上下文统一决定所有窗体显示的位置和方式。窗体一旦创建完毕,系统就会给这个窗体对象发送一个ON_CREATE消息,通知窗体对象窗体已经创建完毕,响应这个消息,可以在窗体创建完毕的同时做一些操作。
注意程序的53、54行,这两句是改变按钮的位置,但这两句不能写在MyButton类的构造器中,因为构造器是在创建对象的时候调用,此时窗体并未创建,也并没有加到窗体上下文中,所以MyButton对象的Parent属性不可用。只有MyButton真正被创建了,加入到了窗体上下文以后,它才知道谁是它的父容器对象,此时Parent属性才可以被访问。
程序运行的结果为:
图2 运行结果图
好了,现在我们用另一种方法来完成同样的功能:
Program.cs
1 | using System; |
2 | using System.Windows.Forms; |
3 | |
4 | namespace Edu.Study.Graphics.SubWindow2 { |
5 | |
6 | /// <summary> |
7 | /// 继承Form类, 定义新的窗体类 |
8 | /// </summary> |
9 | public class MyForm : Form { |
10 | /// <summary> |
11 | /// 按钮类字段 |
12 | /// </summary> |
13 | private Button button; |
14 | |
15 | /// <summary> |
16 | /// 文本框类字段 |
17 | /// </summary> |
18 | private TextBox textbox; |
19 | |
20 | /// <summary> |
21 | /// 构造器 |
22 | /// </summary> |
23 | public MyForm() { |
24 | // 初始化按钮对象 |
25 | this.button = new Button(); |
26 | this.button.Text = "Hello"; |
27 | this.button.Width = 120; |
28 | this.button.Height = 90; |
29 | // 为按钮对象的Click事件增加委托方法 |
30 | this.button.Click += new EventHandler(ButtonClick); |
31 | |
32 | // 初始化文本框对象 |
33 | this.textbox = new TextBox(); |
34 | this.textbox.Left = 10; |
35 | this.textbox.Top = 10; |
36 | |
37 | // 将文本框加入到窗体上 |
38 | this.Controls.Add(this.textbox); |
39 | // 实例化一个MyButton按钮对象并加入到窗体上 |
40 | this.Controls.Add(this.button); |
41 | } |
42 | |
43 | /// <summary> |
44 | /// 覆盖OnLoad方法, 处理窗体第一次被显示时的消息 |
45 | /// </summary> |
46 | protected override void OnLoad(EventArgs e) { |
47 | base.OnLoad(e); |
48 | |
49 | // 设置按钮的位置 |
50 | this.button.Top = (this.Height - this.button.Height) / 2; |
51 | this.button.Left = (this.Width - this.button.Width) / 2; |
52 | } |
53 | |
54 | /// <summary> |
55 | /// 委托方法, 处理按钮被点击事件 |
56 | /// </summary> |
57 | private void ButtonClick(object sender, EventArgs e) { |
58 | // 显示一个消息对话框 |
59 | MessageBox.Show("Hello"); |
60 | // 将文本框控件的内容该为Hello |
61 | this.textbox.Text = "Hello"; |
62 | } |
63 | } |
64 | |
65 | |
66 | static class Program { |
67 | /// <summary> |
68 | /// 应用程序的主入口点。 |
69 | /// </summary> |
70 | static void Main() { |
71 | |
72 | // 为应用程序启动高级外观样式(Windows XP以上有效) |
73 | Application.EnableVisualStyles(); |
74 | // 为应用程序内控件上文本绘制启动GDI+(如果参数为true, 则使用传统的GDI) |
75 | Application.SetCompatibleTextRenderingDefault(false); |
76 | |
77 | Application.Run(new MyForm()); |
78 | } |
79 | } |
80 | } |
本节代码下载
这次我们换了一个思路,采用“事件”和“委托”的概念重新完成程序。程序运行结果和上例完全相同。但代码的架构相差很多。
首先,这次我们继承了Form类,而没有继承Button类,在Form类中,保存了Button类和Textbox类的对象引用,并用事件的方法,采用一个委托来处理按钮被点击时要做的事情。
这种方式归结起来一句话:用事件、委托来处理窗体消息,而尽量少用继承、覆盖的方法。这种方法的好处是:
以后我们尽量采用方式2来编写代码,只有在特殊情况下(例如我们需要一个和.net Framework提供的控件不同的新控件时),才使用继承的方法书写部分代码。