子類化和超類化區別(介紹Windows的窗口、消息、子類化和超類化)(轉)

原文地址:http://maqianli210.blog.sohu.com/75497589.htmlhtml

這篇文章原本只是想介紹一會兒類化和超類化這兩個比較「生僻」的名詞。爲了敘述的完整性而討論了Windows的窗口和消息,也簡要討論了進程和線程。子類化(Subclassing)和超類化(Superclassing)是伴隨Windows窗口機制而產生的兩個複用代碼的方法。不要把「子類化、超類化」與面嚮對象語言中的派生類、基類混淆起來。「子類化、超類化」中的「類」是指Windows的窗口類。程序員

0 運行程序

但願讀者在閱讀本節前先看看"談談Windows程序中的字符編碼"開頭的第0節和附錄0。第0節介紹了Windows系統的幾個重要模塊。附錄0概述了Windows的啓動過程,從上電到啓動Explorer.exe。本節介紹的是運行程序時發生的事情。編程

0.1 程序的啓動

當咱們經過Explorer.exe運行一個程序時,Explorer.exe會調用CreateProcess函數請求系統爲這個程序建立進程。固然,其它程序也能夠調用CreateProcess函數建立進程。windows

系統在爲進程分配內部資源,創建獨立的地址空間後,會爲進程建立一個主線程。咱們能夠把進程看做單位,把線程看做員工。進程擁有資源,但真正在CPU上運行和調度的是線程。系統以掛起狀態建立主線程,即主線程建立好,不會當即運行,而是等待系統調度。系統向Win32子系統的管理員csrss.exe登記新建立的進程和線程。登記結束後,系統通知掛起的主線程能夠運行,新程序纔開始運行。數據結構

這時,在建立進程中CreateProcess函數返回;在被建立進程中,主線程在完成最後的初始化後進入程序的入口函數(Entry-point)。建立進程與被建立進程在各自的地址空間獨立運行。這時,即便咱們結束建立進程,也不會影響被建立進程。框架

0.2 程序的執行

可執行文件(PE文件)的文件頭結構包含入口函數的地址。入口函數通常是Windows在運行時庫中提供的,咱們在編譯時能夠根據程序類型設定。在VC中編譯、運行程序的小知識點討論了Entry-point,讀者能夠參考。函數

入口函數前的過程能夠被看做程序的裝載過程。在裝載時,系統已經作過全局和靜態變量(在編譯時能夠肯定地址)的初始化,有初值的全局變量擁有了它們的初值,沒有初值的變量被設爲0,咱們能夠在入口函數處設置斷點確認這一點。ui

進入入口函數後,程序繼續運行環境的創建,例如調用全部全局對象的構造函數。在一切就緒後,程序調用咱們提供的主函數。主函數名是入口函數決定的,例如main或WinMain。若是咱們沒有提供入口函數要求的主函數,編譯時就會產生連接錯誤。this

0.3 進程和線程

咱們一般把存儲介質(例如硬盤)上的可執行文件稱做程序。程序被裝載、運行後就成爲進程。系統會爲每一個進程建立一個主線程,主線程經過入口函數進入咱們提供的主函數。咱們能夠在程序中建立其它線程。編碼

線程能夠建立一個或多個窗口,也能夠不建立窗口。系統會爲有窗口的線程創建消息隊列。有消息隊列的線程就能夠接收消息,例如咱們能夠用PostThreadMessage函數向線程發送消息。

沒有窗口的線程只要調用了PeekMessage或GetMessage,系統也會爲它建立消息隊列。

1 窗口和消息

1.1 線程的消息隊列

每一個運行的程序就是一個進程。每一個進程有一個或多個線程。有的線程沒有窗口,有的線程有一個或多個窗口。

咱們能夠向線程發送消息,但大多數消息都是發給窗口的。發給窗口的消息一樣放在線程的消息隊列中。咱們能夠把線程的消息隊列看做信箱,把窗口看做收信人。咱們在向指定窗口發送消息時,系統會找到該窗口所屬的線程,而後把消息放到該線程的消息隊列中。

線程消息隊列是系統內部的數據結構,咱們在程序中看不到這個結構。但咱們能夠經過Windows的API向消息隊列發送、投遞消息;從消息隊列接收消息;轉換和分派接收到的消息。

1.2 最小的Windows程序

Windows的程序員大概都看過這麼一個最小的Windows程序

// 例程1
#include "windows.h"
static const char m_szName[] = "窗口";
//////////////////////////////////////////////////////////////////////////////////////////////////// 
// 主窗口回調函數 若是直接用 DefWindowProc, 關閉窗口時不會結束消息循環
static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{ 
 switch (uMsg) {
 case WM_DESTROY:
  PostQuitMessage(0); // 關閉窗口時發送WM_QUIT消息結束消息循環
  break;
 default:
  return DefWindowProc(hWnd, uMsg, wParam, lParam);
 } 
 return 0;
}
//////////////////////////////////////////////////////////////////////////////////////////////////// 
// 主函數 
int __stdcall WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine, int nCmdShow)
{
 WNDCLASS wc; 
 memset(&wc, 0, sizeof(WNDCLASS));
 wc.style = CS_VREDRAW|CS_HREDRAW;
 wc.lpfnWndProc = (WNDPROC)WindowProc;
 wc.hCursor = LoadCursor(NULL, IDC_ARROW);
 wc.hbrBackground = (HBRUSH)(COLOR_WINDOW);
 wc.lpszClassName = m_szName;
 RegisterClass(&wc);  // 登記窗口類
 
 HWND hWnd;
 hWnd = CreateWindow(m_szName,m_szName,WS_OVERLAPPEDWINDOW,100,100,320,240,
  NULL,NULL,hInstance,NULL); // 建立窗口
 ShowWindow(hWnd, nCmdShow);  // 顯示窗口
 
 MSG sMsg; 
 while (int ret=GetMessage(&sMsg, NULL, 0, 0)) {  // 消息循環
  if (ret != -1) {
   TranslateMessage(&sMsg);
   DispatchMessage(&sMsg);
  }
 }
 return 0;
}

這個程序雖然只顯示一個窗口,但常常被用來講明Windows程序的基本結構。在MFC框架內部咱們一樣能夠找到相似的程序結構。這個程序包含如下基本概念:

  • 窗口類、窗口和窗口過程
  • 消息循環

下面分別介紹。

1.3 窗口類、窗口和窗口過程

建立窗口時要提供窗口類的名字。窗口類至關於窗口的模板,咱們能夠基於同一個窗口類建立多個窗口。咱們可使用Windows預先登記好的窗口類。但在更多的狀況下,咱們要登記本身的窗口類。在登記窗口類時,咱們要登記名稱、風格、圖標、光標、菜單等項,其中最重要的就是窗口過程的地址。

窗口過程是一個函數。窗口收到的全部消息都會被送到這個函數處理。那麼,發到線程消息隊列的消息是怎麼被送到窗口的呢?

1.4 消息循環

熟悉嵌入式多任務程序的程序員,都知道任務(至關於Windows的線程)的結構基本上都是:

 while (1) {
  等待信號;
  處理信號;
 }

任務收到信號就處理,不然就掛起,讓其它任務運行。這就是消息驅動程序的基本結構。Windows程序一般也是這樣:

 while (int ret=GetMessage(&sMsg, NULL, 0, 0)) {  // 消息循環
  if (ret != -1) {
   TranslateMessage(&sMsg);
   DispatchMessage(&sMsg);
  }
 }

GetMessage從消息隊列接收消息;TranslateMessage根據按鍵產生WM_CHAR消息,放入消息隊列;DispatchMessage根據消息中的窗口句柄將消息分發到窗口,即調用窗口過程函數處理消息。

1.5 經過消息通訊

建立窗口的函數會返回一個窗口句柄。窗口句柄在系統範圍內(不是進程範圍)標識一個惟一的窗口實例。經過向窗口發送消息,咱們能夠實現進程內和進程間的通訊。

咱們能夠用SendMessage或PostMessage向窗口發送或投遞消息。SendMessage必須等到目標窗口處理過消息纔會返回。我試過:若是向一個沒有消息循環的窗口SendMessage,SendMessage函數永遠不會返回。PostMessage在把消息放入線程的消息隊列後當即返回。

其實只有投遞的消息纔是經過DispatchMessage分派到窗口過程的。經過SendMessage發送的消息,在線程GetMessage時,就已經被分派到窗口過程了,不通過DispatchMessage。

1.5.1 窗口程序與控制檯程序的通訊實例

你們是否是以爲「例程1」沒什麼意思,讓咱們用它來作個小遊戲:讓「例程1」和一個控制檯程序作一次親密接觸。咱們首先將「例程1」的窗口過程修改成:

static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
 static DWORD tid = 0;
 switch (uMsg) {
 case WM_DESTROY:
  PostQuitMessage(0); // 關閉窗口時發送WM_QUIT消息結束消息循環
  break;
 case WM_USER:
  tid = wParam; // 保存控制檯程序的線程ID
  SetWindowText(hWnd, "收到");
  break;
 case WM_CHAR:
  if (tid) {
   switch(wParam) {
   case '1':
    PostThreadMessage(tid, WM_USER+1, 0, 0); // 向控制檯程序發送消息1
    break;
   case '2':
    PostThreadMessage(tid, WM_USER+2, 0, 0); // 向控制檯程序發送消息2
    break;
   }  }
  break;
 default:
  return DefWindowProc(hWnd, uMsg, wParam, lParam);
 } 
 return 0;
}

而後,咱們建立一個控制檯程序,代碼以下:

#include "windows.h"
#include "stdio.h"
static HWND m_hWnd = 0;
void process_msg(UINT msg, WPARAM wp, LPARAM lp)
{
 char buf[100];
 static int i = 1;
 if (!m_hWnd) {
  return;
 }
 switch (msg) {
 case WM_USER+1:
  SendMessage(m_hWnd, WM_GETTEXT, sizeof(buf), (LPARAM)buf);
  printf("你如今叫:%s\n\n", buf);  // 讀取、顯示對方的名字
  break;
 
 case WM_USER+2:
  sprintf(buf, "我是窗口%d", i++);
  SendMessage(m_hWnd, WM_SETTEXT, sizeof(buf), (LPARAM)buf); // 修改對方名字
  printf("給你更名\n\n");
  break;
 }
}
 
int main()
{
 MSG sMsg;
 printf("Start with thread id %d\n", GetCurrentThreadId());
 m_hWnd = FindWindow(NULL,"窗口");
 if (m_hWnd) {
  printf("找到窗口%x\n\n", m_hWnd);
  SendMessage(m_hWnd, WM_USER, GetCurrentThreadId(), 0);
 }
 else {
  printf("沒有找到窗口\n\n");
 }
 
 while (int ret=GetMessage(&sMsg, NULL, 0, 0)) {  // 消息循環
  if (ret != -1) {
   process_msg(sMsg.message, sMsg.wParam, sMsg.lParam);
  }
 }
 return 0;
}

你們能看懂這遊戲怎麼玩嗎?首先運行「例程1」wnd,而後運行控制檯程序msg。msg會找到wnd的窗口,並將本身的主線程ID發給wnd。wnd收到msg的消息後,會顯示收到。這時,wnd和msg已經創建了通訊的渠道:wnd能夠向msg的主線程發消息,msg能夠向wnd的窗口發消息。

咱們若是在wnd窗口按下鍵'1',wnd會向msg發送消息1,msg收到後會經過WM_GETTEXT消息得到wnd的窗口名稱並顯示。咱們若是在wnd窗口按下鍵'2',wnd會向msg發送消息2,msg收到後會經過WM_SETTEXT消息修改wnd的窗口名稱。

這個小例子演示了控制檯程序的消息循環,向線程發消息,以及進程間的消息通訊。

1.5.2 地址空間的問題

不一樣的進程擁有獨立的地址空間,若是咱們在消息參數中包含一個進程A的地址,而後發送到進程B。進程B若是在本身的地址空間裏操做這個地址,就會發生錯誤。那麼,爲何上例中的WM_GETTEXT和WM_SETEXT能夠正常工做?

這是由於WM_GETTEXT和WM_SETEXT都是Windows本身定義的消息,Windows知道參數的含義,並做了特殊的處理,即在進程B的空間分配一塊內存做爲中轉,並在進程A和進程B的緩衝區之間複製數據。例如:在1.5.1節的例子中,若是咱們設置斷點觀察,就會發現msg發送的WM_SETTEXT消息中的lParam不等於wnd接收到的WM_SETTEXT消息中的lParam。

若是咱們在本身定義的消息中傳遞內存地址,系統不會作任何特殊處理,因此必然發生錯誤。

Windows提供了WM_COPYDATA消息用來向窗口傳遞數據,Windows一樣會爲這個消息做特殊處理。

在進程間發送這些須要額外分配內存的消息時,咱們應該用SendMessage,而不是PostMessage。由於SendMessage會等待接收方處理完後再返回,這樣系統纔有機會額外釋放分配的內存。在這種場合使用PostMessage,系統會忽略要求投遞的消息,讀者能夠在msg程序中試驗一下。

2 子類化和超類化

窗口類是窗口的模板,窗口是窗口類的實例。窗口類和每一個窗口實例都有本身的內部數據結構。Windows雖然沒有公開這些數據結構,但提供了讀寫這些數據的API。

例如:用GetClassLong和SetClassLong函數能夠讀寫窗口類的數據;用GetWindowLong和SetWindowLong能夠讀寫指定窗口實例的數據。使用這些接口,能夠在運行時讀取或修改窗口類或窗口實例的窗口過程地址。這些接口是子類化的實現基礎。

2.1 子類化

子類化的目的是在不修改現有代碼的前提下,擴展示有窗口的功能。它的思路很簡單,就是將窗口過程地址修改成一個新函數地址,新的窗口過程函數處理本身感興趣的消息,將其它消息傳遞給原窗口過程。經過子類化,咱們不須要現有窗口的源代碼,就能夠定製窗口功能。

子類化能夠分爲實例子類化和全局子類化。實例子類化就是修改窗口實例的窗口過程地址,全局子類化就是修改窗口類的窗口過程地址。實例子類化隻影響被修改的窗口。全局子類化會影響在修改以後,按照該窗口類建立的全部窗口。顯然,全局子類化不會影響修改前已經建立的窗口。

子類化方法雖然是二十年前的概念,卻很好地實踐了面向對象技術的開閉原則(OCP:The Open-Closed Principle):對擴展開放,對修改關閉。

2.2 超類化

超類化的概念更簡單,就是讀取現有窗口類的數據,保存窗口過程函數地址。對窗口類數據做必要的修改,設置新窗口過程,再換一個名稱後登記一個新窗口類。新窗口類的窗口過程函數仍是僅處理本身感興趣的消息,而將其它消息傳遞給原窗口過程函數處理。使用GetClassInfo函數能夠讀取現有窗口類的數據。

3 MFC中的消息循環和子類化

MFC將子類化方法應用得淋漓盡致,是一個不錯的例子。候捷先生的《深刻淺出MFC》已經將MFC的主要框架分析得很透徹了,本節只是看看MFC的消息循環,簡單分析MFC對子類化的應用。

3.1 消息循環

隨便創建一個MFC單文檔程序,在視圖類中添加WM_RBUTTONDOWN的處理函數,並在該處理函數中設置斷點。運行,斷下後,查看調用堆棧:

CHelloView::OnRButtonDown(unsigned int, CPoint)
CWnd::OnWndMsg(unsigned int, unsigned int, long, long *)
CWnd::WindowProc(unsigned int, unsigned int, long)
AfxCallWndProc(CWnd *, HWND__ *, unsigned int, unsigned int, long)
AfxWndProc(HWND__ *, unsigned int, unsigned int, long)
AfxWndProcBase(HWND__ *, unsigned int, unsigned int, long)
USER32! 7e418734()
USER32! 7e418816()
USER32! 7e4189cd()
USER32! 7e4196c7()
CWinThread::PumpMessage()
CWinThread::Run()
CWinApp::Run()
AfxWinMain(HINSTANCE__ *, HINSTANCE__ *, char *, int)
WinMain(HINSTANCE__ *, HINSTANCE__ *, char *, int)
WinMainCRTStartup()
KERNEL32! 7c816fd7() 

WinMainCRTStartup是這個程序的入口函數。候捷先生已經詳細介紹過AfxWinMain。咱們就看看CWinThread::PumpMessage中的消息循環:

BOOL CWinThread::PumpMessage()
{
 if (!::GetMessage(&m_msgCur, NULL, NULL, NULL)) {
  return FALSE;
 }
 if (m_msgCur.message != WM_KICKIDLE && !PreTranslateMessage(&m_msgCur))
 {
  ::TranslateMessage(&m_msgCur);
  ::DispatchMessage(&m_msgCur);
 }
 return TRUE;
}

這就是MFC程序主線程中的消息循環,它把發送到線程消息隊列的消息分派到線程的窗口。

3.2 子類化

CWnd::CreateEx在建立窗口前調用SetWindowsHookEx函數安裝了一個鉤子函數_AfxCbtFilterHook。窗口剛建立好,鉤子函數_AfxCbtFilterHook就被調用。_AfxCbtFilterHook調用SetWindowLong將窗口過程替換爲AfxWndProcBase,並將SetWindowLong返回的原窗口地址保存到成員變量oldWndProc。上節調用堆棧中的AfxWndProcBase就是由此而來。

可見,經過CWnd::CreateEx建立的全部窗口都會被子類化,即它們的窗口過程都會被替換爲AfxWndProcBase。MFC爲何要這樣作?

