程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> ATL布幔之下的秘密(5)

ATL布幔之下的秘密(5)

編輯:關於VC++

介紹

很多人認為ATL只是用來編寫COM組件的,其實你也可以使用ATL 中的窗口類來創建基於窗口的應用程序。雖然你可以將基於MFC的程序轉換為ATL ,但是ATL中對於UI(譯注:用戶界面)組件的支持太少了。所以,這就要求你 需要自己編寫很多代碼。例如,在ATL中沒有文檔/視圖,所以在你想使用它的時 候就需要自己實現了。在本篇中,我們將要探究一些關於窗口類的秘密,以及 ATL技術實現的秘密。WTL(Window Template Library,窗口模板庫),雖然到 現在(譯注:本文於2002年10月27日發表在CodeProject)還不為Microsoft所支 持,但是它在制作圖形應用程序方面跨出了一大步。WTL就是基於ATL的窗口類的 。

在開始討論基於ATL的程序之前,讓我們從一個經典的Hello world程序開始吧。這個程序完全用SDK編寫,並且我們中幾乎所有人都已經熟悉它了。

程序66.

#include <windows.h>
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
           LPSTR lpCmdLine, int nCmdShow)
{
  char szAppName[] = "Hello world";
  HWND hWnd;
   MSG msg;
  WNDCLASS wnd;

  wnd.cbClsExtra  = NULL;
  wnd.cbWndExtra  = NULL;
  wnd.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
  wnd.hCursor    = LoadCursor(NULL, IDC_ARROW);
  wnd.hIcon     = LoadIcon (NULL, IDI_APPLICATION);
  wnd.hInstance   = hInstance;
  wnd.lpfnWndProc  = WndProc;
  wnd.lpszClassName = szAppName;
  wnd.lpszMenuName = NULL;
  wnd.style      = CS_HREDRAW | CS_VREDRAW;

  if (!RegisterClass (&wnd))
  {
    MessageBox(NULL, "Can not register window class", "Error",
           MB_OK | MB_ICONINFORMATION);
    return -1;
  }

  hWnd = CreateWindow(szAppName, "Hello world", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,
    CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);

  ShowWindow(hWnd, nCmdShow);
  UpdateWindow(hWnd);

  while (GetMessage(&msg, NULL, 0, 0))
  {
    DispatchMessage(&msg);
  }

  return msg.wParam;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  HDC hDC;
   PAINTSTRUCT ps;
  RECT rect;
  switch (uMsg)
   {
  case WM_PAINT:
    hDC = BeginPaint(hWnd, &ps);
    GetClientRect(hWnd, &rect);
     DrawText(hDC, "Hello world", -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
    EndPaint(hWnd, &ps);
     break;
  case WM_DESTROY:
    PostQuitMessage (0);
    break;
  }

  return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

這個程序沒有什 麼新鮮的東西,它就是顯示了一個窗口,並在窗口中央顯示Hello world。

ATL是一個面向對象的開發庫,也就是說你可以用類來完成工作。讓我們 嘗試著自己來做一些相同的工作,編寫一些微小的類來使我們的工作更加簡單吧 。好了,那我們來編寫一些類來簡化工作——但是編寫這些類應該遵 循一個什麼樣的標准呢?換句話說就是,需要編寫多少類,它們的關系是什麼, 以及擁有什麼樣的方法和屬性。在這裡我並不打算討論整個的面向對象理論,我 們這裡只是編寫一個高質量的庫。為了使我的任務相類似,我將相關的API進行 了分組,並將這些相關的API放在了一個類裡邊。我將所有處理窗口的API放在了 一個類裡,並且它可以和其它的API相關聯,例如字體、文件、菜單等等。所以 我編寫了一個很小的類,並將所有第一個參數為HWND的API放在了這個類中。也 就是說,這個類只是簡單地對窗口API進行了一層包裝。我的類名稱為ZWindow, 當然你可以自由地選擇你喜歡的名稱。這個類是類似這個樣子:

class ZWindow
{
public:
  HWND m_hWnd;
  ZWindow (HWND hWnd = 0) : m_hWnd(hWnd) { }
  inline void Attach(HWND hWnd)
  { m_hWnd = hWnd; }
  inline BOOL ShowWindow(int nCmdShow)
  { return ::ShowWindow(m_hWnd, nCmdShow); }
   inline BOOL UpdateWindow()
  { return ::UpdateWindow (m_hWnd); }
};

在這裡,我只封裝了目前需要的API。你可以向這 個類中添加全部的API。對於這個類來說的唯一優點,就是你不用像API那樣傳遞 HWND參數了,這個類本身會傳遞這個參數。

呃,到現在為止還沒有什麼 特別的。但是,我們的窗口回調函數怎麼辦呢?請記住,這個回調函數的第一個 參數也是HWND,所以對於我們的標准而言,它也應該是這個類中的成員。所以, 我也添加了我們的回調函數。現在,這個類就應該是類似這個樣子了:

class ZWindow
{
public:
  HWND m_hWnd;
  ZWindow(HWND hWnd = 0) : m_hWnd(hWnd) { }
   inline void Attach(HWND hWnd)
  { m_hWnd = hWnd; }
   inline BOOL ShowWindow(int nCmdShow)
  { return ::ShowWindow (m_hWnd, nCmdShow); }
  inline BOOL UpdateWindow()
  {  return ::UpdateWindow(m_hWnd); }
  LRESULT CALLBACK WndProc (HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
  {
    switch (uMsg)
    {
    case WM_DESTROY:
      PostQuitMessage(0);
      break;
     }
    return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
  }
};

你需要為WNDCLASS或WNDCLASSEX的一個域 提供這個回調函數的地址。並且,你需要在創建ZWindow類對象之後像這樣賦值 :

ZWindow zwnd;
WNDCLASS wnd;
wnd.lpfnWndProc = wnd.WndProc;

但是當你編譯程序的時候,編譯器會給出類似這樣的錯誤 :

cannot convert from ''long (__stdcall ZWindow::*) (struct HWND__ *,
  unsigned int,unsigned int,long)'' to ''long (__stdcall *)(struct HWND__ *,
  unsigned int, unsigned int,long)

原因是你不能將成員函數作為回調函數來傳遞。為 什麼呢?因為在成員函數的情況下,編譯器會自動傳給成員函數一個參數,這個 參數是指向這個類的指針,或者換句話說是this指針。所以這就意味著當你在成 員函數中傳遞了n個參數的話,那麼編譯器會傳遞n+1個參數,並且那個附加的參 數就是this指針。這條錯誤消息就表明編譯器不能將成員函數轉換為全局函數。

那麼,如果我們想將成員函數作為回調函數的話,應該怎麼辦呢?如果 我們告訴編譯器,不傳遞第一個this指針參數的話,那麼我們就可以將成員函數 作為回調函數了。在C++中,如果我們將成員函數聲明為static的話,那麼編譯 器就不會傳遞this指針了。這就是static和非static成員函數實質上的不同。

所以,我們可以把ZWindow類中的WndProc聲明為static成員函數。這一 技術也可以用在多線程的情況下,比如當你想要使用成員函數作為一個線程函數 的時候,你就可以將一個static成員函數作為線程函數。

下面就是使用 了ZWindow類的更新程序。

程序67.

#include <windows.h>
class ZWindow
{
public:
  HWND m_hWnd;
  ZWindow(HWND hWnd = 0) : m_hWnd(hWnd) { }
   inline void Attach(HWND hWnd)
  { m_hWnd = hWnd; }
   inline BOOL ShowWindow(int nCmdShow)
  { return ::ShowWindow (m_hWnd, nCmdShow); }
  inline BOOL UpdateWindow()
  {  return ::UpdateWindow(m_hWnd); }
  LRESULT CALLBACK WndProc (HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
  {
    switch (uMsg)
    {
    case WM_DESTROY:
      PostQuitMessage(0);
      break;
     }
    return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
  }
};
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, 
           int nCmdShow)
{
  char szAppName[] = "Hello world";
  HWND hWnd;
  MSG msg;
  WNDCLASS wnd;
  ZWindow zwnd;

  wnd.cbClsExtra  = NULL;
  wnd.cbWndExtra  = NULL;
  wnd.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
  wnd.hCursor    = LoadCursor(NULL, IDC_ARROW);
  wnd.hIcon     = LoadIcon (NULL, IDI_APPLICATION);
  wnd.hInstance   = hInstance;
  wnd.lpfnWndProc  = ZWindow::WndProc;
  wnd.lpszClassName = szAppName;
  wnd.lpszMenuName = NULL;
  wnd.style      = CS_HREDRAW | CS_VREDRAW;

  if (!RegisterClass (&wnd))
  {
    MessageBox(NULL, "Can not register window class", "Error",
           MB_OK | MB_ICONINFORMATION);
    return -1;
  }

  hWnd = CreateWindow(szAppName, "Hello world", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,
    CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);
   zwnd.Attach(hWnd);

  zwnd.ShowWindow(nCmdShow);
   zwnd.UpdateWindow();
  while (GetMessage(&msg, NULL, 0, 0))
  {
    DispatchMessage(&msg);
  }

  return msg.wParam;
}

這個程序只是簡單示范了一下 ZWindow的用法,說實話,這個類就不會做什麼特別的了。它只是對Windows API 的一層包裝,唯一的優點就是你不需要傳遞HWND參數了,但是你必須得在調用成 員函數的時候輸入對象的名稱。

對於以前,你這樣調用函數:

ShowWindow(hWnd, nCmdShow);

現在,你可以這麼做:

zwnd.ShowWindow(nCmdShow);

到現在為止,這並不是一個明顯的 優點。

我們來看看在WndProc中如何處理窗口消息。在前一個程序中,我 們只處理了一個函數,也就是WM_DESTROY。如果你想要處理更多的消息,那麼可 以在switch語句中加入更多的case。讓我們來修改一下WndProc,處理一下 WM_PAINT。就像這個樣子:

switch (uMsg)
{
case WM_PAINT:
  hDC = ::BeginPaint(hWnd, &ps);
   ::GetClientRect(hWnd, &rect);
  ::DrawText(hDC, "Hello world", -1, &rect, DT_CENTER | DT_VCENTER  DT_SINGLELINE);
  ::EndPaint(hWnd, &ps);
  break;
case WM_DESTROY:
  ::PostQuitMessage(0);
  break;
}

這個代碼很正確,它會在窗口的正中顯示Hello world。但是,為什 麼要用BeginPaint、GetClientRect和EndPaint這些API呢?根據我們的標准,這 些API都應該作為ZWindow的成員函數來使用的——它們的第一個參數 都是HWND。

因為所有這些函數都是非static函數。並且,你不能在 static成員函數中調用非static成員函數。為什麼呢?因為它們的區別就是this 指針,非static成員函數擁有this指針,而static函數沒有。如果我們通過某種 手段將this指針傳遞給了static成員函數,那麼我們就可以在static成員函數中 調用非static成員函數了。讓我們看看下面的程序。

程序 68.

#include <iostream>
using namespace std;
class C
{
public:
  void NonStaticFunc()
  {   
    cout << "NonStaticFun" << endl;
  }
  static void StaticFun(C* pC)
  {
    cout << "StaticFun" << endl;
     pC->NonStaticFunc();
  }
};
int main()
{
  C objC;
  C::StaticFun(&objC);
  return 0;
}

程序的輸出為:

StaticFun
NonStaticFun

所以,我們就可以使用和這裡相同的技術,也就是將ZWindow對象的地址存入一 個全局變量,然後利用這個指針調用非static成員函數。下面是前一個程序的更 新版本,在其中我們沒有直接調用窗口的API。

程序69.

#include <windows.h>
class ZWindow;
ZWindow* g_pWnd = NULL;
class ZWindow
{
public:
  HWND m_hWnd;
   ZWindow(HWND hWnd = 0) : m_hWnd(hWnd) { }
  inline void Attach (HWND hWnd)
  { m_hWnd = hWnd; }
  inline BOOL ShowWindow(int nCmdShow)
  { return ::ShowWindow(m_hWnd, nCmdShow); }
  inline BOOL UpdateWindow()
  { return ::UpdateWindow(m_hWnd); }
  inline HDC BeginPaint(LPPAINTSTRUCT ps)
  { return ::BeginPaint(m_hWnd, ps); }
  inline BOOL EndPaint(LPPAINTSTRUCT ps)
  { return ::EndPaint(m_hWnd, ps); }
  inline BOOL GetClientRect(LPRECT rect)
  {  return ::GetClientRect(m_hWnd, rect); }
  BOOL Create(LPCTSTR szClassName, LPCTSTR szTitle, HINSTANCE hInstance,
         HWND hWndParent = 0,  DWORD dwStyle = WS_OVERLAPPEDWINDOW,
         DWORD dwExStyle = 0, HMENU hMenu = 0)
  {
     m_hWnd = ::CreateWindowEx(dwExStyle, szClassName, szTitle, dwStyle,
                 CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
                  CW_USEDEFAULT, hWndParent, hMenu, hInstance, NULL);
     return m_hWnd != NULL;
  }
  static LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
   {
    ZWindow* pThis = g_pWnd;
    HDC hDC;
     PAINTSTRUCT ps;
    RECT rect;
    switch (uMsg)
    {
    case WM_PAINT:
      hDC = pThis->BeginPaint(&ps);
      pThis- >GetClientRect(&rect);
      ::DrawText(hDC, "Hello world", -1, &rect,
            DT_CENTER | DT_VCENTER | DT_SINGLELINE);
      pThis- >EndPaint(&ps);
      break;
    case WM_DESTROY:
      ::PostQuitMessage(0);
       break;
    }
    return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
  }
};
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
          LPSTR lpCmdLine,  int nCmdShow)
{
  char szAppName[] = "Hello world";
  MSG msg;
  WNDCLASS wnd;
  ZWindow zwnd;

  wnd.cbClsExtra  = NULL;
   wnd.cbWndExtra  = NULL;
  wnd.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
  wnd.hCursor    = LoadCursor (NULL, IDC_ARROW);
  wnd.hIcon     = LoadIcon(NULL, IDI_APPLICATION);
  wnd.hInstance   = hInstance;
   wnd.lpfnWndProc  = zwnd.WndProc;
  wnd.lpszClassName = szAppName;
  wnd.lpszMenuName = NULL;
  wnd.style      = CS_HREDRAW | CS_VREDRAW;

  if (!RegisterClass (&wnd))
  {
    MessageBox(NULL, "Can not register window class", "Error",
           MB_OK | MB_ICONINFORMATION);
    return -1;
  }
  g_pWnd = &zwnd;
  zwnd.Create(szAppName, "Hell world", hInstance);
  zwnd.ShowWindow(nCmdShow);
   zwnd.UpdateWindow();
  while (GetMessage(&msg, NULL, 0, 0))
  {
    DispatchMessage(&msg);
  }

  return msg.wParam;
}

那麼,我們終於有了這個可以 工作的程序。現在,讓我們來利用面向對象程序設計。如果我們對於每個消息都 調用函數,並且使這些函數都成為虛函數的話,那麼我們就可以在繼承ZWindow 類之後調用這些函數了。所以,我們可以自定義ZWindow的默認行為。現在, WndProc是類似這個樣子:

static LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam,
                 LPARAM lParam)
{
  ZWindow* pThis = g_pWnd;
   switch (uMsg)
  {
  case WM_CREATE:
    pThis- >OnCreate(wParam, lParam);
    break;
  case WM_PAINT:
    pThis->OnPaint(wParam, lParam);
     break;
  case WM_DESTROY:
    ::PostQuitMessage (0);
    break;
  }
  return ::DefWindowProc (hWnd, uMsg, wParam, lParam);
}

在這裡,OnCreate和OnPaint是 虛函數。並且,當我們從ZWindow繼承一個類的時候,我們就可以重寫所有我們 想自定義的這些函數。下面是一個完整的程序,它示范了在派生類中WM_PAINT消 息的使用。

程序70.

#include <windows.h>
class ZWindow;
ZWindow* g_pWnd = NULL;
class ZWindow
{
public:
  HWND m_hWnd;
  ZWindow(HWND hWnd = 0) : m_hWnd(hWnd) { }
  inline void Attach(HWND hWnd)
  { m_hWnd = hWnd; }
  inline BOOL ShowWindow(int nCmdShow)
   { return ::ShowWindow(m_hWnd, nCmdShow); }
  inline BOOL UpdateWindow()
  { return ::UpdateWindow(m_hWnd); }
   inline HDC BeginPaint(LPPAINTSTRUCT ps)
  { return ::BeginPaint(m_hWnd, ps); }
  inline BOOL EndPaint (LPPAINTSTRUCT ps)
  { return ::EndPaint(m_hWnd, ps); }
  inline BOOL GetClientRect(LPRECT rect)
  { return ::GetClientRect(m_hWnd, rect); }
  BOOL Create(LPCTSTR szClassName, LPCTSTR szTitle, HINSTANCE hInstance,
         HWND hWndParent = 0, DWORD dwStyle = WS_OVERLAPPEDWINDOW,
         DWORD dwExStyle = 0, HMENU hMenu = 0)
  {
     m_hWnd = ::CreateWindowEx(dwExStyle, szClassName, szTitle, dwStyle,
                  CW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT,
                  CW_USEDEFAULT, hWndParent, hMenu, hInstance, NULL);
    return m_hWnd != NULL;
  }
  virtual LRESULT OnPaint(WPARAM wParam, LPARAM lParam)
  {
    HDC hDC;
    PAINTSTRUCT ps;
    RECT rect;
     hDC = BeginPaint(&ps);
    GetClientRect(&rect);
    ::DrawText(hDC, "Hello world", -1, &rect,
          DT_CENTER | DT_VCENTER | DT_SINGLELINE);
     EndPaint(&ps);
    return 0;
  }
   virtual LRESULT OnCreate(WPARAM wParam, LPARAM lParam)
  {
    return 0;
  }
  static LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam,
                   LPARAM lParam)
  {
    ZWindow* pThis = g_pWnd;
    switch (uMsg)
    {
    case WM_CREATE:
      pThis->OnCreate(wParam, lParam);
      break;
    case WM_PAINT:
      pThis ->OnPaint(wParam, lParam);
      break;
     case WM_DESTROY:
      ::PostQuitMessage(0);
       break;
    }
    return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
  }
};
class ZDriveWindow : public ZWindow
{
public:
  LRESULT OnPaint(WPARAM wParam, LPARAM lParam)
  {
    HDC hDC;
     PAINTSTRUCT ps;
    RECT rect;
    hDC = BeginPaint (&ps);
    GetClientRect(&rect);
     SetBkMode(hDC, TRANSPARENT);
    DrawText(hDC, "Hello world From Drive", -1, &rect,
         DT_CENTER | DT_VCENTER | DT_SINGLELINE);
    EndPaint(&ps);
     return 0;
  }
};

