ATL Under the Hood, Part 5

Environment: ATL

Lots of people think that ATL is used only for making COM components. But, in fact, you can create a fully fleshed Windows-based application using ATL by using ATL's Windowing classes. Although you can convert your MFC-based project to ATL, there is very little support of the UI component in ATL, so you have to write lots of code yourself. For example, there is no Document/View in ATL; if you want to make it, you have to implement it yourself. In this part, we are going to explore the windowing classes. And, we'll also try to explore the techniques that ATL uses to do this. The WTL (Window Template Library), until now unsupported by Microsoft, is in fact one step forward towards making a graphical application. WTL is based on the ATL Windowing classes.

Before starting a discussion of any ATL-based program, let's start with a discussion of the classic Hello world program. This program is completely written in SDK and almost all of us are already familiar with it.

Program 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, "Cannot 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);
}

There is nothing new in this program. It just displays a window and displays Hello world at the center of this.

ATL is an object-oriented library; this means you are using classes to do your work. Let's try to do the same thing ourselves and make some small classes to make our work easier. Okay, we are going to make some classes for our work, but what would be the criteria for making classes? In other words, how many classes should we make, and what are their relationships, methods, and properties? I am not planning to discuss the whole object-oriented theory and the process here to make a quality library. To make my task simpler, I'll make a group of related APIs and put those related APIs in one class. I put all the APIs that deal with windows in one class; it can be repeated to other types of APIs such as font, file, menu, and so forth. So, I made a small class and put the entire APIs whose first parameter is HWND in that class. This class is nothing more than a thin wrapper on the Windows APIs. My class Name is ZWindow; you are free to choose whatever name you like. This class is something like this.

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); }

};

Here I put only those APIs that are required at the moment. You may add all the APIs in this class. The only advantage of this class is that now you don't have to pass the HWND parameter for windowing APIs; this class will pass that parameter itself.

Well, there's been nothing special until now. But what about our Window CallBack function? Remember the first parameter of that callback function is also HWND, so, according to our criteria, it should be a member of this class. So I add our callback function in this class, too. Now, this class should be something like this:

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);
  }
};

You have to give the address of the callback function in one field of the WNDCLASS or WNDCLASSEX structure. And, you give it something like this after creating the object of the ZWindow class.

  ZWindow zwnd;
  WNDCLASS wnd;

  wnd.lpfnWndProc = wnd.WndProc;

But, when you compile this program, it will give you an error something like this.

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

The reason is you cannot pass a member function as a callback function. Why? Because in the case of a member function, the compiler automatically passes one parameter to the function and that parameter is the pointer of that class—in other words, this pointer. So it means that when you pass n parameters in the member function, the compiler will pass n+1 parameters; the additional parameter is this pointer. The error message from the compiler shows this, too; the compiler can't convert a member function to a global function.

So, what should we do if we want to use a member function as a callback function? If we tell the compiler not to pass the first parameter to the function, somehow then we can use the member function as a callback function. In C++, if we declare a member function as a static, the compiler doesn't pass this pointer. This is, in fact, the difference between static and non-static member functions.

So, we made WndProc static in the ZWindow class. This technique is also used in the case of threading where you want to use a member function as a thread function; then, you make a static member function as a thread function.

Here is the updated program that uses the ZWindow class.

Program 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, "Cannot 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;
}

This program just shows the usage of ZWindow. And, to be very honest, this class does nothing special. It is just a wrapper on the Window API. The only advantage you get from this is that now you don't need to pass HWND as a parameter. Instead of this, now you have to type the object name when calling the member function.

Like before, this you call the function like this:

  ShowWindow(hWnd, nCmdShow);

And now, you call something like this:

  zwnd.ShowWindow(nCmdShow);

Not a big advantage—until now.

Let's see how we can handle a window message in the WndProc. In the previous program, we handled only one function, WM_DESTROY. If we want to handle more messages, add more cases in the switch statement. Let's modify the WndProc to handle WM_PAINT. It would be something like this:

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;
}

This code is perfectly valid and prints "Hello World" in the center the of window. But why use API's BeginPaint, GetClientRect, and EndPaint when it all should be a member function of the ZWindow class, according to our criteria? Because all of these have HWND as a first parameter.

It's because all those functions are not static. And, you can't call a non-static member function from a static member function. Why? Because the difference is this pointer; a non-static member function has this pointer and a static function doesn't have it. If we somehow pass this pointer to the static member function, we can call the non-static member function from the static member function. Let's take a look at the following program.

Program 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;
}

The output of this program is:

StaticFun
NonStaticFun

