1. 程式人生 > >MFC中CWnd類及其派生類對話方塊、訊息處理、視窗操作

MFC中CWnd類及其派生類對話方塊、訊息處理、視窗操作

CWnd類

我們在螢幕上看到的所有物件都和視窗有關,它們或者派生於CWnd,屬繼承關係,如對話方塊、工具欄、狀態列、子控制元件;或者被CWnd合成,屬服務員與服務物件關係,如圖示、選單、顯示裝置。

CWnd類封裝的視窗操作主要包含視窗的建立和銷燬、操作視窗風格、操作視窗狀態、視窗子類化、獲取指定視窗等。

當然,CWnd還實現了其他功能:

1、繪製視窗

GetDC()//取得客戶區顯示裝置上下文

GetWindowsDC()//取得整個視窗的顯示裝置上下文

ReleaseDC()

BeginPaint()

EndPaint()

PrintClient()

RedrawWindow()//重繪客戶區的某區域

2、操作視窗子控制元件

GetDlgItem():取得(臨時的)控制元件物件指標

SetDlgItemText()和GetDlgItemText():設定、取得控制元件標題

SubclassDlgItem():將控制元件控制代碼與相應類相關聯

DlgDirList()和DlgDirListComboBox():以檔案列表或目錄列表填充(組合框)列表框

CheckDlgButton()和CheckRadioButton():設定複選框(單選按鈕)狀態。

GetNextDlgTabItem():取得下一個WS_TABSTOP風格控制元件

3、視窗定時器

SetTimer():設定定時器

KillTimer():銷燬定時器

4、視窗訊息的相關函式

GetCurrentMessage():取得當前被處理的訊息

PreTranslateMessage():可過載的虛擬函式。被UI執行緒的訊息迴圈呼叫,可以過濾視窗收到的訊息,過濾出的訊息得以分發

SendMessage():向本視窗傳送訊息。不通過訊息迴圈,直接呼叫視窗函式處理訊息。視窗函式執行完畢,該函式才返回

PostMessage():向本視窗寄送訊息。將訊息放入訊息佇列,立即返回。

Default():為所有的視窗訊息提供預設處理。對無需處理的訊息或希望預設處理的訊息,可以使用Default()

5、預設的訊息處理函式

在實際的程式設計中,很少呼叫Default(),因為針對大部分訊息,CWnd類已經定義了相應得處理函式,封裝了Default()的呼叫。同時,有一些特殊的訊息,如WM_SYSCOLORCHANGE,僅執行系統級的處理是不夠的,框架必須針對訊息完成一些例行的操作。

////////////////////////////////////////////////

、CFrameWnd類

在編寫文件/檢視結構的應用程式時,CFrameWnd作為主視窗管理檢視和文件物件。檢視物件和控制條都成為CFrameWnd的子視窗,它們分享客戶區,其位置被CFrameWnd有效的安排。

CFrameWnd的建立

Creage()和LoadFrame()。前者主要建立視窗,而後者先組織引數,再呼叫前者。

LoadFrame()的形參簡潔,在建立視窗的同時,完成許多主窗體的初始化工作。

2、管理檢視物件

檢視無非是主框架視窗的一個ID為AFX_IDW_PANE_FIRST,帶有邊框的子視窗,由CFrameWnd封裝並建立的。成員函式CFrameWnd::OnCreateClient()用於建立檢視視窗,它是在該類的WM_CREATE訊息處理函式中被呼叫的。一個主視窗可能包含多個檢視,它們或者是通過CSplitterWnd在客戶區拆分建立的,或者直接在客戶區以子視窗形式建立。

在主視窗建立後,檢視也建立了,一般要呼叫CFrameWnd::InitialUpdateFrame()進行初始化,該函式設定第一個檢視ID為AFX_IDW_PANE_FIRST。

3、管理控制條

主視窗有豐富的控制條,它們都是由CControlBar派生的。

CFrameWnd封裝了對這些的操作,如:

EnableDockinng();
DockControlBar();
FloatControlBar();
ShowControlBar();
SaveBarState();
LoadBarState();
GetDockState();
SetDockState();
SetMessageText();
RecalcLayout()

4、傳送命令訊息

命令訊息是指選單、工具欄、加速鍵及命令按鈕向其所在視窗傳送的WM_COMMAND訊息。主框架視窗通常包含應用程式的系統主選單和工具欄,而加速鍵往往在LoadFrame()中裝入主視窗,它們向主視窗傳送命令訊息。

處WM_COMMAND外,其他以WM_開頭的訊息叫視窗訊息,視窗訊息與某一視窗緊密相關,應該由接收訊息的視窗處理。而命令訊息往往與具體的視窗無關。

