To WTL or Not to WTL, That Is the Question

The WTL is an extension to the ATL, developed by the same ATL team, and shipped by Microsoft with the January 2000 Platform SDK (available for download from Microsoft), although undocumented. The WTL extends the ATL windowing classes by providing a lightweight framework for writing Win32 applications and controls, specialized views, GDI objects, and utility classes.

The WTL itself consists of a relatively lightweight 750Kb of headers, plus three sample apps, and a Visual Studio WTL AppWizard. Compare that with the 1Mb of headers and implementation files required by ATL.

To install the WTL, all you have to do is:

  • Copy the WTL directory structure from the Platform SDK to the location of your choice.
  • Add the WTL\include directory to the list of include directories in VC++.
  • Copy the file appwiz\atlapp60.awx to the Custom App Wizard directory in the VC++ installation, %VCDIR%\Common\MSDev98\Template, where %VCDIR% is the directory where VC++ 6.0 is installed.

Design features of the WTL--and incidentally, advantages over the MFC--include:

  • Mostly templated, so results in smaller code size. For example, 24Kb for a hello world SDI app versus 440Kb for the MFC statically-linked equivalent, or 24Kb+1Mb dynamically linked.
  • No interdependencies and can be freely mixed with straight SDK code.
  • Does not enforce any particular application model, especially in comparison to the MFC application framework.

The range of WTL classes covers:

  • Standard controls (editboxes, listboxes, buttons, and others)
  • Common controls (including listview, treeview, progressbar, and updown,)
  • Internet Explorer (IE) controls (rebar, flat scrollbar, date-time picker, and others)
  • Command bar, menu, and update UI classes
  • Common dialogs
  • Property sheet and page classes
  • Frame windows, MDI frame and child frames, splitters, scrolling windows
  • DC and GDI object classes (pen, brush, bitmap, etc)
  • PrintInfo and Devmode classes
  • Utility classes: includes CPoint, CRect, CSize, and CString.

The WTL AppWizard offers you SDI, MDI, Multi-threaded SDI, and Dialog-based applications. Multi-SDI is like IE or Windows Explorer, where you seem to have multiple instances open, but they are just multiple views of the same process. The views can be generic CWindowImpl-based, or based on a Form, ListBox, Edit, ListView, TreeView, RichEdit, or HTML control. You can choose whether your application has a rebar, commandbar (like Windows CE), toolbar or statusbar. Your application can host ActiveX controls and can even be a COM server.

Hello WTL