So, we can use the same technique here; for example, we can store the address of the ZWindow object in a global variable and then call a non-static member function from that pointer. Here is the updated version of the previous program in which we are not calling the windowing API directly.

Program 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, "Cannot 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;
}

So, finally we have a working program. Now let's take advantage of object-oriented programming. If we call a function on each message and make that function virtual, we can call those functions when we inherit the class from ZWindow. So, we can customize the default behavior of ZWindow. Now, the WndProc is something like this:

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);
}

Here, OnCreate and OnPaint are virtual functions. And, when we inherit the class from ZWindow, we can override all those functions that we want to customize. Here is a complete program that shows the usage of the WM_PAINT message in the drive class.

Program 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;
  }
};

The output of this program is the message "Hello world from Drive" in a window. Everything works fine until we work on more than one drive class. The problem started when we drove more than one class from ZWindow. Then, all the messages go to the last drive class of ZWindow. Let's take a look at the following program.

Program 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, "Cannot 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;
}

The output of this program shows the same message box, no matter on which window you click.



Click here for a larger image.

You get the same message box no matter where you click on any window. This means the message is not properly propagated to the appropriate window. In fact, each window has its own window procedure that handles all the messages of that window. But here we use the callback function of the second drive class with the first window, so we can't we execute the message handler of the first window?

Here, our main problem is to associate the callback function of the window with the appropriate window. This means HWND should be associated with the appropriate Drive class. So, messages should go to the right window. There can be more than one solution of this problem; let's take a look at each solution one by one.

The first obvious solution that comes in mind and can be easily implemented is to make a global structure that stores the HWND with the appropriate Drive class address. But there are two main problems with this approach. First, the structure becomes larger and larger when more and more windows are added in the program. And, the second problem is that there is of course searching time involved in that global structure and it is time consuming to search it when that structure becomes very large.

The main purpose of ATL is to make the program as small as possible and as fast as possible. This technique fails on both criteria. This method is not only slow but also consumes lots of memory when there are lots of windows involved in the program.

The other possible solution is to use the cbWndExtra field of the WNDCLASS or WNDCLASSEX structure. There is still one question: Why not use cbClsExtra instead of cbWndExtra? The answer is simple: cbClsExtra stores the extra bytes for each class and cbWndExtra stores extra bytes for each window from the class. And, you can create more than one window from the same class, so if you use cbClsExtra, you can't distinguish the different window's callback function from cbClsExtra because it is the same of all those windows that are created by the same class. And, you store the address of the appropriate drive class in this field.

It seems like a good solution; at least, it seems better then the first one. But there are still two problems with this solution. The first one is that if the user wants to use cbWndExtra, he/she might overwrite the data that is written by using this technique. So, the client of this class has to be careful not to lose that information when using cbWndExtra. Okay, fine; you have decided and documented why not to use cbWndExtra when using your library, but there is still one more problem. This method is not very fast, and again against the rule of ATL—that ATL should be as small and as fast as possible.

ATL uses neither the first method nor the second. The method that ATL used is called Thunk. Thunk is a small set of code to do some work; this term is used in different context. It may be possible that you have heard of two types of Thunking.

Universal Thunking

Universal Thunking enables you to call a 32-bit function from 16-bit code. It is available for both Win 9x and Win NT/2000/XP. This is also known as Generic Thunking.

Generic Thunking

Generic Thunking enables you to call a 16-bit function from 32-bit code. It is available only on Win 9x because Win NT/2000/XP are true 32-bit operating systems so there is no logical reason to call a 16-bit function from 32-bit code. This is also known as Flat Thunking.

ATL doesn't use any of this because you are not going to mix 16-bit and 32-bit code in ATL. In fact ATL inserts a small code to call the correct Windows procedure.

Let's start some basic concepts before studying ATL thunking. Take a look at the following simple program.

Program 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;
}

The output of this program is:

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

The sum of the sizes of the integer and character should be 5, not 8. Okay; let's change the program a little bit and add one more member variable into the program to see what's going on.

Program 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;
}

The output of this program is same as previous one. So, what is going on here? Let's change the program a little bit more to see what is going on Under the Hood.

Program 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;
}

The output of this program is:

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

This is due to the word alignment of the structure and union members. If you notice carefully, you can conclude that each variable that is outside the structure is stored at an address which is divisible by 4. The reason for this is to increase the performance. So, here the structure allocates at the multiple of 4; that is, at memory location 4683576—ch1 has the same address. Member ch2 stores just next to this memory location and int is stored at memory location 4683580. Why not at 4683578? Because this is not divisible by 4. Now, there is a question. What is at memory locations 4683578 and 4683579? The answer is garbage if the variable is made local and zero if it is made static or global. Let's take a look at the following program to better understand this.

