Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WTL for MFC Programmers, Part I - ATL GUI Classes

0.00/5 (No votes)
22 Dec 2005 51  
An introduction to WTL programming for MFC developers.

Contents

README.TXT

This is the stuff I want you to read first, before proceeding on or posting messages to this article's discussion board.

This series of articles originally covered WTL 7.0 and written for VC 6 users. Now that VC 8 is out, I felt it was about time to update the articles to cover VC 7.1. ;) (Also, the automatic 6-to-7 conversion done by VC 7.1 doesn't always go smoothly so VC 7.1 users could get stuck when trying to use the demo source code.) So as I go through and update this series, the articles will be updated to reflect new WTL 7.1 features, and I'll have VC 7.1 projects in the source downloads.

Important note for VC 2005 users: The Express edition of VC 2005 does not come with ATL (or MFC for that matter) so you can't build ATL or WTL projects with the Express version.

If you are using VC 6, then you need the Platform SDK. You can't use WTL without it. You can use the web install version, or download the CAB files or an ISO image and run the setup locally. Be sure you use the utility to add the SDK include and lib directories to the VC search path. You can find this in the Visual Studio Registration folder in the Platform SDK program group. It's a good idea to get the latest Platform SDK even if you're using VC 7 so you have the latest headers and libs.

You need WTL. Download version 7.1 from Microsoft. See the articles "Introduction to WTL - Part 1" and "Easy installation of WTL" for some tips on installing the files. Those articles are rather out-of-date now, but still contain some good info. The WTL distribution also has a readme file with installation instructions. One thing which I don't think is mentioned in those articles is how to add the WTL files to the VC include path. In VC 6, click Tools|Options and go to the Directories tab. In the Show directories for combo box, select Include files. Then add a new entry that points to the directory where you put the WTL header files. In VC 7, click Tools|Options, click Projects then VC++ Directories. In the Show directories for combo box, select Include files. Then add a new entry that points to the directory where you put the WTL header files.

Important: While we're on the subject of the VC 7 include path, you must make a change to the default directory list if you haven't updated your Platform SDK. Make sure that $(VCInstallDir)PlatformSDK\include is first in the list, above ($VCInstallDir)include, as shown here:

 [VC7 include path list - 26K]

You need to know MFC, and know it well enough that you understand what's behind the message map macros, and can edit the code marked "DO NOT EDIT" with no problems.

You need to know Win32 API programming, and know it well. If you learned Windows programming by going straight into MFC, without learning how messages work at the API level, you are unfortunately going to have trouble in WTL. If you don't know what a message's WPARAM and LPARAM mean, you should read other articles on API-level programming (there are lots of them here at CodeProject) so you understand.

You need to know C++ template syntax. See the VC Forum FAQ for links to C++ FAQs and template FAQs.

Since I haven't used VC 8 yet, I don't know if the sample code will compile on 8. Hopefully the 7-to-8 upgrade process will work better than the 6-to-7 process did. Please post on this article's forum if you have any trouble with VC 8.

Introduction to the Series

WTL rocks. It does. It has a lot of the power of MFC's GUI classes, yet produces substantially smaller executables. If you're like me, and learned GUI programming with MFC, you've become quite comfortable with all the control wrappers MFC provides, as well as with the flexible message handling built into MFC. If you're also like me and don't relish the idea of several hundred K of MFC framework getting added to your programs, WTL is for you. However, there are a few barriers we have to overcome:

  • ATL-style templates look weird at first.
  • No ClassWizard support, so writing message maps involves some manual labor.
  • No documentation in MSDN, you need to find it somewhere else, or even look at the WTL source.
  • No reference books you can buy and keep on your shelf.
  • It has that "not officially supported by Microsoft" stigma
  • ATL/WTL windowing is sufficiently different from MFC windowing that not all your knowledge transfers over.

On the other hand, the benefits of WTL are:

  • No complex doc/view framework to learn or work around.
  • Has some essential UI features from MFC, like DDX/DDV and "update command UI" functionality.
  • Actually improves on some MFC features (e.g., more flexible splitter windows)
  • Much smaller executables compared to a statically-linked MFC app.
  • You can apply bug fixes to WTL yourself and not affect existing apps (compare this with replacing the MFC/CRT DLLs to fix a bug in one app, and having other apps break)
  • If you still need MFC, MFC and ATL/WTL windows can co-exist peacefully (for a prototype at work, I ended up creating an MFC CFrameWnd that contained a WTL CSplitterWindow, which contained MFC CDialogs -- that wasn't me showing off, it was modifying existing MFC code but using the nicer WTL splitter).

In this series, I will first introduce the ATL windowing classes. WTL is, after all, a set of add-on classes to ATL, so a good understanding of ATL windowing is essential. Once I've covered that, I'll introduce WTL features and show how they make GUI programming a lot easier.

Introduction to Part I

WTL rocks. But before we get to why, we need to cover ATL first. WTL is a set of add-on classes to ATL, and if you've been strictly an MFC programmer in the past, you may never have encountered the ATL GUI classes. So please bear with me if I don't get to WTL right off the bat; the detour into ATL is necessary.

In this first part, I will give a little background on ATL, cover some essential points you need to know before writing ATL code, quickly explain those funky ATL templates, and cover the basic ATL windowing classes.

ATL Background

ATL and WTL history

The Active Template Library... kind of an odd name, isn't it? Old-timers might remember that it was originally the ActiveX Template Library, which is a more accurate name, since ATL's goal is to make writing COM objects and ActiveX controls easier. (ATL was also developed during the time that Microsoft was naming new products ActiveX-something, just as new products nowadays are called something.NET.) Since ATL is really about writing COM objects, it only has the most basic of GUI classes, the equivalent of MFC's CWnd and CDialog. Fortunately, the GUI classes are flexible enough to allow for something like WTL to be built on top of them.

WTL had two major revisions as a Microsoft-owned project, numbered 3 and 7. (The version numbers were chosen to match the ATL version numbers, that's why they're not 1 and 2.) Version 3.1 is obsolete now so it will not be covered in this series. Version 7.0 was a major update to version 3, and version 7.1 added some bug fixes and minor features.

Following version 7.1, Microsoft made WTL an open-source project, hosted at Sourceforge. The latest release from that site is version 7.5. I have not looked at 7.5 yet, so this series will not cover 7.5 at this time. (I'm already behind by two versions! I'll catch up eventually.)

ATL-style templates

Even if you can read C++ templates without getting a headache, there are two things ATL does that might trip you up at first. Take this class for example:

class CMyWnd : public CWindowImpl<CMyWnd>
{
...
};

That actually is legal, because the C++ spec says that immediately after the class CMyWnd part, the name CMyWnd is defined and can be used in the inheritance list. The reason for having the class name as a template parameter is so ATL can do the second tricky thing, compile-time virtual function calls.

To see this in action, look at this set of classes:

template <class T>
class B1
{
public: 
    void SayHi() 
    {
    T* pT = static_cast<T*>(this);   // HUH?? I'll explain this below

 
        pT->PrintClassName();
    }
 
    void PrintClassName() { cout << "This is B1"; }
};
 
class D1 : public B1<D1>
{
    // No overridden functions at all

};
 
class D2 : public B1<D2>
{
    void PrintClassName() { cout << "This is D2"; }
};
 
int main()
{
D1 d1;
D2 d2;
 
    d1.SayHi();    // prints "This is B1"

    d2.SayHi();    // prints "This is D2"

 
    return 0;
}

The static_cast<T*>(this) is the trick here. It casts this, which is of type B1*, to either D1* or D2* depending on which specialization is being invoked. Because template code is generated at compile-time, this cast is guaranteed to be safe, as long as the inheritance list is written correctly. (If you wrote class D3 : public B1<D2> you'd be in trouble.) It's safe because the this object can only be of type D1* or D2* (as appropriate), and nothing else. Notice that this is almost exactly like normal C++ polymorphism, except that the SayHi() method isn't virtual.

To explain how this works, let's look at each call to SayHi(). In the first call, the specialization B1<D1> is being used, so the SayHi() code expands to:

void B1<D1>::SayHi()
{
D1* pT = static_cast<D1*>(this);
 
    pT->PrintClassName();
}

Since D1 does not override PrintClassName(), D1's base classes are searched. B1 has a PrintClassName() method, so that is the one called.

Now, take the second call to SayHi(). This time, it's using the specialization B1<D2>, and SayHi() expands to:

void B1<D2>::SayHi()
{
D2* pT = static_cast<D2*>(this);
 
    pT->PrintClassName();
}

This time, D2 does contain a PrintClassName() method, so that is the one that gets called.

The benefits of this technique are:

  • It doesn't require using pointers to objects.
  • It saves memory because there is no need for vtbls.
  • It's impossible to call a virtual function through a null pointer at runtime because of an uninitialized vtbl.
  • All function calls are resolved at compile time, so they can be optimized.

While the saving of a vtbl doesn't seem significant in this example (it would only be 4 bytes), think of the case where there are 15 base classes, some of those containing 20 methods, and the savings adds up.

ATL Windowing Classes

OK, enough background! Time to dive into ATL. ATL is designed with a strict interface/implementation division, and that's evident in the windowing classes. This is similar to COM, where interface definitions are completely separate from an implementation (or possibly several implementations).

ATL has one class that defines the "interface" for a window, that is, what can be done with a window. This class is called CWindow. It is nothing more than a wrapper around an HWND, and it provides wrappers for almost all of the User32 APIs that take an HWND as the first parameter, such as SetWindowText() and DestroyWindow(). CWindow has a public member m_hWnd that you can access if you need the raw HWND. CWindow also has a operator HWND method, so you can pass a CWindow object to a function that takes an HWND. There is no equivalent to CWnd::GetSafeHwnd().

CWindow is very different from MFC's CWnd. CWindow objects are inexpensive to create, since there is only one data member, and there is no equivalent to the object maps that MFC keeps internally to map HWNDs to CWnd objects. Also unlike CWnd, when a CWindow object goes out of scope, the associated window is not destroyed. This means you don't have to remember to detach any temp CWindow objects you might create.

The ATL class that has the implementation of a window is CWindowImpl. CWindowImpl contains the code for such things as window class registration, window subclassing, message maps, and a basic WindowProc(). This is unlike MFC where everything is in one class, CWnd.

There are also two separate classes that contain the implementation of a dialog box, CDialogImpl and CAxDialogImpl. CDialogImpl is used for plain dialogs, while CAxDialogImpl is used for dialogs that host ActiveX controls.

Defining a Window Implementation

Any non-dialog window you create will derive from CWindowImpl. Your new class needs to contain three things:

  1. A window class definition
  2. A message map
  3. The default styles to use for the window, called the window traits

The window class definition is done using the DECLARE_WND_CLASS or DECLARE_WND_CLASS_EX macro. Both of these define an ATL struct CWndClassInfo that wraps the WNDCLASSEX struct. DECLARE_WND_CLASS lets you specify the new window class name and uses default values for the other members, while DECLARE_WND_CLASS_EX lets you also specify a class style and window background color. You can also use NULL for the class name, and ATL will generate a name for you.

Let's start out a new class definition, and I'll keep adding to it as we go through this section.

class CMyWindow : public CWindowImpl<CMyWindow>
{
public:
    DECLARE_WND_CLASS(_T("My Window Class"))
};

Next comes the message map. ATL message maps are much simpler than MFC maps. An ATL map expands into a big switch statement; the switch looks for the right handler and calls the corresponding function. The macros for the message map are BEGIN_MSG_MAP and END_MSG_MAP. Let's add an empty map to our window.

class CMyWindow : public CWindowImpl<CMyWindow>
{
public:
    DECLARE_WND_CLASS(_T("My Window Class"))
 
    BEGIN_MSG_MAP(CMyWindow)
    END_MSG_MAP()
};

I'll cover how to add handlers to the map in the next section. Finally, we need to define the window traits for our class. Window traits are a combination of window styles and extended window styles that are used when creating the window. The styles are specified as template parameters so the caller doesn't have to be bothered with getting the styles right when it creates our window. Here's a sample traits definition using the ATL class CWinTraits:

typedef CWinTraits<WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,
                   WS_EX_APPWINDOW> CMyWindowTraits;
 
class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CMyWindowTraits>
{
public:
    DECLARE_WND_CLASS(_T("My Window Class"))
 
    BEGIN_MSG_MAP(CMyWindow)
    END_MSG_MAP()
};

The caller can override the styles in the CMyWindowTraits definition, but generally this is not necessary. ATL also has a few predefined CWinTraits specializations, one of which is perfect for top-level windows like ours, CFrameWinTraits:

typedef CWinTraits<WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN |
                     WS_CLIPSIBLINGS,
                   WS_EX_APPWINDOW | WS_EX_WINDOWEDGE>
        CFrameWinTraits;

Filling in the message map

The ATL message map is one area that is lacking in developer-friendliness, and something that WTL greatly improves on. ClassView does at least give you the ability to add message handers, however ATL doesn't have message-specific macros and automatic parameter unpacking like MFC does. In ATL, there are just three types of message handlers, one for WM_NOTIFY, one for WM_COMMAND, and one for everything else. Let's start by adding handlers for WM_CLOSE and WM_DESTROY to our window.

class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CFrameWinTraits>
{
public:
    DECLARE_WND_CLASS(_T("My Window Class"))
 
    BEGIN_MSG_MAP(CMyWindow)
        MESSAGE_HANDLER(WM_CLOSE, OnClose)
        MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
    END_MSG_MAP()
 
    LRESULT OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
    {
        DestroyWindow();
        return 0;
    }
 
    LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
    {
        PostQuitMessage(0);
        return 0;
    }
};

You'll notice that the handlers get the raw WPARAM and LPARAM values; you have to unpack them yourself when a message uses those parameters. There is also a fourth parameter, bHandled. This parameter is set to TRUE by ATL before the handler is called. If you want ATL's default WindowProc() to handle the message as well after your handler returns, you set bHandled to FALSE. This is different than MFC, where you have to explicitly call the base-class implementation of a message handler.

Let's add a WM_COMMAND handler as well. Assuming our window's menu has an About item with ID IDC_ABOUT:

class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CFrameWinTraits>
{
public:
    DECLARE_WND_CLASS(_T("My Window Class"))
 
    BEGIN_MSG_MAP(CMyWindow)
        MESSAGE_HANDLER(WM_CLOSE, OnClose)
        MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
        COMMAND_ID_HANDLER(IDC_ABOUT, OnAbout)
    END_MSG_MAP()
 
    LRESULT OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
    {
        DestroyWindow();
        return 0;
    }
 
    LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
    {
        PostQuitMessage(0);
        return 0;
    }
 
    LRESULT OnAbout(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
    {
        MessageBox ( _T("Sample ATL window"), _T("About MyWindow") );
        return 0;
    }
};

Notice that the COMMAND_HANDLER macro does unpack the message parameters for you. The NOTIFY_HANDLER macro is similar, and unpacks the WM_NOTIFY message parameters.

Advanced Message Maps and Mix-in Classes

One major difference in ATL is that any C++ class can handle messages, unlike MFC where message-handling duties are split between CWnd and CCmdTarget, plus several classes that have a PreTranslateMessage() method. This ability lets us write what are commonly called "mix-in" classes, so that we can add features to our window simply by adding classes to the inheritance list.

A base class with a message map is usually a template that takes the derived class name as a template parameter, so it can access members of the derived class like m_hWnd (the HWND member in CWindow). Let's look at a mix-in class that paints the window background by handling WM_ERASEBKGND.

template <class T, COLORREF t_crBrushColor>
class CPaintBkgnd
{
public:
    CPaintBkgnd() { m_hbrBkgnd = CreateSolidBrush(t_crBrushColor); }
    ~CPaintBkgnd() { DeleteObject ( m_hbrBkgnd ); }
 
    BEGIN_MSG_MAP(CPaintBkgnd)
        MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBkgnd)
    END_MSG_MAP()
 
    LRESULT OnEraseBkgnd(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
    {
    T*   pT = static_cast<T*>(this);
    HDC  dc = (HDC) wParam;
    RECT rcClient;
 
        pT->GetClientRect ( &rcClient );
        FillRect ( dc, &rcClient, m_hbrBkgnd );
        return 1;    // we painted the background

    }
 
protected:
    HBRUSH m_hbrBkgnd;
};

Let's go through this new class. First, CPaintBkgnd has two template parameters: the name of the derived class that is using CPaintBkgnd, and a color to use for the background. (The t_ prefix is often used for template parameters that are plain values.)

The constructor and destructor are pretty simple, they create and destroy a brush of the color passed as t_crBrushColor. Next comes the message map, which handles WM_ERASEBKGND. Finally, there's the OnEraseBkgnd() handler which fills in the window with the brush created in the constructor. There are two things of note going on in OnEraseBkgnd(). First, it uses the derived class's window functions (namely GetClientRect()). How do we know that there even is a GetClientRect() in the derived class? The code wouldn't compile if there weren't! The compiler checks that the derived class T contains the methods that we call through the pT variable. Second, OnEraseBkgnd() has to unpack the device context from wParam.

To use this mix-in class with our window, we do two things. First, we add it to the inheritance list:

class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CFrameWinTraits>,
                  public CPaintBkgnd<CMyWindow, RGB(0,0,255)>

Second, we need to get CMyWindow to pass messages along to CPaintBkgnd. This is called chaining message maps. In the CMyWindow message map, we add the CHAIN_MSG_MAP macro:

class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CFrameWinTraits>,
                  public CPaintBkgnd<CMyWindow, RGB(0,0,255)> 
{
...
typedef CPaintBkgnd<CMyWindow, RGB(0,0,255)> CPaintBkgndBase;
 
    BEGIN_MSG_MAP(CMyWindow)
        MESSAGE_HANDLER(WM_CLOSE, OnClose)
        MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
        COMMAND_HANDLER(IDC_ABOUT, OnAbout)
        CHAIN_MSG_MAP(CPaintBkgndBase)
    END_MSG_MAP()
...
};

Any messages that reach the CHAIN_MSG_MAP line without being handled are passed on to the map in CPaintBkgnd. Note that WM_CLOSE, WM_DESTROY, and IDC_ABOUT will not be chained, because once those are handled, the message map search ends. The typedef is necessary because CHAIN_MSG_MAP is a preprocessor macro that takes one parameter; if we wrote CPaintBkgnd<CMyWindow, RGB(0,0,255)> as the parameter, the commas would make the preprocessor think that we're calling it with more than one parameter.

You could conceivably have several mix-in classes in the inheritance list, each with a CHAIN_MSG_MAP macro so that messages are passed to it. This is different from MFC, where each CWnd-derived class can have only one base class, and MFC automatically passes unhandled messages to the base class.

Structure of an ATL EXE

Now that we have a complete (if not entirely useful) main window, let's see how to use it in a program. ATL EXEs have one or more global variables that roughly correspond to the global CWinApp variable in an MFC program (normally called theApp). This area of ATL changed radically between VC6 and VC7, so I will cover the two versions separately.

In VC 6

An ATL executable contains a global CComModule variable that must be called _Module. Here's how we start our stdafx.h:

// stdafx.h:

#define STRICT
#define WIN32_LEAN_AND_MEAN
 
#include <atlbase.h>        // Base ATL classes

extern CComModule _Module;  // Global _Module

#include <atlwin.h>         // ATL windowing classes

atlbase.h will include the basic Windows headers, so there's no need to include windows.h, tchar.h, etc. In our CPP file, we declare the _Module variable:

// main.cpp:

CComModule _Module;

CComModule has explicit initialization and shutdown functions that we need to call in WinMain(), so let's start with those:

// main.cpp:

CComModule _Module;
 
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hInstPrev,
                   LPSTR szCmdLine, int nCmdShow)
{
    _Module.Init(NULL, hInst);
    _Module.Term();
}

The first parameter to Init() is only used in COM servers. Since our EXE is not a server, we can pass NULL. ATL doesn't provide its own WinMain() or message pump like MFC, so to get our program running, we create a CMyWindow object and add a message pump.

// main.cpp:

#include "MyWindow.h"

CComModule _Module;
 
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hInstPrev,
                   LPSTR szCmdLine, int nCmdShow)
{
    _Module.Init(NULL, hInst);
 
CMyWindow wndMain;
MSG msg;
 
    // Create & show our main window

    if ( NULL == wndMain.Create ( NULL, CWindow::rcDefault, 
                                 _T("My First ATL Window") ))
        {
        // Bad news, window creation failed

        return 1;
        }
 
    wndMain.ShowWindow(nCmdShow);
    wndMain.UpdateWindow();
 
    // Run the message loop

    while ( GetMessage(&msg, NULL, 0, 0) > 0 )
        {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
        }
 
    _Module.Term();
    return msg.wParam;
}

The only unusual thing in the above code is CWindow::rcDefault, which is a RECT member of CWindow. Using that as the window's initial RECT is like using CW_USEDEFAULT for the width and height with the CreateWindow() API.

Under the hood, ATL uses some assembly-language black magic to connect the main window's handle to its corresponding CMyWindow object. The upside of this is that there is no problem passing CWindow objects between threads, something that fails miserably with CWnds in MFC.

In VC 7

ATL 7 split up the module-management code into several classes. CComModule is still present for backwards-compatibility, but code written in VC 6 that is converted by VC 7 doesn't always compile cleanly (if at all) so I'll cover the new class here.

In VC 7, the ATL headers automatically declare global instances of all the module classes, and the Init() and Term() methods are called for you, so those manual steps aren't necessary. Our stdafx.h therefore looks like:

// stdafx.h:

#define STRICT
#define WIN32_LEAN_AND_MEAN
 
#include <atlbase.h>        // Base ATL classes

#include <atlwin.h>         // ATL windowing classes

The WinMain() function doesn't call any _Module methods, and looks like:

// main.cpp:

#include "MyWindow.h"

 
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hInstPrev,
                   LPSTR szCmdLine, int nCmdShow)
{
CMyWindow wndMain;
MSG msg;
 
    // Create & show our main window

    if ( NULL == wndMain.Create ( NULL, CWindow::rcDefault, 
                                 _T("My First ATL Window") ))
        {
        // Bad news, window creation failed

        return 1;
        }
 
    wndMain.ShowWindow(nCmdShow);
    wndMain.UpdateWindow();
 
    // Run the message loop

    while ( GetMessage(&msg, NULL, 0, 0) > 0 )
        {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
        }
 
    return msg.wParam;
}

Here's what our window looks like:

 [First ATL window - 4K]

Nothing particularly exciting, I'll admit. To spice it up, we'll add an About menu item that shows a dialog.

Dialogs in ATL

As mentioned earlier, ATL has two dialog classes. We'll use CDialogImpl for our about dialog. Making a new dialog class is almost like making a new frame window class; there are just two differences:

  1. The base class is CDialogImpl instead of CWindowImpl.
  2. You need to define a public member called IDD that holds the resource ID of the dialog.

Here is the start of a new class definition for an about dialog:

class CAboutDlg : public CDialogImpl<CAboutDlg>
{
public:
    enum { IDD = IDD_ABOUT };
 
    BEGIN_MSG_MAP(CAboutDlg)
    END_MSG_MAP()
};

ATL has no built-in handlers for the OK and Cancel buttons, so we need to code them ourselves, along with a WM_CLOSE handler that is called if the user clicks the close button in the title bar. We also need to handle WM_INITDIALOG so that the keyboard focus is set properly when the dialog appears. Here is the complete class definition with message handlers.

class CAboutDlg : public CDialogImpl<CAboutDlg>
{
public:
    enum { IDD = IDD_ABOUT };
 
    BEGIN_MSG_MAP(CAboutDlg)
        MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
        MESSAGE_HANDLER(WM_CLOSE, OnClose)
        COMMAND_ID_HANDLER(IDOK, OnOKCancel)
        COMMAND_ID_HANDLER(IDCANCEL, OnOKCancel)
    END_MSG_MAP()
 
    LRESULT OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
    {
        CenterWindow();
        return TRUE;    // let the system set the focus

    }
 
    LRESULT OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
    {
        EndDialog(IDCANCEL);
        return 0;
    }
 
    LRESULT OnOKCancel(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
    {
        EndDialog(wID);
        return 0;
    }
};

I used one handler for both OK and Cancel to demonstrate the wID parameter, which is set to either IDOK or IDCANCEL, depending on which button is clicked.

Showing the dialog is similar to MFC, you create an object of the new class and call DoModal(). Let's go back to our main window and add a menu with an About item that shows our new about dialog. We'll need to add two message handlers, one for WM_CREATE and one for the new menu item IDC_ABOUT.

class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CFrameWinTraits>,
                  public CPaintBkgnd<CMyWindow,RGB(0,0,255)>
{
public:
    BEGIN_MSG_MAP(CMyWindow)
        MESSAGE_HANDLER(WM_CREATE, OnCreate)
        COMMAND_ID_HANDLER(IDC_ABOUT, OnAbout)
        // ...

        CHAIN_MSG_MAP(CPaintBkgndBase)
    END_MSG_MAP()
 
    LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
    {
    HMENU hmenu = LoadMenu ( _Module.GetResourceInstance(),  // _AtlBaseModule in VC7

                             MAKEINTRESOURCE(IDR_MENU1) );
 
        SetMenu ( hmenu );
        return 0;
    }
 
    LRESULT OnAbout(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
    {
    CAboutDlg dlg;
 
        dlg.DoModal();
        return 0;
    }
    // ...

};

One small difference in modal dialogs is where you specify the dialog's parent window. In MFC you pass the parent to the CDialog constructor, but in ATL you pass it as the first parameter to DoModal(). If you don't specify a window, as in the code above, ATL uses the result of GetActiveWindow() (which will be our frame window) as the parent.

The LoadMenu() call also demonstrates one of the CComModule methods, GetResourceInstance(). This returns the HINSTANCE of a module that holds resources, similar to AfxGetResourceHandle(). The default behavior is to return the HINSTANCE of the EXE. (There is also CComModule::GetModuleInstance(), which functions like AfxGetInstanceHandle().)

Note that OnCreate() is different between VC6 and 7, which is due to the different module-management classes. GetResourceInstance() is now in CAtlBaseModule, and we call the global _AtlBaseModule object that ATL sets up for us.

Here is our revised main window and the about dialog:

 [About box - 5K]

I'll Get to WTL, I Promise!

But it will be in Part 2. Since I'm writing these articles for MFC developers, I felt that it was best to do an intro to ATL first, before diving into WTL. If this has been your first exposure to ATL, now might be a good time to write some simple apps on your own, so you get the hang of message maps and using mix-in classes. You can also experiment with ClassView's support for ATL message maps, as it can add message handlers for you. To get started in VC 6, right-click the CMyWindow item and pick Add Windows Message Handler on the context menu. In VC 7, right-click the CMyWindow item and pick Properties on the context menu. In the properties window, click the Messages button on the toolbar to see a list of window messages. You can add a handler for a message by going to its row, clicking the right column to turn it into a combo box, clicking the combo box arrow, then clicking the <Add> item in the drop-down list.

In Part II, I will cover the base WTL windowing classes, the WTL AppWizard, and the much nicer message map macros.

Copyright and license

This article is copyrighted material, (c)2003-2005 by Michael Dunn. I realize this isn't going to stop people from copying it all around the 'net, but I have to say it anyway. If you are interested in doing a translation of this article, please email me to let me know. I don't foresee denying anyone permission to do a translation, I would just like to be aware of the translation so I can post a link to it here.

The demo code that accompanies this article is released to the public domain. I release it this way so that the code can benefit everyone. (I don't make the article itself public domain because having the article available only on CodeProject helps both my own visibility and the CodeProject site.) If you use the demo code in your own application, an email letting me know would be appreciated (just to satisfy my curiosity about whether folks are benefiting from my code) but is not required. Attribution in your own source code is also appreciated but not required.

Revision History

  • March 22, 2003: Article first published.
  • December 15, 2005: Updated to cover ATL changes in VC 7.1.

Series Navigation: � Part II (WTL GUI Base Classes)

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here