A Quick MFC and WTL Comparison

By: Kenn Scribner for Visual C++ Developer


Download the code

Background: I really like working with ATL, but it would have been nice to work with the Win32 API in such a clear and simple manner. Now you can, just in time for this to be deprecated by WinForms. :) (WinForms are .NET's answer to traditional Windows thick client/desktop application programming.) But give WTL a try and see what you think.  If you like C++, templates, and Windows programming, you should like WTL.

A Quick MFC and WTL Comparison

As I write this, I had just completed a little utility for my wife that would help keep her dial-up network connected. We use ISDN to dial into her medical school, which places the majority of her lectures online. The problem she has with this is the remote server disconnects her while she's working through her lectures. It apparently monitors idle network activity, and after a very short inactive period logs the student off. This can be more than a mere irritation, especially when she's preparing for an exam.

So being the gregarious and loving spouse, I challenged myself to write in five minutes a little MFC application to solve this problem. The good news is I met the challenge, at least initially. As for the bad news, well...as with most projects, this utility grew a little, and soon I had spent an entire evening embellishing it! (To many, myself included, this isn't truly bad news as I had fun doing so!) But my wife is delighted and has since passed copies to her fellow students. I believe this is a good thing, especially if one of them elects to enter geriatrics--I may need their services some day!

What does this have to do with the Windows Template Library (WTL) and the Microsoft Foundation Classes (MFC)? The key lies in the phrase "passed copies" in my previous paragraph. For my wife to be able to pass copies of my MFC-based application to her friends, I had to statically compile the MFC libraries. Initially, I did so with debug-level code, and I was amazed to find my tiny application grew to over 2MB in size! Ouch! Even a release-level version consumed over 200K of disk space.

I then wondered how large would a similar application be if I implemented the same functionality in WTL. I can hardly write COM code without ATL, so why not take a deeper look into WTL for traditional Windows client code? Here is what I found, given the scenario I've outlined. Since WTL is not as commonly used as MFC, I'll also give you a brief tour through the WTL version of my utility application.

Getting to Know WTL

Chances are you haven't worked with WTL. There isn't space available in this article for me to adequately describe WTL in detail, but I can point you in a couple of directions for help. First, I've included WTL with the sample application's download to make it easier for you to find and use it. Second, there are several good online sources for WTL information, including source code, descriptions, tutorials, and other handy stuff. Here is a list of several URLs you should visit if you're interested in WTL:

Note: I patched the original WTL files with the revised code you'll find at  CodeProject to correct a few ugly and not so ugly bugs.

If you like, you can download the patched files here. To the best of my knowledge the patched files reflect the suggestions made at CodeProject, but I can accept no responsibility for the correctness of the patches. Use at your own discretion. However, I use them myself in my professional work, if that says anything...

Unfortunately, Microsoft considers WTL an unsupported project, so there is a significant lack of documentation coming from Redmond on the subject. But if you're looking for lean and mean Windows code but don't want to dip all the way down to the raw Windows API for everything, WTL is most likely a good alternative. The best news is that WTL consists of several header files (and an AppWizard), so you're free to modify the source code should you need to do so.

WTL and MFC, The Experiment

I wanted to compare apples to apples, and since I had implemented the utility in MFC first, I copied as much of that existing code to the WTL project as I could. The application itself consists of a dialog-based utility that wraps CreateProcess(). To keep the remote system from sensing inactivity, I "ping" a remote site at regular intervals. I could have written the ping functionality myself, using icmp.dll, but to meet my five-minute challenge, I decided to simply simulate this command line at the DOS prompt:

> ping www.endurasoft.com

If you're not familiar with ping, it's a utility that prompts the remote host for status information. The ping program reads the returning network packets and determines if the remote host is available, and if it is, how long was the roundtrip latency. The term ping stands for packet internet groper.

Any system with network capability will have ping.exe loaded, so barring catastrophic conditions, CreateProcess() should be able to locate ping.exe, run it, and generate enough spurious network traffic to keep the remote host happy for awhile. Of course I selected a well-known Internet URL to ping (well known to me, anyway).

When handling WM_INITDIALOG, both versions of the Pinger utility start the timer and issue an initial round of pings. After the initial pings are made, the timer is configured to issue the WM_TIMER message after the user-selectable period has expired. To keep things a bit simpler, I elected to single-thread the application, in which case various buttons are disabled while the ping process is in progress.

WTL and MFC, The Results

The only quantitative result I was interested in determining was to compare compiled executable file sizes. This application is not computationally intensive, so I didn't consider runtime latencies to be a critical piece of information. Even so, there are several qualitative conclusions we can draw from the file sizes, which I address in the next section.

The resulting file sizes for pinger.exe are shown in Table 1. Note the MFC versions include statically linked MFC and C runtime libraries (for easy distribution). These files were built using Visual Studio, Version 6, Service Pack 4 running under Windows 2000 Server. The WTL versions do not require any additional WTL support libraries.

Table 1--Relative Compiled File Sizes MFC/WTL

Version Size (in KB on disk)
MFC Debug 2030
MFC Release 212
WTL Debug 420
WTL Release 56

WTL and MFC, The Conclusions

For each case, the WTL executable image is roughly 4 times smaller than the MFC image (3.79 times smaller for release, 4.83 times smaller for debug). Granted, this isn't statistically accurate, so we can't draw generalized results for each and every possible WTL and MFC application. However, it does underscore the fact that WTL, like ATL, can be used for creating Windows executables with very small footprints very quickly.

I should note that you could make the MFC executables smaller yet if you used the MFC libraries dynamically. However, this masks the true size of your application. The MFC DLLs are 908 and 972 KB in size (respectively) for debug and release on my system. You are then also responsible for supplying the DLLs to your user base, and as anyone who has written installation programs and installed MFC DLLs knows, this is more than a little challenging to do correctly. It is dangerous to assume the MFC DLLs will be in place for you if you don't install them yourself. The unfortunate part of this is you can very easily damage a user's computer by installing incorrect MFC DLLs, such as those found in the initial release of Visual Studio, Version 6.0.

WTL precludes all of this, and if you keep the ATL_MIN_CRT directive in place as you compile, you won't even need the C Runtime Library. In that case you also won't need to distribute msvcrt.dll, although in many practical cases you can avoid distributing this DLL. It has not changed much over the years (although professional installations do install it as a matter of course if it is required).

So if WTL is so wonderful, why isn't everyone using it? Laying the argument for thin clients aside, part of the answer lies in the fact WTL was only recently introduced. In fact, the first place it became available to the developer community was with the Platform SDK for Windows 2000 (not part of the standard Platform SDK). It came as part of a code sample...very little fanfare for such an evolutionary companion to ATL 3.0. But it's also true Visual Studio supports MFC application development to a greater degree. While there is some ATL support present in Visual Studio, most MFC developers are quite familiar with ClassWizard even though there is a rough equivalent for ATL (and WTL).

MFC also has evolved to the point it supports a great many desirable features, such as MAPI, support for the other Windows logo requirements, sockets, documents (if you like and/or use that pattern), and compound document files. WTL has its share of cool features, but MFC is the clear feature champ. Both environments support framed main window architectures (frame window with separate view window), SDI and MDI applications, split windows, dialog-based applications, and various COM-based classes for COM support.

If you had asked me two years ago which development tool I'd use to create full-featured client-side applications, I would have told you I'd use MFC. However, even given the loss of some of the features provided by MFC, today I'd use WTL to create professional client applications. Not having to install the MFC DLLs on a user's system is a benefit that far outweighs the MFC feature loss, at least in my opinion.

The WTL Pinger Utility

To help introduce you to the WTL, I'll briefly describe the WTL version of my ping utility. I created the WTL Pinger version using the WTL AppWizard, which you'll find included with the WTL files included with the source code download. Like the MFC application AppWizard, you can select from an assortment of application styles, as you see from Figure 1.

I selected a dialog-based application, which led to the second AppWizard page you see in Figure 2.

With the files initially created, I created the utility's user interface, which I've shown in Figure 3.

The WTL AppWizard creates fewer files than the MFC AppWizard, and in this particular case, I made modifications to maindlg.h, resource.h, and pinger.rc. I left the remaining WTL source files unchanged.

As far as the resource file pinger.rc was concerned, I removed the extraneous strings from the string table, updated the version information, removed the (unnecessary) accelerator table, and modified the dialog box to match the MFC version's user interface. I naturally made corresponding changes to resource.h. These items should not be present in a dialog-based application, which I believe to be a bug in the AppWizard.

The bulk of the work is shown in Listing 1, where you see the contents of maindlg.h.

Listing 1. Pinger's maindlg.h File Contents

// maindlg.h : interface of the CMainDlg class

//

/////////////////////////////////////////////////////////////////////////////

#if !defined(AFX_MAINDLG_H__DC16127C_44F9_4BCF_BDAB_52E20D803699__INCLUDED_)

#define AFX_MAINDLG_H__DC16127C_44F9_4BCF_BDAB_52E20D803699__INCLUDED_

 

#if _MSC_VER >= 1000

#pragma once

#endif // _MSC_VER >= 1000

 

class CMainDlg : public CAxDialogImpl<CMainDlg>, CUpdateUI<CMainDlg>,

                 public CMessageFilter, public CIdleHandler

{

   public:

   enum { IDD = IDD_MAINDLG };

   virtual BOOL PreTranslateMessage(MSG* pMsg)

   {

      return IsDialogMessage(pMsg);

   }

 

   virtual BOOL OnIdle()

   {

      return FALSE;

   }

 

   BEGIN_MSG_MAP(CMainDlg)

      MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)

      COMMAND_ID_HANDLER(IDOK, OnOK)

      COMMAND_ID_HANDLER(IDCANCEL, OnCancel)

      COMMAND_ID_HANDLER(IDC_PINGNOW, OnPingNow)

      MESSAGE_HANDLER(WM_SYSCOMMAND, OnSysCommand)

      MESSAGE_HANDLER(WM_TIMER, OnTimer)

   END_MSG_MAP()

 

   BEGIN_UPDATE_UI_MAP(CMainDlg)

   END_UPDATE_UI_MAP()

 

LRESULT OnInitDialog(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)

{

   // center the dialog on the screen

   CenterWindow();

 

   // set icons

   HICON hIcon = (HICON)::LoadImage(_Module.GetResourceInstance(),

                                    MAKEINTRESOURCE(IDR_MAINFRAME),

                                    IMAGE_ICON,

                                    ::GetSystemMetrics(SM_CXICON),

                                    ::GetSystemMetrics(SM_CYICON),

                                    LR_DEFAULTCOLOR);

   SetIcon(hIcon, TRUE);

   HICON hIconSmall = (HICON)::LoadImage(_Module.GetResourceInstance(),

                                         MAKEINTRESOURCE(IDR_MAINFRAME),

                                         IMAGE_ICON,

                                         ::GetSystemMetrics(SM_CXSMICON),

                                         ::GetSystemMetrics(SM_CYSMICON),

                                         LR_DEFAULTCOLOR);

   SetIcon(hIconSmall, FALSE);

 

   // Add "About..." menu item to system menu.

   // IDM_ABOUTBOX must be in the system command range.

   _ASSERTE((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);

   _ASSERTE(IDM_ABOUTBOX < 0xF000);

   CMenu SysMenu = GetSystemMenu(FALSE);

   if(::IsMenu(SysMenu))

   {

      TCHAR szAboutMenu[256];

      if(::LoadString(_Module.GetResourceInstance(), IDS_ABOUTBOX, szAboutMenu, 255) > 0)

      {

         SysMenu.AppendMenu(MF_SEPARATOR);

         SysMenu.AppendMenu(MF_STRING, IDM_ABOUTBOX, szAboutMenu);

      }

   }

   SysMenu.Detach();

 

   // register object for message filtering and idle updates

   CMessageLoop* pLoop = _Module.GetMessageLoop();

   pLoop->AddMessageFilter(this);

   pLoop->AddIdleHandler(this);

   UIAddChildWindowContainer(m_hWnd);

 

   // Subclass and initialize the controls

   m_CURL.Attach(GetDlgItem(IDC_URLTXT));

   m_CURL.SetLimitText(MAX_PATH);

   m_CURL.SetWindowText("www.endurasoft.com");

 

   m_CRateSpin.Attach(GetDlgItem(IDC_RATESPIN));

   m_CRateSpin.SetRange(20,60);

   m_CRateSpin.SetPos(30);

   m_CRateSpin.Invalidate();

 

   // Start the timer...first pass is a 1 second delay

   // before first ping. :)

   m_iTimerID = SetTimer(1,1000);

 

   // Check for problems

   if ( m_iTimerID == -1 ) {

      // Couldn't get a timer, so ask to continue...user

      // will have to ping at will using "Ping Now" button.

      TCHAR szErrMsg[MAX_PATH];

      if( ::LoadString(_Module.GetResourceInstance(),IDS_E_NOTIMER,szErrMsg,MAX_PATH ) > 0) {

         // Error string loaded, so display error message

         int iResponse = MessageBox(szErrMsg,"Error!",MB_YESNO|MB_ICONERROR);

         if ( iResponse == IDNO ) EndDialog(-1);

      } // if

   } // if

   return TRUE;

}

LRESULT OnAppAbout(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)

{

   CAboutDlg dlg;

   dlg.DoModal();

   return 0;

}

LRESULT OnOK(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/)

{

   // TODO: Add validation code

   CloseDialog(wID);

   return 0;

}

LRESULT OnCancel(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/)

{

   CloseDialog(wID);

   return 0;

}

void CloseDialog(int nVal)

{

   // Kill timer

   if ( m_iTimerID != -1 ) KillTimer(m_iTimerID);

 

   // Take down the dialog

   DestroyWindow();

   ::PostQuitMessage(nVal);

}

LRESULT OnSysCommand(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& bHandled)

{

   UINT uCmdType = (UINT)wParam;

   if((uCmdType & 0xFFF0) == IDM_ABOUTBOX)

   {

      CAboutDlg dlg;

      dlg.DoModal();

   }

   else

      bHandled = FALSE;

 

   return 0;

}

LRESULT OnPingNow(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/)

{

   // Do the ping

   DoPing();

   return 0;

}

LRESULT OnTimer(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)

{

   // Check status of suspend checkbox, and if not

   // suspended, ping

   if ( IsDlgButtonChecked(IDC_SUSPEND) == BST_UNCHECKED ) DoPing();

   return 0;

}

 

// Helpers

protected:

   BOOL CreatePing()

   {

      // Get the command line formatting string

      TCHAR szCmd[MAX_PATH+1] = {0};

      ::LoadString(_Module.GetResourceInstance(),IDS_PINGCMD,szCmd,MAX_PATH);

 

      // Get the URL

      TCHAR szURL[MAX_PATH+1] = {0};

      m_CURL.GetWindowText(szURL,MAX_PATH);

 

      // Formulate a command line

      TCHAR szCmdLine[(2*MAX_PATH)+1] = {0};

      wsprintf(szCmdLine,szCmd,szURL);

 

      // Create the ping process and wait for a quit

      STARTUPINFO si = {0};

      si.cb = sizeof(si);

      si.wShowWindow = SW_HIDE;

      BOOL bCreated = CreateProcess(NULL,szCmdLine,

                                    NULL,NULL,

                                    FALSE,DETACHED_PROCESS,

                                    NULL,NULL,

                                    &si,&m_pi);

 

   // Check for problems...

   if ( !bCreated ) {

      // Could not create the process

      TCHAR szErrMsg[MAX_PATH];

      if( ::LoadString(_Module.GetResourceInstance(),IDS_E_NOPROCESS,szErrMsg,MAX_PATH ) > 0) {

         // Error string loaded, so display error message

         int iResponse = MessageBox(szErrMsg,"Error!",MB_YESNO|MB_ICONERROR);

         if ( iResponse == IDNO ) EndDialog(-1);

      } // if

   } // if

 

   return bCreated;

}

 

void DoPing()

{

   // Kill the timer

   KillTimer(m_iTimerID);

   m_iTimerID = -1;

 

   // Disable the cancel and ping buttons

   CWindow wCancelBtn(GetDlgItem(IDCANCEL));

   wCancelBtn.EnableWindow(FALSE);

   CWindow wPingNowBtn(GetDlgItem(IDC_PINGNOW));

   wPingNowBtn.EnableWindow(FALSE);

 

   // Create the ping process

   CreatePing();

 

   // Wait for it to complete

   if ( WaitForSingleObject(m_pi.hProcess,60000) == WAIT_TIMEOUT ) {

      // Error: hung process, so kill it

      TerminateProcess(m_pi.hProcess,-1);

 

      // Ask to continue (we already killed the ping...)

      TCHAR szErrMsg[MAX_PATH];

      if( ::LoadString(_Module.GetResourceInstance(),IDS_E_HUNGPING,szErrMsg,MAX_PATH ) > 0) {

         // Error string loaded, so display error message

         int iResponse = MessageBox(szErrMsg,"Error!",MB_YESNO|MB_ICONERROR);

         if ( iResponse == IDNO ) {

            EndDialog(-1);

            return;

         } // if

      } // if

   } // if

 

   // Enable the cancel and ping buttons

   wCancelBtn.EnableWindow(TRUE);

   wPingNowBtn.EnableWindow(TRUE);

 

   // Start the timer

   m_iTimerID = SetTimer(1,m_CRateSpin.GetPos()*1000);

 

   // Check for problems

   if ( m_iTimerID == -1 ) {

      // Couldn't get the timer...

      TCHAR szErrMsg[MAX_PATH];

      if( ::LoadString(_Module.GetResourceInstance(),IDS_E_NOTIMER,szErrMsg,MAX_PATH ) > 0) {

         // Error string loaded, so display error message

         int iResponse = MessageBox(szErrMsg,"Error!",MB_YESNO|MB_ICONERROR);

         if ( iResponse == IDNO ) EndDialog(-1);

      } // if

   } // if

}

 

// Attributes

   UINT m_iTimerID;

   PROCESS_INFORMATION m_pi;

 

// Controls

   CContainedWindowT<WTL::CEdit> m_CURL;

   CContainedWindowT<WTL::CUpDownCtrl> m_CRateSpin;

};

 

/////////////////////////////////////////////////////////////////////////////

//{{AFX_INSERT_LOCATION}}

// Microsoft Visual C++ will insert additional declarations immediately before the previous line.

#endif // !defined(AFX_MAINDLG_H__DC16127C_44F9_4BCF_BDAB_52E20D803699__INCLUDED_)

The code in Listing 1 probably looks scarier than it really is. The WTL AppWizard automatically created a lot of it. I did make changes to some of the wizard-generated code. For example, to OnInitDialog(), I added the invocation of the "about" dialog to the system menu, so the WTL application would look and feel like its MFC counterpart:

// Add "About..." menu item to system menu.
// IDM_ABOUTBOX must be in the system command range.
_ASSERTE((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
_ASSERTE(IDM_ABOUTBOX < 0xF000);
CMenu SysMenu = GetSystemMenu(FALSE);
if(::IsMenu(SysMenu))
{
   TCHAR szAboutMenu[256];
   if(::LoadString(_Module.GetResourceInstance(), IDS_ABOUTBOX, szAboutMenu, 255) > 0)
   {
      SysMenu.AppendMenu(MF_SEPARATOR);
      SysMenu.AppendMenu(MF_STRING, IDM_ABOUTBOX, szAboutMenu);
   }
}
SysMenu.Detach();

I actually snatched this code from one of the WTL samples. Note I did have to create a definition for IDM_ABOUTBOX in resource.h. The other code I added subclassed the edit and spin controls as well as initiated the Windows timer:

// Subclass and initialize the controls
m_CURL.Attach(GetDlgItem(IDC_URLTXT));
m_CURL.SetLimitText(MAX_PATH);
m_CURL.SetWindowText("www.endurasoft.com");

 

m_CRateSpin.Attach(GetDlgItem(IDC_RATESPIN));
m_CRateSpin.SetRange(20,60);
m_CRateSpin.SetPos(30);
m_CRateSpin.Invalidate();

 

// Start the timer...first pass is a 1 second delay
// before first ping. :)
m_iTimerID = SetTimer(1,1000);

 

// Check for problems
if ( m_iTimerID == -1 ) {
   // Couldn't get a timer, so ask to continue...user
   // will have to ping at will using "Ping Now" button.
   TCHAR szErrMsg[MAX_PATH];
   if( ::LoadString(_Module.GetResourceInstance(),IDS_E_NOTIMER,szErrMsg,MAX_PATH ) > 0) {
      // Error string loaded, so display error message
      int iResponse = MessageBox(szErrMsg,"Error!",MB_YESNO|MB_ICONERROR);
      if ( iResponse == IDNO ) EndDialog(-1);
   } // if
} // if

As I'm using a timer, I had to be sure it was killed when the application exits. I therefore added this code to CloseDialog() (another AppWizard-generated method):

// Kill timer
if ( m_iTimerID != -1 ) KillTimer(m_iTimerID);

Since I added the option to invoke the "about" dialog to the system menu, I had to add a WM_SYSCOMMAND message handler:

LRESULT OnSysCommand(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& bHandled)
{
   UINT uCmdType = (UINT)wParam;
   if((uCmdType & 0xFFF0) == IDM_ABOUTBOX)
   {
       CAboutDlg dlg;
       dlg.DoModal();
   }
   else
      bHandled = FALSE;

 

   return 0;
}

Note that you also must add the command to your message map:

MESSAGE_HANDLER(WM_SYSCOMMAND, OnSysCommand)

Since I added a button to ping immediately, I needed a handler for that:

LRESULT OnPingNow(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
{
   // Do the ping
   DoPing();
   return 0;
}

This handler delegates the work to a helper method DoPing(). The handler for WM_TIMER follows a similar logical vein, except that it checks to see if pings have been suspended:

LRESULT OnTimer(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
   // Check status of suspend checkbox, and if not
   // suspended, ping
   if ( IsDlgButtonChecked(IDC_SUSPEND) == BST_UNCHECKED ) DoPing();
   return 0;
}

The helper methods DoPing() and CreatePing() were relatively unchanged between the MFC and the WTL versions. The minor changes I did have to make involved the message box code to display error conditions and the formulation of the command line string. I didn't use the WTL CString class so I could avoid the C Runtime Library (since I could). Note you could use the STL string class in its place if you still wanted smart string support.

Wrapping Up

I've been using MFC professionally for over five years and have come to know it well. But I have to admit WTL is awesome and well worth a professional C++ developer's time to examine and evaluate. With WTL you're able to create full-fledged Windows applications that rival the size of pure SDK apps with the speed and flexibility of the C++ template. You do lose a bit of functionality as compared to MFC, but I believe you also gain a bit with a simpler installation pattern. I can't say I won't write more MFC-based applications, but I can say that if I write any more professional applications I intend to distribute, they'll be written using the WTL.

Comments? Questions? Find a bug? Please send me a note!


[Back] [Left Arrow] [Right Arrow][Home]