C++ At Work

Web Version Checking, Adding Sound to an App

Paul DiLascia

Code download available at:CAtWork05.exe(185 KB)

Q In your April 2003 column you described how to implement a class called CWebVersion that checks the software version against a file stored on the Web, in order to prompt the user to upgrade the program if the version is out of date. Your implementation uses FTP to download the file, but the ISP that hosts my site doesn't allow FTP connections unless I log in with a username and password. Can I download the version file as a simple Web page using HTTP instead of FTP?

Mark Simpson

Q In your April 2003 column you described how to implement a class called CWebVersion that checks the software version against a file stored on the Web, in order to prompt the user to upgrade the program if the version is out of date. Your implementation uses FTP to download the file, but the ISP that hosts my site doesn't allow FTP connections unless I log in with a username and password. Can I download the version file as a simple Web page using HTTP instead of FTP?

Mark Simpson

A In case you didn't read the April 2003 issue, CWebVersion is a class I wrote to let you compare your program's version number against a version number stored on the Web. I use it in my TraceWin program to notify users when there's a new version to download.

A In case you didn't read the April 2003 issue, CWebVersion is a class I wrote to let you compare your program's version number against a version number stored on the Web. I use it in my TraceWin program to notify users when there's a new version to download.

Yes, you can use HTTP; and no, you don't have to convert your file. I probably should have used HTTP in my original implementation, since HTTP is more widely supported than FTP. Many Web hosting providers disallow anonymous FTP for security reasons. While FTP is more efficient for transferring files (that's what it was designed for), HTTP is fine for fetching a simple text file.

CWebVersion reads a text file that has four comma-separated numbers: the high- and low-order 32-bit major and minor version numbers. To use it, you can write:

