WinForm二三事(一)
在进入正文之前,想请大家先欣赏下面两段代码:
1: //这是一个控制台程序,请先添加System.Windows.Form.dll的引用
2: using System.Windows.Form;
3:
4: public class ConsoleApplicationShowDialog
5: {
6: static void Main()
7: {
8: Form frm = new Form();
9: frm.ShowDialog();
10: }
11: }
1: //这是一个控制台程序,请先添加System.Windows.Form.dll的引用
2: using System.Windows.Form;
3:
4: public class ConsoleApplicationShow
5: {
6: static void Main()
7: {
8: Form frm = new Form();
9: frm.Show();
10: }
11: }
两个代码片段都是控制台程序(编译的时候,请选择ConsoleApplication类型编译)。这两段程序唯一的区别就在于显示窗体的时候第一个使用ShowDialog(就是所谓的模态窗体),第二个使用Show(也就是所谓的非模态窗体)。
经过测试我们发现,使用Show显示出来的窗体一显示就死在那里了,不响应用户的输入,如果你在窗体上放一个按钮,甚至发现按钮都无法显示,点击也无任何响应。而是用ShowDialog显示出来的窗体却不一样,可以响应用户的输入。这是什么原因呢?
为了找到问题的根源,我们来看看Show方法和ShowDialog方法实现的区别。Show方法是在Control里定义的,Form间接的派生自Control类(看起来这里是一个组合模式哦),Show方法代码:
1: public void Show()
2: {
3: this.Visible = true;
4: }
Show方法的代码相当的简单,做的工作仅仅就是将窗体显示出来,那前面第二段代码应该与下面的代码作用是一样的:
1: //这是一个控制台程序,请先添加System.Windows.Form.dll的引用
2: using System.Windows.Form;
3:
4: public class ConsoleApplicationShow
5: {
6: static void Main()
7: {
8: Form frm = new Form();
9: frm.Visible = true;
10: }
11: }
现在再来看看ShowDialog方法,ShowDialog方法有些复杂,但是在这百来行代码中,应该有一条你很熟悉:
1: public DialogResult ShowDialog(IWin32Window owner)
2: {
3: //...省略
4: Application.RunDialog(this);
5: //...省略
6: }
哦,这行代码跟我们千千万万个WinForm程序的启动部分相当类似:
1: public class Program
2: {
3: static void Main()
4: {
5: Form frm = new Form();
6: Application.Run(frm);
7: }
8: }
MSDN对Application.Run的说明是:
Begins running a standard application message loop on the current thread, and makes the specified form visible.
在当前的线程上启动一个标准的应用程序“消息循环”,并且显示指定的窗体
下面是Application.Run的代码:
public static void Run(Form mainForm)
{
ThreadContext.FromCurrent().RunMessageLoop(-1, new ApplicationContext(mainForm));
}
哦?什么是消息循环?如果你是直接进入.Net开发的,没有经过Win32时代的洗礼,那可能对这个消息循环并不是很清楚,在你眼里只有注册事件,处理事件。虽然.Net通过封装,简化了消息循环这种处理用户点击等事件的编程模型,但是.Net底下还是Win32,有的时候我们还是得了解一下,对理解有些问题可能有帮助(后面会提到)。
消息循环(Message Loop)
说Application.Run启动一个消息循环,那么什么是消息循环呢?看下面的代码:
MSG msg;
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
这是一段几乎所有使用Win32 API编写Windows Application的程序里都有的代码。这就是一个消息循环。你不需要透彻的理解上面这段代码,你只需要了解这么一个意思:
Windows为每个Windows程序都维护了一个消息队列,当有用户输入事件的时候,Windows就把这个事件转换为一个称之为“消息”的东东(也就是上面代码中的MSG结构),
在这个消息里包含有一些信息,比如鼠标点击的点啊,消息的类型啊等等。
而上面的while循环中的GetMessage方法就是不断的从这个消息队列里取消息出来,然后处理,这样窗体就能响应用户的输入了。
通过上面的讨论,我们现在大概明白了为啥Show和ShowDialog区别这么大呢,原来ShowDialog启动了一个消息循环,这样用ShowDialog显示出来的窗体就能响应用户的输入事件了,而Show仅仅是设置一下窗体的Visible属性,并没有启动一个消息循环,使用Show显示出来的窗体也就无法响应用户的输入事件了,也就是死在那里了。
上面说,GetMessage取出消息,然后处理,那在哪儿处理呢?在Win32程序中我们还可以看到这样的片段:
LRESULT CALLBACK WndProc(HWND hwnd,UINT message,WPARAM wParam,LPARAM lParam)
{
switch(message)
{
case WM_CREATE:
//处理窗体创建事件
return 0;
case WM_PAINT:
//处理窗体绘制事件
return 0;
//更多事件,比如按钮点击等
}
}
啊,好丑陋的处理方式。原来是根据message的类型,做出不同的处理,而Windows定义了一大堆WM_开头的东东。可我们可爱的.Net,WinForm里面优美的事件处理模型就是基于这个之上的,通过上面的代码,和你在.Net里使用事件的感触你是否能想象出.Net是如何封装这个过程的?
WinForm中的消息处理
实际上在.Net的WinForm中,消息处理的影子还是存在的,并没有消失得无影无踪,在Form中还有这么一个protected的方法:
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case 0x10://......
case 0x11:
//....
}
base.WndProc(ref m);
}
哦,原来与Win32里面的那个一模一样。实际上通过重写这个方法我们可以实现一些正常做法难以实现的东东。
为什么耗时操作要异步
谈了这么多,我们来谈一点我们身边的事情。你应该碰到过这样一个场景:编写一个程序,点击一个按钮之后要做一个比较耗时的操作,比如要更新一大批数据到数据库,这个时候程序就像本文开头那个程序一样,死掉了。用户不管怎么点击,程序变成灰色,标题栏上还显示一个“没有响应”,有的程序甚至连个提示都不给,用户以为真的死掉了,气急败坏的啪嚓一下把程序关了,耗时操作进行到一半就这样被无情终止。这是为什么呢?
通过前面的讨论,我们知道,响应用户的输入就是靠消息循环,而消息循环就是在当前的线程上,也就是我们所谓的那个UI线程,如果一个耗时操作也同在UI线程上,那么消息循环就“卡着”了,也就无法处理后续的消息,程序也就假死了。
那我们如何处理这种耗时操作呢?当然就是将这个耗时操作放到另外一个线程中,不占用UI线程,让消息循环得以继续的进行下去。
(未完待续)