Program 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;
}

The output of this program is:

A
B
&
&
10

The output of this program clearly shows that those spaces contain garbage, as shown in the diagram.



Click here for a larger image.

Now, if we want to avoid waste those spaces, what should we do? We have two options; either use the compiler switch /Zp or use the #pragma statement before declaring the structure.

Program 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;
}

The output of this program is:

Size of structure = 5

It means that now there is no space for word alignment. In fact, ATL uses this technique to make a thunk. ATL uses one structure, which does not use word alignment and uses it to store direct machine code of the microprocessor.

#pragma pack(push,1)
// structure to store the machine code
struct Thunk
{
  BYTE    m_jmp;          // op code of jmp instruction
  DWORD   m_relproc;      // relative jmp
};
#pragma pack(pop)

This type of structure then can contain thunk code, which can be executed on the fly. Let's take a look at the simple case in which we are going to execute our required function by thunk.

Program 77

#include <iostream>
#include <windows.h>
using namespace std;

class C;

C* g_pC = NULL;

typedef void(*pFUN)();

#pragma pack(push,1)
// structure to store the machine code
struct Thunk
{
  BYTE    m_jmp;          // op code of jmp instruction
  DWORD   m_relproc;      // relative jmp
};
#pragma pack(pop)

class C
{
public:
  Thunk m_thunk;

  void Init(pFUN pFun, void* pThis)
  {
    // op code of jump instruction
    m_thunk.m_jmp = 0xe9;
    // address of the appropriate function
    m_thunk.m_relproc = (int)pFun - ((int)this+sizeof(Thunk));

    FlushInstructionCache(GetCurrentProcess(),
                          &m_thunk, sizeof(m_thunk));
  }

  // this is our callback function
  static void CallBackFun()
  {
    C* pC = g_pC;

    // initilize the thunk
    pC->Init(StaticFun, pC);

    // get the address of thunk code
    pFUN pFun = (pFUN)&(pC->m_thunk);

    // start executing thunk code which will call StaticFun
    pFun();

    cout << "C::CallBackFun" << endl;
  }

  static void StaticFun()
  {
    cout << "C::StaticFun" << endl;
  }
};

int main()
{
  C objC;
  g_pC = &objC;
  C::CallBackFun();
  return 0;
}

The output of this program is:

C::StaticFun
C::CallBackFun