if (CWebVersion::Online()) { CWebVersion webver("www.mysite.com"); if (webver.ReadVersion("myversion.txt")) { // dwVersionMS and dwVersionLS now // hold the version numbers } }

The static member function CWebVersion::Online calls ::InternetQueryOption with INTERNET_OPTION_CONNECTED_STATE to see if the computer is currently connected to the Internet. If so, CWebVersion::ReadVersion reads the version file from your Web site. Once you've done that, you can compare the numbers with the ones compiled in your program, usually in your VERSIONINFO or DllGetVersion resource. (See my April 1998 column at microsoft.com/msj/0498/c0498.aspx for details on this.) The original CWebVersion used FTP to fetch the file; this month I've modified it to use HTTP. Reading an HTTP file over the Web is easy with MFC's WinInet classes:

// in CWebVersion::ReadVersion CInternetSession session(_T("MySession")); CHttpConnection* pConn = session.GetHttpConnection("www.dilascia.com", INTERNET_DEFAULT_HTTP_PORT); CHttpFile* pFile = pConn->OpenRequest(CHttpConnection::HTTP_VERB_GET, "TraceWinVer.txt"); pFile->SendRequest();

This sequence attempts to download the file www.dilascia.com/TraceWinVer.txt. After calling SendRequest you can call CHttpFile::QueryInfoStatusCode to get the status code—for example, 404 for file not found or 200 for status OK (see wininet.h for a complete list of codes)—then call CHttpFile::Read to read the file into your buffer. CWebVersion::ReadVersion does this, then calls scanf to parse the contents from the format "Mhi,Mlo,mhi,mlo" where Mhi, Mlo, mhi, and mlo are the high- and low-order WORDs of the Major and minor version numbers. CWebVersion stores these in CWebVersion::dwMajorVersion and CWebVersion::dwMinorVersion. Figure 1 shows the full source.

Figure 1 WebVersion

WebVersion.h

WebVersion.h ////////////////// // This class encapsulates over-the-Web version checking. It expects a // text version file that contains four numbers separated by commas, the // same format for FILEVERSION and PRODUCTVERSION in VS_VERSION_INFO. // ReadVersion reads these numbers into dwVersionMS and dwVersionLS. // Uses HTTP, not FTP, to fetch the file. // class CWebVersion { protected: enum { BUFSIZE = 64 }; char m_version[BUFSIZ]; // version number as text CString m_sServer; // server name (www.mysite.com) DWORD m_dwStatus; // status code CString m_sStatus; // status text public: DWORD dwMajorVersion; // version number: most-sig 32 bits DWORD dwMinorVersion; // version number: least-sig 32 bits CWebVersion(LPCTSTR server) : m_sServer(server) { } ~CWebVersion() { } static BOOL Online(); BOOL ReadVersion(LPCTSTR lpFileName); LPCSTR GetVersionText() { return m_version; } DWORD GetStatus() { return m_dwStatus; } CString GetStatusText() { return m_sStatus; } };

WebVersion.cpp

WebVersion.cpp #include „stdafx.h" #include „WebVersion.h" ////////////////// // Check if connected to Internet. // BOOL CWebVersion::Online() { DWORD dwState = 0; DWORD dwSize = sizeof(DWORD); return InternetQueryOption(NULL, INTERNET_OPTION_CONNECTED_STATE, &dwState, &dwSize) && (dwState & INTERNET_STATE_CONNECTED); } ////////////////// // Read version number as string into buffer. Use MFC CHttpFile. // BOOL CWebVersion::ReadVersion(LPCTSTR lpFileName) { ASSERT(lpFileName); BOOL bRet = FALSE; // assume failure m_dwStatus=0; m_sStatus.Empty(); // Read version file using MFC Inet classes. INTERNET_PORT nPort = INTERNET_DEFAULT_HTTP_PORT; CInternetSession session(_T("MySession")); CHttpConnection* pConn = NULL; CHttpFile* pFile = NULL; try { pConn = session.GetHttpConnection(m_sServer, nPort); pFile = pConn->OpenRequest( CHttpConnection::HTTP_VERB_GET, lpFileName); pFile->SendRequest(); pFile->QueryInfoStatusCode(m_dwStatus); if (m_dwStatus==HTTP_STATUS_OK) { const UINT BUFSIZE = 128; UINT nRead = pFile->Read(m_version, BUFSIZE); if (nRead>0) { // read version number in the form Mhi,Mlo,mhi,mlo m_version[nRead] = 0; int Mhi,Mlo,mhi,mlo; _stscanf(m_version, (_T(«%x,%x,%x,%x»)), &Mhi, &Mlo, &mhi, &mlo); dwMajorVersion = MAKELONG(Mlo,Mhi); dwMinorVersion = MAKELONG(mlo,mhi); bRet = TRUE; // success! } } else { pFile->QueryInfo(HTTP_QUERY_STATUS_TEXT, m_sStatus); } } catch (CInternetException* pEx) { ... } session.Close(); delete pFile; delete pConn; return bRet; }

To test CWebVersion, I wrote a program called GetVersion.exe (see Figure 2). When I first tested CWebVersion on my own Web site, I named my version file TraceWinVer.dat. When I tried to download it, I got error 404 (file not found) even though the file was there. At first I thought perhaps I should add .dat to the list of accepted file types in the request header:

static LPCTSTR MyHeaders = _T("Accept: text/dat\r\n"); ... pHttpFile->AddRequestHeaders(MyHeaders);

Figure 2 A Little Test Program

Figure 2** A Little Test Program **

However, this didn't fix the problem. I still got error 404. Well, it turns out my hosting provider had their server configured to prevent browsing .dat extensions. They were happy to change this, but I opted instead to keep the security and rename my version file TraceWinVer.txt. After all, it's text.

If you're programming with the Microsoft® .NET Framework, you can use HttpWebRequest and HttpWebResponse to fetch a file using HTTP, instead of MFC. With the .NET Framework, you create an HttpWebRequest with the full URL, then call GetResponse to send the request and fetch the response:

HttpWebRequest* req = dynamic_cast<HttpWebRequest*>( WebRequest::Create(S"http://www.dilascia.com/TraceWinVer.txt")); req->Timeout = 5000; // 5 sec HttpWebResponse* resp = dynamic_cast<HttpWebResponse*>(req->GetResponse());

The dynamic casts are kind of clunky, but necessary to access the HTTP-specific properties and methods of HttpWebRequest and HttpWebResponse. If you're using Visual Studio® 2005 with C++/CLI, replace the pointers with tracking handles (^) and you don't need the S for managed string literals. To read the contents of the file in .NET, create a StreamReader on the response stream and read it. In code, it looks like this:

StreamReader* strm = new StreamReader(resp->GetResponseStream(), encoding); String* content = strm->ReadToEnd(); strm->Close();

For .NET-heads, I wrote a full GetVersion program in managed C++. You can download it from the MSDN®Magazine Web site.

Q How can I add any sound (not only with the MessageBeep function) to an MFC-based app?

Q How can I add any sound (not only with the MessageBeep function) to an MFC-based app?

Alexander Potapenko

A It's not hard to add sounds to your MFC-based app, but before I show you how, let me remind you that, especially in software, silence is golden. While there are many situations where sound is appropriate (your build failed, e-mail arrived, It's time to buy groceries), in most cases, It's better to keep quiet. Software sounds are the programming equivalent of belching. Have you ever visited one of those Web sites with the home page that plays a little jingle? The first thing most people do is press the Back button. If you must add sounds, please don't forget to provide a Mute option in your program's settings, under Tools | Options.

A It's not hard to add sounds to your MFC-based app, but before I show you how, let me remind you that, especially in software, silence is golden. While there are many situations where sound is appropriate (your build failed, e-mail arrived, It's time to buy groceries), in most cases, It's better to keep quiet. Software sounds are the programming equivalent of belching. Have you ever visited one of those Web sites with the home page that plays a little jingle? The first thing most people do is press the Back button. If you must add sounds, please don't forget to provide a Mute option in your program's settings, under Tools | Options.

