程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#基礎知識 >> .Net Winform開發筆記(四)透過現象看本質

.Net Winform開發筆記(四)透過現象看本質

編輯:C#基礎知識
寫在前面
從一個窗體的創建顯示,再到與用戶的交互,最後窗體關閉,這中間經歷過了一系列復雜的過程,本文將從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#代碼編寫如下:
代碼如下:

View Code
void main()
{
string input = “quit”;
while((input=Console.ReadLine())!=”quit”)
{
Console.WriteLine(“input string :” + input );
}
Console.WriteLine(“thread exit”);
Console.ReadKey();
}

非常簡單的一段代碼,程序運行後,有了while循環,不會馬上結束,它會不停的等待用戶輸入,然後輸出用戶輸入的字符串(模擬響應用戶操作),直到用戶輸入“quit”後,循環才結束。這段利用while循環和Console.ReadLine()寫出來的程序雖然短小簡單,卻是後面我們要談到的Winform應用程序(其實所有的Windows應用程序都一樣,無論是MFC還是Delphi或者其他搞出來的桌面程序)的精髓。當然,這段代碼確實太簡陋了,所以我才說它是精髓,O(∩_∩)O~。既然太簡陋,那我們再改改吧。要改就改復雜一點。
初加工:
代碼如下:

View Code
///矩形類
Class Rect
{
int _id; //矩形唯一標示
string _text; //矩形中心顯示的文本
Size _size; //矩形大小
Point _location; //矩形的位置
Bool _alive; //是否存活
Public int ID
{
Get
{
Return _id
}
Set
{
_id = value;
}
}
Public string Text
{
Get
{
Return _text;
}
Set
{
_text = value;
}
}
Public Size Size
{
Get
{
Return _size;
}
Set
{
_size = value;
}
}
Public Point Location
{
Get
{
Return _location;
}
Set
{
_location = value;
}
}
Public bool Alive
{
Get
{
Return _alive;
}
Set
{
_alive = value;
}
}
Public Rect(int id,string text,Size size,Point location)
{
_id = id;
_text = text;
_size = size;
_location = location;
_alive = true;
Console.WriteLine(“[” + ID.ToString() + “] 號矩形創建成功!”);
}
//矩形對外唯一接口,對矩形的所有操作必須調用此方法,下稱“矩形過程”
Public void RectProc(int id,int type,object leftParam,object rightParam)
{
Switch(type) //這個type就是後面說的“信號類型”, 應該跟Sgl枚舉一一對應
{
Case 1: //移動、改變大小
{
Size newSize = (Size)leftParam;
Position newLocation = (Point)rightParam;
This.Size = newSize;
This.Location = newLocation;
Console.WriteLine(“[” + ID.ToString() + “] 號矩形改變位置:大小為(” + this.Size.Width+”,” + this.Size.Height + “),位置為(”+this.Location.Left + “,” + this.Location.Top + “)” );
Break;
}
Case 2: //顯示信息
{
Console.WriteLine(“[” + ID.ToString() + “] 號矩形顯示信息:大小為(” + this.Size.Width+”,” + this.Size.Height + “),位置為(”+this.Location.Left + “,” + this.Location.Top + “),Text為 ” + this.Text );
Break;
}
Case 3: //關閉
{
Console.WriteLine(“[” + ID.ToString() + “] 號矩形關閉”);
Alive = false;
Break;
}
//……
Default:
{
//默認處理
}
}
}
}
//信號類,表示一種信號,包含信號接收者ID,信號類型Type,信號兩個參數LeftParam、RightParam
Class Signal
{
Int _id; //接受者id
Int _type; //信號類型
Object _leftParam; //參數1
Object _rightParam; //參數2
Public int ID
{
Get
{
Return _id;
}
Set
{
_id = value;
}
}
Public int Type
{
Get
{
Return _type;
}
Set
{
_type = value;
}
}
Public object LeftParam
{
Get
{
Return _leftParam;
}
Set
{
_leftParam = value;
}
}
Public object RightParam
{
Get
{
Return _rightParam;
}
Set
{
_rightParam = value;
}
}
Public Signal(int id,int type,object leftParam,object rightParam)
{
_id = id;
_type = type;
_leftParam = leftParam;
_rightParam = rightParam;
}
}
// 信號類型枚舉 RS即為RectSignal
Enum Sgl
{
RS_POSITIONCHANGE = 1, //移動矩形,大小變化
RS_SHOWINFO = 2, //矩形顯示自己信息
RS_KILL = 3 //關閉矩形
//……很多省略
}
/* 信號格式(不同的Sgl,Signal對象內容完整度不一樣)
* RS_POSITIONCHANGE: ID必須,Type必須,LeftParam必須,RightParam必須
* RS_SHOWINFO ID必須,Type必須
* RS_KILL: ID必須,Type必須
* ……很多省略
*/
/// 主線程
/// 測試代碼
Static class ZZThread
{
Static List<Rect> allRects = new List<Rect>(); //整個線程運行過程中,存在的Rect對象
//線程入口
Public Static void Main()
{
//初始化4個Rect對象,添加到集合中
allRects.Add(new Rect(1,”my name is Rect1”,new Size(100,100),new Point(10,10)));
all.Rects.Add(new Rect(2,”my name is Rect2”,new Size(455,250),new Point(100,150));
allRects.Add(new Rect(3,”my name is Rect3”,new Size(300,500),new Point(250,100));
allRects,Add(new Rect(4,”my name is Rect4”,new Size(300,600),new Point(50,80));
//開始循環接收用戶輸入,作出反應
Signal signal = null;
While(GetSignal(out signal)) //接收信號
{
DispatchSignal(signal); //分配信號到各個Rect
}
Console.WriteLine(“The Thread Exit”);
// Console.ReadKey(); //阻塞查看運行情況
}
Static bool GetSignal(out Signal signal)
{
START:
String input = Console.ReadLine(); //接受用戶輸入
String[] inputs = input.Split(“ ”);
If(inputs.Length == 1) //用戶輸入QUIT,退出
{
If(inputs[0] == “QUIT”)
{
Return false;
}
Else
{
Console.WriteLine(“參數格式錯誤!”);
Goto START;
}
}
// 必須提供Rect的id、以及信號類型,參數可選
// 沒做格式驗證,所有必須輸入整形數據
If(inputs.Length == 2) //只提供了Rect的id和信號類型
{
signal = new Signal(int.parse(intputs[0]),int.Parse(inputs[1]),null,null);
return true;
}
If(inputs.Length == 4) //只提供了Rect的id、信號類型以及第一個參數
{
signal = new Signal(int.Parse(inputs[0]),int.Parse(intputs[1]),new Size(int.Parse(inputs[2]),int.Parse(inputs[3])),null);
return true;
}
If(inputs.Length == 6) //四個參數全部提供
{
signal = new Signal(int.Parse(inputs[0]),int.Parse(inputs[1]),new Size(int.Parse(inputs[2]),int.Parse(inputs[3])),new Point(int.Parse(inputs[4]),int.Parse(inputs[5])));
return true;
}
Console.WriteLine(“參數格式錯誤!”);
Goto START;
}
Static void DispatchSignal(Signal signal)
{
Foreach(Rect rect in allRects)
{
If(rect.ID == signal.ID && rect.Alive)
{
rect.RectProc(signal.ID,signal.Type,signal.LeftParam,signal.RightParam);
break;
}
}
}
}

解釋一下,代碼雖然多了一點,可大概結構還是沒變(其實我們見到的其他所有框架,結構雖然復雜得很,可其精髓的代碼也就不到一半,其余的都是在精髓代碼上擴充來的,增加各種各樣的功能),如你所見,跟之前的意思一樣,線程中有一個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方法中根據不同的信號類型,作出相應的反應。
可能文字不太直觀,上一張圖,來解釋一下,圖文結合更有效。

 
好了,改了之後的代碼復雜很多,當然了,功能也比之前的多了很多,但是還是那句話,大概結構沒有變,一個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類改為:(虛方法以及事件只舉例定義了兩個,現實中應該有很多個)
代碼如下:

View Code
///矩形類
Class Rect
{
int _id; //矩形唯一標示
string _text; //矩形中心顯示的文本
Size _size; //矩形大小
Point _location; //矩形的位置
Bool _alive; //是否存活
Public int ID
{
Get
{
Return _id
}
Set
{
_id = value;
}
}
Public string Text
{
Get
{
Return _text;
}
Set
{
_text = value;
}
}
Public Size Size
{
Get
{
Return _size;
}
Set
{
_size = value;
}
}
Public Point Location
{
Get
{
Return _location;
}
Set
{
_location = value;
}
}
Public bool Alive
{
Get
{
Return _alive;
}
Set
{
_alive = value;
}
}
Public Rect(int id,string text,Size size,Point location)
{
_id = id;
_text = text;
_size = size;
_location = location;
_alive = true;
}
//矩形對外唯一接口,對矩形的所有操作必須調用此方法,下稱“矩形過程”
Public virtual void RectProc(int id,int type,object leftParam,object rightParam)
{
Switch(type) //這個type就是後面說的“信號類型”, 應該跟Sgl枚舉一一對應
{
Case 1: //移動、改變大小
{
Size newSize = (Size)leftParam;
Position newLocation = (Point)rightParam;
This.Size = newSize;
This.Location = newLocation;
OnPositionChanged(new PositionChangedEventArgs(this.Size,this.Location));
Break;
}
Case 2: //顯示信息
{
//調用對應虛方法
Break;
}
Case 3: //關閉
{
Alive = false;
OnKill(new EventArgs());
Break;
}
//……很多省略
Default:
{
//默認處理
}
}
}
Protected virtual void OnPositionChanged(PositionChangedEventArgs e)
{
If(PositionChanged!=null)
{
PositionChanged(this,e);
}
}
Protected virtual void OnKill(EventArgs e)
{
If(Kill!=null)
{
Kill(this,e);
}
}
Public event PositionChangedEventHandler PositionChanged;
Public event EventHandler Kill;
}

還需添加以下委托和類:
代碼如下:

View Code
Public delegate void PositionChangedEventHandler(object sender,PositionChangedEventArgs e)
Public class PositionChangedEventArgs
{
Size _size;
Point _location;
Public Size Size
{
Get
{
Return _size;
}
}
Public Point Location
{
Get
{
Return _location;
}
}
Public PositionChangedEventArgs(Size size,Point location)
{
_size = size;
_location = location;
}
}

再從Rect類派生出一個新的類DeriveRect,該類為Rect類的子類:
代碼如下:

View Code
class DeriveRect:Rect
{
public DeriveRect(int id,string text,Size size,Point location):base(id,text,size,location)
{
}
public override void RectProc(int id,int type,object leftParam,object rightParam)
{
//攔截信號
base.RectProc(int id,int type,object leftParam,object rightParam);
}
protected override void OnPositionChanged(PositionChangedEventArgs e)
{
//添加自己的代碼
//ZZThread.SendSignal(…)發送信號到本線程信號隊列中
base.OnPositionChanged(e); //觸發基類事件
}
//你可以重寫其他虛方法,就像繼承一個Form類,重寫它的虛方法一樣。
}

為了統一處理信號,我們在Sgl枚舉類型中再加一個枚舉變量RS_QUIT,它指示while循環退出(這個信號不再唯一由用戶輸入,為什麼請看前面提出的6條),Sgl枚舉改為:
代碼如下:

View Code
Enum Sgl
{
RS_POSITIONCHANGE = 1, //移動矩形,大小變化
RS_SHOWINFO = 2, //矩形顯示自己信息
RS_KILL = 3, //關閉矩形
RS_QUIT = 4 //退出循環
//……很多省略
}

ZZThread類則改為:(主要增加了一個信號列表,然後修改了一下GetSignal方法,讓其直接從信號列表中獲取信號,而不需要再等待用戶輸入,當然,我這裡沒有寫出專門讓用戶輸入的線程,因為這個示意性代碼本身就是一個Console程序,多線程去接收用戶輸入的話,“輸入內容”會和“響應用戶輸入的內容”相混淆,只需要知道用戶會在另外一個線程中向signalList中添加信號,而這個動作不需要我們有多少了解,原因後面會講到)。另外,現在用戶可以操作的不單單是Rect類對象了,可以是Rect類的派生類,而且你還可以監聽Rect類(或其派生類的事件)。為了在循環體執行期間,控制循環退出,增加了一個PostQuit方法,該方法只是簡單的向signalList隊列添加一個“退出”信號。
代碼如下:

View Code
/// 主線程
/// 測試代碼
Static class ZZThread
{
Static List<Rect> allRects = new List<Rect>(); //整個線程運行過程中,存在的Rect對象
Static Queue signalList = new Queue(); //線程信號隊列(考慮到另有線程接收用戶輸入,也會操作此信號隊列,所以請考慮線程同步問題)
//線程入口
Public Static void Main()
{
//初始化一個Rect對象,一個為DeriveRect對象,前者為主Rect,當它關閉的時候,退出循環,結束線程
Rect chiefRect = new Rect(1,”I am the chief Rect!”,new Size(100,100),new Point(10,10));
chiefRect.Kill += (EventHandler)(delegate(sender,e){PostQuit();}); //監聽主Rect的關閉事件,向所在線程信號隊列發送一個退出信號
Rect derivedRect = new DeriveRect(2,”I am the Derived Rect”,new Size(150,150),new Point(200,200));
allRects.Add(chiefRect);
allRects.Add(derivedRect);
//開始循環從線程的信號隊列裡獲取信號
Signal signal = null;
While(GetSignal(out signal)) //接收信號
{
DispatchSignal(signal); //分配信號到各個Rect
}
Console.WriteLine(“The Thread Exit”);
// Console.ReadKey(); //阻塞查看運行情況
}
Static bool GetSignal(out Signal signal)
{
//從隊列獲取信號,如果隊列為空,阻塞直到隊列有信號為止,否則如果非RS_QUIT,返回true,如果RS_QUIT,則返回false
START:
If(signalList.Count!=0) //注意需要處理線程同步
{
signal = signalList.DeQueue() as Signal;
if(signal.Type != (int)Sgl.RS_QUIT)
{
return true;
}
else
{
return false;
}
}
Else
{
Thread.Sleep(1);
goto START;
}
}
Static void DispatchSignal(Signal signal)
{
Foreach(Rect rect in allRects)
{
If(rect.ID == signal.ID && rect.Alive)
{
rect.RectProc(signal.ID,signal.Type,signal.LeftParam,signal.RightParam);
break;
}
}
}
public static void PostQuit()
{
signalList.EnQueue(new Signal(0,(int)Sgl.RS_QUIT,null,null));
}
public static void SendSignal(int id,int type,object leftParam,object rightParam)
{
signalList.EnQueue(new Signal(id,type,leftParam,rightParam));
}
}

好了,改完了,解釋一下改完後的代碼。改完後的代碼和之前的大概結構仍然相同,一個while循環、一個獲取信號(這裡不再單單是用戶輸入了,還包括循環體內向循環體外發送的信號)的GetSignal方法、一個處理信號(將信號分發給線程中對應的Rect對象以及其派生類)的DispatchSignal方法。
再上張圖,圖文結合,效果槓槓的。


再分析一下,代碼修改之前提出的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循環,保證信號隊列的信號能夠及時處理。上一張圖,看得明白一些:


另外,還有一種內嵌的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代理類
代碼如下:

View Code
public class ZZApplication
{
public static event EventHandler ApplicationExit;
public static event EventHandler ThreadExit;
//用一個Rect對象作為主Rect,開啟信號循環
public static void Start(Rect rect)
{
ZZThread.FromCurrent().StartLoop(1,rect);
}
//執行一次信號循環,進行一次信號處理
public static void DoThing()
{
ZZThread.FromCurrent().StartLoop(2,null);
}
internal static void RaiseThreadExit()
{
if(ThreadExit!=null)
{
ThreadExit(null,EventArgs.Empty);
}
}
internal static void RaiseApplicationExit()
{
if(ApplicationExit!=null)
{
ApplicationExit(null,EventArgs.Empty);
}
}
}

(2)ZZThread類
代碼如下:

View Code
internal class ZZThread
{
private Queue signalList = new Queue();
private List<Rect> allRects = new List<Rect>();
private int mySignalLoopCount = 0;
private static totalSignalLoopCount = 0;
private static Dictionary<int,ZZThread> allZZThreads = new Dictionary<int,ZZThread>();
public static ZZThread FromCurrent() //獲取與當前線程相關聯的ZZThread對象
{
int id = Thread.CurrentThread.ManagedThreadId;
if(allThreads.ContainKey(id))
{
return allThreads[id];
}
else
{
ZZThread zzthread = new ZZThread();
allZZThreads.Add(id,zzthread);
return zzthread;
}
}
//開始一個信號循環,1表示第一層循環,2表示第二層循環
public void StartLoop(int reason, Rect rect)
{
mySignalLoopCount++;
totalSignalLoopCount++;
if(reason == 1)
{
if(mySignalLoopCount!=1)
{
return; //不能嵌套調用第一層循環
}
rect.Kill+=(EventHandler)(delegate(sender,e){PostQuit();});
}
Signal signal = null;
while(GetSignal(out signal))
{
DispatchSignal(signal);
if(reason == 2)
{
break;
}
}
mySignalLoopCount--;
totalSignalCount--;
if(reason == 1) //退出外層循環
{
Dispose();
}
}
public void AddRect(Rect rect)
{
allRects.Add(rect);
}
public void Dispose()
{
if(mySignalCount != 0)
{
PostQuit();
}
else
{
ZZApplication.RaiseThreadExit();
if(totalSignalLoopCount==0)
{
ZZApplication.RaiseApplicationExit();
}
}
}
public void SendSignal(int id,int type,object leftParam,rightParam)
{
signalList.EnQueue(new Signal(id,type,leftParam,rightParam));
}
private bool GetSignal(out Signal signal)
{
START:
If(signalList.Count!=0) //注意需要處理線程同步
{
signal = signalList.DeQueue() as Signal;
if(signal.Type != (int)Sgl.RS_QUIT)
{
return true;
}
else
{
return false;
}
}
Else
{
Thread.Sleep(1);
goto START;
}
}
private void DispatchSignal(Signal signal)
{
Foreach(Rect rect in allRects)
{
If(rect.ID == signal.ID && rect.Alive)
{
rect.RectProc(signal.ID,signal.Type,signal.LeftParam,signal.RightParam);
break;
}
}
}
private void PostQuit()
{
signalList.EnQueue(new Signal(0,(int)RS_QUIT,null,null));
}
}

(3)Rect類(其他派生自Rect)
代碼如下:

View Code
public class Rect
{
int _id; //矩形唯一標示
string _text; //矩形中心顯示的文本
Size _size; //矩形大小
Point _location; //矩形的位置
Bool _alive; //是否存活
Public int ID
{
Get
{
Return _id
}
Set
{
_id = value;
}
}
Public string Text
{
Get
{
Return _text;
}
Set
{
_text = value;
}
}
Public Size Size
{
Get
{
Return _size;
}
Set
{
_size = value;
}
}
Public Point Location
{
Get
{
Return _location;
}
Set
{
_location = value;
}
}
Public bool Alive
{
Get
{
Return _alive;
}
Set
{
_alive = value;
}
}
Public Rect(int id,string text,Size size,Point location)
{
_id = id;
_text = text;
_size = size;
_location = location;
_alive = true;
ZZThread.FromCurrent().AddRect(this); //添加到創建自己的線程的ZZThread對象中
}
public Rect()
{
_id = GetUID() // 獲取Rect的全局唯一標示
_text = “”;
_size = new Size(100,100);
_location = new Point(100,100);
_alive = true;
ZZThread.FromCurrent().AddRect(this); //添加到創建自己的線程的ZZThread對象中
}
//矩形對外唯一接口,對矩形的所有操作必須調用此方法,下稱“矩形過程”
Public virtual void RectProc(int id,int type,object leftParam,object rightParam)
{
Switch(type) //這個type就是後面說的“信號類型”, 應該跟Sgl枚舉一一對應
{
Case 1: //移動、改變大小
{
Size newSize = (Size)leftParam;
Position newLocation = (Point)rightParam;
This.Size = newSize;
This.Location = newLocation;
OnPositionChanged(new PositionChangedEventArgs(this.Size,this.Location));
Break;
}
Case 2: //顯示信息
{
Break;
}
Case 3: //關閉
{
Alive = false;
OnKill(new EventArgs());
Break;
}
//……很多省略
Default:
{
//默認處理
}
}
}
Protected virtual void OnPositionChanged(PositionChangedEventArgs e)
{
If(PositionChanged!=null)
{
PositionChanged(this,e);
}
}
Protected virtual void OnKill(EventArgs e)
{
If(Kill!=null)
{
Kill(this,e);
}
}
Public event PositionChangedEventHandler PositionChanged;
Public event EventHandler Kill;
}

(4)委托等事件參數類
代碼如下:

View Code
Public delegate void PositionChangedEventHandler(object sender,PositionChangedEventArgs e)
Public class PositionChangedEventArgs
{
Size _size;
Point _location;
Public Size Size
{
Get
{
Return _size;
}
}
Public Point Location
{
Get
{
Return _location;
}
}
Public PositionChangedEventArgs(Size size,Point location)
{
_size = size;
_location = location;
}
}

(5)信號類
代碼如下:

View Code
internal class Signal
{
Int _id; //接受者id
Int _type; //信號類型
Object _leftParam; //參數1
Object _rightParam; //參數2
Public int ID
{
Get
{
Return _id;
}
Set
{
_id = value;
}
}
Public int Type
{
Get
{
Return _type;
}
Set
{
_type = value;
}
}
Public object LeftParam
{
Get
{
Return _leftParam;
}
Set
{
_leftParam = value;
}
}
Public object RightParam
{
Get
{
Return _rightParam;
}
Set
{
_rightParam = value;
}
}
Public Signal(int id,int type,object leftParam,object rightParam)
{
_id = id;
_type = type;
_leftParam = leftParam;
_rightParam = rightParam;
}
}

(6)Sgl枚舉類型
代碼如下:

View Code
internal Enum Sgl
{
RS_POSITIONCHANGE = 1, //移動矩形,大小變化
RS_SHOWINFO = 2, //矩形顯示自己信息
RS_KILL = 3, //關閉矩形
RS_QUIT = 4 //退出
//……很多省略
}

客戶端:
代碼如下:

View Code
static class Program
{
/// <summary>
/// 應用程序的主入口點。
/// </summary>
[STAThread]
static void Main()
{
ZZApplication.ApplicationExit += new EventHandler(ZZApplication_ApplicationExit);
ZZApplication.ThreadExit += new EventHandler(ZZApplication_ThreadExit);
ZZApplication.Start(new Rect1());
}
static void ZZApplication_ApplicationExit(object sender,EventArgs e)
{
//應用程序退出
}
static void ZZApplication_ThreadExit(object sender,EventArgs e)
{
//單個信號處理線程退出
}
}
//定義一個新的Rect派生類
class Rect1:Rect
{
public Rect1(int id,string text,Size size,Point location):base(id,text,size,location)
{
Init();
}
public Rect1()
{
Init();
}
private void myChildRect_Kill(object sender,EventArgs e)
{
//大循環耗時計算,不能及時返回
for(int i=0;i<10000*1000;++i)
{
//計算
ZZApplication.DoThing(); //及時處理信號
}
}
}
partial class Rect1:Rect
{
private Rect myChildRect;
private void Init()
{
myChildRect = new Rect();
myChildRect.Kill += new EventHandler(myChildRect_Kill);
}
}

如你所見,使用者在客戶端知道的東西少之又少,只有ZZApplication類、Rect類以及一些委托和事件參數類,其中ZZApplication類主要負責跟ZZThread有關的內容,為使用者和ZZThread類之間起到一個橋梁作用,ZZApplication類中可以放一些ZZThread類對象公共的數據(如代碼中的ApplicationExit、ThreadExit等等);Rect類則完全是為了方便使用者擴展出各種各樣的信號響應者,這就像一個公司剛開始只有一個部門,該部門負責設計編碼測試以及後期維護,那麼每次開會的時候,老板下達命令,只有一個部門負責人響應,現在公司做大了,分出來了開發部、測試部、以及人事部和市場部,現在老板一開會,就會有多個部門負責人響應。這個例子裡面老板就是使用由該框架開發出來的系統的人,而各部門負責人則是Rect類對象或其派生類對象。
為了更直觀的理解這次修改後的代碼,再上一張圖:


總結加過渡:
任何一個系統,都是給用戶使用的,系統要不直接面對用戶,要不間接面對用戶,反正最終都會跟用戶交互。因此,對於任何一個系統,它必備三個部分:第一,接收用戶命令部分;第二,處理命令部分;第三,顯示處理結果部分(讓用戶知道自己的命令產生怎樣的效果)。我們現在來分析一下我們之前每個階段的代碼是否包含以上三個部分:
(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應用程序整個運行流程,再上一張圖:


現在我們知道,對於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~。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved