.Net Winform 开发笔记(四) 透过现象看本质

写在前面:

      从一个窗体的创建显示,再到与用户的交互,最后窗体关闭,这中间经历过了一系列复杂的过程,本文将从Winform应用程序中的Program.cs文件的第一行代码开始,逐步分析一个Winform应用程序到底是怎样从出生走向死亡,这其中包括Form.Show()和Form.ShowDialog()的区别、模式对话框形成的本质原因、消息循环、Windows事件与.net中事件(Event)的区别、System.Windows.Form.Application类的作用、以及我之前一篇博客中(.Net开发笔记(二)网址)面试题中的最后一题,从Windows消息层次讲述点击按钮弹出一个MessageBox的详细过程。

     我承认,不了解以上问题的Coder可能也能写出非常出色非常复杂的Winform应用程序出来,但不是有句老话么,知其然,亦要知其所以然。

     另外,看本篇博客(或者接下来几篇)必须了解Win32编程知识,如果不清楚的同学,可以先上网学习学习,这就像学习MFC最好也得懂点Win32编程,本文不解释什么是Win32 API、什么是句柄、更不会解释什么是回调方法。

一个引子:

      一个线程,具体啥定义我也就不说了,太抽象,我觉得还是把它看做是一个方法(函数),当然包括方法体中调用的其它方法,线程有开始,也有结束,分别可以比作方法的开始和结束,我们不管一个方法体内调用了多少其它方法,只要程序没写错,这个方法肯定有返回的时候,也就是说,在正常情况下,一个线程开始后,肯定会有退出(结束)的时候,那么,如果想让一个线程不会太快结束,我们可以在方法体内写些啥?“阻塞方法!”有人可能马上说,因为阻塞方法一般不会马上返回,只有等它执行完毕后,才会返回,在它返回前,调用它的方法不会继续运行下去,的确,在我学习C++语言的时候,经常写Console程序(那时候也只会写这玩意儿),为了不让黑屏闪一下就消失了,看不到运行结果,我经常在程序最后加上一行“int a;cin>>a;”,我当时也不知道为啥要这样写,只知道这样写了,程序不会马上结束。其实后来才知道,那行代码就是阻塞了整个程序,当你输入一个整数,按下回车,程序就会结束。

      “阻塞方法”确实是一种方法,但是如果我们想在线程执行过程中,与外部(用户)进行交互,也就是说,在线程执行期间,用户可以通过输入来控制线程的运行情况,同样在Console程序中,该怎么实现?现在问题来了,不紧不能让线程马上结束,还要与用户有所交互,而且不应该只交互一次(否则,上面提到的cin>>a;完全够用),该怎么搞?不止交互一次?那么很容易就能想到“循环”,用循环来使线程与用户进行交互再好不过了,为了与本文相联系,用C#代码编写如下:

.Net Winform 开发笔记(四) 透过现象看本质View Code

非常简单的一段代码,程序运行后,有了while循环,不会马上结束,它会不停的等待用户输入,然后输出用户输入的字符串(模拟响应用户操作),直到用户输入“quit”后,循环才结束。这段利用while循环和Console.ReadLine()写出来的程序虽然短小简单,却是后面我们要谈到的Winform应用程序(其实所有的Windows应用程序都一样,无论是MFC还是Delphi或者其他搞出来的桌面程序)的精髓。当然,这段代码确实太简陋了,所以我才说它是精髓,O(∩_∩)O~。既然太简陋,那我们再改改吧。要改就改复杂一点。

初加工:

.Net Winform 开发笔记(四) 透过现象看本质View Code

解释一下,代码虽然多了一点,可大概结构还是没变(其实我们见到的其他所有框架,结构虽然复杂得很,可其精髓的代码也就不到一半,其余的都是在精髓代码上扩充来的,增加各种各样的功能),如你所见,跟之前的意思一样,线程中有一个While循环、接收用户输入、响应用户输入(操作)。不一样的是,将接受用户输入部分封装到一个GetSignal方法中去了,将响应用户输入部分封装到一个DispatchSignal方法中去了,为了更好的反应用户操作可以“多样化”(不再是以前输入一个字符串,线程再将源字符串输出),我定义了一个Rect类,该类表示一个矩形,可以供用户操作,我还定义了一个Signal类,该类表示一个信号,用户的所有输入都可以看做是一个信号,信号中包括信号接受者(ID)、信号类型、以及信号可能附带的参数,此外,(不要嫌麻烦O(∩_∩)O~)我还定义了一个信号类型枚举,用来表示用户操作的类型。

