深入剖析MFC中Windows訊息機制

來源:互聯網
上載者:User

近來學習自繪控制項的過程中,發現windows訊息牽涉到了很多方面,如果不學好,估計自繪這塊很難走下去.所以,看了一些文章,覺得好就轉載了.

轉載內容如下

本人對Windows系統、MFC談不上有深入的瞭解,但對MFC本身封裝API的機制很有興趣,特別是讀了候老師的《深入淺出MFC》後,感覺到Visual C++的Application FrameWork十分精製。在以前,我對SDI結構處理訊息有一定的認識,但對於強制回應對話方塊的訊息機制不瞭解,讀了《深入》一書也沒能得到解決,近日,通過在網友的協助和查閱MSDN,自認為已經瞭解。一時興起,寫下這些文字,沒有其它目的,只是希望讓後來者少走彎路,也希望和我一樣喜歡“鑽牛角尖”的人共同討論、學習。如果你是牛人,那麼你現在要謹慎考慮有沒有充足的時間讀這些幼稚文字。

  本文:

  Windows程式和DOS程式的主要不同點之一是:Windows程式是以事件為驅動、訊息機製為基礎。如何理解?

  舉了例子,當你CLICK Windows “開始”BUTTON時,為什麼就會彈出一個菜單呢?

  當你單擊滑鼠左鍵時,作業系統中與MOUSE相關的驅動程式在第一時間內得到這個訊號[LBUTTONDOWN],然後它通知作業系統―――“嗨,滑鼠左鍵被單擊了!”,作業系統得到這一訊號後,馬上要判斷――使用者單擊滑鼠左鍵,這是針對哪個視窗呢?如何判斷?這很簡單!目前狀態中,具有焦點的視窗[或控制項]就是了[這裡當然是“開始”BUTTON了]。然後作業系統馬上向這個視窗發送一條訊息到這個視窗所在進程的訊息佇列,訊息內容應是訊息本身的代號、附加參數、視窗控制代碼…等等了。那麼,只有作業系統才有資格發送訊息至某一視窗的訊息佇列嗎?不然,其它程式也有資格。你可以在你的程式中調用:SendMessage
、PostMessage。這樣,被單擊的視窗得到了一條由作業系統發送的包含CLICK的訊息,作業系統已經暫時不再管視窗的任何事,因為它還要忙於處理其它事務。你的程式得到一條訊息後如何做呢?Windows對於你在“開始”BUTTON上的單擊事件做出如下反映:彈出一菜單。可是,得到訊息到做出反映這一過程是如何?的呢?這就是本文討論的主要內容[當然只是針對MFC了]。

  我首先簡要談一下SDI,然後會花更多文字描述強制回應對話方塊。

  對於SDI視窗,你的應用程式類的InitInstance()大約如下:

BOOL CEx06aApp::InitInstance()
{
  ……………
 CSingleDocTemplate* pDocTemplate;
 pDocTemplate = new CSingleDocTemplate(
  IDR_MAINFRAME,
  RUNTIME_CLASS(CEx06aDoc),
  RUNTIME_CLASS(CMainFrame), // main SDI frame window
  RUNTIME_CLASS(CEx06aView));
 AddDocTemplate(pDocTemplate);
 CCommandLineInfo cmdInfo;
 ParseCommandLine(cmdInfo);
 if (!ProcessShellCommand(cmdInfo))
  return FALSE;
 m_pMainWnd-> ShowWindow(SW_SHOW);
 m_pMainWnd-> UpdateWindow();
 return TRUE;
}

  完成一些如動態產生相關文檔、視,顯示主架構視窗、處理參數行資訊等工作。這些都是顯示在你工程中的“明碼”。我們現在把斷點設定到return TRUE; 一句,跟入MFC源碼中,看看到底MFC內部做了什麼。

  程式進入SRC\WinMain.cpp,下一個大動作應是:

nReturnCode = pThread-> Run();

  注意了,重點來了。F11進入

int CWinApp::Run()
{
 if (m_pMainWnd == NULL & & AfxOleGetUserCtrl())
 {
  // Not launched /Embedding or /Automation, but has no main window!
  TRACE0(" Warning: m_pMainWnd is NULL in CWinApp::Run - quitting application.\n" );
  AfxPostQuitMessage(0);
 }
 return CWinThread::Run();
}

  再次F11進入:

int CWinThread::Run()
{
 ASSERT_VALID(this);

 // for tracking the idle time state
 BOOL bIdle = TRUE;
 LONG lIdleCount = 0;

 // acquire and dispatch messages until a WM_QUIT message is received.
 for (; ; )
 {
  // phase1: check to see if we can do idle work
  while (bIdle & & !::PeekMessage(& m_msgCur, NULL, NULL, NULL, PM_NOREMOVE))
  {
   // call OnIdle while in bIdle state
   if (!OnIdle(lIdleCount++))
   bIdle = FALSE; // assume " no idle" state
  }

  // phase2: pump messages while available
  do
  {
   // pump message, but quit on WM_QUIT
   if (!PumpMessage())
    return ExitInstance();

   // reset " no idle" state after pumping " normal" message
   if (IsIdleMessage(& m_msgCur))
   {
    bIdle = TRUE;
    lIdleCount = 0;
   }

  } while (::PeekMessage(& m_msgCur, NULL, NULL, NULL, PM_NOREMOVE));
 }

 ASSERT(FALSE); // not reachable
}

BOOL CWinThread::IsIdleMessage(MSG* pMsg)
{
 // Return FALSE if the message just dispatched should _not_
 // cause OnIdle to be run. Messages which do not usually
 // affect the state of the user interface and happen very
 // often are checked for.

 // redundant WM_MOUSEMOVE and WM_NCMOUSEMOVE
 if (pMsg-> message == WM_MOUSEMOVE || pMsg-> message == WM_NCMOUSEMOVE)
 {
  // mouse move at same position as last mouse move?
  if (m_ptCursorLast == pMsg-> pt & & pMsg-> message == m_nMsgLast)
   return FALSE;

  m_ptCursorLast = pMsg-> pt; // remember for next time
  m_nMsgLast = pMsg-> message;
  return TRUE;
 }

 // WM_PAINT and WM_SYSTIMER (caret blink)
 return pMsg-> message != WM_PAINT & & pMsg-> message != 0x0118;
}

  這是SDI處理訊息的中心機構,但請注意,它覺對不是核心!

  分析一下,在無限迴圈FOR內部又出現一個WHILE迴圈

while (bIdle & &
!::PeekMessage(& m_msgCur, NULL, NULL, NULL, PM_NOREMOVE))
{
 // call OnIdle while in bIdle state
 if (!OnIdle(lIdleCount++))
  bIdle = FALSE; // assume " no idle" state
}

  這段代碼是當你程式進程的訊息佇列中沒有訊息時,會調用OnIdle做一些後備工作,臨時對象在這裡被刪除。當然它是虛函數。其中的PeekMessage,是查看訊息佇列,如果有訊息返回TRUE,如果沒有訊息返回FALSE,這裡指定PM_NOREMOVE,是指查看過後不移走訊息佇列中剛剛被查看到的訊息,也就是說這裡的PeekMessage只起到一個檢測作用,顯然返回FALSE時[即沒有訊息],才會進入迴圈內部,執行OnIdle,當然了,你的OnIdle返回FLASE,會讓程式不再執行OnIdle。你可能要問:

 

