wxWidgets源碼分析(4) - 消息處理過程
目錄
- 消息處理過程
- 消息如何到達wxWidgets
- Win32消息與wxWidgets消息的轉換
- 菜單消息處理
- 消息處理鏈(基於wxEvtHandler)
- 消息處理鏈(基於wxWindow)
- 總結
消息處理過程
消息如何到達wxWidgets
Windows程序有其自身運行的一套規律,::SendMessage
是MS提供的windows消息發送接口,用戶調用這個接口後會進入到MS系統庫程序,此接口指定了目標HWND和消息參數,Windows系統內部會查找指定HWND,然後通過gapfnScSendMessage
接口調用用戶的消息處理函數。
所以我們每次看到消息處理函數都是通過gapfnScSendMessage
Win32消息與wxWidgets消息的轉換
wxWidgets註冊窗口時同時指定了窗口處理函數wxWndProc
,當收到消息後系統會調用此函數來處理消息。
處理過程如下:
- 調用
wxFindWinFromHandle
根據當前消息指定的HWND來查找對應的wxWindow,如果沒有則需要與最近創建的一個窗口關聯起來。 - 接著調用窗口的
MSWWindowProc
方法來進行消息處理。
// Main window proc LRESULT WXDLLEXPORT APIENTRY _EXPORT wxWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { wxWindowMSW *wnd = wxFindWinFromHandle(hWnd); // 關聯窗口 if ( !wnd && gs_winBeingCreated ) { wxAssociateWinWithHandle(hWnd, gs_winBeingCreated); wnd = gs_winBeingCreated; gs_winBeingCreated = NULL; wnd->SetHWND((WXHWND)hWnd); } LRESULT rc; if ( wnd && wxGUIEventLoop::AllowProcessing(wnd) ) rc = wnd->MSWWindowProc(message, wParam, lParam); else rc = ::DefWindowProc(hWnd, message, wParam, lParam); return rc; }
MSWWindowProc
是在windows平臺下特有的虛函數,對於Frame類來說就是wxFrame::MSWWindowProc
,消息根據Message類型來執行不同的函數:
- WM_CLOSE: 退出操作,直接調用
wxFrame::Close
; - WM_SIZE: 調用
wxFrame::HandleSize
; - WM_COMMAND: 需要特殊處理,註釋中寫的很清楚,wxWidgets提供了一套自己的機制來進行父窗口和子窗口之間的消息調用,所以就不要再使用Win32提供的功能了。
- 如果自己沒有處理,則給父類
wxFrameBase::MSWWindowProc
處理;
WXLRESULT wxFrame::MSWWindowProc(WXUINT message, WXWPARAM wParam, WXLPARAM lParam) { WXLRESULT rc = 0; bool processed = false; switch ( message ) { case WM_CLOSE: processed = !Close(); break; case WM_SIZE: processed = HandleSize(LOWORD(lParam), HIWORD(lParam), wParam); break; case WM_COMMAND: { WORD id, cmd; WXHWND hwnd; UnpackCommand((WXWPARAM)wParam, (WXLPARAM)lParam, &id, &hwnd, &cmd); HandleCommand(id, cmd, (WXHWND)hwnd); processed = true; } break; } if ( !processed ) rc = wxFrameBase::MSWWindowProc(message, wParam, lParam); return rc; }
拿wxFrame::Close
函數舉例,wxFrame
直接使用父類的方法wxWindowBase::Close
,內容就是構造wxWidgets的消息類型,然後調用HandleWindowEvent
方法進行消息處理。
bool wxWindowBase::Close(bool force)
{
wxCloseEvent event(wxEVT_CLOSE_WINDOW, m_windowId);
event.SetEventObject(this);
event.SetCanVeto(!force);
// return false if window wasn‘t closed because the application vetoed the
// close event
return HandleWindowEvent(event) && !event.GetVeto();
}
對於WM_SIZE
,走的路要多一些,調用關系如下,最終由wxWindowMSW::HandleSize
處理,這裏會產生wxSizeEvent
消息,隨後將消息遞交給HandleWindowEvent
處理:
wxFrame::MSWWindowProc()
-> wxTopLevelWindowMSW::MSWWindowProc()
-> wxWindowMSW::MSWWindowProc()
-> wxWindowMSW::MSWHandleMessage()
-> wxWindowMSW::HandleSize()
bool wxWindowMSW::HandleSize(int WXUNUSED(w), int WXUNUSED(h), WXUINT wParam)
{
switch ( wParam )
{
case SIZE_RESTORED:
wxSizeEvent event(GetSize(), m_windowId);
event.SetEventObject(this);
processed = HandleWindowEvent(event);
}
}
也就是不管是哪個消息,最終還是轉換稱wxEvent消息,然後調用當前窗口的HandleWindowEvent
函數處理,要註意的是此時消息已經是wxWidgets內部的消息類型了。
菜單消息處理
接下來我們驗證菜單消息在wxWidgets中的處理,首先在wxWidgets工程中增加靜態消息映射表,並實現相應的代碼:
const long ID_MenuUser = wxNewId();
BEGIN_EVENT_TABLE(debugWXFrame,wxFrame)
EVT_MENU(ID_MenuUser, debugWXFrame::OnCheckMenu)
EVT_UPDATE_UI(ID_MenuUser, debugWXFrame::OnCheckMenuUI)
END_EVENT_TABLE()
void debugWXFrame::OnCheckMenu(wxCommandEvent& event) {}
void debugWXFrame::OnCheckMenuUI(wxUpdateUIEvent& event) {}
我們接著看下Command消息的處理,這個用的非常多,收到Command類型的消息後調用HandleCommand
處理,Frame類只處理工具欄、菜單和加速鍵命令,實現過程:
- 調用
FindItemInMenuBar
根據消息中指定的ID來查找對應的wxMenuItem
,這個函數的實現就是獲取到當前wxFrame
的MenuBar,然後循環查詢,菜單越多查詢的速度也就越慢; - 找到MenuItem後則調用
wxFrameBase::ProcessCommand(mitem)
繼續處理:
bool wxFrame::HandleCommand(WXWORD id, WXWORD cmd, WXHWND control)
{
#if wxUSE_MENUS
#if defined(WINCE_WITHOUT_COMMANDBAR)
if (GetToolBar() && GetToolBar()->FindById(id))
return GetToolBar()->MSWCommand(cmd, id);
#endif
// we only need to handle the menu and accelerator commands from the items
// of our menu bar, base wxWindow class already handles the rest
if ( !control && (cmd == 0 /* menu */ || cmd == 1 /* accel */) )
{
#if wxUSE_MENUS_NATIVE
if ( !wxCurrentPopupMenu )
#endif // wxUSE_MENUS_NATIVE
{
wxMenuItem * const mitem = FindItemInMenuBar((signed short)id);
if ( mitem )
return ProcessCommand(mitem);
}
}
#endif // wxUSE_MENUS
return wxFrameBase::HandleCommand(id, cmd, control);;
}
wxFrame
類本身沒有實現ProcessCommand
,所以將調用父類的方法wxFrameBase::ProcessCommand
,關鍵流程代碼部分調用wxMenu的SendEvent
函數繼續處理。
重點關註,這裏調用的是menu->SendEvent
,所以接下來的調用切換到wxMenu類中進行。
bool wxFrameBase::ProcessCommand(wxMenuItem *item)
{
...
wxMenu* const menu = item->GetMenu();
return menu->SendEvent(item->GetId(), checked);
}
wxMenu的SendEvent
實現是wxMenuBase::SendEvent
方法,此時我們位於wxMenu對象中,所以調用GetEventHandler()
獲得的是wxMenu的EvntHandler。
重點關註win和mb兩個變量,wxMenu首先使用自己的wxEvtHandler進行處理,然後檢查它是否關聯到了win或者menubar,如果有則它還增加了一個標記event.SetWillBeProcessedAgain()
,也就是命令需要被wen或者menubar處理。
win和mb兩個變量代表不同的菜單類型,mb是菜單條中的菜單,win是上下文菜單。
這裏我們調用的是mb->HandleWindowEvent(event)
;
bool wxMenuBase::SendEvent(int itemid, int checked)
{
wxCommandEvent event(wxEVT_MENU, itemid);
event.SetEventObject(this);
event.SetInt(checked);
wxWindow* const win = GetWindow();
wxMenuBar* const mb = GetMenuBar();
wxEvtHandler *handler = GetEventHandler();
if ( handler )
{
if ( win || mb )
event.SetWillBeProcessedAgain();
// 沒有想到調用這個函數的場景?
if ( handler->SafelyProcessEvent(event) )
return true;
}
// If this menu is part of the menu bar, process the event there: this will
// also propagate it upwards to the window containing the menu bar.
if ( mb )
return mb->HandleWindowEvent(event);
// Try the window the menu was popped up from.
if ( win )
return win->HandleWindowEvent(event);
// Not processed.
return false;
}
至此,我們將切換到wxMenuBar::HandleWindowEvent
,所有者為wxMenuBar
,wxMenuBar
繼承自wxWindow
類,它也是一個獨立的窗口,所以這次調用的函數是wxWindowBase::HandleWindowEvent
,調用過程如下:
GetEventHandler()
方法返回的就是自身,wxFrame
本身就是繼承自wxEvtHandler
;ProcessEvent
方法是由父類wxEvtHandler
提供的;
bool wxWindowBase::HandleWindowEvent(wxEvent& event) const
{
return GetEventHandler()->SafelyProcessEvent(event);
}
bool wxEvtHandler::SafelyProcessEvent(wxEvent& event)
{
return ProcessEvent(event);
}
接著調用ProcessEvent
,這個函數是通用的,只要是繼承自wxEvtHandler
都會調用到這裏,下面我們分兩種情況來說明情況:
通用的處理:
- 處理全局過濾器,
wxEvtHandler
內部創建了靜態變量ms_filterList
,用於保存wxEventFilter
列表,用戶可以通過調用靜態函數wxEvtHandler::AddFilter
來向系統中增加過濾器,具體可參考過濾器使用
章節; - 調用
TryBeforeAndHere
僅對本對象處理,調用此函數需要依賴一個標記ShouldProcessOnlyIn
,這個標記僅僅在DoTryChain
中會被設置,也就是只有進入了DoTryChain
函數才會有此標記; - 調用
ProcessEventLocally
執行本對象處理; - 調用
TryAfter
執行parent的處理;
第一種情況: 位於wxMenuBar
中的處理:
此時我們位於
wxMenuBar
中,此類繼承自wxEvtHandler
,所以這裏調用的實際是wxEvtHandler::ProcessEvent
,處理過程:
- 此時是不會有
ShouldProcessOnlyIn
標記,所以不會執行TryBeforeAndHere
;- 進入到
ProcessEventLocally
;由於wxMenuBar
對象中並沒有綁定此菜單的處理函數,所以ProcessEventLocally
是不會處理的;- 進入到
TryAfter
執行parent的處理;
第二種情況: 位於wxFrame
中的處理:
對於wxFrame的
ProcessEvent
流程也有同樣的效果,只不過會在ProcessEventLocally
中處理。
bool wxEvtHandler::ProcessEvent(wxEvent& event)
{
// 處理過濾器
if ( !event.WasProcessed() )
{
for ( wxEventFilter* f = ms_filterList; f; f = f->m_next )
{
int rc = f->FilterEvent(event);
if ( rc != wxEventFilter::Event_Skip )
{
return rc != wxEventFilter::Event_Ignore;
}
}
}
// 只有執行了 DoTryChain() 之後,ShouldProcessOnlyIn()方法才會返回true
// 具體可以參考 wxEventProcessInHandlerOnly 輔助類
if ( event.ShouldProcessOnlyIn(this) )
return TryBeforeAndHere(event);
// Try to process the event in this handler itself.
if ( ProcessEventLocally(event) )
{
return !event.GetSkipped();
}
if ( TryAfter(event) )
return true;
// No handler found anywhere, bail out.
return false;
}
還是分成兩種情況分別說明:
第一種情況: 位於wxMenuBar
中的處理:
wxEvtHandler::TryBeforeAndHere
會調用TryBefore
||TryHereOnly
,TryBefore
我們暫時忽略,重點是TryHereOnly
,在TryHereOnly
函數中,首先超找動態綁定表,然後查找靜態綁定表,如果表中存在處理函數則調用之,否則不會調用,對於本流程來將,wxMenubar
中並沒有綁定任何處理函數,所以TryHereOnly
返回false,進而TryBeforeAndHere
函數返回false,所以需要繼續調用DoTryChain
。
第二種情況: 位於wxFrame
中的處理:
對於wxFrame來說,本例中菜單的消息處理函數綁定在靜態綁定區,所以會在
if ( GetEventHashTable().HandleEvent(event, this) )
中處理掉,返回true。
bool wxEvtHandler::ProcessEventLocally(wxEvent& event)
{
return TryBeforeAndHere(event) || DoTryChain(event);
}
bool wxEvtHandler::TryBeforeAndHere(wxEvent& event)
{
return TryBefore(event) || TryHereOnly(event);
}
bool wxEvtHandler::TryHereOnly(wxEvent& event)
{
// Handle per-instance dynamic event tables first
if ( m_dynamicEvents && SearchDynamicEventTable(event) )
return true;
// Then static per-class event tables
if ( GetEventHashTable().HandleEvent(event, this) )
return true;
return false;
}
繼續進入入wxMenuBar
的DoTryChain
,這裏是通過設置EventHandlerChain來達到消息傳遞的目的,但是在wxWidgets系統中,wxWindow
繼承的過程中明確不使用這種方式進行消息傳遞,而是通過wxWindow
自身的父子關系來進行消息傳遞,所以對於wxMenuBar
來說,這個GetNextHandler
必定返回是空的,所以DoTryChain
返回false,進而wxMenuBar
的ProcessEventLocally
返回false。
bool wxEvtHandler::DoTryChain(wxEvent& event)
{
for ( wxEvtHandler *h = GetNextHandler(); h; h = h->GetNextHandler() )
{
wxEventProcessInHandlerOnly processInHandlerOnly(event, h);
if ( h->ProcessEvent(event) )
{
event.Skip(false);
return true;
}
if ( !event.ShouldProcessOnlyIn(h) )
{
event.Skip();
return true;
}
}
return false;
}
再回到前兩步,此時我們只能通過調用wxMenuBar
的TryAfter(event)
繼續消息傳遞,前文有描述wxMenuBar
繼承自wxWindows
,所以這裏調用的是wxWindowBase::TryAfter
,在下面的調用中,窗口只要可能正常接收消息,則會向上查找parent,然後調用父類的ProcessEvent
繼續處理。
在本例中,wxMenuBar
的parent是wxFrame
,所以會繼續調用wxFrame
的ProcessEvent
繼續處理
bool wxWindowBase::TryAfter(wxEvent& event)
{
if ( event.ShouldPropagate() )
{
if ( !(GetExtraStyle() & wxWS_EX_BLOCK_EVENTS) )
{
wxWindow *parent = GetParent();
if ( parent && !parent->IsBeingDeleted() )
{
wxPropagateOnce propagateOnce(event, this);
return parent->GetEventHandler()->ProcessEvent(event);
}
}
}
return wxEvtHandler::TryAfter(event);
}
wxFrame
的ProcessEvent
的調用順序與wxMenuBar
的相同,只不過wxFrame
會在ProcessEventLocally
方法中返回true,進而導致整個處理流程完成。
消息處理鏈(基於wxEvtHandler)
wxWidgets提供了一種手段,用戶可以將消息處理函數註入到wxEvtHandler類中,而不需要使用繼承方式,實現方法就是用戶自定義一個wxEventHandler類,然後調用wxEvtHandler::SetNextHandler()
將消息處理代碼加入到指定的wxEvtHandler
對象上,使用舉例:
下面的代碼用於將菜單處理放到獨立的wxEvtHandler
類中,通過wxEvtHandler::SetNextHandler
方法將此Handler對象鏈接到wxFrame
上:
註:必須使用wxEvtHandler::SetNextHandler
方法註入,不能直接調用SetNextHandler
,因為wxWindow
重載了這個方法,直接調用不會生效。
const long ID_MenuUser = wxNewId();
class CMyEvtHandler : public wxEvtHandler {
public:
bool ProcessEvent(wxEvent& event) {
if (event.GetEventType() == wxEVT_MENU && event.GetId() == ID_MenuUser) {
wxMessageBox("Menu processed in chain");
return true;
}
event.Skip();
return false;
}
};
debugWXFrame::debugWXFrame(wxWindow* parent,wxWindowID id)
{
...
Menu1->Append(ID_MenuUser, _("Chain Menu"));
wxEvtHandler::SetNextHandler(new CMyEvtHandler);
}
實際調用時,會進入到wxFram::DoTryChain
函數中,由於我們向wxFrame中增加了wxEvtHandler
,此時會取出關系鏈上的wxEvtHandler
逐個調用。
註意到使用這種方式調用時會預先設定只在當前對象中處理標記,通過wxEventProcessInHandlerOnly processInHandlerOnly(event, h);
實現,當processInHandlerOnly
銷毀後標記消失,作用範圍僅僅是在這個循環體內。
bool wxEvtHandler::DoTryChain(wxEvent& event)
{
for ( wxEvtHandler *h = GetNextHandler(); h; h = h->GetNextHandler() )
{
wxEventProcessInHandlerOnly processInHandlerOnly(event, h);
if ( h->ProcessEvent(event) )
{
event.Skip(false);
return true;
}
if ( !event.ShouldProcessOnlyIn(h) )
{
event.Skip();
return true;
}
}
return false;
}
消息處理鏈(基於wxWindow)
除了將消息處理類註入到當前wxEvtHandler
對象中,還有一個辦法就是調用wxWindow::PushEventHandler
將消息處理類註入到當前windows的棧中,兩種方式有區別:
- 註入到
wxEvtHandler
,wxWidgets會優先處理當前對象的wxEvtHandler
,然後檢查當前對象的wxEvtHandler
是否有鏈接其他的wxEvtHandler
,如果有則調用之; - 通過
wxWindow::PushEventHandler
註入的是改寫wxWindow類的當前消息處理對象,當其查找wxWindow對象的消息處理對象時,只調用最後插入的一個,所以,為了保證正常的消息能處理,我們必須在ProcessEvent()
方法中調用下一個wxEvtHandler的方法。
舉例:
const long ID_MenuUser_wxWinChain = wxNewId();
class CMyWinEvtHandler : public wxEvtHandler {
public:
bool ProcessEvent(wxEvent& event) {
if (event.GetEventType() == wxEVT_MENU && event.GetId() == ID_MenuUser_wxWinChain) {
wxMessageBox("Menu processed in chain, id="+wxString::Format("%d", event.GetId()));
return true;
}
if (GetNextHandler())
return GetNextHandler()->ProcessEvent(event);
return false;
}
};
debugWXFrame::debugWXFrame(wxWindow* parent,wxWindowID id)
{
...
Menu1->Append(ID_MenuUser_wxWinChain, _("Chain Menu"));
PushEventHandler(new CMyWinEvtHandler);
}
拿菜單處理舉例,在wxMenuBar
調用wxWindowBase::TryAfter
查找父類調用時,會直接調用父類的方法,對於我們這個例子來說,會直接調用CMyWinEvtHandler::ProcessEvent
方法,所以我們在實現ProcessEvent
必須註意,需要調用GetNextHandler()->ProcessEvent(event)
以保證其他消息的正常處理。
if ( !(GetExtraStyle() & wxWS_EX_BLOCK_EVENTS) )
{
wxWindow *parent = GetParent();
if ( parent && !parent->IsBeingDeleted() )
{
wxPropagateOnce propagateOnce(event, this);
return parent->GetEventHandler()->ProcessEvent(event);
}
}
註:在測試過程中發現總會有the last handler of the wxWindow stack should have this window as next handler
的提示,這個是wxWidgets庫本身代碼的Bug,窗口鏈不需要雙向鏈表,窗口本身的wxEvtHandler
不需要指向任何wxEvtHandler
,因為它就是最後一個。
總結
本節中主要描述了wxWidgets的消息處理過程。
wxWidgets源碼分析(4) - 消息處理過程