现在,我们来理清一下整个线程运行的流程:

  1. ZZThread中的静态方法Main开始运行,线程开始
  2. 新建四个Rect对象,将其加到一个集合中,供用户操作
  3. 开始一个while循环,GetSignal接受用户输入,输入格式需按照规定格式
  4. GetSignal方法返回,如果用户输入不是“QUIT”字符串,返回true,否则返回false,while循环结束,线程退出。
  5. 用户输入不是“QUIT”,GetSignal方法的signal参数即为用户输入的信息(该信号应该包括用户想要操作的对象、操作的类型、以及一些附带参数),其实就是上面的“信号”概念。
  6. 信号有了,需要将信号发给接受者,那么,DispatchSignal方法就负责将信号发给对应的Rect对象(通过rect.ID ?= signal.ID来判断)。
  7. 接受者(Rect对象)使用自己的RectProc来处理信号,RectProc方法中根据不同的信号类型,作出相应的反应

可能文字不太直观,上一张图,来解释一下,图文结合更有效。

.Net Winform 开发笔记(四) 透过现象看本质

图1

好了,改了之后的代码复杂很多,当然了,功能也比之前的多了很多,但是还是那句话,大概结构没有变,一个while循环、一个接收用户输入部分、一个响应用户操作部分。(看完代码和图的同学,或者说有Win32编程基础的同学,到现在为止,可能已经看出这是个啥意思,我们暂且先不说,听我慢慢道来O(∩_∩)O~)

现在我来说说改了之后的代码还有哪些地方的不足:

  1. 每个Rect对象之间无法通信,因为各个Rect对象之间是相互独立的,每个Rect对象在响应用户输入(执行RectProc,下同)的时候不能影响其他的Rect对象,因为你根本不知道另外的Rect对象在哪、什么状态。
  2. 在响应用户输入的时候,也就是while循环体执行期间,我们不能改变while循环条件,让循环结束,意思就是,现在这个线程,有两种情况退出,第一种就是用户直接输入“QUIT”,第二种就是强制关闭程序,后者明显不可取,那么前者一种方法能满足我们的需求吗?答案是不能,现在考虑这种情况:在线程运行期间,所有存在的Rect对象中有一个是主Rect,也就是说,这个主Rect对象跟其他不一样,当这个主Rect对象被用户关闭后(RS_KILL),最好的效果就是,整个线程结束。因此,在主Rect对象处理RS_KILL信号后,应立马“模仿”用户向线程再发送一个“QUIT”字符串,让while循环下一次退出。
  3. Rect对象既然是用户主要操作的目标,那么就应该允许我们在Rect类上继承新的类,来实现更丰富的效果,而且,新扩展出来的类也应该像Rect类一样响应用户输入。
  4. 同样,Rect类对象的一举一动,势必会影响另外一些对象,所以,Rect类应该加上一些事件(此处事件为.net中的Event,它与Windows事件的区别稍后会讲)。
  5. 在Rect类对象响应用户的某一次操作后,可能需要再次通知自己进行其他操作,比如一个Rect对象在响应“改变位置”这个信号之后,立马需要显示自己信息,也就是说在处理完RS_POSITIONCHANGE信号后,立刻需要给自己发一个RS_SHOWINFO信号,它才能显示自己的信息。这就出现一个问题,“信号”会产生“信号”,这个过程完全不需要用户区操控,当然,用户也无法去操控。
  6. 最后,不知道诸位发现没有,用户的输入与Rect对象的响应是(也只能是)同步的,啥叫同步?简单来说就是,A做完什么之后,B才能行动,或者等B行动完后,A才能继续。只有等用户输入后,GetSignal方法才能返回,Rect对象才能做出反应,同理,只有Rect对象响应完成后,用户才可能继续输入,一次输入一次响应,输入没完成,就没有响应,响应没完成,用户也不能输入。理想情况应该是这样的:用户在想要输入的时候就可以输入,而不用去管Rect对象有没有响应完成(DispatchSignal返回),当然,在这种情况下,用户的输入仍然会陆陆续续的被响应。