Here StaticFun is called through the thunk, which is initialized in the Init member function. The execution of program is something like this:

  • CallBackFun
  • Init (to initialize the thunk)
  • Get the address of thunk
  • Execute thunk
  • Thunk code calls StaticFun


  • Click here for a larger image.

    ATL uses the same technique to call the correct Callback function, but it did one more thing before calling the function. Now, ZWindow has one more virtual function, ProcessWindowMessage, that doesn't do anything in this class. But every drive class of ZWindow overrides this to handle their own message. The process is the same: We store the address of ZWindow's drive class in one pointer to call the drive class virtual function, but now the WindowProc name is StartWndProc. Here, ATL uses the technique to replace the HWND with this pointer. But what about HWND, do we lose it? In fact, we have already stored the HWND in the ZWindow class member variable.

    To achieve this, ATL uses a little bit bigger structure as compared to the previous program.

    #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;   // relative jmp
    };
    #pragma pack(pop)
    

    And, at the time of initialization, writes the op code of "mov dword ptr [esp +4], pThis". It is something like this:

    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));
    }
    

    And after initializing the thunk code, it gets the address of thunk and sets the new callback function to thunk code. And then the thunk code will call WindowProc, but now the first parameter is not HWND; it is this pointer. So we can safely cast it into ZWindow* and call the ProcessWindowMessage function?

    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;
    }
    

    Now the correct window procedure will be called for each window. The whole process is shown in the following diagram.



    Click here for a larger image.

    The complete code of the following program is attached due to the length of code. I hope to explore the other mysteries of ATL with you in upcoming parts of this series.

    Downloads

    Download source - 4 Kb


    About the Author

    Zeeshan Amjad

    C++ Developer at Bechtel Corporation. zamjad.wordpress.com

    Comments

    • Better known as Closures.

      Posted by Legacy on 01/13/2004 12:00am

      Originally posted by: Sam

      This thunking technique is more widely known as partial closures or currying.

      Excellent article which shows how to implement this technique is C/C++. I really found it useful in developing callbacks from C/C++ to Python.

      Reply
    • Excellent Article

      Posted by Legacy on 01/06/2003 12:00am

      Originally posted by: robert Mng'anya

      This article as help me in pursuing my solo mission of writing a Window Base class ,which can be use by begginner programmer to write window base soft/prog.
      
      on top of that i have gain considerable understanding OOP
      concept in C++.

      But this is my suggstion on the implemantation of the ZWindow,instead of using a global ointer of this class to access non-static member function this is what u should do


      #include <iostream.h>

      class Class
      {

      public:
      void Show()
      {
      //store th pointer of the class
      //pass it to static /callback fn

      Bob((void*)this);

      }
      static void Bob(void * pThis)
      {
      //cast the this pointer
      //to class pointer
      Class* p= (Class*)pThis;
      //call any function
      p->Abe();
      }

      virtual void Abe()
      {
      cout<<"Abe is a liar "<<endl;

      }


      };

      //inherit from Class
      class B:public Class
      {
      public:

      virtual void Abe()
      {

      cout<<"Abe in class B"<<endl;
      }


      };
      //inherit from B
      class C:public B
      {
      public:
      void Abe()
      {

      cout<<"Abe in C"<<endl;
      }


      };


      void main()
      {
      C pClass;

      //call th C Abe fn by calling Show and then
      //show will call Bob and Bob will call C::Abe
      pClass.Show();
      }


      this code was written using VC++ 6.0

      I hope this will be useful

      Reply
    • A suggestion...

      Posted by Legacy on 10/28/2002 12:00am

      Originally posted by: Paul

      In my opinion, the code presented in this article is a poor example of writing a GUI-based program in the ATL. The ATL does include primative objects for handling windows. Reimplementing the functionality in your own class isn't necessary, nor desireable.

      It would be of greater benefit to your readers to explain how to use the built-in windowing functionality of the ATL, rather than implementing it yourself. That would also lead to further articles that explain how the WTL extends the ATL.

      • ....

        Posted by john on 07/22/2014 03:42pm

        dude, he made the article to explain the 'mysteries' of how atl accomplishes what it does. The author never intended the article to be used as a guide for ATL development, but as a 'what's under the hood' kind of exploration into how things work with ATL.

        Reply
      Reply
    • ATL Windows

      Posted by Legacy on 10/28/2002 12:00am

      Originally posted by: Juan Valdez

      WTL 7.0 does a much better job now and supports more features which make porting your MFC apps over to it much more transparent. WTL= Powerful ATL + MFC-Like Object Implementation. Get yourself hooked up today!

      WTL for Microsoft Windows:
      What do you want to code today?
      Yes you can-can!
      Down with the Butterfly!

      Reply
    • One Problem

      Posted by Legacy on 10/26/2002 12:00am

      Originally posted by: Kashif Aqeel

      It seems like there is one little problem is Program 67. The WndProc should be static as indicated earlier in the article but in the code its not declaed static

      Reply
    • Excellent

      Posted by Legacy on 10/26/2002 12:00am

      Originally posted by: Richard

      Very good article, thanks.

      Reply
    • this article illustrates why nobody should ever use atl to write windows applications

      Posted by Legacy on 10/26/2002 12:00am

      Originally posted by: Allen


      nt

      Reply
    • Comment

      Posted by Legacy on 10/25/2002 12:00am

      Originally posted by: Jas

      I've used ATL/C++/MFC. Let's face it, who is going to use ATL to develop windows applications - no one.

      If you were given the chance to develop a windows application, what would you choose - ATL or C#? You don't need to answer.

      Reply
    Leave a Comment
    • Your email address will not be published. All fields are required.

    Top White Papers and Webcasts

    • Webinar on September 23, 2014, 2 p.m. ET / 11 a.m. PT Mobile commerce presents an array of opportunities for any business -- from connecting with your customers through mobile apps to enriching operations with mobile enterprise solutions. Join guest speaker, Michael Facemire, Forrester Research, Inc. Principal Analyst, as he discusses the new demands of mobile engagement and how application program interfaces (APIs) play a crucial role. Check out this upcoming webinar to learn about the new set of …

    • On-demand Event Event Date: September 10, 2014 Modern mobile applications connect systems-of-engagement (mobile apps) with systems-of-record (traditional IT) to deliver new and innovative business value. But the lifecycle for development of mobile apps is also new and different. Emerging trends in mobile development call for faster delivery of incremental features, coupled with feedback from the users of the app "in the wild." This loop of continuous delivery and continuous feedback is how the best mobile …

    Most Popular Programming Stories

    More for Developers

    Latest Developer Headlines

    RSS Feeds