CCmdTarget類定義了一個OnCmdMsg()虛擬函式,用於處理命令訊息,派生類可以過載它。CFrameWnd就是通過過載該函式實現命令訊息的分發:首先將命令訊息傳給活動檢視,如果檢視沒有處理,它將被傳送給關聯的文件物件,檢視或者文件都沒有處理該訊息時,即它們沒有建立該訊息的訊息對映,則由主視窗本身處理,若主視窗沒有處理,最後嘗試應用類,如果還是沒有處理,該訊息就由預設過程處理。

注意:主視窗直接呼叫檢視、應用類的OnCmdMsg()虛擬函式處理命令訊息,並沒有通過SendMessage()或PostMessage()將訊息轉發。OnCmdMsg()僅在類中搜索訊息對映表,查詢該命令的處理函式。找不到則返回。所以檢視類只有通過訊息對映,才能處理主視窗轉發的命令訊息。如果使用CView::WindowProc()捕捉該類訊息,會一無所獲。

5、必要的訊息處理

為了管理控制條和檢視,CFrameWnd為幾個視窗訊息建立了訊息對映,專門進行處理。

OnInitMenuPopup();
OnEnterIdle();
OnMenuSelect();
OnToolTipText();
OnUpdateKeyIndicator();
OnUpdateControlBarMenu();
OnSize();
OnHScroll();
OnVScroll();
OnClose();

////////////////////////////////////////////////////////////

CView類

1、關聯文件物件

一個檢視物件只關聯一個文件物件,但一個文件物件可以關聯多個檢視,每個檢視物件以不同的形式表示文件資料。

在文件/檢視框架程式中,文件物件總是在檢視之前建立,而在檢視的WM_CREATE訊息處理函式中,建立了它與文件物件的關聯,m_pDocument->AddView(this),將當前檢視加入文件物件的檢視列表中,因為一個文件可關聯多個檢視。同時,檢視類定義了成員函式GetDocument(),返回文件物件的指標。檢視總是在文件物件之前銷燬,在檢視解構函式中,與文件物件接觸關聯,m_pDocument->RemoveView(this)。

2、檢視的繪製

視窗繪製工作總是在WM_PAINT訊息處理中進行的,視窗需要繪製時,它會收到系統發來的WM_PAINT訊息。繪製過程,我們需要準備顯示裝置控制代碼,最後要釋放控制代碼。但是我們無需載入WM_PAINT訊息處理函式OnPaint(),我們只需要在OnDraw()繪製,並且OnDraw()準備了一個顯示裝置,最後也無需釋放,這些都由OnPaint()為我們準備的。

OnDraw()在檢視基類CView中定義為純虛擬函式:virtual void OnDraw(CDC* pDC) = 0;

所以CView是抽象基類,不能例項化,而派生類必須過載OnDraw()。

3、虛擬函式virtual void OnUpdate(CView* pSender, LPARAM, CObject*)

當文件資料發生變化時,文件物件呼叫CDocument::UpdateAllView()通知所有檢視,檢視的OnUpdate()成員被呼叫。所以,過載的OnUpdate()應該能夠根據需要,將文件資料的變化反映在檢視上。而CView::OnUpdate()只是簡單的使客戶區無效,導致客戶區重畫。

4、虛擬函式virtual void OnInitialUpdate()

在初始建立、呼叫OnCreate()之後,或者在File|New、File|Open命令之後被框架呼叫。基類CView::OnInitalUpdate()只是簡單的呼叫OnUpdate(),可以過載它完成初始化工作,但注意,它可能被多次呼叫。

5、虛擬函式virtual void CalcWindowRect(LPRECT lpClientRect, UNIT nAdjustType)

每當主框架視窗的客戶區尺寸發生變化,或者控制條的位置發生變化,需要重新排列客戶區時,呼叫該函式,根據檢視客戶區尺寸計算檢視視窗的尺寸。

我們知道,排列主視窗客戶區是由CFrameWnd::RecalcLayout()完成的。顯然,檢視的CalcWindowRect()函式也是由它觸發呼叫的。主視窗的客戶區尺寸減掉所有控制條佔用的都分,剩下的區域分給檢視,這部分割槽域作為實參傳入CalcWindowRect()。在CalcWindowRect()函式內,需要計算檢視視窗的尺寸。

6、虛擬函式virtual void PostNcDestroy()

在檢視視窗關閉時呼叫的成員函式,它與CFrameWnd::PostNcDestroy完成相同的功能,即刪除檢視物件。

void CView::PostNcDestroy()

{delete this;}

這樣,可以不必關心檢視的釋放工作,即使它在堆中構造。