分析一下上面6条,其中1、2、5条其实意思差不多,就是在while循环体执行期间,需要“模仿”用户输入,然而现在的情况是,GetSignal方法是“主动型”的,只有它主动去接收用户输入,它才会有结果,当它没有准备好,就算有输入,也不会被接收。这样看来,我们只有增加一个类似“缓冲区”的东西,不管GetSignal有没有准备,所有的输入信号全部存放在这个缓冲区中,等到GetSignal准备好获取输入信号时,直接从这个缓冲区中取得。

说到“缓冲区”,我们第一应该想到用“队列”,不错,就是队列!我们来看一下MSDN上对“队列”(Queue类)的解释:

Queues are useful for storing messages in the order they were received for sequential processing. This class implements a queue as a circular array. Objects stored in a Queue are inserted at one end and removed from the other.

大概意思就是队列一般用于存储需要按顺序处理的消息。

     第6条其实也可以用队列来实现,用户不停地向队列输入,而不用管Rect对象是否立刻去响应,队列起到一个缓冲的作用,当然,如果这样设计的话,用户输入和Rect对象响应输入应该不在同一线程,这就要用到多线程了。

     第3、4条其实就是OO中的继承、虚方法引起的“多态性”,以及.net中常用到的Observer模式,用Event很好实现。

升华:

     经过分析,Rect类改为:(虚方法以及事件只举例定义了两个,现实中应该有很多个)

.Net Winform 开发笔记(四) 透过现象看本质View Code

    还需添加以下委托和类:

.Net Winform 开发笔记(四) 透过现象看本质View Code

    再从Rect类派生出一个新的类DeriveRect,该类为Rect类的子类:

.Net Winform 开发笔记(四) 透过现象看本质View Code

    为了统一处理信号,我们在Sgl枚举类型中再加一个枚举变量RS_QUIT,它指示while循环退出(这个信号不再唯一由用户输入,为什么请看前面提出的6条),Sgl枚举改为:

.Net Winform 开发笔记(四) 透过现象看本质View Code

    ZZThread类则改为:(主要增加了一个信号列表,然后修改了一下GetSignal方法,让其直接从信号列表中获取信号,而不需要再等待用户输入,当然,我这里没有写出专门让用户输入的线程,因为这个示意性代码本身就是一个Console程序,多线程去接收用户输入的话,“输入内容”会和“响应用户输入的内容”相混淆,只需要知道用户会在另外一个线程中向signalList中添加信号,而这个动作不需要我们有多少了解,原因后面会讲到)。另外,现在用户可以操作的不单单是Rect类对象了,可以是Rect类的派生类,而且你还可以监听Rect类(或其派生类的事件)。为了在循环体执行期间,控制循环退出,增加了一个PostQuit方法,该方法只是简单的向signalList队列添加一个“退出”信号。

.Net Winform 开发笔记(四) 透过现象看本质View Code

好了,改完了,解释一下改完后的代码。改完后的代码和之前的大概结构仍然相同,一个while循环、一个获取信号(这里不再单单是用户输入了,还包括循环体内向循环体外发送的信号)的GetSignal方法、一个处理信号(将信号分发给线程中对应的Rect对象以及其派生类)的DispatchSignal方法。

    再上张图,图文结合,效果杠杠的。

.Net Winform 开发笔记(四) 透过现象看本质

图2

再分析一下,代码修改之前提出的6条不足,基本上全部解决了

  1. 在Rect对象(或其派生类对象,下同)处理信号的时候,只要知道任何一个相同线程中的其它Rect对象的ID,那么就可以利用ZZThread.SendSignal()向其发送信号。
  2. 同理,在某一Rect对象(我们在这成为主Rect)关闭的时候(处理RS_QUIT信号),它可以通过ZZThread.PostQuit()方法向循环发送退出信号。
  3. 通过允许Rect类被继承,就可以实现多样化的效果,响应用户输入不再仅仅只是Rect类了,还可以是Rect的派生类。
  4. 通过向响应者(Rect类)添加事件(Event)的方法,外部其他Rect对象就可以监听到事件的发生,做出相应响应。
  5. 与1相似,在处理某一信号的时候,完全可以通过ZZThread.SendSignal方法将ID参数设为自己的ID,向自己发送一个信号,这就可以达到“在处理完一个信号后紧接着向自己发送另外一个信号”的功能。
  6. 通过增加信号队列和一个专门接受用户输入的线程(以上示意性代码中未给出),完全可以达到“让用户输入和Rect对象响应”异步发生。