程序的輸出是一個窗 口中的一條“Hello world from Drive”消息。在我們使用派生類之 前,可以說一切都是順利的。當我們從ZWindow派生出多於一個類的時候,問題 就會發生。這樣,所有的消息就都會流向ZWindow最後繼承的那個派生類。讓我 們看看以下的程序。

程序71.

#include <windows.h>
class ZWindow;
ZWindow* g_pWnd = NULL;
class ZWindow
{
public:
  HWND m_hWnd;
  ZWindow(HWND hWnd = 0) : m_hWnd(hWnd) { }
  inline void Attach(HWND hWnd)
  { m_hWnd = hWnd; }
  inline BOOL ShowWindow(int nCmdShow)
   { return ::ShowWindow(m_hWnd, nCmdShow); }
  inline BOOL UpdateWindow()
  { return ::UpdateWindow(m_hWnd); }
   inline HDC BeginPaint(LPPAINTSTRUCT ps)
  { return ::BeginPaint(m_hWnd, ps); }
  inline BOOL EndPaint (LPPAINTSTRUCT ps)
  { return ::EndPaint(m_hWnd, ps); }
  inline BOOL GetClientRect(LPRECT rect)
  { return ::GetClientRect(m_hWnd, rect); }
  BOOL Create(LPCTSTR szClassName, LPCTSTR szTitle, HINSTANCE hInstance,
         HWND hWndParent = 0, DWORD dwStyle = WS_OVERLAPPEDWINDOW,
         DWORD dwExStyle = 0, HMENU hMenu = 0, int x = CW_USEDEFAULT,
        int y = CW_USEDEFAULT, int nWidth = CW_USEDEFAULT,
        int nHeight = CW_USEDEFAULT)
  {
    m_hWnd = ::CreateWindowEx(dwExStyle, szClassName, szTitle, dwStyle,
                  x, y, nWidth, nHeight, hWndParent, hMenu,
                   hInstance, NULL);
    return m_hWnd != NULL;
   }
  virtual LRESULT OnPaint(WPARAM wParam, LPARAM lParam)
  {
    HDC hDC;
    PAINTSTRUCT ps;
     RECT rect;
    hDC = BeginPaint(&ps);
     GetClientRect(&rect);
    ::DrawText(hDC, "Hello world", -1, &rect,
          DT_CENTER | DT_VCENTER | DT_SINGLELINE);
    EndPaint(&ps);
     return 0;
  }
  virtual LRESULT OnLButtonDown(WPARAM wParam, LPARAM lParam)
  {
    return 0;
  }
  virtual LRESULT OnCreate(WPARAM wParam, LPARAM lParam)
   {
    return 0;
  }
  virtual LRESULT OnKeyDown(WPARAM wParam, LPARAM lParam)
  {
    return 0;
  }
  static LRESULT CALLBACK StartWndProc(HWND hWnd, UINT uMsg,
                     WPARAM wParam, LPARAM lParam)
  {
    ZWindow* pThis = g_pWnd;
    if (uMsg == WM_NCDESTROY)
       ::PostQuitMessage(0);
    switch (uMsg)
    {
    case WM_CREATE:
      pThis->OnCreate(wParam, lParam);
      break;
    case WM_PAINT:
       pThis->OnPaint(wParam, lParam);
      break;
    case WM_LBUTTONDOWN:
      pThis- >OnLButtonDown(wParam, lParam);
      break;
     case WM_KEYDOWN:
      pThis->OnKeyDown(wParam, lParam);
      break;
    case WM_DESTROY:
       ::PostQuitMessage(0);
      break;
     }
    return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
  }
};
class ZDriveWindow1 : public ZWindow
{
public:
  LRESULT OnPaint(WPARAM wParam, LPARAM lParam)
  {
    HDC hDC;
     PAINTSTRUCT ps;
    RECT rect;
    hDC = BeginPaint (&ps);
    GetClientRect(&rect);
     ::SetBkMode(hDC, TRANSPARENT);
    ::DrawText(hDC, "ZDriveWindow1", -1, &rect,
          DT_CENTER | DT_VCENTER | DT_SINGLELINE);
    EndPaint (&ps);
    return 0;
  }
  LRESULT OnLButtonDown(WPARAM wParam, LPARAM lParam)
  {
     ::MessageBox(NULL, "ZDriveWindow1::OnLButtonDown", "Msg", MB_OK);
    return 0;
  }
};
class ZDriveWindow2 : public ZWindow
{
public:
   LRESULT OnPaint(WPARAM wParam, LPARAM lParam)
  {
     HDC hDC;
    PAINTSTRUCT ps;
    RECT rect;
     hDC = BeginPaint(&ps);
    GetClientRect (&rect);
    ::SetBkMode(hDC, TRANSPARENT);
     ::Rectangle(hDC, rect.left, rect.top, rect.right, rect.bottom);
    ::DrawText(hDC, "ZDriveWindow2", -1, &rect,
          DT_CENTER | DT_VCENTER | DT_SINGLELINE);
     EndPaint(&ps);
    return 0;
  }
   LRESULT OnLButtonDown(WPARAM wParam, LPARAM lParam)
  {
     ::MessageBox(NULL, "ZDriveWindow2::OnLButtonDown", "Msg", MB_OK);
    return 0;
  }
};
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
          LPSTR lpCmdLine,  int nCmdShow)
{
   char szAppName[] = "Hello world";
  MSG msg;
  WNDCLASS wnd;
  ZDriveWindow1 zwnd1;
  ZDriveWindow2 zwnd2;

  wnd.cbClsExtra  = NULL;
   wnd.cbWndExtra  = NULL;
  wnd.hbrBackground = (HBRUSH) GetStockObject(GRAY_BRUSH);
  wnd.hCursor    = LoadCursor (NULL, IDC_ARROW);
  wnd.hIcon     = LoadIcon(NULL, IDI_APPLICATION);
  wnd.hInstance   = hInstance;
   wnd.lpfnWndProc  = ZWindow::StartWndProc;
  wnd.lpszClassName = szAppName;
  wnd.lpszMenuName = NULL;
  wnd.style      = CS_HREDRAW | CS_VREDRAW;

  if (!RegisterClass (&wnd))
  {
    MessageBox(NULL, "Can not register window class", "Error",
            MB_OK | MB_ICONINFORMATION);
    return -1;
  }
  g_pWnd = &zwnd1;
  zwnd1.Create(szAppName, "Hell world", hInstance);
  zwnd1.ShowWindow (nCmdShow);
  zwnd1.UpdateWindow();
  g_pWnd = &zwnd2;
  zwnd2.Create(szAppName, "Hello world", hInstance, zwnd1.m_hWnd,
    WS_VISIBLE | WS_CHILD | ES_MULTILINE, NULL, NULL, 0, 0, 150, 150);
  while (GetMessage (&msg, NULL, 0, 0))
  {
    DispatchMessage (&msg);
  }

  return msg.wParam;
}

