Introduction
Smasher! is a simple video game developed using WTL. It is reminiscent of
arcade-style video games like "Whack The Gopher". The objective of the game is
to smash Debtz bugs using a smasher hammer. The game does not use DirectX or
OpenGL. Our primary development goals were to build a small, self-contained
executable that can be easily distributed by email or FTP, does not require a
lot of supporting libraries, and runs on operating systems from Windows 95
forward. WTL helped us reach those goals.
This article discusses how we implemented dialog skins, transparent bitmaps,
a memory leak checker, a mechanism for user option settings, and a
self-contained help system. It also discusses the WTL Release build problem.
Dialog Skinning
Skinned applications are quite popular and there are several ways to
implement them. We chose to adapt a method devised by Bjarke Viksoe. He has
created a number of excellent WTL controls and a visit to his website,
www.viksoe.dk, is highly recommended.
The basic premise is to create a patterned background brush and provide it
in response to WM_CTLCOLORDLG
messages for the dialog. Other
WM_CTLCOLOR...
message handlers do the same thing for other
controls. Our adaptation creates the pattern brush in the application
Run()
function, just after the dialog is created but before it is
shown, and assigns it to the dialog class using SetClassLong()
as follows:
SetClassLong( dlgMain.m_hWnd, GCL_HBRBACKGROUND,
(LONG_PTR)CreatePatternBrush(AtlLoadBitmapImage(IDB_BACKGROUND,
LR_DEFAULTCOLOR | LR_SHARED)) );
There are several things happening here. A bitmap resource, IDB_BACKGROUND,
is loaded from resources to create a pattern brush. The brush is assigned to
the dialog class as the class background brush. After the brush is loaded in
this way, it is available to all dialogs of the dialog class, #32770. The
control color dialog message handler provides the brush in a manner similar to
that below.
WNDCLASSEX m_wc;
LRESULT OnCtlColorDlg(UINT, WPARAM, LPARAM lParam, BOOL&)
{
GetClassInfoEx(_Module.GetModuleInstance(), "#32770", &m_wc);
return (LRESULT)m_wc.hbrBackground; }
Message handlers for dialogs and static controls are wrapped in a template,
CDialogSkin< >
and typedef'd as CBackground
for the
Smasher! application. Each dialog that uses the skin adds
CBackground
to its inheritance list and chains the skin's message
map. The chained map is first in the message map list, like this:
BEGIN_MSG_MAP(COptionsDlg)
CHAIN_MSG_MAP(CBackground)
...
END_MSG_MAP()
Transparent Bitmap Static
Many methods have been devised for displaying transparent bitmaps. We
elected to port Chris Becke's excellent implementation to WTL. His method is
presented in depth in Part 2 of his tutorial: Bitmap Basics - A GDI
tutorial.
CSGTransStatic
, our class for transparent bitmaps static and
picture controls, uses an image bitmap and a mask bitmap for transparency.
It would be nice if one could assign an image list, with its built-in
transparency, to any Windows control, but that is not possible.
The bitmap and its mask are created and assigned to member variables
when the control is subclassed or when the bitmap is changed. Then, in
OnPaint()
, the two images are blitted onto the device context using
SRCAND
followed by SRCPAINT
:
...
hdcMem->SelectBitmap(hbmMask);
hdc.BitBlt(0, 0, bm.bmWidth, bm.bmHeight, hdcMem->m_hDC, 0, 0, SRCAND);
hdcMem->SelectBitmap(hbmImage);
hdc.BitBlt(0, 0, bm.bmWidth, bm.bmHeight, hdcMem->m_hDC, 0, 0, SRCPAINT);
...
A Note On Porting To WTL
Porting to WTL from Win32 is fairly straightforward. The key thing
to remember is that WTL often tries to protect you from yourself. By this we
mean that it wraps Windows objects safely (most of the time) and does much
of the messy work of loading structures and destroying objects. For example,
in Win32, you declare a paint structure and issue BeginPaint
and
EndPaint
instructions:
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
...
EndPaint(hwnd, &ps);
The WTL class CPaintDC
performs that for you. WTL classes
usually take care of window and device context handles, too. For example,
ShowWindow(hwnd, SW_SHOW)
in Win32 translates to
ShowWindow(SW_SHOW)
in WTL.
The "Poor Man's" Leak Checker
When developing an application that relies heavily on bitmaps, cursors, and
other GDI resources, one quickly discovers the value of checking for memory
leaks. VC++ provides a painless way to do so in Debug mode. Simply include
crtdbg.h
in the application source file (we put it at the top
of smasher.cpp, before stdafx.h
):
#if _DEBUG
#include < crtdbg.h >
#endif
Next, place this function call as the very first thing in
_tWinMain()
:
#if _DEBUG
_CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_CHECK_ALWAYS_DF |
_CRTDBG_LEAK_CHECK_DF );
#endif
While Bounds Checker and other commercial products certainly have their
place, this built-in capability alerted us to several memory leaks.
Handling User Options
Smasher! has three user options. Users may change the amount of play time,
the game's difficulty level, and the smasher size (i.e. the cursor). The options
class, CSGOptions
, follows the model of WTL's
CAppModule
and is defined as a global variable in the application
CPP. Three member variables are loaded from and saved to the registry. The
following code snippet shows how a spin control for difficulty level is set in
the Options dialog:
_Options.ReadFromRegistry();
m_spinDifficulty = GetDlgItem(IDC_SPIN_DIFFICULTY);
m_spinDifficulty.SetRange(1, 10);
m_spinDifficulty.SetPos(_Options.g_Difficulty);
Here, the value for the smasher size is extracted from its radio group. This
code is only called if the user clicked OK; otherwise changes are discarded.
if (IsDlgButtonChecked(IDC_SSIZEBIG) == BST_CHECKED)
_Options.g_SmasherSize = SMSH_BIG;
_Options.WriteToRegistry();
Resource Management
When the lead tester got her hands on Smasher!, it did not take long for
her to crash the application. This was a reminder of one of the pitfalls of
developing software on Windows NT: Different Windows OSes do not handle system
resources the same way. The game ran smoothly on Windows NT, swapping cursors
in and out, loading bitmaps, very much enjoying itself. However, it quickly ran
out of resources on the tester's Windows ME system.
Fortunately, a bit of research and the use of WTL helpers solved the
problem. WTL offers a set of resource loaders for cursors, bitmaps, etc. Their
general form is AtlLoadSomething
. We use the WTL helpers in all
cases where resources are loaded from the executable as shown in the following
example. Note particularly the use of LR_SHARED
, which is vitally
important for Windows 9x/ME.
UINT nFlags = LR_SHARED | LR_DEFAULTSIZE | LR_DEFAULTCOLOR;
switch (smashersize) {
case SMSH_BIG:
{
m_smasherDown = AtlLoadCursorImage(IDC_BIGDOWN, nFlags);
m_smasherUp = AtlLoadCursorImage(IDC_BIGUP, nFlags);
break;
} }
Self-contained Help
Since one of the goals of the application was a self-contained executable,
we could not use traditional help files or HTML help. Instead, we use a tab
control, a read-only edit control, and several resource strings.
1. Tab Control
We created a tab control template, derived from CTabCtrl
, and
use it to switch among various help and copyright messages. It provides an
erase background handler for the skin background brush. It also uses the
TCS_BUTTONS
style to make it appear "flat", and contains a tooltip
control. To make adding tabs and tips easier, the following method is used:
LRESULT AddTab(LPTSTR tTitle, LPTSTR tTip = NULL)
{ RECT rcTab;
TCITEM tab;
tab.mask = TCIF_TEXT;
tab.pszText = tTitle;
int nTab = GetItemCount();
InsertItem(nTab, &tab);
GetItemRect(nTab, &rcTab);
m_tips.AddTool(m_hWnd, tTip, &rcTab, nTab + 1000);
return nTab; }
Two interesting factors were uncovered while devising this method. Both
pertain to the fourth parameter, nIDTool
, of the
AddTool()
method of CToolTipCtrl
. First, Windows NT
automatically creates a tooltip for certain controls and assigns it tool ID 0.
AddTool()
compensates by restricting use of tool ID 0. Second,
tooltip IDs must be equal to or greater than the total number of tooltips. In
other words, a tab control with three tabs should have tool IDs no lower than
3, 4, and 5 or the tips will not display correctly.
2. Edit Control
Various Smasher! help and copyright messages are displayed in a slightly
customized edit. We wanted a white background instead of the normal gray of
read-only edits. CSGEdit
becomes essentially read-only by throwing
away selection, character, and mouse messages like this:
BEGIN_MSG_MAP(CSGEdit)
MESSAGE_HANDLER(EM_SETSEL, OnMessage)
MESSAGE_HANDLER(WM_CHAR, OnMessage)
MESSAGE_RANGE_HANDLER(WM_MOUSEFIRST, WM_MOUSELAST, OnMessage)
END_MSG_MAP()
LRESULT OnMessage(UINT, WPARAM, LPARAM, BOOL&)
{
return 0; }
3. String Table Resources
The following method, from the Help dialog class, is used to load string
resources from the executable for display in the edit control. When a user
clicks a tab, the TCN_SELCHANGE
notify code handler calls
SetHelpText(tabID)
to display the appropriate help text. Strings
were entered into the String Table resource with two \r\n
line
breaks between paragraphs.
void SetHelpText(int nTabID)
{ TCHAR cHelpText[1000];
switch (nTabID) {
case 0:
{
AtlLoadString(IDS_SOFTHELP, cHelpText, 1000);
break;
} }
m_helpEdit.SetWindowText(cHelpText); }
WTL Release Builds
It did not take us long to encounter the dreaded "LIBCMT.LIB(crt0.obj) :
error LNK2001: unresolved external symbol _main". This message occurs when you
use templates and functions that require C-RunTime (CRT) startup routines.
For example, memchr
, strcmp
, vector
and
hundreds of others. Any functions defined as _CRTIMP
and templates
using such functions will not Release build with default WTL settings.
There are two solutions. You can replace any functions that require CRT. If
you read through the WTL source code, you will find the WTL programmers at
Microsoft used many substitute functions. For example, lstrcmp
is
used in place of strcmp
, and a number of helpers like
CString's _cstrchr
were written to replace CRT functions.
Alternatively, you can remove _ATL_MIN_CRT
from the preprocessor
definitions for the Release project. If you do that, MSVCRTxx.dll must be
available to your application at runtime.
Conclusion
We did finally meet our development goals. Smasher! is a self-contained exe,
is relatively small (136K, zips to 32K), does not require many external
libraries, and has run on every system from Win95 SR1 to Windows XP. It does
not run on a fresh install of Win95 "classic" from floppy disk unless the
optional Internet Explorer is installed.
We hope you enjoy playing Smasher! and can use some of its techniques in
your own programs.
Acknowledgements
Bjarke Viksoe, for the skinned dialog class. Chris Becke, for the bitmap
transparency. Andy Williams and Knuth, Mitchell, Moore, for the random number
generator. Several CodeProject articles provided ideas and tips.
Terms Of Use
Smasher! is copyrighted free software. MAY NOTS: You may not claim that
Smasher! is your creation. You may not modify the copyright or any other
notices and redistribute the software or its source code. You may not charge
for distribution of the software. MAYS: You may reuse any portion of the source
code in your own programs in any form you wish as long as all applicable
copyrights are maintained and any usage restrictions of the copyright holders
are followed. You may distribute Smasher! in binary form by any means to
anybody as long as it is distributed for free.
SMASHER! IS DISTRIBUTED AS-IS, WITHOUT WARRANTIES OF ANY KIND.