以上6条确实完美解决了,现在继续考虑几个问题(你们可能知道改完之后的这个东西肯定不会是我们最终想要的,因为它貌似跟我要讲的Winform几乎没有任何联系,所以,继续考虑下面几个问题)。

由于每个问题跟上一个问题有联系,所以我依次给出了问题的解决办法。

1. 这个只能在一个线程中使用,也就是说,同一个程序中,只能存在一个这样的线程,因为ZZThread类是个静态类,所有的成员也是静态的,如果多个线程使用它的话,就会全乱了。举例看下面:

Thread th1 = new Thread((ThreadStart)(delegate(){

      ZZThread.Main();

}));

th1.Start();

Thread th2 = new Thread((ThreadStart)(delegate(){

      ZZThread.Main();

}));

th2.Start();

th1与th2两个线程中,用到的signalList、allRects是同一个,两个线程中的while循环也是从同一个信号队列中去取信号,然后分配给同一个Rect对象集合中的对象,虽然可以做一些同步“线程安全”的处理,但是仍然有问题,仔细想一想(比如发送RS_QUIT信号想让本线程退出,到底哪个退出不确定)。因此,理想情况应该是这样的:每一个线程有自己的信号队列(signalList),有自己的Rect对象集合(allRects),有自己的while循环和自己的DispatchSignal方法,换句话说,两个线程之间不应该有瓜葛,而应该互不影响,相互独立。(当然,除了这些,两个线程理论上可以有其他联系,后面会提到)。

解决方法:

      既然ZZThread类是静态的,那么我们就可以把它设置成非静态,每个线程对应一个ZZThread对象,这样线程与线程之间就不会有影响,每个线程都有自己的信号队列、自己的Rect对象集合以及自己的while循环和DispatchSignal方法。当然,如果这样处理的话,就应该考虑怎么确保每个线程拥有自己的ZZThread对象,就是说,怎么保证一个线程能找到与它对应的ZZThread对象?很简单,每个线程都有唯一一个ID(整个系统范围内唯一),可以定义一个Dictionary<int,ZZThread>字典,在每个线程中要使用ZZThread对象的地方,先根据线程ID(这个可以随时取得,只要在同一线程中,ID肯定相同)查找字典,如果存在,直接拿出来使用,如果不存在,说明还没有创建,那就新建一个ZZThread对象,加到字典中,将新建的ZZThread对象拿来使用。这样的话,在每个线程的任何一个地方要使用ZZThread对象的话,都能通过该方法取得同一个ZZThread对象。但要考虑怎么去维护这样一个字典?

2. ZZThread类不能直接暴露给使用者。还是考虑多个线程的情况,ZZThread类中的while循环入口(之前一直是Main方法)、以及诸如像PostQuit、SendSignal等(后面可能还会增加)都是public类型的,如果ZZThread直接暴漏给使用者,使用者完全可以在一个线程中使用另外一个ZZThread对象(注:1中的解决方法只解决了“怎样让一个线程正确地使用同一个ZZThread对象”,并没有解决“一个线程只能使用一个ZZThread对象”)。

解决方法:

     一个很好的解决方法就是将“精髓部分”封装起来,封装成一个库(或者模块、框架随便叫),只对外开放必要的类,而像ZZThread这样的类也就没必要开放,最关键的是,1中提到的字典也不应该对外开放,使用者的不正当操作很可能破坏该字典。

3. 考虑一种情况,Rect对象在响应信号时(RectProc执行期间),耗时时间太长,即DispatchSignal方法长时间不能返回,也就是说,长时间不能再次调用GetSignal方法,导致线程中信号大量累积,不能及时处理,因此用户的输入也不会及时得到响应,造成用户体验明显下降,这时候改怎么处理?