程序的輸出表明,不管你單擊了哪個窗口,都會彈出相同的 MessageBox。

不管 你單擊了哪個窗口,你都會獲得相同的消息框。這就意味著消息並沒有傳遞給適 當的窗口。事實上每個窗口都擁有自己的窗口過程,這些窗口過程處理窗口的所 有消息。但是在這裡,我們對第一個窗口使用了第二個窗口的回調函數,所以我 們就不能對第一個窗口的消息進行處理了。

現在,我們最主要的問題是 將窗口的回調函數和相應的窗口關聯起來。這就意味著HWND應該和相應的派生類 關聯起來,所以消息應該發送給正確的窗口。解決這個問題可以有若干種方法, 讓我們來一個一個看一看。

首先我想出了一個最明顯的解決方法,我們 可以很容易地實現。方法是創建一個全局的結構,這個結構存儲HWND和相應的派 生類。但是,這個方法有兩個主要的問題。第一,這個結構會在窗口逐漸加入程 序的過程中越變越大;第二,在結構變得很大之後,在這個結構中進行搜索肯定 也會花費大筆時間。

而ATL的最主要目的就是使程序盡可能地小和快。並 且,上述技術對於這兩個標准都達不到。這個方法不單單是慢,還會在程序中包 含大量窗口的情況下占用大量內存。