7、虛擬函式virtual BOOL OnCmdMsg(UINT, int, void*, AFX_CMDHANDLERINFO*)

Cview::OnCmdMsg首先查詢自身的命令訊息對映,若本身沒有處理該訊息,將機會留給與其關聯的文件物件。

8、虛擬函式virtual void OnActivateView(BOOL, CView*, CView*)

當檢視被啟用為活動檢視,或由活動轉為非活動時,呼叫該函式通知檢視。CView::OnActivateView只是實現設定該檢視為焦點。

//////////////////////////////////////////////////////

CDialog類

對話方塊時通過對話方塊模版建立起來的,只需要一個以模版為實參的建立命令,如CDialog::Create,就可以完成對話方塊視窗及其子控制元件的建立工作,所有建立細節都由對話方塊模版來指示。

模態對話方塊的訊息迴圈

當呼叫對話方塊的Domodal()成員後,就建立了模態對話方塊。它可以禁止它所屬的主視窗,以及該主視窗的子視窗(除了重疊視窗和普通彈出視窗)。

1、模態對話方塊的建立和模式迴圈

其實“模態”不是對話方塊的專利,模態特性封裝與CWnd中,如果採取與模態對話方塊相同的建立方法,普通視窗也可以使模態的。這個方法就是在建立窗體後,呼叫CWnd::RunModalLoop()模式迴圈函式。它與CWnd::Run()相似,也是一個訊息迴圈泵,而且CWnd::RunModalLoop()得訊息處理還要複雜一些。

CDialog::DoModal()函式的操作過程:首先AfxGetResourceHandle()和LoadResource(hInst,hResource)裝入對話方塊資源,在建立模版對話方塊之前,::EnableWindow(hWndParent, FALSE)禁止父視窗的滑鼠和鍵盤輸入,禁止父視窗也間接的禁止父視窗的下屬視窗,但不包括下屬的重疊視窗和普通彈出視窗。CreateDlgIndirect(lpDialog Template, CWnd::FromHandl(hWndParent), hInst))通過資源模版建立對話方塊及其子控制元件,建立成功後,進入模式迴圈RunModalLoop(),當用戶選擇IDOK和IDCANCEL時,模式迴圈退出,對話方塊將被銷燬。::EnableWindow(hWndParent, TRUE)恢復父視窗的工作狀態,間接地恢復其兄弟視窗。SetActiveWindows(hWndParent)啟用父視窗,DestroyWindow()銷燬該對話方塊。

RunModalLoop()實現的訊息迴圈:這個訊息迴圈完成UI執行緒訊息迴圈(由CWinThread::Run封裝)的全部功能,同時為處理模態視窗的特殊訊息,添加了必要的處理程式碼。模態視窗建立後,就進入這個迴圈,其中的訊息迴圈泵暫時代替了UI執行緒的訊息迴圈泵,為所有視窗提取並分發資訊。但所有被禁止的視窗無法接收滑鼠和鍵盤的訊息,除非使用了PostMessage()命令。

RunModalLoop()的實現過程:RunModalLoop(DWORD dwFlags)形參可以使地下三個的組合:MLF_NOIDLEMSG(訊息佇列空閒時,不傳送WM_ENTERIDLE訊息給當前主視窗)

MLF_NOKICKIDLE(訊息佇列空閒時,不傳送WM_KICKIDLE訊息給當前模態對話方塊)

MLF_SHOWONIDLE(訊息佇列空閒時,重新整理顯示當前對話方塊(僅一次))