OK, now that I'm off my soapbox I can tell you about a Windows® function called PlaySound that does what you want. It's defined in <mmsystem.h> and you have to link with winmm.lib. PlaySound takes the name of a sound file or resource and plays the sound. The following shows an example:

PlaySound("woofwoof.wav",NULL,SND_NODEFAULT);

Here the special flag SND_NODEFAULT tells Windows: don't play the default sound (MessageBeep) if you can't find the file. Figure 3 shows the other flags. As with so many Windows functions, there are many different ways to use PlaySound, many of them poorly documented. In fact, some flags continue to mystify me. Never fear, I'll shine what light I can into this fog.

Figure 3 PlaySound Flags

Flag Description
SND_APPLICATION Play app-specific sound. (Read from registry key HKCU\AppEvents\Schemes\Apps\progname).
SND_ALIAS The pszSound parameter is a system-event alias in the registry or the WIN.INI file.
SND_ALIAS_ID The pszSound parameter is a resource ID.
SND_ASYNC Play sound asynchronously. PlaySound returns immediately after launching the sound. Call PlaySound(NULL) to stop.
SND_FILENAME Play a sound file; pszSound is the file name.
SND_LOOP Play sound repeatedly until PlaySound is called again with the pszSound parameter set to NULL. You must also use SND_ASYNC to indicate an asynchronous sound event.
SND_MEMORY Play sound image in memory.
SND_NODEFAULT Don't play default sound if the requested sound can't be found.
SND_NOSTOP Don't stop the current sound already playing. If this flag is not specified, PlaySound attempts to stop the currently playing sound so that the device can be used to play the new sound.
SND_NOWAIT If the driver is busy, return immediately without playing the sound.
SND_RESOURCE Play a sound (WAVE) resource.

One of the most useful ways to play a sound is with the SND_APPLICATION flag, which plays an application-associated sound. For example:

PlaySound("AppExit",NULL, SND_APPLICATION|SND_NODEFAULT);

Figure 4 Happy Sounds

Figure 4** Happy Sounds **

This plays your AppExit sound. And what exactly is that? Windows looks in the registry for HKCU\AppEvents\Schemes\Apps\AppExit and reads the value for .current. If the .current value is a file name like exit.wav, Windows plays it. Windows searches the current directory, the Windows directory, the Windows system directory, and your PATH environment variable. Why are app-associated sounds cool? Because users can customize them with the Control Panel sound applet (see Figure 4). PlaySound also has a SND_RESOURCE flag that lets you play sounds from your resource file. To play a resource sound, you must first add the sound to your resource (.rc) file:

AppExit WAVE "res\\STExit.wav"

Note the resource must be a WAVE resource—a critical detail that isn't documented, as far as I can tell. The resource compiler embeds the WAV file in your EXE so now you can use SND_RESOURCE to play it, like so:

PlaySound("AppExit", AfxGetResourceHandle(), SND_RESOURCE);

PlaySound needs the module handle for the module that contains the resource, which you can get by calling AfxGetResourceHandle (which for most apps is the same as AfxGetInstanceHandle). In the previous snippets, the resource identifier is a string ("AppExit"), but you can also use an integer ID if you specify SND_ALIAS_ID.

To simplify your life, I wrote a little class, CSoundMgr, that makes implementing app sounds as easy as eating cherries. CSoundMgr lets you define logical sounds and play them by ID. It has functions to register your sounds for you, and lets you mute your app in one fell swoop by changing a flag. CSoundManager even searches for default sounds in your resource file. To show how it works in practice, I wrote a test program called—what else?—SoundTest. SoundTest is a typical MFC dialog-based app. It displays the current values for five application sounds (see Figure 5).