另一個可能的解決方案是使用 WNDCLASS或WNDCLASSEX結構的cbWndExtra域。還有一個問題是,為什麼不用 cbClsExtra,而要用cbWndExtra呢?答案很簡單,cbClsExtra為每個窗口類存儲 額外的字節,而cbWndExtra為每個窗口存儲額外的字節。並且,你可能會從一個 窗口類創建多個窗口,這樣,如果你使用了cbClsExtra的話,那麼你就不能通過 cbClsExtra區別不同的回調函數了,因為對於這些相同窗口類產生的窗口來說這 個值是一樣的。然後,將相應的派生類地址存儲到cbWndExtra中。

這個 和方法看起來比第一個要好,但是它仍然有兩個問題。第一,如果用戶希望使用 cbWndExtra,那麼他/她就可能會覆蓋著一技術所使用的數據,這樣客戶就需要 在使用cbWndExtra的時候十分注意了,以防丟失信息。那麼好了,你可以在文檔 中寫明在使用你的庫時不要使用cbWndExtra,但是仍然會有一個問題:這個方法 並不是很快,又一次違背了ATL的規則——ATL應該盡可能地小和快。

ATL沒有使用這兩個方法中的任何一個,它使用的方法被稱作Thunk。 Thunk是一個小系列的代碼,並且這一術語被用在不同的地方。你可能曾經聽過 兩種Thunking:

Universal Thunking

Universal Thunking允許在 16位代碼中調用32位的函數,在Win 9x和Win NT/2000/XP下都可以使用,也被稱 作Generic Thunking。

General Thunking

General Thunking允許 在32位代碼中調用16位的函數,它只能用在Win 9x中,因為Win NT/2000/XP是純 32位操作系統,所以在32位代碼中調用16位的函數不合乎邏輯。General Thunking也被稱作Flat Thunking。

ATL沒有使用這兩種方法,因為你不 會在ATL中將16位和32位的代碼混合。事實上,ATL插入了一小段代碼來調用正確 的窗口過程。

在研究ATL的Thunking之前,讓我們先從一些基礎概念開始 。請看下面的簡單程序。

程序72.

#include <iostream>
using namespace std;
struct S
{
  char ch;
   int i;
};
int main()
{
  cout << "Size of character = " << sizeof(char) << endl;
  cout << "Size of integer = " << sizeof(int) << endl;
  cout << "Size of structure = " << sizeof(S) << endl;
  return 0;
}

程序的輸出為:

Size of character = 1
Size of integer = 4
Size of structure = 8

一個整型和一個字符的尺寸 之和應該是5而不是8。那麼讓我們略微修改一下程序,再添加一個成員變量,看 看會發生什麼。