int CWnd::RunModalLoop(DWORD dwFlags)
{
ASSERT(::IsWindow(m_hWnd)); //視窗必須已經建立且不在模式狀態

//m_nFlags值為WF_MODALLOOP標誌進入模態

ASSERT(!(m_nFlags & WF_MODALLOOP));
// 以下變數用於Idle處理
BOOL bIdle = TRUE;
LONG lIdleCount = 0;
BOOL bShowIdle = (dwFlags & MLF_SHOWONIDLE) &&
   !(GetStyle() & WS_VISIBLE);
HWND hWndParent = ::GetParent(m_hWnd);
m_nFlags |= (WF_MODALLOOP|WF_CONTINUEMODAL);
MSG* pMsg = &AfxGetThread()->m_msgCur;//取得儲存當前訊息的緩衝
//獲取和派發訊息直到模式狀態結束
for (;;)
{
   ASSERT(ContinueModal());
   //第一階段,判斷是否可以進行Idle處理
   while (bIdle &&!::PeekMessage(pMsg, NULL, NULL, NULL, PM_NOREMOVE))
   {
    ASSERT(ContinueModal());
    //必要的話,當Idle時顯示對話方塊視窗
    if (bShowIdle)
    {
     ShowWindow(SW_SHOWNORMAL);
     UpdateWindow();
     bShowIdle = FALSE;
    }
    // 進行Idle處理
    //必要的話傳送WM_ENTERIDLE訊息給父視窗
    if (!(dwFlags & MLF_NOIDLEMSG) &&hWndParent != NULL && lIdleCount == 0)
    {
     ::SendMessage(hWndParent, WM_ENTERIDLE,
      MSGF_DIALOGBOX, (LPARAM)m_hWnd);
    }
    //必要的話傳送WM_KICKIDLE訊息給父視窗
    if ((dwFlags & MLF_NOKICKIDLE) ||
     !SendMessage(WM_KICKIDLE, MSGF_DIALOGBOX, lIdleCount++))
    {
     //終止Idle處理
     bIdle = FALSE;
    }
   }
   //第二階段,提取並分發訊息

   do
   {
    ASSERT(ContinueModal());
    // 若是WM_QUIT訊息,則傳送該訊息到訊息佇列,返回;否則傳送訊息。
    if (!AfxGetThread()->PumpMessage())
    {
     AfxPostQuitMessage(0);
     return -1;
    }
    //收到特殊訊息,是否重新整理顯示該對話方塊視窗
    if (bShowIdle &&
     (pMsg->message == 0x118 || pMsg->message == WM_SYSKEYDOWN))
    {
     ShowWindow(SW_SHOWNORMAL);
     UpdateWindow();
     bShowIdle = FALSE;
    }
    if (!ContinueModal())//可能是關閉當前對話方塊訊息,判斷是否結束模式迴圈
     goto ExitModal;
    //在派發了“正常 ”訊息後,重新開始Idle處理
    if (AfxGetThread()->IsIdleMessage(pMsg))
    {
     bIdle = TRUE;
     lIdleCount = 0;
    }
   } while (::PeekMessage(pMsg, NULL, NULL, NULL, PM_NOREMOVE));
}
ExitModal://使用者已關閉對話方塊,結束模式迴圈。
m_nFlags &= ~(WF_MODALLOOP|WF_CONTINUEMODAL);
return m_nModalResult;
}

RunModalLoop()與Run()不同的是,RunModalLoop()既可以向父視窗傳送WM_ENTERIDLE訊息,也可以向當前視窗傳送與空閒訊息等同的WM_KICKIDLE訊息,使對話方塊有能力在空閒時完成一定得操作。同時允許重新整理顯示對話方塊。但注意CWinThread::OnIdle()在這裡不被呼叫。

在傳送WM_KICKIDLE訊息時,由於它在訊息泵中不被分發處理,所有隻能是用SendMessage()而不是PostMessage()傳送該訊息。

2、結束模式迴圈

只要在對話方塊中呼叫CDialog::EndDialog()就可以結束模式迴圈。結束後必須呼叫DestroyWindow()銷燬對話方塊(DoModal()退出前已經完成)。用CDialog::Create()建立非模態,則必須自己呼叫EndDialog()和DestroyWindow()。

3、建立普通的模態對話方塊

(1)、呼叫EnableWindow()禁止程式主視窗。

(2)、使用CWnd::Create()等建立命令,建立該視窗。可以使彈出視窗,也可以使重疊視窗。

(3)、呼叫模式迴圈函式RunModalLoop(DWORD dwFlags),根據實際需要設定實參,如果需要空閒處理,還須手工新增訊息對映。

(4)、當關閉視窗時,呼叫EndModalLoop(int nResult),根據實際需要結束程式碼。

(5)、啟用主視窗,呼叫DestroyWindow()摧毀當前模態視窗。一定要確保視窗銷燬前已經結束了模式迴圈。

//////////////////////////////////////////////////////////////////

對話方塊的命令訊息路由

如果一個對話方塊(無論是彈出和重疊,不分模態與非模態)的父視窗是主框架或檢視,該對話方塊的選單命令就可以在對個物件中處理,它的處理路由是:對話方塊->檢視->文件物件->主框架->應用類。一旦命令訊息被處理,將不再繼續傳遞。

首先搜尋該物件的訊息對映,若該物件沒有處理這個命令訊息,則若命令ID在[0x8000,0xF000]範圍內,則向下傳遞,否則終止處理。(命令ID在[0x8000,0xF000]:全域性命令,包括選單、加速鍵、系統命令等;命令範圍在[0xF000,0xFFFF]:Windows系統命令(不被傳遞))。若是命令ID在[0x8000,0xF000]範圍內,則傳遞給父視窗,有父視窗負責命令路由。如果父視窗也沒有處理,則將它交給應用類,在父視窗不是主框架視窗時,傳給應用類是有意義的。