解决方法:

     既然DispatchSignal方法不能及时返回,导致信号队列的信号不能即使被处理,那么我们可以在Rect对象处理信号的耗时操作中(RectProc执行期间),执行适当次数的while循环,也就是说,在一个while循环体内,再次执行一个while循环,及时处理信号,这就是嵌套执行while循环了,当然,内部的while循环跟外部的while循环有稍微差别,即内部while循环每次就执行一次,执行完后,继续做其他的耗时操作,如果需要大量循环处理一个内容,可以在每次循环结束后调用一次while循环,保证信号队列的信号能够及时处理。上一张图,看得明白一些:

.Net Winform 开发笔记(四) 透过现象看本质

图3

另外,还有一种内嵌的while循环,它不止执行一次循环就退出,而是当某种条件为真时,才退出,这种现在涉及不到。总之,我们可以看出一个线程中可以有多个while循环来处理信号,这些while循环大多是嵌套调用的(不排除这种情况:一个while循环退出后,接着再跟一个while循环,这两个while循环完全相同,但这种出现的几率很少,以后讲到Winform相关的时候我会谈到这个东西的)。由此可以看出,一个线程中的while循环有好几种,所以我们需要每次调用while循环的时候加以区别。

4. 由1中得知一个应用程序可能由好几个需要处理信号的线程组成,每个线程之间相互独立,由2中得知,需要将不必要的类型封装起来,只向用户提供部分类型,使用者利用仅有提供的公开类型就可以写出各种各样自己想要的效果。既然要将代码关键部分与用户可扩展部分分开,那么现在就要分清哪些东西不需要使用者操心、而哪些则需要使用者操心。

解决方法:

      前面说过,ZZThread类肯定不能公开,也就是说while循环、信号队列、Rect对象集合都不公开,Sgl枚举和Signal类也没必要公开,因为使用者根本不需要知道这些东西(从框架设计角度,这些东西也不是公开的),使用者唯一需要了解的就是Rect类,使用者知道了Rect类之后,就可以从该类派生出各种各样的子类,在子类中重写Rect的虚方法,然后注册子类的一些事件等等(这要求必须提前将Rect类中需要处理的信号考虑完整,也就是说一切信号类型在Rect类中全部都有默认处理,而且还要完善虚方法,子类才能重写任何一个它想重写的虚方法)。另外,如果ZZThread类不公开,那么使用者怎么让线程进入while循环?因此,需要定义一个代理类,该代理类跟普通类不一样(见代码),然后将该代理类公开给使用者。

精包装:

框架部分

(1)ZZAplication代理类

.Net Winform 开发笔记(四) 透过现象看本质View Code

(2)ZZThread类

.Net Winform 开发笔记(四) 透过现象看本质View Code

 

(3)Rect类(其他派生自Rect)

.Net Winform 开发笔记(四) 透过现象看本质View Code

(4)委托等事件参数类

.Net Winform 开发笔记(四) 透过现象看本质View Code

(5)信号类

.Net Winform 开发笔记(四) 透过现象看本质View Code

(6)Sgl枚举类型

.Net Winform 开发笔记(四) 透过现象看本质View Code

客户端:

.Net Winform 开发笔记(四) 透过现象看本质View Code

     如你所见,使用者在客户端知道的东西少之又少,只有ZZApplication类、Rect类以及一些委托和事件参数类,其中ZZApplication类主要负责跟ZZThread有关的内容,为使用者和ZZThread类之间起到一个桥梁作用,ZZApplication类中可以放一些ZZThread类对象公共的数据(如代码中的ApplicationExit、ThreadExit等等);Rect类则完全是为了方便使用者扩展出各种各样的信号响应者,这就像一个公司刚开始只有一个部门,该部门负责设计编码测试以及后期维护,那么每次开会的时候,老板下达命令,只有一个部门负责人响应,现在公司做大了,分出来了开发部、测试部、以及人事部和市场部,现在老板一开会,就会有多个部门负责人响应。这个例子里面老板就是使用由该框架开发出来的系统的人,而各部门负责人则是Rect类对象或其派生类对象。

    为了更直观的理解这次修改后的代码,再上一张图:

.Net Winform 开发笔记(四) 透过现象看本质

图4

