The Making of CandyBarZWritten by Matt Wagner |
IntroductionIBM seems to lack the imagination or desire to make the Workplace Shell an aesthetically pleasing environment. Fortunately, however, they made it extremely easy for programmers to replace parts of OS/2 with enhanced functionality. My shareware utility, CandyBarZ, is one such enhancement. For those not familiar with it, CandyBarZ replaces PM titlebars with gradient-filled goodies. In this article, I want to discuss some of the steps involved in creating CandyBarZ. We will see how to replace a system window class, which messages need to be handled, and how to make the code safe. We will also briefly look at the CBSetup program and how it communicates with the CandyBarZ DLL. Some TerminologyPrivate Window Class - A private window class is a window class that is available only to the application that registered it. Public Window Class - A public window class is a window class that is available to all PM programs. WinRegisterClass RevisitedEvery PM C programmer is familiar with the WinRegisterClass function, at least as far as registering private window classes. This same API is used to register public window classes and to replace existing public window classes. For convenience, a portion of the OS/2 toolkit documentation on WinRegisterClass is reproduced here. HAB hab; /* Anchor-block handle. */ PSZ pszClassName; /* Window-class name. */ PFNWP pfnWndProc; /* Window-procedure identifier. */ ULONG flStyle; /* Default-window style. */ ULONG cbWindowData; /* Reserved storage. */ BOOL rc; /* Window-class-registration indicator. */ rc = WinRegisterClass( hab, pszClassName, pfnWndProc, flStyle, cbWindowData ); hab (HAB) - input Anchor-block handle. pszClassName (PSZ) - input Window-class name. pfnWndProc (PFNWP) - input Window-procedure identifier. flStyle (ULONG) - input Default-window style. cbWindowData (ULONG) - input Reserved storage.When replacing an existing public window class, the pszClassName parameter should be set to the name of the existing class. In CandyBarZ, this is WC_TITLEBAR. The flStyle and cbWindowData parameters require some additional work, as the existing class has styles and window data that we need to preserve. Before calling the WinRegisterClass function, then, we first need to get some information about the existing window class. This is done via WinQueryClassInfo, HAB hab; CLASSINFO ci; WinQueryClassInfo( hab, WC_TITLEBAR, &ci ); The CLASSINFO structure is defined as typedef struct _CLASSINFO { ULONG flClassStyle; /* Class-style flags. */ PFNWP pfnWindowProc; /* Window procedure. */ ULONG cbWindowData; /* Number of window words. */ } CLASSINFO;Once we have this information, we simply need to set the CS_PUBLIC bit of the flStyle parameter to WinRegisterClass and add any additional window words to be used by our application to the cbWindowData parameter. Following is a snippet from CandyBarZ in which the WC_TITLEBAR class is replaced. Error checking has been removed for brevity. CLASSINFO ci = { 0 }; // get original class info WinQueryClassInfo( NULLHANDLE, WC_TITLEBAR, &ci ); // register class. note replacement procedure, modified class styles, // and additional window words WinRegisterClass( NULLHANDLE, WC_TITLEBAR, CandyBarZTBProc, ci.flClassStyle | CS_PUBLIC, ci.cbWindowData + sizeof( void * ) );Trivial, no? No. We've covered the how, but not the when or where. We can't just up and replace the system titlebar at any old time. Public window classes may only be registered by PMSHELL when the system boots. This means that, first, this code must reside in a DLL and, second, PMSHELL must be instructed to load the DLL. Additionally, we must decide where, within the DLL, the class is to be registered. I will assume that the reader knows how to build an OS/2 DLL. To make PMSHELL load the DLL, we must append the name of our DLL to the SYS_DLLS->LoadPerProcess key in OS2.INI. This key is simply a space-delimited list of DLLs to be loaded at system boot time, for example SND WWHOOK CANDYBARinstructs PMSHELL to load SND.DLL, WWHOOK.DLL, and CANDYBAR.DLL. As for where in the DLL to register the window class, the _DLL_InitTerm function seems to be the logical choice, as it will be called automatically by OS/2 when the DLL is loaded. To ensure that this function is only called the first time that your DLL is loaded, build the DLL as INITGLOBAL TERMGLOBAL. The final step to the replacement process is to ensure that all windows of this class have access to the address of the original window procedure and the location of the window words. This is easily done by placing this information in shared memory, as does CandyBarZ: // allocate the structure DosAllocNamedSharedMem( ( PPVOID )&ptbShare, "\\SHAREMEM\\TBSHARE", sizeof( TBSHARE ), PAG_READ | PAG_WRITE | PAG_COMMIT ); // store important information ptbShare->ulDataOffset = ci.cbWindowData; ptbShare->pfnOrigWndProc = ci.pfnWindowProc;Note that the offset to our data within the window words is exactly the original size of the window words. To put this together into a rudimentary _DLL_InitTerm function, we have ULONG _System _DLL_InitTerm( HMODULE hmod, ULONG ulInitTerm ) { TBSHARE *ptbShare; CLASSINFO ci; switch( ulInitTerm ) { case 0: // init { _CRT_init(); // initialize C runtime environment // get original titlebar class information WinQueryClassInfo( NULLHANDLE, WC_TITLEBAR, &ci ); // allocate shared memory DosAllocNamedSharedMem( ( PPVOID )&ptbShare, "\\SHAREMEM\\TBSHARE", sizeof( TBSHARE ), PAG_READ | PAG_WRITE | PAG_COMMIT ); // store necessary information ptbShare->ulDataOffset = ci.cbWindowData; ptbShare->pfnOrigWndProc = ci.pfnWindowProc; // register new class WinRegisterClass( NULLHANDLE, WC_TITLEBAR, CandyBarZTBProc, ci.flClassStyel | CS_PUBLIC, ci.cbWindowData + sizeof( void * ) ); } break; case 1: { _CRT_term(); // terminate C runtime environment } break; } return( 1 ); }Of course, the _DLL_InitTerm function can perform additional work, such as setting default values for the operation of our window class. The Window ProcedureNow that our DLL is loaded by the system, it should probably do something. When we registered the window class above, we provided the name of a replacement window procedure. Let's take a look at how this window procedure should be written. The first question that arises is which messages should be handled. Of course, WM_CREATE and WM_DESTROY are necessary. The remainder are dependent upon the window class being replaced and the functionality to be provided by the new procedure. The CandyBarZ window procedure is quite simple, as the messages received by titlebars are few, and those that need to be handled, WM_CREATE, WM_DESTROY, WM_ACTIVATE, TBM_SETHILITE, and WM_PAINT, are even fewer. The remaining messages are passed on to the original window procedure. This is key: except for the additional handling of the WM_CREATE and WM_DESTROY messages, a replacement window procedure is no more complex than subclassing a window. Of course, since we have our finger in every application's pie, we need to take extra care to make sure that finger is clean. We will look at code safety tips in the next section. It's likely that every case in our procedure will require access to either the window words' offset or the address of the original window procedure. So, we might as well acquire the shared memory from the beginning: MRESULT EXPENTRY CandyBarZTBProc( HWND hwnd, ULONG msg, MPARAM mp1, MPARAM mp2 ) { TBSHARE *ptbShare; DosGetNamedSharedMem( ( PPVOID )&ptbShare, "\\SHAREMEM\\TBSHARE", PAG_READ ); . . . }Note that the shared memory is acquired as read only. This greatly simplifies the code, eliminating the need to serialize access to the data. Any data that needs to be modified by the instance should be placed in window words. Believe it or not, we have seen everything that goes into creating a replacement window class. The details of the window procedure are class specific, but the only thing that sets it apart from a subclassing window procedure is the need to handle the WM_CREATE and WM_DESTROY messages. The CBSetup ProgramTo communicate options to the replacement window class, it is necessary to develop a second program. For CandyBarZ, this second program is CBSetup. The principle behind the program is simple. Collect the user's options until an Apply or Save command is received. Then, acquire the shared memory as writable and make the changes. Finally, broadcast a message to the system so that existing instances of the window class are updated. A few things should be kept in mind here. First, make the changes as quickly as possible. If the setup program is interrupted in the middle of the changes, the shared memory area may be in an unknown state. Second, to be sure that the broadcast message is not misinterpreted, use an atom rather than a WM_USER message. Following is the entirety of the "Apply" button handling used in CBSetup, with error checking removed for brevity. case PBID_APPLYACTIVE: { HATOMTBL hAtomTbl; ATOM Atom; CBCLR *cbclr; PSTBSHARE *ptbShare; // get handle of system atom table hAtomTbl = WinQuerySystemAtomTable(); // locate atom previously added with a call to // WinAddAtom Atom = WinFindAtom( hAtomTbl, PSTB_COLORCHANGE_ATOM ); // get window words, where we've been storing options cbclr = ( CBCLR * )WinQueryWindowPtr( hwnd, QWL_USER ); // get shared memory DosGetNamedSharedMem( ( PPVOID )&ptbShare, PSTB_SHARE, PAG_READ | PAG_WRITE ); // make our changes ptbShare->lActiveColorTop = cbclr->csCurrent.lActiveTop; ptbShare->lActiveColorBottom = cbclr->csCurrent.lActiveBottom; ptbShare->flActive = cbclr->csCurrent.flActive; WinEnableWindow( WinWindowFromID( hwnd, PBID_APPLYACTIVE ), FALSE ); WinEnableWindow( WinWindowFromID( hwnd, PBID_UNDOACTIVE ), FALSE ); // broadcast to children of desktop WinBroadcastMsg( HWND_DESKTOP, Atom, ( MPARAM )0, ( MPARAM )0, BMSG_POST | BMSG_DESCENDANTS ); // broadcast to children of object window WinBroadcastMsg( HWND_OBJECT, Atom, ( MPARAM )0, ( MPARAM )0, BMSG_POST | BMSG_DESCENDANTS ); } break;Of course, the DLL's window procedure must include code to handle the atom. The WM_CREATE message handling should include a call to WinAddAtom to acquire access to the atom. The code to handle the atom itself will likely be similar to the WM_CREATE handling. As we're already writing this second program, we may as well allow it to take care of installation/uninstallation chores. This includes, as mentioned previously, instructing PMSHELL to load the module at boot. The following code from CBSetup shows how to do this. case PBID_INSTALL: { . . . // get destination for CandyBarZ ini file. hwndControl = WinWindowFromID( hwnd, EFID_INIDEST ); WinQueryWindowText( hwndControl, sizeof( szDir ), szDir ); _makepath( szFile, NULL, szDir, "CandyBar", "ini" ); // and write it to OS2.INI PrfWriteProfileString( HINI_USERPROFILE, "CandyBarZ", "Profile", szFile ); // get length of the SYS_DLLS->LoadPerProcess key and // allocate storage PrfQueryProfileSize( HINI_USERPROFILE, "SYS_DLLS", "LoadPerProcess", &cbSysDlls ); DosAllocMem( ( PPVOID )&pszSysDlls, cbSysDlls + sizeof( "CANDYBAR" ) + 32, PAG_READ | PAG_WRITE | PAG_COMMIT ); memset( pszSysDlls, 0, cbSysDlls + sizeof( "CANDYBAR" ) + 32 ); // get the key itself PrfQueryProfileString( HINI_USERPROFILE, "SYS_DLLS", "LoadPerProcess", NULL, pszSysDlls, cbSysDlls ); // add CANDYBAR to the string and write it back out sprintf( pszSysDlls + strlen( pszSysDlls ), " CANDYBAR" ); PrfWriteProfileString( HINI_USERPROFILE, "SYS_DLLS", "LoadPerProcess", pszSysDlls ) ) DosFreeMem( pszSysDlls ); . . . }It may also be wise to make sure that the destination directory for the replacement class DLL is included in the LIBPATH statement in CONFIG.SYS. This may consist of restricting the user's choices to only these directories (as CandyBarZ does) or adding the user's choice to the LIBPATH if not already present. The (highly recommended) uninstallation process simply consists of undoing any changes made to the OS2.INI. Keeping It SafeBecause every window of the replaced class will be handled by your DLL, you must take extra care to minimize the possibility of problems. A bug in a system DLL does not simply crash a single application. It can result in crashes, hangs, and reduced functionality of the entire system. I will share a number of problems that I've run into in the course of developing CandyBarZ.
ConclusionWithin a week of conceiving the idea, I had a basic working replacement for OS/2's WC_TITLEBAR class. The concept is very simple: put a window procedure and a function to register it in a DLL and instruct PMSHELL to load that DLL. The apparent simplicity can be deceiving, however. As this new window procedure is an element of virtually every application, extreme care must be taken to develop quick, clean code. |