如果父視窗不是主框架,應用類得到最後的處理機會;如果父視窗是主框架,最後應用類(UI執行緒)的程式碼是多餘的。若父視窗是檢視,而檢視是主框架的子視窗,而對話方塊時彈出或重疊視窗,所以不能被檢視控制,而最終由主框架接管。

疑問:為什麼說“對話方塊的父視窗是檢視,而檢視是主框架的子視窗,而對話方塊時彈出或重疊視窗,所以不能被檢視控制,而最終由主框架接管。”?疑問中……

////////////////////////////////////////////////////////////////////

1、檢索視窗

1、1根據標題和視窗類查詢視窗

CWnd類的靜態函式FindWindow()或者同名的API函式,只要是重疊或彈出視窗,都可以用該函式查詢視窗。

HWND FindWindow(LPCTSTR lpClassName, //目標視窗的視窗類名稱

LPCTSTR lpWindowName//目標視窗的視窗標題名稱);

1、2根據視窗的相對位置或所屬關係查詢視窗

API函式GetWindow()可以用於列舉所有視窗。

HWND GetGetWindow(HWND hWnd,//作為參照物的視窗控制代碼

UINT nCmd//目標視窗與參照視窗的位置關係)

這樣,將FindWindow()與GetWindow()結合使用,前者查到符合條件的Z-Order順序最高的視窗,後者以這個視窗為起點,反覆列舉當前所得控制代碼的下一個視窗,就可以得到所有符合條件的視窗了。

/////////////////////////////////////////////////////////////

CWnd類封裝了兩個函式:GetClientRect()和GetWindowRect(),分別用於返回視窗的螢幕座標和客戶區座標的矩形區域。ScreenToClient()和ClientToScreen(),用於螢幕座標與客戶區座標之間的轉換。以上的四個函式都是通過封裝同名API實現。

對於彈出視窗和重疊視窗,GetClientRect()和GetWindowRect()取得的rect尺寸是不相等的,因為客戶區不包括標題欄、邊框等區域;如果是子視窗,則取得的rect尺寸相等,因為子視窗一般只有客戶區。

//////////////////////////////////////////////////////////

一、windows視窗之間的關係包括所有與被所有關係,父子關係等。

作業系統為每個視窗例項都分配一個記憶體空間,該空間被稱為視窗例項的資訊結構。該結構包含了視窗例項的所有資訊,其中有四個視窗控制代碼:1、本視窗例項的Z_Order順序最高的子視窗控制代碼;2、本視窗例項的下一個兄弟視窗的控制代碼(子視窗之間稱為兄弟視窗);3、本視窗例項的父視窗控制代碼;4、本視窗例項的所有者視窗控制代碼。

二、桌面視窗(第一層視窗)

windows初始化時,首先建立一個桌面視窗,其他所有視窗都顯示在其上。::GetDesktopWindow()可以取得桌面視窗的控制代碼。桌面視窗位於系統視窗層次的最上層。

三、頂級視窗(第二層視窗)

它沒有被設定為WS_CHILD風格的視窗,但它是桌面的子視窗。雖然頂級視窗之間存在兄弟關係,但它們之間可以建立一種所有與被所有的關係(所有者視窗和受控視窗)。受控視窗位於所有者視窗前段,即它的Z_Order順序可以被所有者視窗被啟用而提高。所有者視窗最小化時,受控視窗被隱藏。當使用CreateWindow()和CreateWindowEx()建立一個頂級視窗時,引數hWndParent決定所有者視窗,若hWndParent為子視窗,則由系統搜尋該子視窗的上級視窗,直到找到最近的頂級視窗,作為被建立視窗的所有者。

四、子視窗(第三層及其以下視窗)

子視窗以同樣的方式和父視窗連線。決定頂級視窗之間的Z_Order順序的規則,同樣適合子視窗之間,即由擴充套件風格WS_EX_TOPMOST和視窗啟用的先後順序共同決定。

五、重疊視窗與彈出視窗的區別

它們都是頂級視窗。前者總有標題欄和邊框,並且總是自動設定WS_CLIPSIBLINGS風格。呼叫CreateWindow()和CreateWindowEx()建立重疊視窗時,可以指定預設的視窗尺寸引數,即CW_USEDEFAULT,由系統設定視窗的初始尺寸。彈出視窗同樣自動設定WS_CLIPSIBLINGS風格,但其他風格必須專門指定,不可以使用CW_USEDEFAULT作為初始尺寸。

文件資訊