程序73.

#include <iostream>
using namespace std;
struct S
{
  char ch1;
  char ch2;
  int i;
};
int main()
{
  cout << "Size of character = " << sizeof(char) << endl;
  cout << "Size of integer = " << sizeof(int) << endl;
  cout << "Size of structure = " << sizeof(S) << endl;
   return 0;
}

程序的輸出和前一個一樣。那麼這裡發生了什麼?再 修改一下程序,看看布幔之下發生了什麼吧。

程序74.

#include <iostream>
using namespace std;
struct S
{
  char ch1;
  char ch2;
  int i;
}s;
int main()
{
  cout << "Address of ch1 = " << (int)&s.ch1 << endl;
  cout << "Address of ch2 = " << (int)&s.ch2 << endl;
  cout << "Address of int = " << (int)&s.i << endl;
  return 0;
}

程序的輸 出為:

Address of ch1 = 4683576
Address of ch2 = 4683577
Address of int = 4683580

這是由於結構和聯合成員的字對齊的緣故 。如果你注意觀察的話,你就能推斷出來這個結構外的每個變量都存儲在能被4 整除的地址上,這是為了提高處理器的性能。所以,這裡的結構分配了4的整數 倍的內存空間,也就是4683576,ch1和它有相同的地址。ch2成員存儲在這個位 置之後,而int i存儲在4683580的位置上。這個位置不是4683578的原因是它不 能被4整除。現在的問題是,4683578和4683579的位置上是什麼呢?答案是如果 變量是本地變量,那麼這裡是垃圾值;如果是static或全局變量,那麼是0。讓 我們看看下面這個程序來更好地理解這一點。

