-----路過的朋友,若發現錯誤或有好的建議,歡迎在下面留言,謝謝!-----
引子
“Windows 程序分為‘程序代碼’和‘UI(User Interface)資源’兩大部份,兩部份最后以RC編譯器(資源編譯器)整合為一個完整的EXE 文件。所謂UI 資源是指功能菜單、對話框外貌、程序圖標、光標形狀等等東西。這些UI 資源的實際內容(二進制代碼)系借助各種工具產生,並以各種擴展名存在,如.ico、.bmp、.cur 等等。程序員必須在一個所謂的資源描述檔(.rc)中描述它們。RC 編譯器讀取RC 檔的描述后將所有UI資源檔集中制作出一個.RES 檔,再與程序代碼結合在一起,這才是一個完整的Windows可執行件。”
以上是侯捷先生在《深入淺出MFC》中關於windows程序開發流程的一段論述,我覺得他說的很是精辟,簡明扼要的說清楚了Windows程序的兩方面,特放在此與大家分享。下文中我還還要引用他的這本書中的內容。
我的忠告
說到windows編程,我認為首先要先了解清楚windows程序由生到死的整個機制和過程。當你對微軟定的這個“游戲規則”了然於胸后,我想后面的學習將會對你來說很輕松。所以,請你認真看懂這一回的內容。
這兩回我要交代的東西還真是不少,既要講解windows程序機制又要引進windows中很多概念。在此我很欣賞王爽老師的教學觀點——知識屏蔽。有關概念等到用的時候我再解釋,不用的我先不提,很多書開始都是先一股腦的把很多概念都拋出來,讓讀者看的迷迷糊糊的,反而增加了讀者的畏懼心理。我認為一邊用一邊在介紹概念反而會讓讀者更好理解一點。(至少對於我來說是這樣)。
我還要再強調一下,這兩回的的主要內容是講解運行機制,我打算以一邊講機制一邊讀程序的方式來進行。考慮到大家對很多概念和很多函數都是第一次接觸,所以我只是以“寫意”形式來搞。只要你對這機制和過程能宏觀和整體上掌握了解,那你就是成功的。遇到概念我會簡單介紹(小字內容是對概念的擴展,有余力的讀者可以看看),看了不理解,這很正常,請繼續看下去,從下面我對概念的運用上你再慢慢體會這概念的含義,這個概念理解過程正如我們小的時候學說話,聽得多了自然就懂了(現在不必一時糾結於此)。很多函數我只說它的作用,具體用法和為什么這么用請你在這一回不要操心(函數用法我以后會講解,以后你看得多了,閉着眼都能寫出來,不必急於一時),以防分心從而破壞了對了解windows機制和程序有生到死過程的連貫性,請記住我們的目的 ——對windows機制和程序有生到死過程宏觀和整體了解,其他是浮雲,哈哈。
先來上一段代碼——請別害怕
1. #include <windows.h> 2. LRESULT CALLBACK WinSunProc( 3. HWND hwnd, // handle to window 4. UINT uMsg, // message identifier 5. WPARAM wParam, // first message parameter 6. LPARAM lParam // second message parameter 7. ); 8. int WINAPI WinMain( 9. HINSTANCE hInstance, // handle to current instance 10. HINSTANCE hPrevInstance, // handle to previous instance 11. LPSTR lpCmdLine, // command line 12. int nCmdShow // show state 13. ) 14. { 15. WNDCLASS wndcls; //定義一個wndcls窗口類對象 16. wndcls.cbClsExtra=0; //類變量占用的存儲空間 17. wndcls.cbWndExtra=0; //實例變量占用的存儲空間 18. wndcls.hbrBackground=(HBRUSH)GetStockObject(BLACK_BRUSH);//指定窗口類畫刷句柄 19. wndcls.hCursor=LoadCursor(NULL,IDC_ARROW); //指定窗口類光標句柄 20. wndcls.hIcon=LoadIcon(NULL,IDI_APPLICATION); //指定窗口類圖標句柄 21. wndcls.hInstance=hInstance; //包含窗口過程的程序的實例句柄 22. wndcls.lpfnWndProc=WinSunProc; //指向窗口過程函數 23. wndcls.lpszClassName="Hello World"; //指定窗口類名字 24. wndcls.lpszMenuName=NULL; //指定菜單資源名字 25. wndcls.style=CS_HREDRAW | CS_VREDRAW; //指定窗口類型樣式 26. RegisterClass(&wndcls); //注冊窗口 27. HWND hwnd; //定義句柄變量 28. hwnd=CreateWindow //創建窗口,返回系統為窗口分配的句柄 29. ("Hello World", //類名,指定該窗口所屬的類 30. "Hello World Program", //窗口的名字,即在標題欄中顯示的文本 31. WS_OVERLAPPEDWINDOW, //該窗口的風格 32. 0, //窗口左上角相對於屏幕左上角的初始X坐標 33. 0, //窗口左上角相對於屏幕左上角的初始Y坐標 34. 600, //窗口的寬度 35. 400, //窗口的高度 36. NULL, //一個子窗口的父窗口的句柄,或隸屬窗口的擁有者窗口的句柄 37. NULL, //菜單句柄 38. hInstance, //創建窗口對象的應用程序的實例句柄 39. NULL); //創建窗口時指定的額外參數 40. ShowWindow(hwnd,SW_SHOWNORMAL); 顯示窗口 41. UpdateWindow(hwnd);更新窗口 42. MSG msg; 43. while(GetMessage(&msg,NULL,0,0)) 44. { 45. TranslateMessage(&msg); 46. DispatchMessage(&msg); 47. } 48. return msg.wParam; 49. } 50. LRESULT CALLBACK WinSunProc( 51. HWND hwnd, // handle to window 52. UINT uMsg, // message identifier 53. WPARAM wParam, // first message parameter 54. LPARAM lParam // second message parameter 55. ) 56. { 57. switch(uMsg) 58. { 59. case WM_CREATE: 60. MessageBox(hwnd,"Window Created","message",MB_OK); 61. break; 62. case WM_PAINT: 63. HDC hDC; 64. PAINTSTRUCT ps; 65. hDC=BeginPaint(hwnd,&ps); 66. TextOut(hDC,0,0,"Hello World!",strlen("Hello World!")); 67. EndPaint(hwnd,&ps); 68. break; 69. case WM_DESTROY: 70. PostQuitMessage(0); 71. break; 72. default: 73. return DefWindowProc(hwnd,uMsg,wParam,lParam); 74. } 75. return 0; 76. }
在Windows XP環境下打開VC6.0(雖然有點老了,但對我們來說功能夠用了),從File菜單中選擇New,在Nem對話框中,單擊Project標簽,選擇Win32 Application。輸入Project Name與Location后點確定。在下一個出現的對話框選Empty Workspace,再按下Finish。進入項目后,再新建一個C++ Sourse File,將以上代碼貼上后,點擊BuildExecute按鈕后將出現一個提示框(如圖),點擊確定后,會出現一個窗口(如圖)。
概念解釋:
窗口是屏幕上與一個應用程序相關的矩形區域,它是用戶與產生該窗口的應用程序之間的可視界面,他接收用戶的輸入,並以文本或圖形的格式顯示輸出內容。對應用程序來說,窗口是應用程序控制下的屏幕上的一個矩形區 域,應用程序創建並控制窗口的所有方面。當用戶啟動一個應用程序時,一個窗口就被創建。每當用戶操作窗口中的對象時,程序就有所響應。
它一般由邊框、標題欄、控制欄最小化圖標、最大化圖標、關閉圖標水平滾動條、垂直滾動條、菜單欄和客戶區構成。①
如常見的記事本就是一個很好的例子:
Windows程序運行的機制——一消息為基礎,事件驅動之(引用侯捷老師《深入淺出MFC》)
概念解釋:
事件與消息
事件由用戶(操作電腦的人)觸發且只能由用戶觸發(如用戶按下了鼠標按鈕,就產生一鼠標事件),操作系統能夠感覺到由用戶觸發的事件,並將此事件轉換為一個(特定的)消息發送到程序的消息隊列中。②
消息隊列
消息被發送到隊列中。“消息隊列”是在消息的傳輸過程中保存消息的容器。消息隊列管理器在將消息從它的源中繼到它的目標時充當中間人。隊列的主要目的是提供路由並保證消息的傳遞;如果發送消息時接收者不可用,消息隊列會保留消息,直到可以成功地傳遞它。(來自百度百科)
Windows 程序的進行系依靠外部發生的事件來驅動。換句話說,程序不斷等待(利用一個while 回路),等待任何可能的輸入,然后做判斷,然后再做適當的處理。上述的“輸入”是由操作系統捕捉到之后,以消息形式(一種數據結構)進入程序之中。操作系統如何捕捉外圍設備(如鍵盤和鼠標)所發生的事件呢?噢,USER 模塊③(不懂先不管它)掌管各個外圍的驅動程序,它們各有偵測回路。
如果把應用程序獲得的各種“輸入”分類,可以分為由硬件裝置所產生的消息(如鼠標移動或鍵盤被按下),放在系統消息隊列(system message queue)中,以及由Windows 系統或其它Windows 程序傳送過來的消息,放在程序消息隊列(application message queue)中。以應用程序的眼光來看,消息就是消息,來自哪里或放在哪里其實並沒有太大區別,反正程序調用GetMessage API 就取得一個消息,程序的生命靠它來推動。所有的GUI(Graphical User Interface,圖形用戶界面) 系統,包括UNIX的X Window 以及OS/2 的Presentation Manager,都像這樣,是以消息為基礎的事件驅動系統。
可想而知,每一個Windows 程序都應該有一個回路如下(先熟悉一下即可):
MSG msg;
while (GetMessage(&msg, NULL, NULL, NULL)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// 以上出現的函數都是Windows API 函數
僅了解這個while循環有可以不斷處理消息的作用即可。
Windows程序生與死——開始分析代碼了!
不要被上面的代碼行數嚇壞了,對就是這么多行才產生一個窗口,沒辦法微軟就是這么規定的,這個簡簡單單的窗口可以說是麻雀雖小,五臟俱全。但是幾乎所有的windows程序都是有它為骨架擴展而來的,弄懂它你就會了一半的windows基本編程,剩下的工作頂多就是學學函數,填充一下它罷了。
一個程序的一生要經歷出生、運行、死亡三個階段,且聽我慢慢道來……
出生:
正如在C程序中的進入點是函數main一樣,Windows程序的進入點是WinMain,總是像這樣出現:見行8 (再強調一下,只講函數作用,具體用法參數含義后面講)
引用孫鑫老師的精彩講解:
“創建一個完整的窗口,需要經過下面幾個操作步驟:
設計一個窗口類;
注冊窗口類;
創建窗口;
顯示及更新窗口。
(請先記住這個順序,先不要問為什么這樣)
1.設計一個窗口類
一個完整的窗口具有許多特征,包括光標(鼠標進入該窗口時的形狀)、圖標、背景色等。窗口的創建過程類似於汽車的制造過程。我們在生產一個型號的汽車之前,首先要對該型號的汽車進行設計,在圖紙上畫出汽車的結構圖,設計各個零部件,同時還要給該型號的汽車取一個響亮的名字,例如“奧迪A6”。在完成設計后,就可以按照“奧迪A6”這個型號生產汽車了。
類似地,在創建一個窗口前,也必須對該類型的窗口進行設計,指定窗口的特征。當
然,在我們設計一個窗口時,不像汽車的設計這么復雜,因為Windows 已經為我們定義好
了一個窗口所應具有的基本屬性,我們只需要像考試時做填空題一樣,將需要我們填充的部分填寫完整,一種窗口就設計好了。(精彩的比喻)”
行15-行25:定義一個wndcls窗口類對象,並填充它。見代碼注釋(遇到新概念或看不懂先跳過,以后詳細講,下同)
“2.注冊窗口類
在設計完汽車后,需要報經國家有關部門審批,批准后才能生產這種類型的汽車。同
樣地,設計完窗口類(WNDCLASS)后,需要調用RegisterClass 函數對其進行注冊,注
冊成功后,才可以創建該類型的窗口。”
行26:注冊窗口
“3.創建窗口——步驟3
設計好窗口類並且將其成功注冊之后,就可以用CreateWindow 函數產生這種類型的
窗口了。”
行27-行39:創建窗口(遇到新概念或看不懂先跳過,以后詳細講)
“4.顯示及更新窗口
(1)顯示窗口
窗口創建之后,我們要讓它顯示出來,這就跟汽車生產出來后要推向市場一樣。調用
函數ShowWindow 來顯示窗口。
(2)更新窗口
在調用 ShowWindow 函數之后,我們緊接着調用UpdateWindow 來刷新窗口,就好像
我們買了新房子,需要裝修一下。”
行41、41:顯示及更新窗口
運行:
請回顧上回“windows程序運行機制——一消息為基礎,以事件驅動之”,程序由一系列復雜的過程創建出來顯示在屏幕上后,程序就時刻在等待消息,然后處理消息,這就是它“活着”。
程序是如何等待消息的呢,又是怎樣處理的呢?請看代碼,我們就來到了while消息循環
見行43-49 (只大致知道函數作用即可,現在不必深究其原理和用法)
函數GetMessage可以從消息隊列中檢索出與此應用程序窗口有關連的消息(一般情況,函數返回非零值讓循環一直進行,什么時候函數返回零,消息循環結束,我們后面講),並將消息的具體內容存於具有MSG類型的一個變量中(即msg,MSG類型后面講),然后交由函數TranslateMessage對該消息進行翻譯(翻譯后面講),緊接着,函數DispatchMessage將消息發送到適當的“對象”處理。
while消息循環如此往復進行,程序就能“活着”,不斷進行等待消息、處理消息。
關於這個神秘的“對象”叫做窗口過程,它是一個函數(即行50-76,行2-7是此函數的聲明),用來處理消息。你仔細觀察你會發現這個函數的主體就是switch選擇語句,那他選擇的WM_CREATE,WM_PAINT,WM_DESTROY這又是什么呢,或許你已經猜到了,這些就是要特意處理的消息,如果傳進來的消息沒有響應它的case 語段,那就一律交由default語段處理(看來它是必不可少的)。仿佛windows一切神秘的面紗已經漸漸地被我們掀開了!一個windows程序原來是這樣這樣的:它首先要被創建並顯示在桌面上,這要經過設計一個窗口類、注冊窗口類、創建窗口、顯示及更新窗口等過程,你想要它有什么功能,就要在窗口過程函數switch選擇語句中特意加一case語段來處理與你這功能相關的消息,對於不相關的消息一率交由default語段處理。可以這樣說很多應用程序之所以不同,區別就在於其窗口過程的不同。至於while消息循環,這是固定的寫法,我們只要這么寫,具體消息的路由都是windows系統自動完成的,我們不用操心。
死亡:
講到此或許讀者應該可以推斷出一點有關程序死亡的一點信息了:窗口程序要 “死亡”肯定是要窗口函數收到某種消息並響應,讓窗口銷毀,同時還要結束while消息循環。(就先了解到這兒,下回再深入)
這次就先講到這了,希望讀者對windows程序的“一生”和它的運行機制有那么一點印象,能以宏觀的角度看待它,那我就能感到心滿意足了。這一回大家主要是理解,下一回我將詳細講代碼中函數的具體用法和相關細節,需要大家多記憶。好了,大家后會有期。
① 說一下大家可能不熟悉的:
控制框是每個窗口左上方的小圖片,每個應用程序都使用它。在控制圖標上單擊鼠標鍵會使Windows顯示系統菜單。系統菜單它提供了諸如還原、移動、大小、最小化、最大化以及關閉這樣的標准操作。
通常客戶區占據了窗口最大的部分(即記事本中可以寫字的區域)。這是應用程序的基本輸出區域。應當由應用程序來復雜管理客戶區。另外,應用程序可以輸出到客戶區。
② 補充:
我們通常說:“某一件事發生了”和“向什么發送某一個消息”。比如在桌面上單擊鼠標時,某一件事發生了,Windows首先知道這件事的發生,然后使用函數SendMessage向桌面發送一個消息,證明有某件事發生了。這就是“事件驅動、消息處理”的原理。
事件是一個動作——用戶觸發的動作。
消息是一個信息——傳遞給系統的信息。這里強調的是:
可以說“用戶觸發了一個事件”,而不能說“用戶觸發了一個消息”。
用戶只能觸發事件,而事件只能由用戶觸發。
一個事件產生后,將被操作系統轉換為一個消息,所以一個消息可能是由一個事件轉換而來(或者由操作系統產生)。
一個消息可能會產生另一個消息,但一個消息決不能產生一個事件——時間只能由用戶觸發。
總結(事件:消息的來源)
事件:只能由用戶通過外設的輸入產生。
消息:(產生消息的來源有三個)
(1) 由操作系統產生。
(2) 由用戶觸發的事件轉換而來。
(3) 由另一個消息產生。
以上援引自《事件與消息的區別》
Windows的消息可分為四種類型:
(1)輸入消息:對鍵盤和鼠標輸入作反應。這類輸入消息首先放在系統消息隊列中,然后Windows將它們送入應用程序的消息隊列,使消息得到處理。
(2)控制消息:用來與Windows的特殊控制對象,例如,對話框、列表框、按鈕等進行雙向通信。這類消息一般不通過應用程序的消息隊列,而是直接發送到控制對象上。
(3)系統消息:對程式化的事件或系統時鍾中斷作出反應。有些系統消息,例如大部分DDE消息(程序間進行動態數據交換時所使用的消息)要通過Windows的系統消息隊列。而有些系統消息,例如窗口的創建及刪除等消息直接送入應用程序的消息隊列。
(4)用戶消息:這些消息是程序員創建的,通常,這些消息只從應用程序的某一部分進入到該應用程序的另一部分而被處理,不會離開應用程序。用戶消息經常用來處理選單操作:一個用戶消息與選單中的一選項相對應,當它在應用程序隊列中出現時被處理。
③ 模塊
在Windows中,術語“模塊”一般是指任何能被裝入內存中運行的可執行代碼和數據的集合。更明確地講,模塊指的就是一個.EXE文件(又稱為應用程序模塊),或一個動態鏈接庫(DLL — Dynamic Linking Library,又被稱為動態鏈接庫模塊或DLL模塊),或一個設備驅動程序,也可能是一個程序包含的能被另一個程序存取的數據資源。模塊一詞也被用於特指自包含的一段程序。例如,一個可單獨編譯的源文件,或該源文件被編譯器處理之后所生成的目標程序。當制作一個程序時,模塊一詞用於指被連接在一起的許多模塊中的某個模塊。
Windows本身由幾個相關的模塊組成,Windows API函數就是在Windows啟動時裝入內存中的幾個動態鏈接庫模塊實現的。其中的三個主要模塊是USER.EXE(用於窗口管理等)、KERNEL.EXE(用於內存管理的多任務調度)和GDI.EXE(圖形設備接口,用於圖形輸出等)。
注:如未另注明,概念解釋均來自《Windows編程基礎 --概述》一文。
文中很多內容都引自《Windows編程基礎 --概述》《深入淺出MFC》《VC++深入詳解》,在此特向這三位作者致敬!