讓咱們再看看調用堆棧中的CWnd::WindowProc函數:

LRESULT CWnd::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
 LRESULT lResult = 0;
 if (!OnWndMsg(message, wParam, lParam, &lResult))
  lResult = DefWindowProc(message, wParam, lParam);
 return lResult;
}

按照侯捷先生的介紹,CWnd::OnWndMsg就是「MFC消息泵」的入口,消息經過這個入口流入MFC消息映射中的消息處理函數。消息泵只會處理咱們定製過的消息,咱們沒有添加過處理的消息會原封不動地流過"消息泵",進入DefWindowProc函數。在DefWindowProc函數中,消息會傳給子類化時保存的原窗口地址oldWndProc。

CWnd::CreateEx裏的鉤子會子類化全部窗口嗎?其實不盡然。的確,MFC全部窗口相關的類都是從CWnd派生的,這些類的實例在建立窗口時都會調用CWnd::CreateEx,都會被子類化。可是,經過對話框模板建立的窗口是經過CreateDlgIndirect建立的,不通過CWnd::CreateEx函數。

但這點其實也不是問題,由於若是咱們想經過MFC定製一個控件的消息映射,就必須先子類化這個控件,MFC仍是有機會將窗口過程替換成本身的AfxWndProcBase。下一節將介紹對話框控件的子類化。

4 子類化和超類化的例子

我寫了一個很簡單的對話框程序,用來演示子類化和超類化。這個對話框程序有兩個編輯框,我將編輯框的右鍵菜單換成了一個消息框。兩個編輯框的定製分別採用了子類化和超類化技術:

 

4.1 子類化的例子

首先從CEdit派生出CMyEdit1,定製WM_RBUTTONDOWN的處理。不少文章都建議咱們在對話框的OnInitDialog中用SubclassDlgItem實現子類化:

 m_edit1.SubclassDlgItem(IDC_EDIT1, this);

這樣作固然能夠。其實若是咱們已經爲IDC_EDIT1添加過CMyEdit1對象:

void CSubclassingDlg::DoDataExchange(CDataExchange* pDX)
{
 CDialog::DoDataExchange(pDX);
 //{{AFX_DATA_MAP(CSubclassingDlg)
 DDX_Control(pDX, IDC_EDIT1, m_edit1);
 //}}AFX_DATA_MAP
}

DDX_Control會自動幫咱們完成子類化,沒有必要手工調用SubclassDlgItem。你們能夠經過在PreSubclassWindow中設置斷點看看。

經過DDX_Control或者SubclassDlgItem子類化控件的效果是同樣的,MFC都是把窗口過程替換成AfxWndProcBase。用戶添加過處理函數的消息經過MFC消息泵流入用戶的處理函數。

4.2 必經之路:PreSubclassWindow

PreSubclassWindow是一個很好的定製控件的位置。若是咱們經過重載CWnd::PreCreateWindow定製控件,而用戶在對話框中使用控件。因爲對話框中的控件窗口是經過CreateDlgIndirect建立,不通過CWnd::CreateEx函數,PreCreateWindow函數不會被調用。

其實,用戶要在對話框中使用定製控件,必須用DDX或者SubclassDlgItem函數子類化控件,這時PreSubclassWindow必定會被調用。

若是用戶直接建立定製控件窗口,CWnd::CreateEx函數就必定會被調用,控件窗口必定會被子類化以安裝MFC消息泵。因此在MFC中,PreSubclassWindow是建立窗口的必經之路。

4.3 超類化的例子

我不多看到超類化的例子(除了羅雲彬的Win32彙編),在大多數應用中,子類化技術已經足夠了。但我仍是寫了一個例子:CMyEdit2從CEdit派生。CMyEdit2::RegisterMe獲取窗口類Edit的信息,保存原窗口過程,設置新窗口過程MyWndProc和新名稱MyEdit,登記一個新窗口類。新窗口過程MyWndProc定製本身須要處理的消息,將其它消息送回原窗口過程。

我在對話框的OnInitDialog中先調用CMyEdit2::RegisterMe登記新窗口類,而後建立窗口。這樣建立窗口必須通過CWnd::CreateEx,因此MFC仍是會把窗口過程換成AfxWndProcBase。沒有被MFC消息映射攔截的消息纔會流入MyWndProc。

5 結束語

這篇文章介紹了一些Windows和MFC的基礎知識。寫這篇文章的目的不是介紹什麼編程技巧,而是讓咱們更瞭解程序運行時發生的事情。

相關文章
相關標籤/搜索