Introduction
Lots of people thing that ATL is made only for making COM components. But in fact you can create full flash windows based application using ATL by using
the Windowing classes of ATL. Although you can convert your MFC based project to ATL but there is very little support
for UI component in ATL, so you have to write lots of code yourself. For
instance there is no Document/View in ATL and if you want to make it then you have to implement it yourself. In this part we are going to explore the windowing classes.
We also try to explore the techniques which ATL uses to do this. WTL (Window Template Library),
was untill now unsupported by Microsoft and is in fact one step forward towards making graphical application. WTL is based on ATL Windowing classes.
Before starting the discussion of any ATL based program let's start our discussion with
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, "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);
}
There is nothing new in this program. This program just displays a window and display Hello world at center of this.
ATL is object oriented library, 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. Ok 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 are should make, what are their relationship, method and properties. I am not planning to discuss the whole object oriented theory and the process here to make quality library. To make my task similar I make group of related API's and put those related API in one class. I put all the API's, which deals with window in one class and it can be repeated to other type of API's like for font, file, menu etc. So I made a small class and put the entire API's whose first parameter is
HWND
in that class. This class is nothing more than a thing wrapper on the Windows API's. 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 API's which are required at the moment. You may add all the API's in this class. The only advantage of this class is that now you don't have to pass the
HWND
parameter for windowing API's, this class will pass that parameter itself.
Well nothing special till now. But what about our Window CallBack function? Remember the first parameter of that call back function is also
HWND
, so it should be member of this class according to our criteria. 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 callback function in one field of WNDCLASS
or
WNDCLASSEX
structure. And you give it something like this after creating the object of
ZWindow
class.
ZWindow zwnd;
WNDCLASS wnd;
wnd.lpfnWndProc = wnd.WndProc;
But when you compile this program this will give 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 can not pass member function as a call back function. Why? Because in case of member function compiler automatically passes one parameter to the function and that parameter is the pointer of that class or in other words this pointer. So it means when you pass n parameters in the member function then compiler will pass n+1 parameters and the additional parameter is this pointer. The error message from the compiler shows this too that compiler can't convert member function to global function.
So what should we do if we want to use member function as a call back function? If we tell the compiler to not pass first parameter to the function some how then we can use the member function as a call back function. In C++ if we declare member function as a static then compiler doesn't pass this pointer, this is in fact the difference between static and non static member function.
So we made WndProc
static in ZWindow class. This technique is also use in case of threading where you want to use member function as a thread function then you make static member function as a thread function.
Here is update program which use 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, "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;
}
This program just shows the usage of ZWindow
. And to be very honest this class do nothing special. It is just wrapper on Window API, the only advantage you get from this is that now you don't need to pass
HWND
as a parameter, but instead of this now you have to type the object name when calling member function.
Like before this you call function like this
ShowWindow(hWnd, nCmdShow);
And now you call something like this
zwnd.ShowWindow(nCmdShow);
Not a big advantage till now.
Let's see how we can handle window message in the WndProc
. In the previous program we handle only one function i.e.
WM_DESTROY
. If we want to handle more messages then add more case 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 print "Hello World" in center of Window. But why use API's
BeginPaint
, GetClientRect
and EndPaint
when it all should be a member function of ZWindow class according to our criteria, because all of these have
HWND
as a first parameter.
Because all those function are not static. And you can't call non static member function from static member function. Why? Because the difference is this pointer, non static member function has this pointer and static function doesn't have it. If we somehow pass this pointer to static member function then we can call the non static member function from 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 i.e. store the address of ZWindow object in a global variable and then call non static member function from that pointer. Here is update version of the previous program in which we are not calling 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, "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;
}
So we finally we have working program. Now let's take the advantage of Object Oriented programming. If we call function on each message and made that function virtual then we can call those functions when we inherit 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 function. And when we inherit the class from ZWindow then we can override all those function which we want to customize. Here is a complete program which shows the usage of
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 a message "Hello world from Drive" in a window. Everything works fine until we work on one drive class. The problem started when we drive more than one class from ZWindow. Then all the message goes 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, "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;
}
The output of this program shows the same message box no matter on which window you click.
You get the same message box no matter you click on any window. This means message is not properly propagate to appropriate window. In fact each window has their own window procedure, which handles all the messages of that window. But here we use the call back function of second drive class with the first window, so we can't executer the message handler of first window.
Here our main problem is to associate the callback function of window with appropriate window. Means
HWND
should be associated with appropriate Drive class. So message should be go to 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 come in mind, which can be easily implemented is to make a global structure, which stores the
HWND
with appropriate Drive class address. But there are two main problems with this approach. Fist the structure becomes larger and larger when more and more window will be added in the program. And second problem is that there is of course searching time involve 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 as small as possible and as fast as possible. And this technique fails on both criteria. This method is not only slow but also consumes lots of memory when there are lots of window involves in the program.
The other possible solution is to use cbWndExtra
field of WNDCLASS
or
WNDCLASSEX
structure. There is still one question why not use cbClsExtra
instead of
cbWndExtra
? The answer is simple cbClsExtra
store the extra bytes for each class and
cbWndExtra
store extra byes for each window from the class. And you can create more than one window from the same class so if you use
cbClsExtra
then you can't distinguish the different window call back function from
cbClsExtra
because it is same of all those window which are create by the same class. And store the address of appropriate drive class in this field.
It seems good solution at lest seems better then the first one. But there are still two problems in this solution. The first one is that if user wants to use
cbWndExtra
then he/she might overwrite the data which is written by using this technique. So client of this class has to be careful not to lose that information when using
cbWndExtra
. Ok fine, you have decided and documented that not use cbWndExtra when using your library, but there is still one more problem. This method is not very much fast, again against the rule of ATL, that ATL should be as small and as fast as possible.
ATL neither use first method nor second. The method which ATL used is called Thunk. Thunk is a small set of code to do some work and this term is used in different context. It may be possible that you have listen two type of Thunking
Universal Thunking
Universal Thunking enables to call 32-bit function from 16-bit code. Available on both Win 9x and Win NT/2000/XP. This is also known as Generic Thunking.
General Thunking
Generic Thunking enables to call 16-bit function from 32-bit code. It is available only on Win 9x because Win NT/2000/XP are true 32 bit operating system so there is no logical reason to call 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 insert a small code to call the correct Window procedure.
Lets star some basic concepts before the study of thunking of ATL. 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 integer and character should be 5 not 8. Ok lets change a program 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? Change a program 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 word alignment of structure and union members. If you notice carefully then you can conclude this that each variable which is outside the structure is store at address which is dividable of 4. The reason of this is to increase the performance. So here structure allocate at the multiple of 4 i.e. at memory location 4683576 i.e. ch1 has the same address. Member ch2 store just next to this memory location and int I store at memory location 4683580, why not at 4683578, because this is not dividable of 4. Now there is a question what is at memory location 4683578 and 4683579? The answer is garbage if 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
Out put of this program clearly shows that those spaces contain garbage as show in the diagram.
Now if we want to avoid waste those space then what should we do? We have two option, either use the compiler switch /Zp or use #pragma statement before declare 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 now there is no space for word alignment. In fact ATL use this technique to make thunk. ATL use one structure, which are not using word alignment and use it to store direct machine code of microprocessor.
#pragma pack(push,1)
struct Thunk
{
BYTE m_jmp;
DWORD m_relproc;
};
#pragma pack(pop)
This type of structure can contain then 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)
struct Thunk
{
BYTE m_jmp;
DWORD m_relproc;
};
#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;
pC->Init(StaticFun, pC);
pFUN pFun = (pFUN)&(pC->m_thunk);
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 call StaticFun
ATL use the same technique to call the correct Callback function but did one more thing before calling the function. Now ZWindow has one more virtual function
ProcessWindowMessage
which doesn't do anything in this class. But every drive class of ZWindow override this to handle their own message. The process is 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 use the technique to replace the
HWND
with this pointer. But what about HWND
, do we loose it? In fact we have already store the
HWND
in ZWindow class member variable.
To achieve this we ATL use little bit bigger structure as compare to previous program.
#pragma pack(push,1)
struct _WndProcThunk
{
DWORD m_mov;
DWORD m_this;
BYTE m_jmp;
DWORD m_relproc;
};
#pragma pack(pop)
And at the time of initialization write 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;
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 initialize the thunk code, get the address of thunk and set the new call back function to thunk code. And then thunk code will call
WindowProc
, but now the first parameter is not HWND
, in fact 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.
The complete code of the following program is attached with the following article due to the length of code. Hope to explore the other mysterious of ATL in coming part of this series.