程序75.

#include <iostream>
using namespace std;
struct S
{
  char ch1;
  char ch2;
  int i;
};
int main()
{
  S s = { ''A'', ''B'', 10};
  void* pVoid = (void*)&s;
  char* pChar = (char*)pVoid;
  cout << (char)*(pChar + 0) << endl;
  cout << (char)*(pChar + 1) << endl;
  cout << (char)*(pChar + 2) << endl;
   cout << (char)*(pChar + 3) << endl;
  cout << (int)*(pChar + 4) << endl;
  return 0;
}

程序的輸出為:

A
B


10

程序的輸出清楚地表明,那些空間中是垃圾值,就 像下表一樣。

現在 ,如果我們不想浪費那些空間的話應該怎麼做呢?有兩個選擇:或者使用編譯器 開關/Zp,或者在聲明結構之前使用#pragma語句。

程序 76.

#include <iostream>
using namespace std;
#pragma pack(push, 1)
struct S
{
  char ch;
   int i;
};
#pragma pack(pop)
int main()
{
   cout << "Size of structure = " << sizeof(S) << endl;
  return 0;
}

程序的輸出為:

Size of structure = 5

這就意味著現在已經沒有字對齊了。事實上,ATL使用這一技術來制作thunk。 ATL使用了一個結構,這個結構沒有使用字對齊,並且這個結構中直接儲存了微 處理器的機器代碼。