总结加过渡:

     任何一个系统,都是给用户使用的,系统要不直接面对用户,要不间接面对用户,反正最终都会跟用户交互。因此,对于任何一个系统,它必备三个部分:第一,接收用户命令部分;第二,处理命令部分;第三,显示处理结果部分(让用户知道自己的命令产生怎样的效果)。我们现在来分析一下我们之前每个阶段的代码是否包含以上三个部分:

(1)一个引子:

该部分可以说是,麻雀虽小,五脏俱全,包含“接收用户命令”的Console.ReadLine()、“处理命令”的Console.WriteLine()、和“显示处理结果”的Console.WriteLine()(这里处理命令部分和显示处理结果部分明显是一个东西),代码虽然简陋,却包含了一个完整系统的所有部分,所以我之前说它是整个系统的“精髓”,其实一点都不假。

(2)初加工:

这部分说它是“初加工”,其实不太合适,因为相对于修改之前,变化确实大了点,用“初”字来形容不太贴切,但我又确实想不到更简单而又与前面不重复的例子,所以只好这样了。这部分其实也是完整的包含三个部分的,它有“接收用户命令”的GetSignal方法、“处理命令”的Rect.RectProc方法以及“显示处理结果”Console.WriteLine方法(包含在Rect.RectProc方法中)。

(3)升华:

从这部分开始,系统逐渐变得不完善,一是因为我要跟后面讲Winform关联起来,二是说句实话,代码多了复杂起来后,再想模拟一个完整的系统结构太困难,根本不容易,读者看起来也顾东顾不了西了。这部分只包含一个部分,那就是处理命令部分,没错,它就只包含“处理命令的部分”,没有接收用户命令(我前面说过需要另开线程接收用户输入),也没有显示处理结果。这个听起来好像让人太难接收,叫它“升华”,代码居然减少到只包含一个部分,好吧,这个留着以后再解释,现在太早。该部分把全部重点放在了“处理命令”部分,扩充了Rect类,可以从它派生出各种各样的子类来响应命令。

(4)精包装:

顾名思义,包装有点封装的意思,该部分从框架设计角度来考虑代码的实现,将使用者无需了解的部分封装起来,提供若干必需的接口。跟“升华”阶段一样,代码只有“处理命令”部分,不同的,一是前面说的封装部分类型,公开部分类型;二是将“处理信号”这个逻辑对象化,一个线程使用一个ZZThread类对象,各个线程拥有自己的信号队列、自己的Rect对象集合、自己的信号循环等等,各自的信号循环获取各自的信号队列中的信号,分配给各自的Rect对象,由各自的Rect对象进行处理,各个线程在处理信号这个方面没有任何交集(简单设计的话,应该是这样的,但如果需要实现复杂效果的话就会涉及到各个线程之间发送信号,这个就麻烦点,以后在讲Winform部分会提到)。

      既然这部分标题叫“总结加过渡”,那明显有两个意思,“总结”刚才已经搞过了,现在来说说后面的事情,其实相信大部分人已经看出来前面那些代码到最后有点Winform结构的意思,我不敢说它完全就是,但至少大概结构就是“精包装”阶段那样的,不信请查看.net源码,我之所以先扯30多页word文档连看似跟Winform半毛关系也没有的东西,而没有一上来直接拿Application、Form、WndProc、OnClick甚至消息循环、消息队列等等这些开刀,我只是想让诸位在没有任何Winform思想干扰的情况下,从底层对整个系统结构有个大概的了解,这就像先给人传授一种技能,人们都已经使得很熟练了,哪天突然叫你研究原理的东西,你肯定会先从你熟悉的地方一点点往底层原理性方面走,却不知终点在哪,搞不好走偏了,进了无底洞。再者,说句不好听的,很多人连Control.WndProc是什么都不知道,更别说什么消息循环了,一上来扯这些概念,相当一部分人肯定会蒙,毕竟,本文并不打算只服务于基础扎实的读者O(∩_∩)O~。

      注:以上(包括接下来的)所有代码均未测试,不知是否可以运行,全部都在word中敲进去的,如果能运行的话当然更好,不能的话,那就权当作是伪代码吧,看看思路就ok,再者我觉得这个运行也看不出啥效果。

高潮:

     此高潮非彼高潮,我也不知道用啥词儿来做该部分标题,只能想出这个词,因为相对于本文整个大标题来讲,前面的全可以当做扯淡,或者前奏,前奏完了,必然是高潮。

     我不知道各位心中对Windows桌面应用程序到底是个什么概念?窗体跟代码怎么关联?鼠标键盘又跟窗体怎么关联?程序的第一个窗体怎么出现?程序又是怎么被终结?等等诸如此类问题,不知各位心中是否有所了解。其实这些问题确实不太容易搞懂,谁叫如今框架越来越“先进”,封装得越来越抽象,对我们这些应用级开发人员来讲,操作系统又像是一坨大便,正常人是搞不懂它的组成结构的,再者谁没事闲得蛋疼去研究一坨屎?所以,两座大山挡在我们前面,一个是操作系统,一个是框架(广义上讲,他两是一个东西),前者我们绕不过去,因为我们写的程序要跑在它上面,肯定需要它的各种支撑,后者我们还是绕不过去,谁想一下子退回石器时代,使用一个字符串还要去判断它是否以’\0’结尾?想用个容器还要自己去写几十行代码,搞不好调试都要一上午?更别说现在正在谈的Winform应用程序,如果还像传统Win32编程那样,你想拖两控件写写事件处理程序就能出来一个汽车使用管理软件来?所以,既然无法摆脱它们,那就去战胜它们。

     显而易见,Winform应用程序(或其他Windows桌面应用程序,下同)属于典型的需要与用户交互的系统,应该包含上面提到的三个完整的部分:“接收用户命令”、“处理命令”、“显示处理结果”。下面我们来做一个对号入座,将Winform开发中的一些概念与之前的代码做一个一一映射(当然,只能简单的一一对应,Winform内部实现实际上比我们之前写的代码复杂得多,而且好多我都只能靠模仿,因为它许多部分跟操作系统紧密相关,除了写C#代码去模仿,我没办法给你们解释它到底怎么做到的):

1)接收用户命令:

    Winform应用程序当然是靠键盘鼠标等输入设备,而我们之前的代码没有这部分,我那时候说过,需要另开线程接收用户输入,当然这个只能是简单的模仿,Winform应用程序接收用户命令要比这个复杂得多。打个比方,鼠标在某一窗体上点击,操作系统就能捕获该事件(此事件为Windows事件,跟.Net编程中的事件不同,前者可以说是物理意义上的,而后者更为抽象),然后将该事件转换成一个Windows消息,该消息为一种数据结构,携带消息接受者信息(被点击的窗口句柄)、消息类型(比如WM_LBUTTONDOWN)以及一些参数(鼠标坐标等),然后操作系统将该数据结构发送到被点击窗体所在线程的消息队列中,之后,操作系统不会再去管了。我之前的博客中已经说过,我们的应用程序是读不懂键盘鼠标的,唯独能读懂的就是数据,所以操作系统在“接收用户命令”部分帮了我们很大的忙,直接将一些物理事件转换成一种统一的数据结构,然后投递给线程消息队列。

      我们可以看出,Winform应用程序接收用户命令已经太强大了,相比我们之前(一个引子和初加工中)的Console.ReadLine()接收输入,然后还要把输入转换成标准的Signal,用户输入很容易出错,因此,相比起来,两个差别实在太大。

2)处理命令:

     这部分可以说大体上还是一样的,Winform应用程序中UI线程(也就是 我们说的管理界面的线程)中有while消息循环,不停地将线程消息队列中的消息取出,分配给目标窗口,然后调用目标窗口的窗口过程(WndProc),这个基本上跟我们前面写的代码一样。只是我想说的是,我们之前的代码中模仿了消息队列(signalList)以及线程中的窗口集合(allRects),Winform应用程序中这两个东西是靠操作系统维护的。

3)显示处理结果:

     跟“接收用户命令”一样,我们之前的代码中没有“显示处理结果”部分,在Winform中,众所周知的是,鼠标按住标题栏移动鼠标,结果就是窗体跟着鼠标移动,摁住鼠标移动就是“用户命令”,窗体跟着鼠标移动就是“处理结果”,直接通过图形展示给用户了。这其中的奥秘就是,Winform应用程序中,窗体的窗口过程在处理消息的时候,调用了Windows API,API操作窗体,让其改变位置。而我们的代码在“一个引子和初加工”阶段,唯一能做的,就是在窗口过程中将自己(Rect对象)的信息Console.WriteLine()显示出来,以此来模仿显示处理结果。