Figure 5 SoundTest

Figure 5** SoundTest **

The first step to define sounds for SoundTest is to create IDs for the sounds. I used an enum with five values: MYSND_HAPPY, MYSND_UNHAPPY, and so on. Don't use zero for any sound ID; CSoundMgr::PlaySound(0) stops the current sound, equivalent to ::PlaySound(NULL, NULL, 0). With IDs defined, you can use the macros defined in SoundMgr.h to build a sound table, like so:

BEGIN_SOUND_MAP(MySounds) DEFINE_SOUND(MYSND_HAPPY, _T("ST_Happy"), _T("SoundTest Happy")) ... END_SOUND_MAP()

Each table entry holds three items: an ID, a logical name, and a GUI name. The ID is used to play the sound. The logical name (for example, ST_Happy) is the name used internally for the registry keys and default sound resources. The GUI name is the human-readable name users see when they use the Sounds control panel applet—for example, "SoundTest Happy" as in Figure 4. Once you've defined your table, the next step is to create a CSoundMgr initialized from your table:

// THE sound manager for this app CSoundMgr SoundMgr(MySounds);

You only need one CSoundMgr for the entire app. Finally, if you want default built-in sounds, you must add a WAVE resource for each logical sound name that has a default. For example:

ST_HAPPY WAVE "res\\STHappy.wav" ST_UNHAPPY WAVE "res\\STUnhappy.wav"

Now with the sounds defined, all you have to do to play a sound is write the following:

SoundMgr.PlaySound(MYSND_HAPPY)

And CSoundMgr plays the happy sound. CSoundMgr looks first in the registry for HKCU\AppEvents\Schemes\Apps\SoundTest\ST_Happy\.current; if there's no such registry key/value, CSoundMgr::PlaySound plays the sound resource with the same logical name, ST_Happy. You can call CSoundMgr::IsRegistered to see if your sounds are registered—if not, call CSoundMgr::Register to register them, as shown here:

if (!SoundMgr.IsRegistered()) SoundMgr.Register();

CSoundMgr::Register creates all the registry keys required for users to customize sounds in the Sounds control panel applet. It doesn't actually set any of the key values, but rather leaves them empty so CSoundMgr::PlaySound will use the default resource sounds. If you don't want default sounds, don't create resources for them, or use the following:

CSoundMgr::m_bUseResourceSounds = FALSE;

Implementing CSoundMgr is fairly straightforward. Most of the code is spent groveling through the registry, perhaps one of the most onerous chores in all of Windows-based programming (see Figure 6). Lucky for you I've already done the work. The subkeys for each logical sound are in HKCU\AppEvents\Schemes\Apps\progname, where progname is your program's name, the same string you use for program Settings, the one returned by ::AfxGetAppName. Each subkey holds the sound file name in the .current value. CSoundMgr doesn't create the values, only the keys, since the Sounds Control Panel applet creates a .current value when the user changes a sound. The default value for each logical sound key is the human-readable GUI name for the sound, but Windows ignores this value as far as I can tell; it looks for GUI names in a different key, HKCU\AppEvents\EventNames. Go figure.

Figure 6 SoundMgr

SoundMgr.h

#pragma once // one of these for each application sound struct APPSOUND { UINT id; // ID for programming LPCTSTR log_name; // logical event name for registry LPCTSTR gui_name; // user-visible event name }; ////////////////// // Class to manage app sounds. Use macros to create a static table, then // construct one of these initialized from the table. // class CSoundMgr { protected: APPSOUND* m_soundtab; // table of APPSOUNDs // helper fns BOOL GetSoundFileName(LPCTSTR logname, CString& s); BOOL GetLogicalName(UINT id, CString& logname); public: BOOL m_bUseResourceSounds; // look for default sounds in res file? BOOL m_bEnableSounds; // enable/disable sound CSoundMgr(APPSOUND* tab); ~CSoundMgr() { } CString GetSoundSchemeKey(LPCTSTR logname=NULL); BOOL PlaySound(UINT id, DWORD dwAddFlags=0); BOOL PlaySound(LPCTSTR logname, DWORD dwAddFlags=0); BOOL IsRegistered(); // are sounds registered? BOOL Register(); // register sounds }; ////////////////// // Macros to build sounds table. // #define BEGIN_SOUND_MAP(name) APPSOUND name[] = { #define DEFINE_SOUND(id, regname, guiname) { id, regname, guiname }, #define END_SOUND_MAP() { 0, NULL, NULL } };

SoundMgr.cpp

#include "stdafx.h" #include "SoundMgr.h" ////////////////// // Construct sound manager from table. // CSoundMgr::CSoundMgr(APPSOUND* tab) : m_soundtab(tab) { m_bUseResourceSounds = TRUE; // default: use default resource sounds m_bEnableSounds = TRUE; // default: sounds enabled } ////////////////// // Get name of registry key for sound events: // HKCU\AppEvents\Schemes\Apps\logname, where logname is optional. // CString CSoundMgr::GetSoundSchemeKey(LPCTSTR logname) { CString key; key.Format(_T("AppEvents\\Schemes\\Apps\\%s"), AfxGetAppName()); if (logname) { key += _T("\\"); key += logname; } return key; } SoundMgr.h #pragma once // one of these for each application sound struct APPSOUND { UINT id; // ID for programming LPCTSTR log_name; // logical event name for registry LPCTSTR gui_name; // user-visible event name }; ////////////////// // Class to manage app sounds. Use macros to create a static table, then // construct one of these initialized from the table. // class CSoundMgr { protected: APPSOUND* m_soundtab; // table of APPSOUNDs // helper fns BOOL GetSoundFileName(LPCTSTR logname, CString& s); BOOL GetLogicalName(UINT id, CString& logname); public: BOOL m_bUseResourceSounds; // look for default sounds in res file? BOOL m_bEnableSounds; // enable/disable sound CSoundMgr(APPSOUND* tab); ~CSoundMgr() { } CString GetSoundSchemeKey(LPCTSTR logname=NULL); BOOL PlaySound(UINT id, DWORD dwAddFlags=0); BOOL PlaySound(LPCTSTR logname, DWORD dwAddFlags=0); BOOL IsRegistered(); // are sounds registered? BOOL Register(); // register sounds }; ////////////////// // Macros to build sounds table. // #define BEGIN_SOUND_MAP(name) APPSOUND name[] = { #define DEFINE_SOUND(id, regname, guiname) { id, regname, guiname }, #define END_SOUND_MAP() { 0, NULL, NULL } }; SoundMgr.cpp #include "stdafx.h" #include "SoundMgr.h" ////////////////// // Construct sound manager from table. // CSoundMgr::CSoundMgr(APPSOUND* tab) : m_soundtab(tab) { m_bUseResourceSounds = TRUE; // default: use default resource sounds m_bEnableSounds = TRUE; // default: sounds enabled } ////////////////// // Get name of registry key for sound events: // HKCU\AppEvents\Schemes\Apps\logname, where logname is optional. // CString CSoundMgr::GetSoundSchemeKey(LPCTSTR logname) { CString key; key.Format(_T("AppEvents\\Schemes\\Apps\\%s"), AfxGetAppName()); if (logname) { key += _T("\\"); key += logname; } return key; } ////////////////// // Play logical sound from ID. // BOOL CSoundMgr::PlaySound(UINT id, DWORD dwAddFlags) { if (id==0) { ::PlaySound(NULL,NULL,0); // ID 0 = stop playing async sound return TRUE; } CString logname; return GetLogicalName(id, logname) ? PlaySound(logname, dwAddFlags) : FALSE; } ////////////////// // Play logical sound using logical name. // BOOL CSoundMgr::PlaySound(LPCTSTR logname, DWORD dwAddFlags) { if (m_bEnableSounds) { CString soundname; if (GetSoundFileName(logname, soundname) && !soundname.IsEmpty()) { return ::PlaySound(soundname, NULL, dwAddFlags|SND_APPLICATION|SND_NODEFAULT); } else if (m_bUseResourceSounds) { // play sound resource w/same logical name return ::PlaySound(logname, AfxGetResourceHandle(), dwAddFlags|SND_RESOURCE|SND_NODEFAULT); } } return FALSE; } ////////////////// // Get logical sound name from ID. Search table for sound that matches. // BOOL CSoundMgr::GetLogicalName(UINT id, CString& logname) { if (m_soundtab) { for (int i=0; m_soundtab[i].id; i++) { if (m_soundtab[i].id==id) { logname = m_soundtab[i].log_name; return TRUE; } } } return FALSE; } ////////////////// // Read current sound file name from registry. // BOOL CSoundMgr::GetSoundFileName(LPCTSTR logname, CString& s) { CString key = GetSoundSchemeKey(logname); // get registry key // open key and read the ".current" value. BOOL bRet = FALSE; HKEY hkey; if (RegOpenKey(HKEY_CURRENT_USER, key, &hkey)==ERROR_SUCCESS) { TCHAR val[255]={0}; LONG vlen=sizeof(val)/sizeof(TCHAR); if (RegQueryValue(hkey, _T(".current"), val, &vlen)==ERROR_SUCCESS) { s = val; // return in caller’s string bRet = TRUE; // success } RegCloseKey(hkey); } return bRet; // failed :( } ////////////////// // Test if sounds are registered. Algorithm: see if first logical sound is // registered. Note this is not foolproof if someone deletes other keys. // BOOL CSoundMgr::IsRegistered() { ASSERT(m_soundtab); CString key = GetSoundSchemeKey(m_soundtab[1].log_name); HKEY hkey; if (RegOpenKey(HKEY_CURRENT_USER, key, &hkey)==ERROR_SUCCESS) return TRUE; RegCloseKey(hkey); return FALSE; } // Helper function to open a registry key, creating if necessary static LONG RegCreateOpenKey(HKEY hkey, LPCTSTR lpSubKey, PHKEY phkRes) { return RegOpenKey(hkey, lpSubKey, phkRes)==ERROR_SUCCESS ? ERROR_SUCCESS : RegCreateKey(hkey, lpSubKey, phkRes); } ////////////////// // Register app-defined sounds. Note this only creates all the keys, // it does NOT set the values (file names) since the app will play default // sounds from the resource file. Creating the keys is all that’s required // to let users change the sounds. // BOOL CSoundMgr::Register() { ASSERT(m_soundtab); BOOL bRet = FALSE; static LPCTSTR APPS = _T("AppEvents\\Schemes\\Apps"); static LPCTSTR LABELS = _T("AppEvents\\EventLabels"); HKEY hkApps,hkLabels; if (RegOpenKey(HKEY_CURRENT_USER, APPS, &hkApps)==ERROR_SUCCESS) { if (RegOpenKey(HKEY_CURRENT_USER, LABELS, &hkLabels)==ERROR_SUCCESS) { HKEY hkApp; if (RegCreateOpenKey(hkApps, AfxGetAppName(), &hkApp)==ERROR_SUCCESS) { // opened event key, now add all the sounds for (int i=0; m_soundtab[i].id; i++) { HKEY hk; LPCTSTR lpName = m_soundtab[i].gui_name; DWORD dwLen = (DWORD)_tcslen(m_soundtab[i].gui_name); // add app event RegCreateKey(hkApp, m_soundtab[i].log_name, &hk); RegSetValue(hk, NULL, REG_SZ, lpName, dwLen); RegCloseKey(hk); // add event name RegCreateKey(hkLabels, m_soundtab[i].log_name, &hk); RegSetValue(hk,NULL,REG_SZ, lpName, dwLen); RegCloseKey(hk); } RegCloseKey(hkApp); bRet = TRUE; // success! (maybe) } RegCloseKey(hkLabels); } RegCloseKey(hkApps); } return bRet; }

So you have to create another set of keys, one for each logical sound name, whose default value is the human-readable GUI name. Of course, if you use CSoundMgr, you can forget all about registry keys. Just define your sounds and call CSoundMgr::Register. If the user has customized sounds, calling Register won't clobber them since it only creates those keys that don't already exist. If you want to implement a reset command to restore sounds to their defaults, you should delete HKCU\AppEvents\Schemes\Apps\progname and then call Register again.

One last bit of advice: in choosing your logical names, be careful to avoid collisions with other apps. Unfortunately, all the event names live in the same namespace within HKCU\AppEvents\EventNames, so I recommend using a prefix like I did for SoundTest, where all the logical names begin with ST_. It makes them easier to find too, since REGEDT32 lists the registry keys in alphabetical order.

SoundTest has an Enable Sounds checkbox that lets users turn sound on or off. The command handler for this button toggles the variable CSoundMgr::m_bEnableSounds. I leave you to download and ponder the source for full details. Happy programming!

Send your questions and comments for Paul to  cppqa@microsoft.com.

Paul DiLascia is a freelance software consultant and Web/UI designer-at-large. He is the author of Windows++: Writing Reusable Windows Code in C++ (Addison-Wesley, 1992). In his spare time, Paul develops PixieLib, an MFC class library available from his Web site, www.dilascia.com.