#pragma pack(push,1)
// 存儲機器代碼的結 構
struct Thunk
{
  BYTE  m_jmp; // jmp指 令的操作碼
  DWORD  m_relproc; // 相對jmp
};
#pragma pack(pop)

這種類型的結構保存了thunk代碼,它可以在不工 作的時候執行。讓我們來看看下面這種簡單的情況,我們將要使用thunk來執行 我們想要執行的函數。

程序77.

#include <iostream>
#include <windows.h>
using namespace std;
class C;
C* g_pC = NULL;
typedef void(*pFUN)();
#pragma pack (push,1)
// 存儲機器代碼的結構
struct Thunk
{
   BYTE  m_jmp; // jmp指令的操作碼
  DWORD  m_relproc; // 相對jmp
};
#pragma pack(pop)
class C
{
public:
  Thunk  m_thunk;
  void Init(pFUN pFun, void* pThis)
  {
    // 跳轉指令的操作碼
     m_thunk.m_jmp = 0xe9;
    // 相應函數的地址
     m_thunk.m_relproc = (int)pFun - ((int)this+sizeof(Thunk));
     FlushInstructionCache(GetCurrentProcess(),
                 &m_thunk, sizeof(m_thunk));
  }
  // 這 是回調函數
  static void CallBackFun()
  {
     C* pC = g_pC;
    // 初始化thunk
    pC->Init (StaticFun, pC);
    // 獲得thunk代碼地址
    pFUN pFun = (pFUN)&(pC->m_thunk);
    // 開始執行thunk代碼 ,調用StaticFun
    pFun();
    cout << "C::CallBackFun" << endl;
  }
  static void StaticFun()
  {
    cout << "C::StaticFun" << endl;
  }
};
int main()
{
  C objC;
  g_pC = &objC;
   C::CallBackFun();
  return 0;
}

程序的輸出為:

C::StaticFun
C::CallBackFun

在這裡, StaticFun是通過thunk調用的,而thunk是在Init成員函數中初始化的。程序的 執行是類似這個樣子

·CallBackFun

·Init(初始 化thunk)

·獲得thunk地址

·執行 thunk

·Thunk代碼調用StaticFun

ATL 也使用了相同的技術來調用正確的回調函數,但是它在調用函數之前還做了一件 事情。現在ZWindow又有了一個虛函數ProcessWindowMessage,它在這個類中什 麼也不做。但是,ZWindow的每個派生類都需要重寫它來處理自己的消息。整個 的處理過程和我們將ZWindow的派生類地址存入一個指針並調用派生類的虛函數 是相同的,但是現在WindowProc的名字是StartWndProc。在這裡,ATL使用了這 一技術來將HWND參數替換為了this指針。但是,HWND怎麼樣了,我們就這麼失去 它了嗎?事實上,我們已經將HWND存入了ZWindow類的成員變量中了。

要 達到這一點,ATL使用了一個較前一個程序大一點的結構。

#pragma pack (push,1)
struct _WndProcThunk
{
  DWORD  m_mov; // mov dword ptr [esp+0x4], pThis (esp+0x4 is hWnd)
  DWORD  m_this;
  BYTE  m_jmp; // jmp WndProc
  DWORD  m_relproc; // 相對jmp
};
#pragma pack(pop)

並且,在 初始化的時刻,寫入操作碼“mov dword ptr [esp +4], pThis”。 是類似這個樣子:

void Init(WNDPROC proc, void* pThis)
{
  thunk.m_mov = 0x042444C7; //C7 44 24 04
  thunk.m_this = (DWORD)pThis;
  thunk.m_jmp = 0xe9;
  thunk.m_relproc = (int)proc - ((int)this+sizeof(_WndProcThunk));
   FlushInstructionCache(GetCurrentProcess(), &thunk, sizeof (thunk));
}

並且,在初始化thunk代碼之後,獲得thunk的地址並 向thunk代碼設置新的回調函數。然後,thunk代碼會調用WindowProc,但是現在 第一個參數就不是HWND了,事實上它是this指針。所以我們可以將它安全的轉換 為ZWindow*,並調用ProcessWindowMessage函數。

static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg,
                   WPARAM wParam, LPARAM lParam)
{
  ZWindow* pThis = (ZWindow*)hWnd;
  if (uMsg == WM_NCDESTROY)
     PostQuitMessage(0);
  if (!pThis->ProcessWindowMessage (pThis->m_hWnd, uMsg, wParam, lParam))
    return ::DefWindowProc(pThis->m_hWnd, uMsg, wParam, lParam);
   else
    return 0;
}

現在,每個窗口正確 的窗口過程就可以被調用了。整個的過程如下圖所示:

由於 代碼長度所限,程序的完整代碼將隨本文配套提供。我希望能在本系列中之後的 文章中繼續探究ATL的其它秘密。

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved