第四部分:消息映射
應用程序放在窗口中的任何用戶界面對象都具有兩種可控制的特性:1) 它的外觀,2) 它響應事件的行為。在上一講中,你已經學習了CStatic控制和如何使用樣式屬性來定制用戶界面對象的外觀。這些概念可用於MFC中的所有不同控制類。
在本講中,我們將介紹CButton控制,以理解消息映射和簡單的事件處理。然後還要介紹使用CScrollBar控制的稍微復雜點的例子。
理解消息映射
在第二講中,MFC程序不包括主要函數或時間循環。所有的事件處理都是作為CWinApp的一部分在後台處理的。因為它們是隱藏的,所以我們需要一種方法來告訴不可見的時間循環通告我們應用程序所感興趣的事件。這需要一種叫做消息映射的機制。消息映射識別感興趣的事件然後調用函數來響應這些事件。
例如,如果你要編寫一個程序,當用戶按下標有“退出”的按鈕時要退出應用程序。在程序中,你編寫代碼來建立按鈕:你指示按鈕應如何動作。然後,為其父窗口建立用戶單擊按鈕時的消息映射,它試圖要傳遞消息給其父窗口。為了建立父窗口的消息,你要建立截取消息映射的機制,並且使用按鈕的消息。當一指定的按鈕事件發生時,消息映射會請求MFC調用一指定的函數。在這種情況下,單擊退出按鈕就是所感興趣的事件。然後你把退出應用程序的代碼放到指定的函數中。
其它的工作就由MFC來做了。當程序執行時,用戶單擊“退出”按鈕時,按鈕就會自己加亮。然後MFC自動調用相應的函數,並且程序會終止。只使用很少的幾行代碼你就響應了用戶事件。
CButton類
在上一講中所討論的CStatic控制是唯一不響應用戶時間的控制。Windows中所有的其它控制都可響應用戶事件。第一,當用戶處理它們時,它們會自動更新其外觀(例如,當用戶單擊按鈕時,按鈕會自己加亮以給用戶一個反饋)。第二,每個不同的控制都要發送信息給你的代碼以使程序能響應用戶的需要。例如,當單擊按鈕時,按鈕就會發送一個命令消息。如果你編寫代碼接收消息,則你的代碼就能響應用戶事件。
為了理解這個過程,我們從CButton控制開始。下面的代碼說明了建立按鈕的過程:
// button1.cpp
#include
#define IDB_BUTTON 100
// Declare the application class
class CButtonApp : public CWinApp
{
public:
virtual BOOL InitInstance();
};
// Create an instance of the application class
CButtonApp ButtonApp;
// Declare the main window class
class CButtonWindow : public CFrameWnd
{
CButton *button;
public:
CButtonWindow();
};
// The InitInstance function is called once
// when the application first executes
BOOL CButtonApp::InitInstance()
{
m_pMainWnd = new CButtonWindow();
m_pMainWnd->ShowWindow(m_nCmdShow);
m_pMainWnd->UpdateWindow();
return TRUE;
}
// The constructor for the window class
CButtonWindow::CButtonWindow()
{
CRect r;
// Create the window itself
Create(NULL,
"CButton Tests",
WS_OVERLAPPEDWINDOW,
CRect(0,0,200,200));
// Get the size of the client rectangle
GetClientRect(&r);
r.InflateRect(-20,-20);
// Create a button
button = new CButton();
button->Create("Push me",
WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,
r,
this,
IDB_BUTTON);
}
上面的代碼與前面介紹的代碼幾乎相同。CButton類的Create函數共有5個參數。前四個與CStatic的相同。第五個參數為按鈕的資源ID。資源ID是用來標識消息映射中按鈕的唯一整數值。常數值IDB_BUTTON已經在程序的頂部做了定義。“IDB_”是任選的,只是該常量ID是用來表示按鈕的。它的值為100,因為100以內的值都為系統所保留。你可以使用任何大於99的值。
CButton類所允許的樣式屬性與CStatic類的是不同的。定義了11個不同的“BS”(“Button Style”)常量。完整的“BS”常量列表可在用Search命令查找CButton,並選擇“button style”。這裡我們要用的是BS_PUSHBUTTON樣式,它表示我們要一正常的的按鈕方式來顯示該按鈕。我們還使用了兩個熟悉的“WS”屬性: WS_CHILD和WS_VISIBLE。我們將在後面介紹其它一些樣式。
當你運行代碼時,會注意到按鈕響應了用戶事件。既它加亮了。除此之外它沒有做任何事情,因為我們還沒有教它怎樣去做。我們需要編寫消息映射來使按鈕做一些感興趣的事情。
建立消息映射
下面的代碼包含有消息映射,也包含有新的處理單擊按鈕的函數(當用戶單擊按鈕時會響一下喇叭)。它只是前面代碼的一個簡單的擴充:
// button2.cpp
#include
#define IDB_BUTTON 100
// Declare the application class
class CButtonApp : public CWinApp
{
public:
virtual BOOL InitInstance();
};
// Create an instance of the application class
CButtonApp ButtonApp;
// Declare the main window class
class CButtonWindow : public CFrameWnd
{
CButton *button;
public:
CButtonWindow();
afx_msg void HandleButton();
DECLARE_MESSAGE_MAP()
};
// The message handler function
void CButtonWindow::HandleButton()
{
MessageBeep(-1);
}
// The message map
BEGIN_MESSAGE_MAP(CButtonWindow, CFrameWnd)
ON_BN_CLICKED(IDB_BUTTON, HandleButton)
END_MESSAGE_MAP()
// The InitInstance function is called once
// when the application first executes
BOOL CButtonApp::InitInstance()
{
m_pMainWnd = new CButtonWindow();
m_pMainWnd->ShowWindow(m_nCmdShow);
m_pMainWnd->UpdateWindow();
return TRUE;
}
// The constructor for the window class
CButtonWindow::CButtonWindow()
{
CRect r;
// Create the window itself
Create(NULL,
"CButton Tests",
WS_OVERLAPPEDWINDOW,
CRect(0,0,200,200));
// Get the size of the client rectangle
GetClientRect(&r);
r.InflateRect(-20,-20);
// Create a button
button = new CButton();
button->Create("Push me",
WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,
r,
this,
IDB_BUTTON);
}
主要修改了三個方面:
CButtonWindow的類說明現在包含了一個新的成員函數和一個新的表示消息映射的宏。HandleButton函數是正常的C++函數,它通過afx_msg標簽確定為消息處理函數。該函數需要一些特殊的約束,例如,它必須是void型並且它不能接收任何參數。DECLARE_MESSAGE_MAP宏建立了消息映射。函數和宏都必須是public型的。
HandleButton函數作為成員函數以同樣的方式來建立。在該函數中,我們調用了Windows API中的MessageBeep函數。
用宏來建立消息映射。在代碼中,你可以看見BEGIN_MESSAGE_MAP宏接收兩各參數。第一個指定了使用消息映射的類的名稱。第二個是基類。然後是ON_BN_CLICKED宏,接受兩個參數控制的ID和該ID發送命令消息時所調用的函數。最後,消息映射用END_MESSAGE_MAP來結束。
當用戶單擊按鈕時,它向其包含該按鈕的父窗口發送了一個包含其ID的命令消息。那是按鈕的缺省行為,這就是該代碼工作的原因。按鈕向其父窗口發送消息,是因為它是子窗口。父窗口截取該消息並用消息映射來確定所要調用的函數。MFC來安排,只要指定的消息一出現,相應的函數就會被調用。
ON_BN_CLICKED消息是CButton發送的唯一感興趣的消息。它等同於CWnd中的ON_COMMAND消息,只是一個更簡單方便的同義詞而已。
改變大小的消息
在上面的代碼中,由於有了消息映射,從CFrameWnd繼承來的應用程序窗口認出按鈕有按鈕產生的單擊消息並響應之。加入消息映射的ON_BN_CLICKED宏指定了按鈕的ID和窗口在接收到來自按鈕的命令消息時應調用的函數。因為只要用戶單擊了按鈕,按鈕就會自動把其ID發送父窗口,這樣才能允許代碼正確地處理按鈕事件。
作為該應用程序的主窗口的框架窗口自己也有傳遞消息的能力。大約有100不同的消息可用,它們都是從CWnd類繼承來的。從MFC幫助文件中浏覽CWnd類的成員函數,你就會看到所有的這些消息。查看所有以“On”開頭的成員函數。
你可能已經注意到了,至今為止所有的代碼都不能很好地處理尺寸變化。當窗口變化大小時,窗口的框架會做相應的調整,但是窗口中調的內容仍原處不動。可以通過處理尺寸變化的事件來更好的處理這一問題。任何窗口發送的消息之一就是變尺寸消息。該消息是當改變形狀時發出的。我們可以使用該消息來控制框架中子窗口的大小,如下所示:
// button3.cpp
#include
#define IDB_BUTTON 100
// Declare the application class
class CButtonApp : public CWinApp
{
public:
virtual BOOL InitInstance();
};
// Create an instance of the application class
CButtonApp ButtonApp;
// Declare the main window class
class CButtonWindow : public CFrameWnd
{
CButton *button;
public:
CButtonWindow();
afx_msg void HandleButton();
afx_msg void OnSize(UINT, int, int);
DECLARE_MESSAGE_MAP()
};
// A message handler function
void CButtonWindow::HandleButton()
{
MessageBeep(-1);
}
// A message handler function
void CButtonWindow::OnSize(UINT nType, int cx,
int cy)
{
CRect r;
GetClientRect(&r);
r.InflateRect(-20,-20);
button->MoveWindow(r);
}
// The message map
BEGIN_MESSAGE_MAP(CButtonWindow, CFrameWnd)
ON_BN_CLICKED(IDB_BUTTON, HandleButton)
ON_WM_SIZE()
END_MESSAGE_MAP()
// The InitInstance function is called once
// when the application first executes
BOOL CButtonApp::InitInstance()
{
m_pMainWnd = new CButtonWindow();
m_pMainWnd->ShowWindow(m_nCmdShow);
m_pMainWnd->UpdateWindow();
return TRUE;
}
// The constructor for the window class
CButtonWindow::CButtonWindow()
{
CRect r;
// Create the window itself
Create(NULL,
"CButton Tests",
WS_OVERLAPPEDWINDOW,
CRect(0,0,200,200));
// Get the size of the client rectangle
GetClientRect(&r);
r.InflateRect(-20,-20);
// Create a button
button = new CButton();
button->Create("Push me",
WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,
r,
this,
IDB_BUTTON);
}
為了理解上面的代碼,從窗口的消息映射開始。你會發現入口ON_WM_SIZE。該入口表示消息映射是對來自CButtonWindow對象的變尺寸消息發生響應。變尺寸消息是當用戶改變窗口的大小時產生的。該消息來自窗口本身,而不是作為ON_COMMAND消息由按鈕向其父窗口發送的。這是因為窗口框架不是子窗口。
要注意的是消息映射中的ON_WM_SIZE入口沒有參數。你在MFC文檔中CWnd類,消息映射中的ON_WM_SIZE入口總是調用OnSize函數,並且該函數必須接收三個參數。OnSize函數必須是消息映射所屬類的成員函數,並且該函數必須用afx_msg來說明(正如上面在CButtonWindow的定義中所見到的一樣)。
如果你查看MFC文檔,就會發現CWnd中有近100名為“On...”的函數。CWnd::OnSize是其中之一。所有這些函數都在消息映射中有形如ON_WM_對應的標簽。例如,ON_WM_SIZE對應OnSize。ON_WM_入口不接收任何參數,如ON_BN_CLICKED一樣。參數是假設的並自動傳遞給相應的如OnSize的“On...”函數。
重復一遍,因為它很重要: OnSize函數總是與消息映射中的ON_WM_SIZE入口想對應。你必須命名處理函數OnSize, 並且它必須接收三個參數。不同的函數的參數會有所不同。
上面的代碼中在OnSize函數自身的內部,有三行代碼修改了按鈕在窗口中的尺寸。你可以在該函數中輸入任何你想要的代碼。
調用GetClientRect是為了恢復窗口用戶區域的新尺寸。該矩形會被縮小,並調用按鈕的MoveWindow函數。MoveWindow是從CWnd繼承來的,改變尺寸和移動子窗口是在一步完成的。---www.bianceng.cn
當你執行上面改變窗口大小的程序時,你就會發現按鈕自己能正確地改變大小。在代碼中,變尺寸事件他國消息映射中的OnSize函數而產生一調用,它調用MoveWindow函數來改變按鈕的大小。
窗口消息
查看MFC文檔,你可以看到主窗口處理的各種各樣的CWnd消息。有些與我們上面介紹的類似。例如,ON_WM_MOVE消息是當用戶移動窗口時發送的消息,ON_WM_PAINT消息是當窗口的任何部分需要重畫時發出的。至今為止,我們的所有程序,重畫工作都是自動完成的,因為是控制自己來負責其外觀。如果你自己使用GDI命令來在用戶區域中繪制,應用程序就應負責重畫工作。因此ON_WM_PAINT就變得重要了。
還有一些發送給窗口的事件消息更深奧。例如,你可以使用ON_WM_TIMER消息與SetTimer函數來使接收預先設置的時間間隔。下面的代碼給出了該過程。當你運行該代碼時,程序會每隔1秒鐘鳴笛一聲。你可以用其它更有用的功能來代替鳴笛。
// button4.cpp
#include
#define IDB_BUTTON 100
#define IDT_TIMER1 200
// Declare the application class
class CButtonApp : public CWinApp
{
public:
virtual BOOL InitInstance();
};
// Create an instance of the application class
CButtonApp ButtonApp;
// Declare the main window class
class CButtonWindow : public CFrameWnd
{
CButton *button;
public:
CButtonWindow();
afx_msg void HandleButton();
afx_msg void OnSize(UINT, int, int);
afx_msg void OnTimer(UINT);
DECLARE_MESSAGE_MAP()
};
// A message handler function
void CButtonWindow::HandleButton()
{
MessageBeep(-1);
}
// A message handler function
void CButtonWindow::OnSize(UINT nType, int cx,
int cy)
{
CRect r;
GetClientRect(&r);
r.InflateRect(-20,-20);
button->MoveWindow(r);
}
// A message handler function
void CButtonWindow::OnTimer(UINT id)
{
MessageBeep(-1);
}
// The message map
BEGIN_MESSAGE_MAP(CButtonWindow, CFrameWnd)
ON_BN_CLICKED(IDB_BUTTON, HandleButton)
ON_WM_SIZE()
ON_WM_TIMER()
END_MESSAGE_MAP()
// The InitInstance function is called once
// when the application first executes
BOOL CButtonApp::InitInstance()
{
m_pMainWnd = new CButtonWindow();
m_pMainWnd->ShowWindow(m_nCmdShow);
m_pMainWnd->UpdateWindow();
return TRUE;
}
// The constructor for the window class
CButtonWindow::CButtonWindow()
{
CRect r;
// Create the window itself
Create(NULL,
"CButton Tests",
WS_OVERLAPPEDWINDOW,
CRect(0,0,200,200));
// Set up the timer
SetTimer(IDT_TIMER1, 1000, NULL); // 1000 ms.
// Get the size of the client rectangle
GetClientRect(&r);
r.InflateRect(-20,-20);
// Create a button
button = new CButton();
button->Create("Push me",
WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,
r,
this,
IDB_BUTTON);
}
在上面的程序內部,我們建立了一個按鈕,如前所示,改變尺寸的代碼沒有變動。在窗口的構造函數中,我們添加了SetTimer函數的調用。該函數接收三個參數:時鐘的ID(可以同時使用多個時鐘,每次時鐘關閉時都會把ID傳遞給所調用的函數),時間以毫秒為單位。在這裡,我們向函數傳送了NULL,以使窗口消息映射自己自動發送函數。在消息映射中,我們已經通知了ON_WM_TIMER消息,它會自動調用OnTimer函數來傳遞已經關閉了的時鐘的ID。
當程序運行時,它每隔1毫秒鳴笛一聲。每次時鐘的時間增量流逝,窗口都會發送消息給自己。消息映射選擇消息給OnTimer函數,它鳴笛。你可以在此放置更有用的代碼。
滾動條控制
Windows用兩種不同的方式來處理滾動條。一些控制,如編輯控制和列表控制,可以帶有滾動條。在這種情況下,滾動條會被自動處理,不不要額外的代碼來處理。
滾動條也可以作為單獨的元件來使用。當這樣使用時,滾動條就擁有獨立的權力。你可以參見MFC參考手冊中有關CScrollBar的有關章節。滾動條控制的建立與前面介紹的靜態標簽和按鈕的一樣。它有四個成員函數允許你設置和獲取滾動條的位置和范圍。
下面的代碼演示了建立水平滾動條的過程和其消息映射:
// sb1.cpp
#include
#define IDM_SCROLLBAR 100
const int MAX_RANGE=100;
const int MIN_RANGE=0;
// Declare the application class
class CScrollBarApp : public CWinApp
{
public:
virtual BOOL InitInstance();
};
// Create an instance of the application class
CScrollBarApp ScrollBarApp;
// Declare the main window class
class CScrollBarWindow : public CFrameWnd
{
CScrollBar *sb;
public:
CScrollBarWindow();
afx_msg void OnHScroll(UINT nSBCode, UINT nPos,
CScrollBar* pScrollBar);
DECLARE_MESSAGE_MAP()
};
// The message handler function
void CScrollBarWindow::OnHScroll(UINT nSBCode,
UINT nPos, CScrollBar* pScrollBar)
{
MessageBeep(-1);
}
// The message map
BEGIN_MESSAGE_MAP(CScrollBarWindow, CFrameWnd)
ON_WM_HSCROLL()
END_MESSAGE_MAP()
// The InitInstance function is called once
// when the application first executes
BOOL CScrollBarApp::InitInstance()
{
m_pMainWnd = new CScrollBarWindow();
m_pMainWnd->ShowWindow(m_nCmdShow);
m_pMainWnd->UpdateWindow();
return TRUE;
}
// The constructor for the window class
CScrollBarWindow::CScrollBarWindow()
{
CRect r;
// Create the window itself
Create(NULL,
"CScrollBar Tests",
WS_OVERLAPPEDWINDOW,
CRect(0,0,200,200));
// Get the size of the client rectangle
GetClientRect(&r);
// Create a scroll bar
sb = new CScrollBar();
sb->Create(WS_CHILD|WS_VISIBLE|SBS_HORZ,
CRect(10,10,r.Width()-10,30),
this,
IDM_SCROLLBAR);
sb->SetScrollRange(MIN_RANGE,MAX_RANGE,TRUE);
}
Windows會區分水平和垂直滾動條,同時還支持CScrollBar中一稱為尺寸盒的控制。尺寸盒是一個小方塊。它處於水平和垂直滾動條的交叉處,呀鼠標拖動它會自動改變窗口的大小。在後面的代碼中你看到如何用Create函數的SBS_HORZ樣式來建立一水平滾動條。在建立了滾動條之後,馬上用SetScrollRange中的MIN_RANGE和MAX_RANGE龍個常數給出了滾動條的范圍0~100(它們定義在程序的頂部)。
事件處理函數OnHScroll來自CWnd類。我們使用該函數是因為該代碼建立了水平滾動條。對於垂直滾動條應使用OnVScroll。在代碼中,消息映射與滾動函數相聯系,並使滾動條在用戶操作時發出鳴笛聲。當你運行該程序時,你可以單擊箭頭、拖動滾動條上的小方塊等等。每次操作都會出現鳴笛聲,但是滾動條上的小方塊實際上不會移動,因為我們還沒有把它與實際的代碼相關聯。
每次滾動條調用OnHScroll時,你的代碼都要確定用戶的操作。在OnHScroll函數內部,你可以檢驗傳遞給處理函數的第一參數,如下所示。如果你與上面的代碼一起使用,滾動條的小方塊就會移動到用戶操作的位置處。
// The message handling function
void CScrollBarWindow::OnHScroll(UINT nSBCode,
UINT nPos, CScrollBar* pScrollBar)
{
int pos;
pos = sb->GetScrollPos();
switch ( nSBCode )
{
case SB_LINEUP:
pos -= 1;
break;
case SB_LINEDOWN:
pos += 1;
break;
case SB_PAGEUP:
pos -= 10;
break;
case SB_PAGEDOWN:
pos += 10;
break;
case SB_TOP:
pos = MIN_RANGE;
break;
case SB_BOTTOM:
pos = MAX_RANGE;
break;
case SB_THUMBPOSITION:
pos = nPos;
break;
default:
return;
}
if ( pos < MIN_RANGE )
pos = MIN_RANGE;
else if ( pos > MAX_RANGE )
pos = MAX_RANGE;
sb->SetScrollPos( pos, TRUE );
}
SB_LINEUP和SB_LINEDOWN的不同常數值在CWnd::OnHScroll函數文檔中有介紹。上面的代碼首先使用GetScrollPos函數來恢復滾動條的當前位置。然後使用開關語句來確定用戶對滾動條的操作。SB_LINEUP 和SB_LINEDOWN常數值意味著垂直方向,但也可用於水平方向表示左右移動。SB_PAGEUP和SB_PAGEDOWN是用在用戶單擊滾動條時。SB_TOP和SB_BOTTOM用於當用戶移動滾動條小方塊到滾動條的頂部和底部。SB_THUMBPOSITION用於當用戶拖動小方塊到指定位置時。代碼會自動調整位置,然後確保它在設置其新位置時仍然在范圍內。一旦設置了滾動條,小方塊就會移動到適當的位置。
垂直滾動條的處理也是類似的,只是要用OnVScroll函數中的SBS_VERT樣式。
理解消息映射
消息映射結構只能用於MFC。掌握它和如何在你的代碼中應用它是很重要的。
可能純C++使用者會對消息映射產生疑問: 為什麼Microsoft不用虛擬函數來替代消息映射?虛擬函數是MFC中處理消息映射的標准C++方式,所以使用宏DECLARE_MESSAGE_MAP和BEGIN_MESSAGE_MAP可能有些怪異。
MFC使用消息映射來解決虛擬函數的基本問題。參見MFC幫助文件中的CWnd類。它包含200多個成員函數,所有的成員函數當不使用消息映射時都是虛擬的。現在來看一下所有CWnd類的子類。MFC中大約有近30個類是以CWnd為基類的。這包括所有的可見控制如按鈕、靜態標簽和列表。現在想象一下,MFC使用虛擬函數,並且你建立一應用程序包含有20個控制。CWnd中的200個虛擬函數中的每個都需要自己的虛擬函數表,並且一個控制的每個例程都應有一組200個虛擬函數與之關聯。則程序可能就有近4,000個虛擬函數表在內存中,這對內存有限的機器來說是個大問題。因為其中的大部分是不用的。
消息映射復制了虛擬函數表的操作,但是它是基於需要的基礎之上的。當你在消息映射中建立一個入口時,你是在對系統說,“當你看見一個特殊的消息時,請調用指定的函數”。只有這些函數實際上被重載到消息映射中,著就節省了內存和CPU的負擔。
當你用DECLARE_MESSAGE_MAP和BEGIN_MESSAGE_MAP說明消息映射時,系統會通過你的消息映射選擇所有的消息。如果消息映射處理了給定的消息,則你的函數會被調用,卸車也就停留在此。但是,如果你的消息映射中不包含某個消息的入口,則系統會把該消息發送第二個BEGIN_MESSAGE_MAP指定的類。那個類可能會也可能不會處理它,如此重復。最後,如果沒有消息映射處理一給定的消息,該消息會到由一缺省的處理函數來處理。
結論
本講中所介紹的所有消息映射處理概念可適用於Windows NT中所有的控制和窗口。在大部分情況下,你可以使用ClassWizard來安裝消息映射的入口,它將在後面的有關ClassWizard、AppWizard和資源編輯器一文中介紹。