4)我们代码中的“信号循环”对应于Winform应用程序中的“消息循环”。

5)我们代码中的GetSignal对应于Winform应用程序中的GetMessage,当然后者为API方法,具体内部实现不清楚,GetSignal只是为了模仿。

6)我们代码中的DispatchSignal对应于Winform应用程序中的DispatchMessage,后者也是API方法,具体内部实现不清楚,DispatchSignal只是为了模仿。

7)我们代码中的Signal类对应于Winform应用程序中的Message结构体,不同框架中的可能不太一样,但基本上都是含有窗口句柄、消息类型、W参数、L参数。

8)我们代码中的Sgl.RS_QUIT枚举类型对应于Winform应用程序中的WM_QUIT,其他类似。

9)我们代码中的Rect类对应于Winform应用程序中的Form类。

10)我们代码中的Rect.RectProc对应于Winform应用程序中的Form.WndProc。

11)我们代码中的Rect1(最后精包装阶段),很明显对应于Winform应用程序中的开发时,自动生成的Form1类。

12)我们代码中的ZZApplication类对应于Winform应用程序开发中的Application类。

13)我们代码中的ZZApplication.Start()对应于Winform应用程序开发中的Application.Run()。

14)我们代码中的ZZApplication.DoThing()对应于Winform应用程序开发中的Application.DoEvents()。

15)至于我们代码中的ZZThread类,跟Winform应用程序开发中的ThreadContext类相似(该类没有对外公布,查看.net源码可以看详细信息 )

为了更清楚的了解Winform应用程序整个运行流程,再上一张图:

.Net Winform 开发笔记(四) 透过现象看本质

图5

      现在我们知道,对于Winform应用程序来讲,鼠标键盘等操作可以视为“用户输入命令”,只是Winform应用程序并不能直接识别此命令,因此需要操作系统作为桥梁,将鼠标键盘等“Windows事件”转换成程序可以识别的数据结构(Windows消息),再投递到相关线程中去,再由线程中的消息循环获取消息,分派给本线程中对应的窗体,调用窗口过程,在窗口过程中我们再根据需要处理消息,一般是调用Windows API,只是Winform中对API做了一层封装,再加进去了OO思想,引进一些虚方法、事件(Event)等等概念,让使用者更方便的编写窗口过程。因此,当我们用鼠标点击Winform窗体,Winform代码中会激发Click事件,我们再在事件处理程序中写一些逻辑代码,这一过程拐了好几个弯,并没有我们想象的那么简单:点击鼠标,鼠标激发Click事件,调用事件处理程序。

     另外,我们还能总结一个结论,我们在Winform中编写的所有跟UI有关的代码,其实基本上都是扩展窗体(控件)的“窗口过程”,我们例子中的“RectProc”。最后,“点击一个button,弹出一个messagebox对话框,从windows消息层次描述该过程”这个问题已经很清楚明了了。送一句话,对Windows操作系统的概括:

     它是一个以消息为基础,事件驱动的多任务抢占式操作系统。

这句话完完整整的说明了所有Windows桌面应用程序开发规律。

     写到目前为止,我并没有讲到winform框架的一个整体结构,更没贴出相关代码,之后我也没打算这样做,因为这块东西实在是太多,有兴趣的可以用Reflector研究.net源码,再者,前面模仿的例子完全可以拿来类比。之后几篇我打算挑几个没说完的继续说,但也只是很小的地方,比如最前面提到的Form.Show()与Form.ShowDialog()的区别,模式对话框形成的本质原因等等。

     断断续续写了两个多礼拜,可能前面讲的和后面说的偶尔不太相呼应,请包涵,另外希望有帮助,O(∩_∩)O~。

 

作者:周见智 
出处:http://www.cnblogs.com/xiaozhi_5638/ 
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

标签: c#Winform

本文转自周见智博客博客园博客,原文链接:http://www.cnblogs.com/xiaozhi_5638/archive/2013/01/03/2843374.html,如需转载请自行联系原作者
上一篇:.Net开发笔记(八) 动态编译


下一篇:冬季实战营第四期学习报告