In this exercise, we'll create a WTL equivalent of the simple "Hello World" app explained in my last article (see ATL Windows).
  1. Create a new WTL AppWizard application. Call it HelloWorld. From the WTL AppWizard Step1 dialog, accept all the defaults and click the Next button. At Step 2, once again, leave all the defaults again (including Toolbar, Rebar, Command Bar, Status Bar and view window), and click the Finish button. Now, build and run the application. You will have a very normal Win32 app with a fairly regular frame and view, menu, toolbar, status bar, and about box. Although the File|Exit, View|Toolbar, View|Statusbar, and Help|About menuitems/toolbar buttons work, the rest don't. On the other hand, the menu has icons corresponding to the toolbar buttons:
  2. Now examine the code. First, note there is a standard ATL CComModule global, initialized and terminated in the _tWinMain. Examine this function, and you will see that the only other work that _tWinMain does is to initialize the common controls through a call to InitCommonControlsEx and call the global wizard-generated Run function. The Run function creates the main frame window and a CMessageLoop object, calls ShowWindow on the mainframe, and then CMessageLoop::Run on the CMessageLoop object. This in turn essentially just calls good old GetMessage and DispatchMessage.
  3. Next, look at the CMainFrame class generated by the AppWizard. All the parent classes are in WTL\ATLFrame.h or WTL\ATLApp.h. The main functionality comes from CFrameWindowImpl.
  4. CUpdateUI is connected with the UPDATE_UI_MAP, and eventually to OnViewToolBar and OnViewStatusBar functions in our derived CMainFrame class. These do the expected ShowWindow and SetCheck behaviour.

    The inheritance from CMessageFilter and CIdleHandler means that the class must implement a filter that weeds out messages before they are dispatched for example, to change the way that keystrokes are handled), and an idle handler that is called when there aren't any messages in the queue.

    Also, both the derived view class and a CComandBarCtrl object are embedded child members of the frame.

    class CMainFrame : public CFrameWindowImpl<CMainFrame>, 
    public CUpdateUI<CMainFrame>,
     public CMessageFilter, 
    public CIdleHandler
    {
    public:
     DECLARE_FRAME_WND_CLASS(NULL, IDR_MAINFRAME)
    
     CHelloWorldView m_view;
     CCommandBarCtrl m_CmdBar;
    
     BEGIN_MSG_MAP(CMainFrame)
     MESSAGE_HANDLER(WM_CREATE, OnCreate)
      COMMAND_ID_HANDLER(ID_APP_EXIT, OnFileExit)
      COMMAND_ID_HANDLER(ID_FILE_NEW, OnFileNew)
      COMMAND_ID_HANDLER(ID_VIEW_TOOLBAR, OnViewToolBar)
      COMMAND_ID_HANDLER(ID_VIEW_STATUS_BAR, OnViewStatusBar)
      COMMAND_ID_HANDLER(ID_APP_ABOUT, OnAppAbout)
      CHAIN_MSG_MAP(CUpdateUI<CMainFrame>)
      CHAIN_MSG_MAP(CFrameWindowImpl<CMainFrame>)
     END_MSG_MAP()
    
     BEGIN_UPDATE_UI_MAP(CMainFrame)
      UPDATE_ELEMENT(ID_VIEW_TOOLBAR, UPDUI_MENUPOPUP)
      UPDATE_ELEMENT(ID_VIEW_STATUS_BAR, UPDUI_MENUPOPUP)
     END_UPDATE_UI_MAP()
    };
    
  5. The only significant function in the frame class is the OnCreate handler. This initializes the CComandBarCtrl object to attach the menu and load the command bar images (the icons on the menu). In effect, the CComandBarCtrl class converts a menu described by a menu resource into a toolbarmaking it easier to associate the same command IDs and images for menuitems and toolbar buttons. The frame then goes on to create a toolbar, a rebar and a statusbar. It then initializes the view. The final step is to add the frame's message filter and idle handler to the CComModule application object. .Message filtering is a technique to route a message between windows in your application after GetMessage pulls it off your queue but before it gets processed with Translate/DispatchMessage.
  6. LRESULT OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, 
    LPARAM /*lParam*/, BOOL& /*bHandled*/)
    {
     HWND hWndCmdBar = m_CmdBar.Create(m_hWnd, 
                           rcDefault, 
                           NULL, 
                           ATL_SIMPLE_CMDBAR_PANE_STYLE);
    
     m_CmdBar.AttachMenu(GetMenu());
     m_CmdBar.LoadImages(IDR_MAINFRAME);
     SetMenu(NULL);
     
     HWND hWndToolBar = CreateSimpleToolBarCtrl(m_hWnd, 
                            IDR_MAINFRAME, 
                            FALSE, 
                            ATL_SIMPLE_TOOLBAR_PANE_STYLE);
    
     CreateSimpleReBar(ATL_SIMPLE_REBAR_NOBORDER_STYLE);
     AddSimpleReBarBand(hWndCmdBar);
     AddSimpleReBarBand(hWndToolBar, NULL, TRUE);
     CreateSimpleStatusBar();
    
     m_hWndClient = m_view.Create(m_hWnd, rcDefault, NULL, 
     WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | 
     WS_CLIPCHILDREN, WS_EX_CLIENTEDGE);
    
     UIAddToolBar(hWndToolBar);
     UISetCheck(ID_VIEW_TOOLBAR, 1);
     UISetCheck(ID_VIEW_STATUS_BAR, 1);
    
     CMessageLoop* pLoop = _Module.GetMessageLoop();
     pLoop->AddMessageFilter(this);
     pLoop->AddIdleHandler(this);
    
     return 0;
    }
    
  7. The view class derive simply from CWindowImpl, and the wizard generates one message handler - for WM_PAINT - with the usual TODO comment:
  8. class CHelloWorldView: public CWindowImpl<CHelloWorldView>
    {
    public:
     DECLARE_WND_CLASS(NULL)
    
     BOOL PreTranslateMessage(MSG* pMsg)
     {
      pMsg;
      return FALSE;
     }
    
     BEGIN_MSG_MAP(CHelloWorldView)
      MESSAGE_HANDLER(WM_PAINT, OnPaint)
     END_MSG_MAP()
    
     LRESULT OnPaint(UINT /*uMsg*/, WPARAM /*wParam*/, 
     LPARAM /*lParam*/, BOOL& /*bHandled*/)
     {
      CPaintDC dc(m_hWnd);
    
      //TODO: Add your drawing code here
    
      return 0;
     }
    };
    
  9. At this point, make a trivial change. Add code to the OnPaint to TextOut the usual "Hello World" string. Build and test.

    Compare the ease of developing this WTL app as opposed to the previous ATL version. OK, some of the ease comes simply from having a wizard to do the grunt work. But don't forget all the extra goodies we've got for free - frame+view, cool menus, about box, toolbar and status bar, including show/hide functionality and Update-UI handling. Also, compare with the MFC equivalent - the MFC AppWizard will give you a menu, toolbar, statusbar and about box, but how easy is it to get toolbar button icons into a menu in the MFC? And, again, compare the sizes of the executable files, especially for a release build.

  10. Suppose we now want to emulate the ATL Scribble app from my last article...

    Well, if you right click on the view, select Add Windows Message Handler, and get a handler for WM_LBUTTONDOWN, the generated code would look like this:

  11. LRESULT OnLButtonDown(UINT uMsg, 
                          WPARAM wParam, 
                          LPARAM lParam, 
                          BOOL& bHandled)
    {
     return 0;
    }
    
    The message map entry should now look like this:
    MESSAGE_HANDLER(WM_LBUTTONDOWN, OnLButtonDown)
    

    Since we're using the ATL here, the wizard doesn't provide cracked messages in the way that the MFC does. "But hang on a sec," I hear you say, "Didn't your last article promise me WTL will crack messages for me?" Well, yes. In fact, WTL does supply a set of message-cracking macros, in ATLCRACK.H. If you examine this file, you'll see there is a macro for each Windows message. All you have to do is use the appropriate macro, and implement the handler with the corresponding signature. Plus, you have to use the BEGIN_MSG_MAP_EX macro instead of the usual BEGIN_MSG_MAP. The new macro provides a way for cracked handlers to retrieve the current message and specify if the message was handled or not. This is because the cracked handlers don't have the boolean argument of raw ATL handlers. Instead, BEGIN_MSG_MAP_EX defines an additional method, SetMessageHandled for the purpose.

    For example, consider the macro for WM_LBUTTONDOWN:

    #define MSG_WM_LBUTTONDOWN(func) \
     if (uMsg == WM_LBUTTONDOWN) \
     { \
      SetMsgHandled(TRUE); \
      func((UINT)wParam, CPoint(GET_X_LPARAM(lParam), \
     GET_Y_LPARAM(lParam))); \
      lResult = 0; \
      if(IsMsgHandled()) \
      return TRUE; \
     }
    

    Note that the use of CPoint requires the prior inclusion of ATLMISC.H.

  12. So, #include "atlmisc.h" and "atlcrack.h" at the top of your HelloWorldView.h, manually change your view's message map to the EX version, and add the following cracker macros and handlers. Remember, if you want to use the GDI objects such as CPen, you'll have to #include "atlgdi.h" s well as declare two CPoints m_startPoint and m_endPoint in the view and initialize in the constructor to (-1,-1).
  13. BEGIN_MSG_MAP_EX(CHelloWorldView)
     MSG_WM_LBUTTONDOWN(OnLButtonDown)
     MSG_WM_LBUTTONUP(OnLButtonUp)
     MSG_WM_MOUSEMOVE(OnMouseMove)
    END_MSG_MAP()
    
    LRESULT OnLButtonDown (UINT flags, CPoint point)
    {
     m_startPoint = point;
     return 0;
    }
    
    LRESULT OnLButtonUp (UINT flags, CPoint point)
    {
     m_startPoint.x = m_startPoint.y = -1;
     return 0;
    }
    
    LRESULT OnMouseMove (UINT flags, CPoint point)
    {
     m_endPoint = point;
    
     CClientDC dc(this->m_hWnd);
     CPen np;
     np.CreatePen(PS_SOLID, 2, RGB(255,0,0));
     HPEN op = dc.SelectPen(np.m_hPen);
    
     if (m_startPoint.x != -1 )
     {
      dc.MoveTo(m_startPoint.x, m_startPoint.y, NULL);
      dc.LineTo(m_endPoint.x, m_endPoint.y);
      m_startPoint.x = m_endPoint.x;
      m_startPoint.y = m_endPoint.y;
     }
    
     dc.SelectPen(op);
     return 0;
    }
    
  14. To add simple menu/toolbar support for changing the color of the pen as in the previous ATL article, just add a new menu, "Color" with three menu items "Red", "Green" and "Blue". Also add three corresponding toolbar buttons, and make sure they have the same IDs - this is normal behavior, after all. Code the command handlers to change a COLORREF member in the view. That's all you need to do to get the icons into the coolmenu. Remember, the coolbar is created based on the menu IDs and any corresponding toolbar buttons found. Add entries to the view's message map, and the corresponding handlers, like this:
  15. COMMAND_ID_HANDLER_EX(ID_COLOR_RED, OnColorRed)
    COMMAND_ID_HANDLER_EX(ID_COLOR_GREEN, OnColorGreen)
    COMMAND_ID_HANDLER_EX(ID_COLOR_BLUE, OnColorBlue)
    
    LRESULT OnColorRed(UINT, int, HWND)
    {
     m_color = RGB(255,0,0);
     return 0;
    }
    
    LRESULT OnColorGreen(UINT, int, HWND)
    {
     m_color = RGB(0,255,0);
     return 0;
    }
    
    LRESULT OnColorBlue(UINT, int, HWND)
    {
     m_color = RGB(0,0,255);
     return 0;
    }
    
  16. If you build at this point, you'll find that the coolmenu and toolbar display show up, but the messages don't get routed to the view class. Why is this? Well, as you may remember from Windows Programming 101, command messages originate at the frame class, so the coolmenu/toolbar command messages will be sent to the frame's message map. Since the ATL/WTL uses a somewhat different strategy for routing messages from one class to another from that used by the MFC (see my first article on ATL Windows), you'll have to add this to the frame's message map:
  17. CHAIN_MSG_MAP_MEMBER(m_view)
    

So some things are a little different from the MFC, although you can see the logical extension from the ATL. But what about the big picture? Serious developers shouldn't be too put off by a paucity of wizard support. On the other hand, coolmenus are cool, but are they really worth changing from the MFC? Well, bear in mind that WTL is just ATL++, and ATL is the serious developer's tool of choice for anything COM-related. Is this enough of a reason to use it? After all, WTL is undocumented.

The bottom line is that WTL is just a (sourcecode) extension to ATL, which brings up the question what support do you need? Internally, Microsoft has been using early versions of WTL for years because it produces such small, efficient applications, and finally both the ATL/WTL team at Microsoft and the ATL/WTL community at large (especially Developmentor: www.develop.com) are all committed to continuing support for WTL.

ATL/WTL won't replace MFC overnight, but so many projects could be produced faster, and run faster with less overhead, plus hooking into easy COM support, by choosing ATL/WTL instead of MFC. I've been using the MFC for 10 years, but the ATL/WTL combo is so compellingly seductive. If preserving our investment in older technology were the only criterion, we'd all still be writing COBOL, and admittedly some of us do, but where would you rather be?