November 1999
MyBand is Your Band: More Reusable MFC Goodies with Band Objects and COMToys |
Band objects come in three flavors: desk, info, and comm bands. Desk bands live in the task bar; info and comm bandsalso called Explorer Barslive in Microsoft Internet Explorer. MyBands.dll implements all three kinds of band and demonstrates how easy it is to write a band object. |
This article assumes you're familiar with C++, COM, Internet Explorer |
Code for this article: bandObj.exe (93KB)
Paul DiLascia is the author of Windows ++: Writing Reusable Code in C++ (Addison-Wesley, 1992) and a freelance consultant and
writer-at-large. He can be reached at askpd@pobox.com
or http://pobox.com/~askpd.
|
Not too long ago someone asked me how to add an edit control to his Windows® task bar. He wanted to know how to get the HWND so he could add his control as a child window. I had to break the bad news: unfortunately, that sort of thing isn't done any more. You don't go mucking around the system the way you did in the old days. It's not allowed. Nowadays,
you write COM interfaces so your extensions can communicate with the operating system in a polite, orderly fashion. May I have your menu, please? Why, certainly, here it is. Would you execute this command now, please? But of course. That's IContextMenu for you, just one of many little interfaces that pop up in various kinds of shell extensions.
But among all these interfaces, I couldn't find any way to add a window to the task bar. I was about to tell my poor reader "no can do," when I discovered band objects and IDeskBand. (They were new at the time.) Right away I knew IDeskBand was the kind of interface I could love: it has only one method! So I decided, why not: I'll write my own desk band. With only one function, how hard could it be? Several months and thousands of code lines later (not to mention Advil tablets and extended therapy sessions), I'm here to report my results. It's not that band objects are so hard (they aren't), but my project grew and grew. It wasn't enough to write one band object; I had to build a framework. Then it wasn't enough to have the framework; I had to have my own general-purpose COM programming system. Reusability is the Holy Grail of programming, and like Indiana Jones, I'm always hunting it. In the end, I built my BandObj framework and some reusable COM goodies I called COMToys. I'll describe BandObj here and COMToys in the sequel. Meet MyBands
|
![]() |
Figure 1 The Web Search Band |
I implemented the Web Search Band in a DLL called MyBands.dll. Astute readers will notice the plural. In fact, band objects come in three flavors: desk, info, and comm bands. Desk bands live in the task bar; info and comm bandsalso called Explorer Barslive in Microsoft® Internet Explorer (see Figure 3). The Internet Explorer History, Favorites, and Search windows are all info bands. MyBands.dll implements all three kinds of band, but the info and comm bands are of the dopey "Hello, world" variety. |
![]() |
Figure 2 Standalone Search Band |
To install MyBands, download the code (93KB), compile it, put the DLL somewhere, and type the following: |
|
The info and comm bands will appear in the Toolbars menu the next time you run Internet Explorer. The desk band is a little trickier: you have to restart Explorer. Ctrl-Alt-Del, kill process, wait for rebirth. When you do, Web Search Band appears in the Toolbars menu as in Figure 4. |
![]() |
Figure 3 Info and Comm Bands in Internet Explorer |
To see how easy it is to write a band object using BandObj, let's look at MyBands. MyBands is composed of several modules, but the only one that has anything to do with band objects is the main module, MyBands.cpp (see Figure 5). MyBands has an app class called CMyBandsDll, which is derived from CWinApp via CBandObjDll (BandObj) and COleControlModule (MFC). Like a normal MFC app, CMyBandsDll has an InitInstance function. |
|
This resembles doc/view, except instead of calling AddDocTemplate, you call AddBandClass. For each band class you must supply the class ID (GUID), MFC runtime class, category, and resource IDs. The category ID is just a GUID that tells Windows what kind of band your class isinfo, comm, or desk. As you'd expect, MyBands uses a separate class for each kind of band. CMyInfoBand, CMyCommBand, and CMyDeskBand are each derived from CBandObj and each uses DECLARE/IMPLEMENT_DYNCREATE so MFC can create instances dynamically using its normal runtime mechanism and COleObjectFactory. They have constructors that initialize information about the band in a DESKBANDINFO struct, CBandObj::m_dbiDefault. For example, the desk band has a default width of 100 and variable height. |
|
The comm band has a fixed height of 30 pixels and no title. |
|
Believe it or not, that's pretty much it as far as the band object part of MyBands goes. The rest of MyBands deals with implementing the band behavior and looks like a normal MFC app. For example, CMyDeskBand has an OnCreate handler that creates an edit control, and CMyCommBand has a WM_PAINT handler that draws a subtle advertising message. |
|
Band objects don't have top-level menus like frames, but they can have context menus if you like. CBandObj handles the dirty details. All MyBands has to do is create a menu with the same resource ID as the band class. Commands magically arrive at MyBands' ON_COMMAND handlers through all the normal channels. Except for the GUIDs and category IDs, you'd hardly even know MyBands is a COM object. BandObj hides all the handshaking, leaving you free to write your band. This is how life should be. |
![]() |
Figure 4 Desk Band in the Toolbars Menu |
Meet BandObj
Now that you've seen you how trivial it is to write a band object using BandObj (define a GUID, call AddBandClass), let's dig down a layer and see how BandObj works. What I've been calling BandObj is actually three classes: CBandObjDll, CBandObjFactory, and CBandObj (see Figure 6). CBandObjFactory is the class factory that makes CBandObj objects. Generally, you never need to use it directly. When you call AddBandClass from your InitInstance function, CBandObjDll creates a new factory and adds it to a list. |
|
OnCreateFactory is a virtual function that simply returns a new factory. |
|
I provided OnCreateFactory in case you ever have the perverse desire to improve my code by deriving your own specialized factory class. If so, derive from CBandObjFactory and override OnCreateFactory to make BandObj use it. The args I omitted for the sake of clarity are the same ones you pass to AddBandClass: the class ID, MFC runtime class, category ID, and resource ID. CBandObjFactory passes the first two to MFC and keeps the last two itself. |
|
Registering MyBands
|
|
CLSID, InprocServer32, ThreadingModel
it's all COM 101. The only thing that's new for bands is the previously mentioned category ID, which goes under the key Implemented Categories. In general, a COM object declares to the world what categories it implements by listing them under HKCR\CLSID\guid\Implemented Categories.
Of course, you don't register a COM object by handCOM objects are expected to register themselves. When you type |
|
regsvr32 calls the special entry DllRegisterServer to register MyBands. If you add /u before the file name, it calls DllUnregisterServer. BandObj.cpp provides default implementations for these standard entries as well as the others, DllGetClassObject and DllCanUnloadNow. The default implementations call special MFC functions designed to do the right thing. |
|
COleObjectFactory::UpdateRegistryAll loops over all the factories and calls UpdateRegistry(TRUE) for each one. DllUnregisterServer is similar, except it calls MFC with FALSE. COleObjectFactory::UpdateRegistry does a pretty good job registering controls, but it knows nothing about bands or categories, so it looks like I have to write some code to register the band. Registration Frustration? Not
|
|
The funny-looking thing with angle brackets is an ATL smart pointer (more on that later). CoCreateInstance creates a registrar object and you're ready to roll. Because I have a neurotic aversion to seeing angle brackets in my code except where I'm comparing things or doing a shift, I wrote a little COMToys class to simplify it even further. |
|
The constructor calls CoCreateInstance so you don't have to. Just instantiate CTRegistrar and call the methods. IRegistrar has methods to register and unregister scripts from a file orbetter yetresource. Figure 7, from atliface.h, shows all the IRegistrar methods. CBandObjFactory::UpdateRegistry has a generic implementation that looks for a REGISTRY resource with the same ID as your factory and calls the registrar to load it. |
|
This is another place where, following MFC tradition, BandObj makes good use of resource IDs. All you have to do to register or unregister your band object is write a registration scriptyou don't have to write any code! And all I had to write was this simple function. Actually, you don't even have to write a registry script because BandObj comes with one that works for any band object. All you have to do is use it. |
|
But wait a minute. How can three different COM objects possibly use the same registration script? Don't they each have different names and class IDs? This is where IRegistrar really shows its mettle. Take a look at BandObj.rgs in Figure 8. What are those funny tags %CLSID%, %ClassName%, and %MODULE%? Those are variables. Before processing your script, the registrar replaces %CLSID%, %ClassName%, and %MODULE% with the actual class ID, class name, and module name. How does it know what values to use? Because you tell itor rather, BandObj does. You may have noticed the call to OnInitRegistryVariables in UpdateRegistry. That's where BandObj defines its variables. |
|
Here's a full list of variables I built into CBandObjFactory, automatically defined by BandObj. |
|
To add your own, such as %TimeStamp% or %MyReleaseVersion%, just derive a new factory class and override OnInitRegistryVariables. Don't forget to call the base class! Since MFC calls UpdateRegistry for each factory, the variables get reinitialized for each class. So for MyBands, %CLSID% is CLSID_MYINFOBAND for the first factory, CLSID_MYCOMMBAND for the second, and CLSID_MYDESKBAND for the third. The same script works for all three. It's way cool.
IRegistrar is so cool I wrote my own command-line utility, RGSRUN, to load RGS files. It's great for testing scripts and debugging, or just deleting extra junk from your registry, something REGEDIT can't do. You can download RGSRUN from the link at the top of this article. Figure 9 shows the whole program. I use RGSRUN in my autoexec.bat with a file, autoexec.rgs, that sets various Explorer settings Windows seems to insist on bashing every time it boots. Take that, Windows! Categorical Imperative
|
|
Once again, ATL smart pointers and a little COMToys class, CTCatRegister, make programming easy and bulletproof. Just instantiate and go. Getting to the Bottom of Band Objects
|
![]() |
Figure 10 Band Object Interfaces |
The best way to understand band objectsor any COM object for that matteris to examine the sequence of events as the object starts upin this case when a user selects your band from the Toolbars menu until the time they close it. The best way to do that is to start with some sample code that works and add a lot of TRACE diagnostics. BandObj has the diagnostics built in, using one of my goodies from a previous article: TRACEFN. TRACEFN uses a special class and my own version of AfxTrace to generate indented diagnostic output, so you can see the stack. Figure 11 shows the output of a typical MyBands session, beginning when the user selects Web Search Band from the Toolbars menu. Here's the play-by-play. Windows (Explorer for desk bands, Internet Explorer for info and comm bands) discovers your band object by looking for COM objects that implement CATID_DeskBand, CATID_InfoBand, or CATID_CommBand, and adds the name of your band to its Toolbars menu as in Figure 4. When the user selects your band from the menu, Windows calls CoCreateInstance or its equivalent. COM calls your DLL's DllGetClassObject, which calls AfxDllGetClassObject. MFC searches for a factory with the right ID and returns it. COM then calls IClassFactory::CreateInstance and voilàthere's your band object! More COM 101. Next, Windows queries for IDeskBand and IObjectWithSite. CBandObj implements these interfaces in the normal MFC wayusing nested classes, interface maps, and BEGIN/END_INTERFACE_PARTso MFC returns the right pointers. Windows calls IObjectWithSite::SetSite to give you a pointer (IUnknown*) back to the container running your show. CBandObj::XObjectWithSite::GetSite calls the virtual function CBandObj::OnSetSite, converting the nested class method to a parent class virtual function call so you can override it easily. The default implementation stores the site in m_spSite. You can QueryInterface m_pSite for any interface the container implements. CBandObj uses it to get the HWND of its parent window. |
|
When Windows calls SetSite, it expects you to create your window. This isn't obvious from the spec; I only found out the hard way. CBandObj::OnSetSite calls a virtual function OnCreateWindow to do it. My default implementation registers and creates a generic invisible window. |
|
Band objects are apartment threaded, so it's important to protect globals. It's always a good idea to keep globals as close as possible to the functions that use them. In this case, OnCreateWindow is the only function that uses bRegistered, so it's a static function. You can override OnCreateWindow to create your own window class, and/or override PreCreateWindow to change some properties. Just remember: your window should be invisible at this point, so make sure to create it without WS_VISIBLE.
Next, Windows calls IOleWindow::GetWindow to get your window's HWND. (This is why you have to create your window in SetSite.) CBandObj returns m_hWnd. After that, Windows calls IDeskBand::GetBandInfo requesting information about your band such as how big it wants to be, whether it has variable or fixed height, and the background color and title. CBandObj stuffs the DESKBANDINFO with a combination of defaultswhich you can set in your band object's constructorand information from your resource file. For example, it gets the title from your resource string. Next, Windows calls IDockingWindow:: ShowDW to show your window. CBandObj calls CWnd:: ShowWindow. It's alive! Windows queries for IInputObject if the user types a key and IContextMenu if the user right-clicks for a context menu. If you implement these interfaces, Windows will use them. CBandObj has default implementations that grab the accelerators and menu from your resource file. All you have to do is add the resources to your project. The menu itself is stored in a data member, m_contextMenu, which you are free to manipulate. CMyDeskBand doesn't have a resource menu; instead it creates the menu dynamically after reading the user's profile settings (see Figure 12). Before appending the menu to the container (when the container calls IContextMenu::QueryContextMenu), CBandObj routes it through the MFC command highway so all your ON_UPDATE_COMMAND_UI handlers work. This is how CMyDeskBand puts a checkmark next to the currently selected search engine in Figure 12. |
![]() |
Figure 12 Instant Menu |
If the user invokes a command by typing an accelerator key or selecting a menu item, Windows calls IContextMenu::InvokeCommand. CBandObj does the same MFC routing as for initializing the menu, so the command magically arrives at your ON_COMMAND handlers in the usual way. When the user closes your band, Windows calls IDockingWindow::CloseDW. CBandObj sends a WM_CLOSE andpoofyour window is gone. But the CBandObj object lives on until Windows releases it. It's all very straightforward and logicalif you use TRACE! Band Object Bugaboos
|
|
There's just one problem with tool bands. When you right-click in Explorer to show your band, it has the name Radio (see Figure 13). Actually, it has the name of the first tool band registered. This is a known bugsee Knowledge Base article Q231621. |
![]() |
Figure 13 Will the Real Radio Please Stand Up! |
Back to MyBands
You're probably sick of band objects by now; you probably hope you never see another band object in your lifetime. Well, that's good because I've said all I have to say about them. Before I go, let me tell you just a few more things about MyBandsthe part that actually implements the bands. It's mostly straightforward, but there were some slings and arrows worth mentioning. First, CMyCommBand and CMyInfoBand are each derived from a common base class, CMyIEBand. CMyIEBand implements a common feature: when you click on the band, it sends the browser to the MSJ or MIND home page. To do so, the band needs an IWebBrowser2 interface so it can call IWebBrowser2::Navigate. Getting the Web browser proved to be something of a minor mystery. Obviously, it has to come from the site, so my first attempt was to write |
|
which of course does a QueryInterface for IWebBrowser2. But QueryInterface fails with E_NOINTERFACE and a NULL pointer. The container doesn't implement IWebBrowser2. So where do you get it? Here's the required voodoo. |
|
IServiceProvider is a general-purpose way for COM objects to supply interfaces implemented by other objects they know about. At first it seems the same as QueryInterface, but there's a subtle difference: the container itself doesn't implement IWebBrowser2, but it knows how to get an object that does.
Next, let's look at CMyDeskBand (the Web Search Band). The interesting thing here is the edit control, implemented in a class CEditSearch. To receive a WM_CHAR message when the user presses Enter, I had to add a WM_GETDLGCODE handler that returns DLGC_WANTALLKEYS. See? All that Windows 3.1 knowledge is still useful. Now when the user presses Enter, CEditSearch gets it and OnChar calls the DoSearch function to build a URL from the user's input. This requires replacing spaces with +, so if you search Yahoo for "beanie baby sex", the URL would be http://ink.yahoo.com/bin/query?p=beanie+baby+sex&z= 2&hc=0&hs=0, which CEditSearch passes to IWebBrowser2::Navigate. CEditSearch has the URLs for several search engines built in, but you can add more by editing MyBands.ini (see Figure 14). The easiest way to get the URL is to go to your favorite portal page, search for "MYSEARCH", and copy the resulting URL from the Address bar into MyBands.ini. Speaking of INI filesMyBands uses one! The registry is so cumbersome for storing simple human-editable configuration settings that I implemented a little class called CIniFile to help me avoid it. |
|
This tells your app to put the INI file in the same directory as your program or DLL, not \Windows. MFC will use an INI file by default if you don't call SetRegistryKey, but it puts the file in \Windows. I like to keep config files with their programs, so you can delete the whole directory to remove it without worrying about extra crud floating around. CIniFile gives you the option to use \Windows if you like, or specify a different file name. It works by setting your app's m_pszRegistryKey to NULL and m_pszProfileName to the name of the INI file. Of course, if you use an INI file, you don't get automatic user-specific settings on a multiuser machine the way you do with registry settings. So sue me. Or use SetRegistryKey.
Getting the context menu (see Figure 12) to work in the edit control required a little fidgeting. When the user right-clicks an edit control, the edit control tries to display its own Cut/Copy/Paste menu. No problem. Just override WM_CONTEXTMENU. But none of the MFC command routing stuff works because the MFC OnInitMenuPopup handler is implemented in CFrameWnd, not CWndeven though any window can display a menu. This problem comes up from time to time in other situations. What you need to fix it once and for all is a little gadget you can drop in any CWnd that routes WM_INITMENUPOPUP through the system. I implemented one called CPopupMenuInitHandler, derived from the CSubclassWnd (née CMsgHook) class I originally described in my "More Fun with MFC" article in the March 1997 issue of MSJ, and which continually reappears in my columns. CSubclassWnd lets you dynamically subclass any CWnd to trap messages. CPopupMenuInitHandler dynamically subclasses the edit control to trap WM_INITMENUPOPUP. When it sees WM_INITMENUPOPUP, it invokes another class/function, CPopupMenuInit::Init, to do the work, which is mostly copied from CFrameWnd. CPopupMenuInit::Init creates a CCmdUI object for each menu item and routes it around the system to all the ON_UPDATE_COMMAND_UI handlers. You can use it independently of CPopupMenuInitHandler any time you want to initialize a menu the MFC way. The code is in MenuInit.cpp. Finally, CEditSearch mimics the activation dynamics found in the Internet Explorer address bar; when you first click it, all the text is selected. Click again to position the cursor. Implementing this simple feature requires handling several messagesWM_MOUSEACTIVATE, WM_ SETFOCUS, WM_LBUTTONDOWN, and any mouse or keyboard messageso I did the whole thing in CEditSearch::WindowProc. Sometimes the old pre-C++ way of doing things is a lot easier than message maps. See the source for details. One final note. Since debugging desk bands is such a pain, I wrote a standalone program unsurprisingly called TestEditSrch to test CEditSearch before I put it in the desk band (see Figure 15). I strongly encourage you to do the same. Get your code to work standalone, then move it to your band. |
![]() |
Figure 15 TestEditSrch |
The code for MyBands/BandObj is too long to print in the magazineand much of it is just tedious MFC nested class COM code. Conclusion
|
![]() For related information see: Creating Custom Explorer Bars and Desk Bands at: http://msdn.microsoft.com/library/psdk/shellcc/shell/bands.htm. Also check http://msdn.microsoft.com for daily updates on developer programs, resources and events. |
From the November 1999 issue of Microsoft Systems Journal.
|