訊息機制和繪圖機制是微軟Windows及其周邊其他產品和生俱來的,是Win  系列OS作為一個作業系統進行微機內部實現的二大支柱和特徵,訊息系統是Windows下一切應用程式間,包括Windows自身,進行互動和通訊的渠道,是Windows實現對運行在其下的任何應用程式進行控制及應用程式對Windows進行響應的解決手段,因此對Windows的編程,無論是在哪種  語言規範和IDE  下,都不可避免地要涉及到訊息處理,雖然有些程式設計語言如  VB  用事件驅動編程機制在很大程度上封裝了訊息的複雜性,但若要深入Win32編程,就必須學習Windows的訊息系統,正如遊戲編程要掌控Win的繪圖機制相同,而只要您一旦深韻了這二大支柱和基本,您就掌控了Win32編程的根本。。
              訊息的產生來源於系統事情(包括計時器事件)和使用者事情,Windows用訊息來調入和關閉(更有其他處理,如繪製一個視窗等)應用程式,一個典型表現是在關機操作中,Windows發一個關機的訊息給任何正在啟動並執行應用程式,告知他們退出記憶體,此時,應用程式用回應訊息的方法來響應OS,因此,訊息是應用程式和WinOS互動的手段..
            訊息的主體是應用程式之間和應用程式和  OS  之間,(這是通俗的說法,其實在一個應用程式的內部,各“視窗”組件之間也存在著訊息的流動,視窗組件和他們的父視窗和上層視窗之間當然也有訊息的傳遞過程(如" 命令傳遞" ,後面在跟蹤一個訊息的路徑中將會詳談),Windows內部即時資料流動的訊息數量是如此的寵大,程式實現之外的手工分析是一種很自不量力的事情)訊息的最終主體卻是視窗和視窗之間,視窗和OS之間  -  因為在MFC的技術規範裡,只有視窗進程才能發送和接收一個訊息並處理他,當然一些非介面視窗類別如文檔類也能處理一個訊息,訊息的最終歸宿是某個視窗類別的成員函數,也就是進入訊息處理函數被處理,或被某個非介面類也就是內部處理類如文檔類處理,系統中預設的視窗類別和使用者註冊的視窗類別都有進程,都能在記憶體中建立實在的視窗對象,視窗對象和視窗類別接收和處理(千萬注意:接收一個訊息和處理一個訊息是相差甚大的二個過程,後面將在討論重新導向一個訊息技術時將談到)發往他或由他主動發往別的視窗進程或OS的訊息,修改視窗進程幹涉視窗進程對訊息的處理過程(而不是接收過程,這個區分的周詳解釋請參見後面從"
注意訊息泵並不是個.." 起的文字)是可能的(視窗進程只是一段函數),但是假如這個視窗進程屬於別人,如系統的視窗類別,您將沒有源程式進行修改,但卻能夠用訊息重定的技術加以幹涉,比如使用者自訂的視窗類別,使用者完萬能夠自訂他的視窗進程,編寫自己的訊息泵,實現對訊息的重新導向,編寫使用者自己的訊息泵屬於Win32編程中重新導向一個訊息的七大技術之一。
            MFC中有七種技術能夠用來重新導向一個訊息,他們是:1,子分類2,超分類3,OnCmdMsg(),4,SetCapture5,編寫自己的訊息泵,6,SetWindowsHookEx(),人們常說的鉤子函數,便是其中之一.
          在談完訊息泵的概念後,我們將一步一步追蹤一個訊息在系統中的路徑,然後才能討論對他的重新導向。
          訊息泵並不是個視窗類別的視窗進程,雖然他們都是函數,同樣都對注入到這個視窗進程的訊息進行工作,而並不最終處理訊息本身(上面已說到原因),訊息泵是個通俗的說法,他只和訊息被發往視窗進程後的接收工作有關而不和處理過程有關(上面也已說到訊息的接收和處理是二不同過程),而視窗進程恰恰相反他只和處理有關不和接收有關下面開始詳述。。
      訊息泵被包含在  CWinApp  的成員函數Run()中..

 

Windows訊息機制

Windows作業系統最大的特點就是其圖形化的操作介面,其圖形化介面是建立在其訊息處理機制這個基礎之上的。如果不理解Windows訊息處理機制,肯定無法深入的理解Windows編程。可惜很多程式員對Windows訊息只是略有所聞,對其使用知之甚少,更不瞭解其內部實現原理,本文試著一步一步向大家披露我理解的Windows訊息機制。可以說,掌握了這一部分知識,就是掌握了Windows編程中的神兵利器,靈活運用它,將會極大的提高我們的編程能力。

 

一、          訊息概述

Windows表單是怎樣展現在螢幕上的呢?眾所周知,是通過API繪製實現的。Windows作業系統提供了一系列的API函數來實現介面的繪製功能,例如:

²            DrawText 繪製文字

²            DrawEdge 繪製邊框

²            DrawIcon 繪製表徵圖

²            BitBlt 繪製位元影像

²            Rectangle 繪製矩形

²            …

再複雜的程式介面都是通過這個函數來實現的。

那什麼時候調用這些函數呢?顯然我們需要一個控制中心,用來進行“發號施令”,我們還需要一個命令傳達機制,將命令即時的傳達到目的地。這個控制中心,就是一個動力源,就像一顆心臟,源源不斷地將血液送往各處。這個命令傳達機制就是Windows訊息機制,Windows訊息就好比是身體中的血液,它是命令傳達的使者。

Windows訊息控制中心一般是三層結構,其頂端就是Windows核心。Windows核心維護著一個訊息佇列,第二級控制中心從這個訊息佇列中擷取屬於自己管轄的訊息,後做出處理,有些訊息直接處理掉,有些還要發送給下一級表單(Window)或控制項(Control)。第二級控制中心一般是各Windows應用程式的Application對象。第三級控制中心就是Windows表單對象,每一個表單都有一個預設的表單過程,這個過程負責處理各種接收到的訊息。如所示:


(註:windows指windows作業系統;視窗:即windows視窗;表單:包括視窗,以及有控制代碼的控制項;control指控制項,控制項本身也可能是一個window,也可能不是;Application即應用程式,應用程式也可能不會用到Windows訊息機制,這裡我們專門討論有訊息迴圈的應用程式)

圖1包含了Windows機制的大部分內容,下面所講的所有內容實際上都是對張圖的解釋或擴充。

訊息是以固定的結構傳送給應用程式的,結構如下:

Public Type MSG

      hwnd As Long

      message As Long

      wParam As Long

      lParam As Long

      time As Long

      pt As POINTAPI

End Type

其中hwnd是表單的控制代碼,message是一個訊息常量,用來表示訊息的類型,wParam和lParam都是32位的附加資訊,具體表示什麼內容,要視訊息的類型而定,time是訊息發送的時間,pt是訊息發送時滑鼠所在的位置。

Windows作業系統中包括以下幾種訊息:

1、標準Windows訊息:

這種訊息以WM_打頭。

2、通知訊息

通知訊息是針對標準Windows控制項的訊息。這些控個包括:按鈕(Button)、組合框(ComboBox)、編輯框(TextBox)、列表框(ListBox)、ListView控制項、Treeview控制項、工具條(Toolbar)、菜單(Menu)等。每種訊息以不同的字串打頭。

3、自訂訊息

編程人員還可以自訂訊息。

 

二、                    關於Windows控制代碼

不是每個控制項都能接收訊息,轉寄訊息和繪製自身,只有具有控制代碼(handle)的控制項才能做到。有控制代碼的控制項本質上都是一個表單(window),它們可以獨立存在,可以作為其它控制項的容器,而沒有控制代碼的控制項,如Label,是不能獨立存在的,只能作為視窗控制項的子控制項,它不能繪製自身,只能依靠父表單將它繪製來。

控制代碼的本質是一個系統自動維護的32位的數值,在整個作業系統的任一時刻,這個數值是唯一的。但該控制代碼代表的表單釋放後,控制代碼也會被釋放,這個數值又可能被其它表單使用。也就是說,控制代碼的數值是動態,它本身只是一個唯一性標識,作業系統通過控制代碼來識別和尋找它所代表的對象。

然而,並非所有的控制代碼都是表單的控制代碼,Windows系統中還中很多其它類型的控制代碼,如畫布(hdc)控制代碼,畫筆控制代碼,畫刷控制代碼,應用程式控制代碼(hInstance)等。這種控制代碼是不能接收訊息的。但不管是哪種控制代碼,都是系統中對象的唯一標識。本文只討論表單控制代碼。

那為什麼控制代碼使視窗具有了如此獨特的特性呢?實際是都是由於訊息的原因。由於有了控制代碼,表單能夠接收訊息,也就知道了該什麼時候繪製自己,繪製子控制項,知道了滑鼠在什麼時候點擊了視窗的哪個部分,從而作出相應的處理。控制代碼就好像是一個人的身份證,有了它,你就可以從事各種社會活動;否則的話,你要麼是一個社會看不到的黑戶,要麼跟在別人後面,通過別人來證明你的存在。

 

三、                    訊息的傳送

1、從訊息佇列擷取訊息:

可以通過PeekMessage或GetMessage函數從Windows訊息佇列中擷取訊息。Windows儲存的訊息佇列是以線程(Thread)來分組的,也就是說每個線程都有自己的訊息佇列。

2、發送訊息

發送訊息到指定表單一般通過以下兩個函數完成:SendMessage和PostMessage。兩個函數的區別在於:PostMessage函數只是向線程訊息佇列中添加訊息,如果添加成功,則返回True,否則返回False,訊息是否被處理,或處理的結果,就不知道了。而SendMessage則有些不同,它並不是把訊息加入到隊列裡,而是直接翻譯訊息和調用訊息處理,直到訊息處理完成後才返回。所以,如果我們希望發送的訊息立即被執行,就應該調用SendMessage。

還有一點,就是SendMessage發送的訊息由於不會被加入到訊息佇列中,所以通過PeekMessage或GetMessage是不能擷取到由SendMessage發送的訊息。

另外,有些訊息用PostMessage不會成功,比如wm_settext。所以不是所有的訊息都能夠用PostMessage的。

還有一些其它的發送訊息API函數,如PostThreadMessage,SendMessageCallback,SendMessageTimeout,SendNotifyMessage等。

 

四、                    訊息迴圈與表單過程

訊息迴圈是應用程式能夠持續存在的根本原因。如果迴圈退出,則應用程式就結束了。

我們來看一看Delphi中封裝的訊息迴圈是怎樣的:

第一步:程式開始運行(Run)

  Application.Initialize;   //初始化

  Application.CreateForm(TForm1, Form1);  //建立主表單

  Application.Run;   //開始運行,準備進行訊息迴圈

如果不建立主表單,應用程式同樣可以存在和運行。

第二步:開始調用訊息迴圈(HandleMessage)

procedure TApplication.Run;

begin

  FRunning := True;

  try

      AddExitProc(DoneApplication);

      if FMainForm < > nil then

      begin

          case CmdShow of

              SW_SHOWMINNOACTIVE: FMainForm.FWindowState := wsMinimized;

              SW_SHOWMAXIMIZED: MainForm.WindowState := wsMaximized;

          end;

          if FShowMainForm then

              if FMainForm.FWindowState = wsMinimized then

                  Minimize else

                  FMainForm.Visible := True;

          Repeat   //註:迴圈開始

              try

                  HandleMessage;

              except

                  HandleException(Self);

              end;

          until Terminated;   //迴圈結束條件

      end;

  finally

      FRunning := False;

  end;

end;

第三步:訊息迴圈中對訊息的處理。

procedure TApplication.HandleMessage;

var

  Msg: TMsg;

begin

  if not ProcessMessage(Msg) then Idle(Msg);

end;

 

function TApplication.ProcessMessage(var Msg: TMsg): Boolean;

var

  Handled: Boolean;

begin

  Result := False;

  if PeekMessage(Msg, 0, 0, 0, PM_REMOVE) then

  begin

      Result := True;

      if Msg.Message < > WM_QUIT then

      begin

          Handled := False;

          if Assigned(FOnMessage) then FOnMessage(Msg, Handled);

          if not IsHintMsg(Msg) and not Handled and not IsMDIMsg(Msg) and

              not IsKeyMsg(Msg) and not IsDlgMsg(Msg) then

          begin

              TranslateMessage(Msg);

              DispatchMessage(Msg);

          end;

      end

      else

          FTerminate := True;

  end;

end;

 

表單過程實際上是一個回呼函數。所謂的回呼函數,實際上就是由Windows作業系統或外部程式調用的函數。回呼函數一般都有規定的參數格式,以地址方式傳遞給調用者。視窗過程中是Windows作業系統調用了,在一個視窗建立的時候,在分配表單控制代碼的時候就需要傳入回呼函數地址。那為什麼我們平時編程看不到這個回呼函數呢?這是由於我們的編程工具已經為我們產生了預設的表單過程,這個過程的要做的事情就是判斷不同的訊息類型,然後做出不同的處理。例如可以為鍵盤或滑鼠輸入建置事件等。

 

五、                    訊息與事件

事件本質上是對訊息的封裝,是IDE編程環境為了簡化編程而提供的有用的工具。這個封裝是在表單過程中實現的。每種IDE封裝了許多Windows的訊息,例如:

事件

訊息

OnActivate

WM_ACTIVATE

OnClick

WM_XBUTTONDOWN

OnCreate

WM_CREATE

OnDblClick

WM_XBUTTONDBLCLICK

OnKeyDown

WM_KEYDOWN

OnKeyPress

WM_CHAR

OnKeyUp

WIN_KEYUP

OnPaint

WM_PAINT

OnResize

WM_SIZE

OnTimer

WM_TIMER

瞭解了這一點後,我們完成可以封裝自己的事件。

通過上面的介紹,相信各位已經對Windows訊息機制有了一定的理解了。通過Windows訊息編程,我們不但可以實現很多常規功能,而且可以實現很多IDE類庫沒有提供的功能;另外,我們還可以通過訊息鉤子,對訊息進行截獲,改變其預設的處理函數,從而突破平台或軟體功能的限制,極大的擴充程式的功能;我們還可以修改預設的表單過程,按自己的要求來響應訊息;或者自訂訊息,實現程式之間的即時通訊等等。通過更加深入的學習,我們還會接觸到更多與Windows訊息機制相關其它Windows相對比較底層的知識,如果能夠這樣,驀然回首,你會發現自己原來離“高手”不遠了。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.