Steve Zimmerman
Steve Zimmerman is a senior software engineer and an adjunct professor at the University of Phoenix. He can be reached at zimsoft@aros.net.
If you’re doing serious commercial software development, it’s likely you are writing an application that must be easily extensible. Perhaps you plan to eventually incorporate features that will be developed by other product teams in your company. Maybe the application will be packaged as several products with varying levels of functionality—named the Standard, Professional, and Enterprise Editions, of course—meaning that you must be able to conditionally enable certain components when the product is installed. You might even have plans to publish an API that allows third-party developers to write extension components that integrate seamlessly into your application.
For example, let’s say you’ve just written a simple word processor like the WordPad application that ships with Windows® 95 and Windows NT® 4.0. Now you want to
create a Professional Edition that provides some of the tools found in other commercial word processors, such as autocorrection, spellchecking, document statistics, and duplicate word removal. In addition, you want to let other vendors provide tools that snap into the application, such
as syntax highlighting, grammar checking, or document
revision management.
While none of these extension components, which I’ll
call snap-ins, are part of the standard product, you want them added to the application’s menu bar and toolbar
once they are installed so they are visually integrated. For now, let me merely define a snap-in as a simple in-process COM server that can be hosted by a client applicationæsimilar to the way that an ActiveX™ control is hosted by an ActiveX control container. A little later on,
I’ll fill you in on the details of a snap-in implementation
I have developed that uses an interface called, appropriately, ISnapIn.
In this article, I’ll show you how to write an application that supports snap-ins. In the spirit of the example given above, I’ll convert the MFC WordPad sample into an extensible snap-in client. I’ll also provide you with three extension components that snap into it: a snap-in that counts and displays the number of the words in the document; a snap-in that removes all duplicate entries of a word (it fixes the “to to” mistake in “I went to to the store”); and a snap-in that automatically corrects common spelling errors.
Before going any further, I want to make it clear that
I’m not attempting to come up with a replacement for
full-featured ActiveX controls. Therefore, if your goal is
to create general-purpose controls such as the Microsoft Calendar Control that support the umpteen control
interfaces expected by a well-rounded ActiveX control container, you’ll have to read elsewhere. Furthermore, I’m
not going to show you how to write a container applica-
tion that is anywhere near as robust as Visual Basic®
or Microsoft Excel. What I am going to do is show you
how to give your application a basic level of extensibility without going to all the effort required to be a full-blown control container.
I should also point out that since the sample code I’ve written uses the Active Template Library (ATL) and takes advantage of new interfaces provided by the ActiveX SDK, I’ve developed it using Visual C++® 5.0. If you’re still using Visual C++ 4.2b, you’ll be able to compile the code, but you’ll need to download the latest version of ATL and create your own makefiles (Visual C++ 5.0 project files are not backwards-compatible).
Before I get to my specific snap-in implementation, let’s talk about the general case. While it has obvious advantages, writing an extensible application is tricky. When designing the application, you can’t make any assumptions about what snap-ins will be available at runtime or you’ll end up releasing a new version of the product every time a new snap-in is created. Similarly, your snap-ins can’t rely too much on the implementation details of the host application or they’ll need to be rewritten every time the application is modified. In short, the less your application and its snap-ins know about each other, the better. With that goal in mind, I’ve come up with the following guidelines.
A snap-in should be self-registering. When a new snap-in is installed, the user should not have to perform any special action from within the host application to enable it. Once registered, the snap-in should appear in the application’s toolbar and menu bar automatically. Similarly, when a snap-in is removed, it should require no action from within the application and should leave no trace.
If appropriate, a snap-in should be able to work with more than one host. While a snap-in must expect specific interfaces to be made available by its clients, the snap-in should work with any application that exposes the interfaces it recognizes. In other words, it should be possible to add a spellchecker snap-in to any application that provides it with an interface method—called something like GetDocumentBuffer—that exposes an LPSTR.
A snap-in should be context sensitive. Depending upon the state of the host application, a snap-in should be able to perform different functions. For example, if a WordPad user selects a block of text with the mouse, a word-count snap-in would display the number of currently selected words. If no text is selected, the snap-in would instead display the total number of words in the document.
A snap-in should receive notification messages from its container when the user clicks the toolbar button or menu item associated with the snap-in, or whenever the internal state of the application changes in a way that affects the snap-in.
While a snap-in could be implemented in a number of ways, it is an ideal candidate for COM due to the nature of the communication between the snap-in and its container. Specifically, a snap-in exposes a well-defined set of functions that allows it to be fully integrated with the host application. In turn, the host exposes one or more interfaces that are recognized by the snap-in. While this model is similar to the traditional ActiveX control container approach, its implementation is likely to be much simpler because a snap-in and its host each need to expose only a single interface (in addition to IUnknown, of course). However, according to the latest ActiveX specification, an ActiveX control is simply a self-registering COM object that supports IUnknown. Thus, in a very real sense, snap-ins are a specialized form of ActiveX controls and a snap-in host is an ActiveX control container!
It is unlikely that my implementation of a snap-in interface—which I named simply ISnapIn for lack of a better name—will meet the needs of every application, but what I’ve come up with will hopefully help you get headed in the right direction. An explanation of each of the ISnapIn methods is shown in Figure 1. You’ll notice that the ISnapIn interface gives the snap-in the flexibility to provide its container with one or more of the following visual elements: menu item text, status bar text, tooltip text, and various sizes of toolbar buttons. However, the snap-in can run silently without any visual interface at all.
Figure 1 ISnapIn Interface Methods
Method Description
HINSTANCE GetResourceInstance(void) Returns a handle to the module that contains the string and bitmap resources used to add the snap-in to the menu bar and toolbar. You can return NULL if the snap-in won’t appear as a menu item.
int GetMenuTextID(void) Returns the resource ID of the menu item text for the snap-in. If you don’t want the snap-in to appear as a menu item, you should return –1.
int GetMessageTextID(void) Returns the resource ID of the status bar text. To provide a tooltip, place that text
at the end of the status bar text, separated by \n. If you don’t want text to appear
in the status bar, you should return –1. This method is only called if the snap-in appears as a menu item.
int GetBitmapID(nSize) Returns the resource ID of the bitmap to be displayed in the application toolbar. Since the application may support various toolbar sizes, the button size is passed
as a parameter. If you don’t want the snap-in to appear in the toolbar, or if you don’t support a particular bitmap size, you should return –1. This method is only called if the snap-in appears as a menu item.
BOOL SupportsInterface(IUnknown* lpUnk) Although an application should only load snap-ins that are marked in the registry as supporting a compatible interface, this method provides an extra safeguard. The application passes this method its context interface immediately after the snap-in is loaded. If the snap-in cannot successfully query an interface that it recognizes, it returns FALSE.
void OnStateChange(IUnknown* lpUnk) Called whenever the state of an application changes in a way that affects the snap-in. The application passes this method a pointer to its context interface.
void IsEnabled(IUnknown* lpUnk) Called by the application to determine whether or not the snap-in menu item should be enabled. In an MFC application, this method is called in response to an ON_UPDATE_
COMMAND_UI message. Thus, depending upon application context, a snap-in may be unavailable to the user.
void OnCommand(IUnknown* lpUnk) Called when the user selects the snap-in from the menu bar or toolbar. The application passes this method a pointer to its context interface.
The ISnapIn interface methods are divided into two functional categories: methods such as GetMenuTextID that provide user-interface integration and methods that respond to user actions. While the user-interface functions are required for menu-merging and toolbar support, the functions that do the real work are the action functions. Specifically, OnStateChange is called whenever the state of the host application changes, and OnCommand is called when the user selects the snap-in from the toolbar, menu, or other user interface mechanism. Obviously, the action performed by these two interface methods depends entirely upon the functionality of the snap-in. In the case of my souped-up version of WordPad, the application calls each snap-in’s OnStateChange method whenever the user changes the text of the document. The word-count snap-in, for example, counts each word in the document and displays the result in the application’s status bar, as shown at the bottom of Figure 2.
Figure 2 The Improved WordPad Application
Obviously, a host application must share information with its snap-ins for them to do anything useful. The WordPad sample, for example, exposes the window handle of the rich edit control containing the user’s document. However, it wouldn’t make sense to implement an ISnapIn interface that always expected its container to pass it a rich edit control—or any other application-specific data, for that matter—because you’d have to come up with a new interface every time you wrote an application that exposed something different. Instead, I’ve implemented ISnapIn so that the only context information passed to the snap-in by its container is a pointer to IUnknown. Of course, IUnknown by itself isn’t very useful, but a snap-in can use it to check for the existence of other interfaces it recognizes by using the QueryInterface method.
The sample snap-ins I wrote look for IRichDocContext—an essentially brain-dead interface I invented that has only two methods: GetRichEditControl and SetStatusText. My new version of WordPad implements IRichDocContext, of course, but the snap-ins it hosts don’t know that they’re integrated with WordPad—they just know that they’re talking to an application that supports IRichDocContext. In your own application, you can expose whatever interfaces you want, but since the entry-point to those interfaces is IUnknown, your snap-ins can implement the exact same ISnapIn interface as the ones I’ve provided.
One of the most important design considerations when writing an extension component is to determine how it will make its existence known to host applications. In other words, if you deploy your application tomorrow and a compatible snap-in is installed on the computer a year from now, how will your application know about it? One method would be for the snap-in to place information about itself in a predefined section of your application’s registry settings. Following this logic, a WordPad-compatible snap-in would place information about itself in the HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\
Applets\WordPad\SnapIns registry section. Each time it was executed, WordPad would check the registry to see if a new snap-in had been added.
While this approach would probably be adequate for snap-ins that are only designed to work with a single application, it suffers from scalability problems. Using this technique, whenever a new snap-in was installed, it would have to determine what compatible applications were already on the machine and place information about itself in the registry section of each application. As a result, a snap-in would have to know an unhealthy amount of information about each of its containers. Furthermore, every time you installed a new application, you’d have to reinstall each of its snap-ins or the application wouldn’t know they existed.
The widely accepted method for solving this problem
has been to place one or more specially-named empty registry keys beneath the CLSID entry of the component as a way of indicating that the component supports a certain category of interfaces. To find its compatible components, an application scans through the list of components found in the registry, looking for CLSID entries marked with
the keys that correspond to the interfaces it requires.
For example, Figure 3 shows the CSLID registry entry
for the Microsoft Calendar Control. You’ll notice that
it contains two empty keys: Control and Programmable.
Because these keys exist, an ActiveX control container
like Visual Basic 4.x knows it can host that control.
Figure 3 The Microsoft Calendar Control Registry Entry
This approach is clearly a much better solution than the one I described earlier because it does not require a control to know anything about its containers. But while this approach has worked fairly well for traditional ActiveX controls, it still has a serious flaw when applied to snap-ins. Suppose you added a special key—named SnapIn, of course—to the CLSID entry for your snap-in, indicating that it supports the ISnapIn interface. This information alone would be insufficient because a host application must also know which snap-ins handle the specific interfaces it exposes. As a result, you’d also have to assign special registry keys for each type of exposed application interface. For example, since WordPad exposes an interface named IRichDocContext, it would have to look for all components marked with two keys: one named SnapIn and the other named something like SupportsRichDocument. If this categorization method were widely used, there would eventually be name collisions in the snap-in category descriptions. Perhaps a software developer in New Jersey would develop an application with an interface called IAmARichDoctor, but also name its corresponding registry key SupportsRichDocument. Or even worse, some guy in Vernal, Utah, would tweak the ISnapIn interface but still use the word SnapIn as its registry keyword. Pretty soon you’d end up with a real mess.
Fortunately, with the release of the ActiveX Platform SDK, there’s a new specification—component categories—that solves these problems. Similar to the special keys used to classify traditional ActiveX controls, component categories are registry entries that describe the type of functionality an ActiveX component supports. However, there are several improvements over the old model.
Instead of using human-readable names, component categories are defined using globally unique identifiers called category identifiers, or CATIDs. For example, the CATIDs registered on my machine are shown in Figure 4. Because a CATID is guaranteed to be unique, there’s no fear of name collisions between different categories. Each CATID has one or more locale-specific text descriptions associated with it, stored in a well-known location in the registry. This makes it easier for control containers to support multiple languages.
Figure 4 Component Categories
In addition to describing the categories it implements, an ActiveX control can also describe the categories it requires from its container. This extra information will help solve a problem that has frustrated ActiveX control developers in the past. Previously, even when a developer created a control that provided all of the required interfaces, he or she might get varied—and often undesired—results due to the different way each control container (Visual Basic 4.x or 5.0, Microsoft Excel, Internet Explorer 3.0, Visual C++ Test
Container, and so on) interacts with the control. Since a developer can now define the exact set of interfaces that
the ActiveX control requires its container to implement, these inconsistencies should eventually disappear. Unfortunately, backward compatibility with old containers will still be a problem.
There is a system-provided COM object called the Component Category Manager that gives you two interfaces—ICatRegister and ICatInformation—that you can use to store and retrieve category information from the registry. No more parsing the registry by hand to find the controls that implement a certain category! While you’ll need to refer to the online help for specific information on how to use those interfaces, here’s how easy it is to create instances of them:
ICatInformation* lpCatInfo;
HRESULT hr = CoCreateInstance(
CLSID_StdComponentCategoriesMgr, NULL,
CLSCTX_INPROC_SERVER, IID_ICatInformation,
(LPVOID*) &lpCatInfo);
ICatRegister* lpCatReg;
hr = CoCreateInstance(
CLSID_StdComponentCategoriesMgr, NULL,
CLSCTX_INPROC_SERVER, IID_ICatRegister,
(LPVOID*) &lpCatReg);
As I mentioned earlier, I came up with a simple interface called ISnapIn that provides just enough information to allow you to integrate a COM object with an application. Naturally, I defined a component category called CATID_
ISnapIn that classifies an ActiveX component as a snap-in. While most component categories indicate support for
several interfaces, registering your component as a member of the CATID_ISnapIn category simply means you guarantee that your object implements ISnapIn. In fact, I’ll confess that the GUID I used to identify the CATID_ISnapIn category is the same one that identifies the ISnapIn interface, as you can see in Figure 5. For the WordPad sample, I also defined a category named CATID_IRichDocContext that applies only to containers supporting my IRichDocContext interface.
// Author: Steve Zimmerman
//
// ISnapIn Interface Definition:
// {6AE74760-83C6-11D0-A2A7-000000000000}
DEFINE_GUID(IID_ISnapIn,
0x6ae74760, 0x83c6, 0x11d0, 0xa2, 0xa7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0);
#define CATID_ISnapIn IID_ISnapIn
DECLARE_INTERFACE_(ISnapIn, IUnknown)
{
STDMETHOD_(BOOL, SupportsInterface)(THIS_ IUnknown* lpUnk) PURE;
STDMETHOD_(HINSTANCE, GetResourceInstance) (THIS_) PURE;
STDMETHOD_(int, GetMenuTextID)(THIS_) PURE;
STDMETHOD_(int, GetMessageTextID)(THIS_) PURE;
STDMETHOD_(int, GetBitmapID)(THIS_ UINT nSize) PURE;
STDMETHOD_(BOOL, IsEnabled)(THIS_ IUnknown* lpUnk) PURE;
STDMETHOD_(void, OnStateChange)(THIS_ IUnknown* lpUnk) PURE;
STDMETHOD_(void, OnCommand)(THIS_ IUnknown* lpUnk) PURE;
};
In production-quality code, you should not use the same GUID to identify both the CATID and its associated interfaces. However, since the code I’ve provided is just a sample, what I’ve done is probably OK. Furthermore, even though the number of possible component categories is infinite, you should avoid defining a new component category if there is an existing category that can be used instead. This suggestion is consistent with the idea that controls and containers should be designed with optimum interoperability in mind. As with regular ActiveX controls, if you are writing production-quality snap-ins or snap-in containers, you should collaborate with other vendors when defining new component categories to ensure that they meet the common requirements of your market. Microsoft has said it plans
to provide an up-to-date list of the component categories developed by itself and Microsoft vendor partners at
http://www.microsoft.com. At press time the exact URL had not been defined, but I expect it will be in the near future. In the meantime, a search of the site for “component categories” may be helpful.
Before I delve into the details of my sample snap-ins, let me explain the enhancements I made to WordPad so that it supports them. When making these changes, I had two goals in mind. First, I wanted to change as few routines as possible. In the spirit of information hiding—a key concept in object-oriented design—I wanted the application to know as little about snap-ins as possible. I’ll also admit that I didn’t want to have to take the time to learn about the inner-workings of WordPad—I just wanted to sneak into the code, add the snap-in logic, and get out! My secondary goal was to encapsulate the snap-in code so that it would be easily reusable. If I’ve done my job well, you’ll be able to take the code I added to WordPad and slip it into your own application with little effort.
What I ended up with was two classes, CSnapInFrame and CSnapIn, that implement the functionality required
to be a snap-in container. An overview of those classes appears in Figure 6. Adding them to the WordPad code that ships with Visual C++ was fairly easy. Since the WordPad project contains many files, I created a SnapIn subdirectory beneath Project Root in which I placed all of the reusable code. Then, I added those files to the project.
Figure 6 CSnapInFrame and CSnapIn Classes
Class Explanation
CSnapInFrame Derived from CFrameWnd, this class looks up the compatible snap-ins in the registry (using the ICatInformation interface), creates a snap-in toolbar, routes command messages to the snap-in, and performs special handling required to enable the snap-in status bar messages and tooltips.
CSnapIn Creates an instance of the specified snap-in class and provides a light wrapper around the ISnapIn interface. Adds the snap-in menu item and toolbar button to the application’s menu bar and toolbar.
I replaced all references to CFrameWnd in mainfrm.h and mainfrm.cpp so that CMainFrame was derived from CSnapInFrame. I created a simple interface, IRichDocContext, that WordPad uses to make its context information available to the snap-ins. The header file for that interface is shown in Figure 7. I added the header file to the project
and added the following code to the CMainFrame class definition:
DECLARE_INTERFACE_MAP()
BEGIN_INTERFACE_PART(ContextObj, IRichDocContext)
STDMETHOD_(HWND, GetRichEditCtrl)();
STDMETHOD_(void, SetStatusText)(LPCTSTR);
END_INTERFACE_PART(ContextObj)
// Author: Steve Zimmerman
//
// IRichDocContext Interface Definition:
// {305BB760-8993-11D0-A2C4-000000000000}
DEFINE_GUID(IID_IRichDocContext,
0x305bb760, 0x8993, 0x11d0, 0xa2, 0xc4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0);
#define CATID_IRichDocContext IID_IRichDocContext
DECLARE_INTERFACE_(IRichDocContext, IUnknown)
{
STDMETHOD_(HWND, GetRichEditCtrl)() PURE;
STDMETHOD_(void, SetStatusText)(LPCTSTR) PURE;
};
If you’ve used OLE with MFC, you’ll immediately recognize this as the way to expose a COM interface. I don’t have the space to explain what these macros do and how they work, so if you’ve never done this type of thing before, I recommend you read (and reread) the MFC help topic entitled “TN038: MFC/OLE IUnknown Implementation.” It is sufficient to say that those macros create an embedded class within CMainFrame—called XContextObj, incidentally—that implements the IRichDocContext interface. They also create a member variable named m_xContextObj that represents an instance of the interface. Thus, somewhere in the source code for CMainFrame, I had to implement the GetRichEditCtrl and SetStatusText methods (see Figure 8). As expected, my GetRichEditCtrl interface method returns the window handle of the rich edit control used to display the WordPad document:
HWND FAR EXPORT CMainFrame::XContextObj::GetRichEditCtrl()
{
METHOD_PROLOGUE(CMainFrame, ContextObj)
CRichEditView* pView =
DYNAMIC_DOWNCAST(CRichEditView,
pThis->GetActiveView());
ASSERT(pView);
return pView->GetRichEditCtrl().GetSafeHwnd();
}
Your applications probably won’t use IRichDocContext—at least I sure hope not, because it is admittedly a kludge—but you can still follow this model.
//
IUnknown* CMainFrame::GetSnapInContext(int nMsg)
{
// This function is called whenever we need to expose
// an interface to the snap-in. If this application
// has just called OnStateChange, which results in a
// call to this function, nMsg will be an application
// defined value that gives us added flexibility.
// Otherwise, nMsg is zero. In the case of WordPad, we
// always return the IRichDocContext interface pointer.
return &m_xContextObj;
}
int CMainFrame::GetSupportedCategories(GUID** ppCatIDs)
{
// CMainFrame supports
static CATID CatIDs[1];
CatIDs[0] = CATID_IRichDocContext;
*ppCatIDs = CatIDs;
return 1;
}
BEGIN_INTERFACE_MAP(CMainFrame, CSnapInFrame)
INTERFACE_PART(CMainFrame, IID_IRichDocContext, ContextObj)
END_INTERFACE_MAP()
HWND FAR EXPORT CMainFrame::XContextObj::GetRichEditCtrl()
{
METHOD_PROLOGUE(CMainFrame, ContextObj)
CRichEditView* pView =
DYNAMIC_DOWNCAST(CRichEditView, pThis->GetActiveView());
ASSERT(pView);
return pView->GetRichEditCtrl().GetSafeHwnd();
}
void FAR EXPORT CMainFrame::XContextObj::SetStatusText(LPCTSTR lpText)
{
METHOD_PROLOGUE(CMainFrame, ContextObj)
pThis->SetMessageText(lpText);
}
ULONG FAR EXPORT CMainFrame::XContextObj::AddRef()
{
METHOD_PROLOGUE(CMainFrame, ContextObj)
return pThis->ExternalAddRef();
}
ULONG FAR EXPORT CMainFrame::XContextObj::Release()
{
METHOD_PROLOGUE(CMainFrame, ContextObj)
return pThis->ExternalRelease();
}
HRESULT FAR EXPORT CMainFrame::XContextObj::QueryInterface(
REFIID iid, void FAR* FAR* ppvObj)
{
METHOD_PROLOGUE(CMainFrame, ContextObj)
return (HRESULT)pThis->ExternalQueryInterface(&iid, ppvObj);
}
Since I wrote CSnapInFrame to be reusable, it knows nothing about the IRichDocContext interface. As a result, it has two pure virtual functions—GetSnapInContext and GetSupportedCategories—that I had to implement in CMainFrame:
IUnknown* CMainFrame::GetSnapInContext(int nMsg)
{
return &m_xContextObj;
}
int CMainFrame::GetSupportedCategories(GUID** ppCatIDs)
{
static CATID CatIDs[1];
CatIDs[0] = CATID_IRichDocContext;
*ppCatIDs = CatIDs;
return 1;
}
In the GetSnapInContext function, although the code actually returns the address of m_xContextObj—an IRichDocContext pointer, in other words—the return value is cast into a pointer to IUnknown. This way, the CSnapInFrame class can pass the host interface along to the snap-in without getting its hands dirty. Thanks to COM, when a snap-in gets the IUnknown pointer, it can simply call QueryInterface to get back the pointer to IRichDocContext. Thus, your application can return a pointer to IDontKnow or IDontCare, but you won’t have to change CSnapInFrame.
The GetSupportedCategories virtual function is the
way an application specifies which container categories it implements. Again, since CSnapInFrame is not application-specific, this function has no default implementation; it must be implemented by any class derived from CSnapInFrame. WordPad only implements one snap-in container category, CATID_IRichDocContext, but your application may expose several. In that case, you’d point ppCatIDs
(see the previous code) to an array containing the CATIDs you implement and return the number of elements in that array.
To notify each snap-in whenever the user changes the WordPad document, I added the following code to CWordPadView::OnEditChange:
CMainFrame* pFrame = DYNAMIC_DOWNCAST(CMainFrame,
AfxGetMainWnd());
if (pFrame)
pFrame->OnStateChange(EN_CHANGE);
As you can see, when the user changes the text in the rich edit control, the application routes that message to the frame window. In turn, CSnapInFrame calls the OnStateChange interface method of each snap-in. The frequency of communication between WordPad and its snap-ins makes it possible for the word count snap-in to keep an accurate count of the number of words in the document, which it displays at the bottom of the screen. Incidentally, you should be aware that if you call OnStateChange too often (especially if your snap-ins perform a large number of calculations each time they are called), your application’s responsiveness will suffer.
To avoid interfering with the registry settings of the real WordPad—it is, after all, an application that ships with Windows 95 and Windows NT 4.0—I changed the enhanced version’s relative registry path from Microsoft\Windows\
CurrentVersion\Applets\WordPad to Microsoft\Microsoft Systems Journal\WordPad. This was more than a matter of politeness, incidentally. As it turns out, WordPad uses MFC’s CDockState class to store and retrieve the position and state of its toolbars. Since my version of WordPad has an additional toolbar, it adds more information to the registry than the original WordPad can handle.
Finally, I changed the project settings so that the output directory for all build configurations is set to a single subfolder. I did this because the data files WordPad uses to convert between document types—namely, Word 6 and rich text format—must be located in the same directory as the application. Rather than place duplicate versions of those files in several different directories (Debug, DebugU, Release, and ReleaseU), I have the linker place the output files in a single directory regardless of the configuration. Since those files are quite large, this change will make a significant difference in the size of the file you have to download to get the sample code!
While it took several steps to wire things up, I was able to add snap-in functionality to WordPad with minimal
code changes. In fact, except for the two snippets of code I described above, all of the changes were specific to CMainFrame. Figure 8 shows the new functions I added to that class.
While I’ll let you sort through the nitty gritty implementation details of CSnapInFrame at your leisure (see Figure 9), here’s a high-level overview of what it does.
Figure 9 the CSnapInFrame Class
SnapFrm.h
// Copyright (c) 1997, Microsoft Systems Journal
// Author: Steve Zimmerman
//
// SnapFrm.h : header file
//
class CSnapIn;
class CSnapInManager;
interface ICatInformation;
interface IEnumCLSID;
class CSnapInFrame : public CFrameWnd
{
protected:
DECLARE_DYNAMIC(CSnapInFrame);
CTypedPtrArray<CPtrArray, CSnapIn*> m_snapIn;
CToolBar m_snapInBar;
void HideSnapInBar();
virtual ~CSnapInFrame();
virtual int CreateSnapInBar();
virtual int AddMenuEntries();
virtual BOOL GetNextSnapInClsID(ICatInformation** ppCatInfo,
IEnumCLSID** ppEnum, CLSID* pClsID);
virtual IUnknown* GetSnapInContext(int nMsg) = 0;
virtual int GetSupportedCategories(GUID** ppCatIDs) = 0;
//{{AFX_MSG(CSnapInFrame)
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
afx_msg void OnUpdateViewSnapInBar(CCmdUI* pCmdUI);
afx_msg BOOL OnViewSnapInBarCheck(UINT nID);
afx_msg void OnUpdateSnapIn(CCmdUI* pCmdUI);
afx_msg void OnSnapIn(UINT nID);
afx_msg BOOL OnToolTipText(UINT, NMHDR* pNMHDR, LRESULT* pResult);
public:
void OnStateChange(int nMsg);
//{{AFX_VIRTUAL(CSnapInFrame)
public:
virtual void ActivateFrame(int nCmdShow = -1);
//}}AFX_VIRTUAL
virtual void GetMessageString(UINT nID, CString& rMessage) const;
};
// These must be defined in order for CSnapInFrame to
// compile correctly. However, you may define them as
// you wish in resource.h if these values conflict
// with yours.
#ifndef ID_VIEW_SNAPINBAR
#define ID_VIEW_SNAPINBAR 59999
#define ID_TOOLS_START 60000
#define ID_TOOLS_END 60100
#endif
SnapFrm.cpp
// Copyright (c) 1997, Microsoft Systems Journal
// Author: Steve Zimmerman
//
// SnapFrm.cpp : implementation file
//
#include "stdafx.h"
#include "..\resource.h"
#include <comcat.h>
#include <afxtempl.h>
#include "ISnapIn.h"
#include "SnapFrm.h"
#include "SnapIn.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CSnapInFrame
IMPLEMENT_DYNAMIC(CSnapInFrame, CFrameWnd)
CSnapInFrame::~CSnapInFrame()
{
for (int nLoop = 0; nLoop < m_snapIn.GetSize(); nLoop++)
delete m_snapIn[nLoop];
}
int CSnapInFrame::CreateSnapInBar()
{
EnableDocking(CBRS_ALIGN_ANY);
m_snapInBar.Create(this, WS_CHILD|WS_VISIBLE|CBRS_TOP,
ID_VIEW_SNAPINBAR);
HDC screenDC = ::GetDC(NULL);
BOOL bLargeIcons = GetDeviceCaps(screenDC, LOGPIXELSX) >= 120;
::ReleaseDC(NULL, screenDC);
if (bLargeIcons)
m_snapInBar.SetSizes(CSize(31,30), CSize(24,24));
else
m_snapInBar.SetSizes(CSize(23,22), CSize(16,16));
m_snapInBar.SetBarStyle(m_snapInBar.GetBarStyle() |
CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC);
m_snapInBar.EnableDocking(CBRS_ALIGN_ANY);
DockControlBar(&m_snapInBar);
return bLargeIcons ? 24 : 16;
}
#define VIEW_MENU_POSITION 2
#define TOOLS_MENU_POSITION 5
int CSnapInFrame::AddMenuEntries()
{
CMenu* pSubMenu = GetMenu()->GetSubMenu(VIEW_MENU_POSITION);
ASSERT(pSubMenu);
pSubMenu->InsertMenu(0, MF_BYPOSITION|MF_STRING|MF_CHECKED,
ID_VIEW_SNAPINBAR, _T("Snap-&In Bar"));
HMENU hMenu = ::CreateMenu();
GetMenu()->InsertMenu(TOOLS_MENU_POSITION, MF_BYPOSITION|MF_POPUP,
(UINT) hMenu, _T("&Tools"));
return TOOLS_MENU_POSITION;
}
void CSnapInFrame::HideSnapInBar()
{
CControlBar* pBar = GetControlBar(ID_VIEW_SNAPINBAR);
if (pBar != NULL)
ShowControlBar(pBar, FALSE, FALSE);
}
void CSnapInFrame::OnStateChange(int nMsg)
{
for (int nLoop = 0; nLoop < m_snapIn.GetSize(); nLoop++)
{
ISnapIn* pInt = m_snapIn[nLoop]->Interface();
pInt->OnStateChange(GetSnapInContext(nMsg));
}
}
BOOL CSnapInFrame::GetNextSnapInClsID(ICatInformation** ppCatInfo,
IEnumCLSID** ppEnum, CLSID* pClsID)
{
ASSERT(ppCatInfo && ppEnum);
// We get a pointer to ICatInformation so that we can find all
// of the compatible SnapIns in the registry.
if (*ppCatInfo == NULL)
{
HRESULT hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr, NULL,
CLSCTX_INPROC_SERVER, IID_ICatInformation, (LPVOID*) ppCatInfo);
if (hr != S_OK)
return FALSE;
}
// We only enumerate the SnapIns that support CATID_ISnapIn
// and are compatible with this application.
if (*ppEnum == NULL)
{
CATID* pCatIDs;
int reqCatIDs = GetSupportedCategories(&pCatIDs);
CATID snapInCatID = CATID_ISnapIn;
HRESULT hr = (*ppCatInfo)->EnumClassesOfCategories(1, &snapInCatID,
reqCatIDs, pCatIDs, ppEnum);
if (hr != S_OK)
{
(*ppCatInfo)->Release();
return FALSE;
}
(*ppEnum)->Reset();
}
ULONG lFetched;
if ((*ppEnum)->Next(1, pClsID, &lFetched) != S_OK)
{
(*ppEnum)->Release();
(*ppCatInfo)->Release();
return FALSE;
}
return TRUE;
}
BEGIN_MESSAGE_MAP(CSnapInFrame, CFrameWnd)
//{{AFX_MSG_MAP(CSnapInFrame)
ON_WM_CREATE()
//}}AFX_MSG_MAP
ON_UPDATE_COMMAND_UI(ID_VIEW_SNAPINBAR, OnUpdateViewSnapInBar)
ON_COMMAND_EX(ID_VIEW_SNAPINBAR, OnViewSnapInBarCheck)
ON_UPDATE_COMMAND_UI_RANGE(ID_TOOLS_START, ID_TOOLS_END, OnUpdateSnapIn)
ON_COMMAND_RANGE(ID_TOOLS_START, ID_TOOLS_END, OnSnapIn)
ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTW, 0, 0xFFFF, OnToolTipText)
ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTA, 0, 0xFFFF, OnToolTipText)
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CSnapInFrame message handlers
int CSnapInFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CFrameWnd::OnCreate(lpCreateStruct) == -1)
return -1;
int nBmpSize = CreateSnapInBar();
CLSID clsID;
IEnumCLSID* pEnum = NULL;
ICatInformation* pCatInfo = NULL;
int nSubMenu;
int nIndex = 0;
while (GetNextSnapInClsID(&pCatInfo, &pEnum, &clsID))
{
CSnapIn* pSnapIn = new CSnapIn(clsID, nIndex + ID_TOOLS_START);
if (!pSnapIn->Interface() ||
!pSnapIn->Interface()->SupportsInterface(GetSnapInContext(0)))
{
delete pSnapIn;
continue;
}
if (m_snapIn.GetSize() == 0)
nSubMenu = AddMenuEntries();
ASSERT(GetMenu()->GetSubMenu(nSubMenu));
if (pSnapIn->AddMenuItem(GetMenu()->GetSubMenu(nSubMenu)))
pSnapIn->AddToolbarButton(m_snapInBar, nBmpSize);
m_snapIn.Add(pSnapIn);
nIndex++;
}
return 0;
}
void CSnapInFrame::ActivateFrame(int nCmdShow)
{
if (m_snapIn.GetSize() == 0)
HideSnapInBar();
CFrameWnd::ActivateFrame(nCmdShow);
}
void CSnapInFrame::GetMessageString(UINT nID, CString& rMessage) const
{
if (nID < ID_TOOLS_START || nID > ID_TOOLS_END)
{
CFrameWnd::GetMessageString (nID, rMessage);
return;
}
CSnapIn* pSnapIn = m_snapIn[nID - ID_TOOLS_START];
HINSTANCE myInst = AfxGetResourceHandle();
AfxSetResourceHandle(pSnapIn->Interface()->GetResourceInstance());
nID = pSnapIn->Interface()->GetMessageTextID();
CFrameWnd::GetMessageString(nID, rMessage);
AfxSetResourceHandle(myInst);
}
void CSnapInFrame::OnUpdateViewSnapInBar(CCmdUI* pCmdUI)
{
CControlBar* pBar = GetControlBar(ID_VIEW_SNAPINBAR);
if (pBar != NULL)
{
pCmdUI->SetCheck((pBar->GetStyle() & WS_VISIBLE) != 0);
return;
}
pCmdUI->ContinueRouting();
}
BOOL CSnapInFrame::OnViewSnapInBarCheck(UINT nID)
{
CControlBar* pBar = GetControlBar(ID_VIEW_SNAPINBAR);
if (pBar != NULL)
{
ShowControlBar(pBar, (pBar->GetStyle() & WS_VISIBLE) == 0, FALSE);
return TRUE;
}
return FALSE;
}
void CSnapInFrame::OnUpdateSnapIn(CCmdUI* pCmdUI)
{
CSnapIn* pSnapIn = m_snapIn[pCmdUI->m_nID - ID_TOOLS_START];
pCmdUI->Enable(pSnapIn->Interface()->
IsEnabled(GetSnapInContext(0)));
}
void CSnapInFrame::OnSnapIn(UINT nID)
{
CSnapIn* pSnapIn = m_snapIn[nID - ID_TOOLS_START];
pSnapIn->Interface()->OnCommand(GetSnapInContext(0));
}
BOOL CSnapInFrame::OnToolTipText(UINT nID, NMHDR* pNMHDR, LRESULT* pResult)
{
if (pNMHDR->idFrom < ID_TOOLS_START || pNMHDR->idFrom > ID_TOOLS_END)
return CFrameWnd::OnToolTipText(nID, pNMHDR, pResult);
CSnapIn* pSnapIn = m_snapIn[pNMHDR->idFrom - ID_TOOLS_START];
HINSTANCE myInst = AfxGetResourceHandle();
AfxSetResourceHandle(pSnapIn->Interface()->GetResourceInstance());
pNMHDR->idFrom = pSnapIn->Interface()->GetMessageTextID();
BOOL bResult = CFrameWnd::OnToolTipText(nID, pNMHDR, pResult);
AfxSetResourceHandle(myInst);
return bResult;
}
At create time, CSnapInFrame always creates a snap-in toolbar, regardless of whether or not it finds any compatible snap-ins. If it finds none, the empty toolbar is hidden from view when the frame window is activated. I had to do this because CDockState does not respond gracefully to a missing toolbar. Consider this perilous scenario: the user installs a snap-in and then runs the host application. When the application terminates, it stores the state of the snap-in toolbar in the registry using CDockState. Later, the user uninstalls the snap-in. The next time the application is executed, CSnapInFrame does not create a snap-in toolbar because there are no snap-ins to display. Consequently, CDockState crashes the application while trying to restore the state of a toolbar that has not been created!
Immediately after creating the snap-in toolbar, CSnapInFrame builds an array of CSnapIn objects used to keep track of each snap-in. It does this by traversing the list of compatible snap-ins using the ICatInformation interface. Incidentally, each snap-in is actually loaded into memory by the CSnapIn constructor using a call to CoCreateInstance. The CSnapIn class stores the pointer to ISnapIn so that it can be used in later communication with that component.
If any snap-ins exist, CSnapInFrame calls its AddMenuEntries function, which adds a command to the View menu that lets the user toggle the visibility of the snap-in toolbar. It also creates a Tools menu, where the menu command for each snap-in is placed. Since AddMenuEntries is a virtual function, you can easily override it if you want your menu to behave differently. Each CSnapIn object adds its snap-in’s toolbar button and menu item to the application window. As I’ve mentioned previously, a hidden snap-in may not provide that information, so CSnapIn is written to handle that case as well.
Because the tool-tip text and status bar message for each snap-in do not reside in the resource module for the application, CSnapInFrame has to perform some sleight-of-hand to display them properly. This is because MFC expects to find those resources in the same module as all of the other resources used by the application. To get around this, CSnapInFrame overrides the GetMessageString and OnToolTipText functions and twiddles the application’s global resource handle (using a call to AfxSetResourceHandle) before and after calling the base-class implementation of those functions. This approach works fine, but it’s not thread-safe. If your application calls AfxGetResourceHandle from simultaneous threads, you’ll need to add thread synchronization to your code to make sure that other threads are properly blocked while the primary thread is processing GetMessageString or OnToolTipText.
CSnapInFrame has three functions—OnSnapIn, OnUpdateSnapInUI, and OnStateChanged—that make calls to the ISnapIn interface functions OnCommand, IsEnabled, and OnStateChanged, respectively. Refer to Figure 9 for details.
As I mentioned at the outset, I wrote three simple snap-ins—a word counter, a duplicate word remover, and an autocorrect tool—but don’t expect to see them for sale anytime soon! I’ve provided them just to give you an idea of the kinds of snap-ins you can build yourself. Unfortunately, unlike the CSnapInFrame and CSnapIn classes, most of the sample snap-in code is specific to its integration with an IRichDocContext host application. Thus, you won’t be able to copy and paste large chunks of the code into your own snap-in projects.
I wrote the snap-ins using a combination of ATL and MFC. Before beginning this project, I often wondered why anyone would want to mix those two frameworks. Now I have an answer: ATL makes working with COM interfaces a painless experience, but it really can’t compare with MFC when it comes to developing dialog boxes. So, I used ATL for everything but the autocorrect preferences dialog box (see Figure 10).
Figure 10 Autocorrect Preferences
Each snap-in exposes an implementation of the ISnapIn interface, which has three methods that get called by the host application in response to user action: OnStateChanged, IsEnabled, and OnCommand (refer to Figure 1). Each snap-in handles those functions differently. The word-count snap-in counts the number of words in the document every time its OnStateChanged method is called. It has no menu item or toolbar button, so its OnCommand and IsEnabled methods are never called. The duplicate word snap-in scans the document for repeated words whenever the user selects it from the menu, but its OnStateChanged method does nothing; its IsEnabled function returns false when the WordPad document is empty. The autocorrect snap-in replaces spelling errors
in the document in its OnStateChanged method and displays a settings dialog box whenever its OnCommand method is called.
It seems to me that the ideal way to register the snap-in component categories would be to derive my own class from CComModule and override its RegisterClassHelper and UnregisterClassHelper functions. However, I was disappointed to find that neither of those functions is virtual—at least, they weren’t at the time of this writing. Of course, I’ll probably get email from you COM experts out there telling me that the best way to do it is with registry scripting. Since I haven’t figured out how to do that yet, my approach was to write three helper functions—RegisterComponentCategory, RegisterClassReqCategory, and RegisterClassImplCategory (see Figure 11)—that you may find useful in your own code. They act as general-purpose wrappers around the calls to the interface methods of ICatRegister. With the help of those functions, adding component category registration to the DllRegisterServer function was
a snap:
STDAPI DllRegisterServer(void)
{
RegisterComponentCategory(CATID_ISnapIn,
_T("Snap-Ins"));
RegisterComponentCategory(CATID_IRichDocContext,
T("Snap-Ins that support the
IRichDocContext interface"));
_ATL_OBJMAP_ENTRY* pEntry = _Module.m_pObjMap;
while (pEntry->pclsid != NULL)
{
RegisterClassImplCategory(pEntry->pclsid,
CATID_ISnapIn);
RegisterClassReqCategory(pEntry->pclsid,
CATID_IRichDocContext);
pEntry++;
}
return _Module.RegisterServer(FALSE) // No typelib
}
// Author: Steve Zimmerman
//
// SnapIns.cpp : Implementation of DLL Exports.
// You will need the NT SUR Beta 2 SDK or VC 4.2 in order to build this
// project. This is because you will need MIDL 3.00.15 or higher and new
// headers and libs. If you have VC 4.2 installed, then everything should
// already be configured correctly.
// Note: Proxy/Stub Information
// To build a separate proxy/stub DLL,
// run nmake -f SnapInsps.mak in the project directory.
#include "stdafx.h"
#include "resource.h"
#include "initguid.h"
#include "comcat.h"
#include "ISnapIn.h"
#include "IRichDoc.h"
#include "RichDoc.h"
#include "WordCount.h"
#include "RemoveDup.h"
#include "Correct.h"
HRESULT RegisterComponentCategory(CATID catid, TCHAR* catDescription,
BOOL bRegister = TRUE);
HRESULT RegisterClassReqCategory(const CLSID* rclsid, CATID rgcatid,
BOOL bRegister = TRUE);
HRESULT RegisterClassImplCategory(const CLSID* rclsid, CATID rgcatid,
BOOL bRegister = TRUE);
#define IID_DEFINED
CComModule _Module;
BEGIN_OBJECT_MAP(ObjectMap)
OBJECT_ENTRY(CLSID_CWordCount, CWordCount)
OBJECT_ENTRY(CLSID_CRemoveDup, CRemoveDup)
OBJECT_ENTRY(CLSID_CAutoCorrect, CAutoCorrect)
END_OBJECT_MAP()
class CMyApp : public CWinApp
{
public:
virtual BOOL InitInstance();
virtual int ExitInstance();
};
CMyApp theApp;
BOOL CMyApp::InitInstance()
{
_Module.Init(ObjectMap, m_hInstance);
return CWinApp::InitInstance();
}
int CMyApp::ExitInstance()
{
_Module.Term();
return CWinApp::ExitInstance();
}
/////////////////////////////////////////////////////////////////////////////
// Used to determine whether the DLL can be unloaded by OLE
STDAPI DllCanUnloadNow(void)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
return (AfxDllCanUnloadNow()==S_OK && _Module.GetLockCount()==0) ? S_OK : S_FALSE;
}
/////////////////////////////////////////////////////////////////////////////
// Returns a class factory to create an object of the requested type
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)
{
return _Module.GetClassObject(rclsid, riid, ppv);
}
/////////////////////////////////////////////////////////////////////////////
// DllRegisterServer - Adds entries to the system registry
STDAPI DllRegisterServer(void)
{
// First we register the ISnapIn and IRichDocContext
// interfaces as special categories. For simplicity,
// I just use the CLSIDs as the CATIDs. In your code,
// you should register ISnapIn (and IRichDocContext if
// you happen to use it) using the same text description
// given here. We can't unregister these categories
// since another module might require them.
RegisterComponentCategory(CATID_ISnapIn, _T("Snap-Ins"));
RegisterComponentCategory(CATID_IRichDocContext,
_T("Snap-Ins that support the IRichDocContext interface"));
// Now we register each control as implementing ISnapIn
// and requiring that its container support IRichDocContext
_ATL_OBJMAP_ENTRY* pEntry = _Module.m_pObjMap;
while (pEntry->pclsid != NULL)
{
RegisterClassImplCategory(pEntry->pclsid, CATID_ISnapIn);
RegisterClassReqCategory(pEntry->pclsid, CATID_IRichDocContext);
pEntry++;
}
// register all the other stuff
return _Module.RegisterServer(FALSE); // No typelib
}
/////////////////////////////////////////////////////////////////////////////
// DllUnregisterServer - Removes entries from the system registry
STDAPI DllUnregisterServer(void)
{
_Module.UnregisterServer();
return S_OK;
}
HRESULT RegisterComponentCategory(CATID catid,
TCHAR* catDescription, BOOL bRegister)
{
ICatRegister* pcr = NULL;
HRESULT hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr,
NULL, CLSCTX_INPROC_SERVER, IID_ICatRegister, (void**)&pcr);
if (FAILED(hr))
return hr;
// Make sure the HKCR\Component Categories\{..catid...}
// key is registered
CATEGORYINFO catinfo;
catinfo.catid = catid;
catinfo.lcid = 0x0409; // English is all for now
// Make sure the provided description is not too long.
// Only copy the first 127 characters if it is
USES_CONVERSION;
int len = min(_tcslen(catDescription), 127);
wcsncpy(catinfo.szDescription, T2W(catDescription), len);
catinfo.szDescription[len] = 0;
if (bRegister)
hr = pcr->RegisterCategories(1, &catinfo);
else
hr = pcr->UnRegisterCategories(1, &catid);
pcr->Release();
return hr;
}
HRESULT RegisterClassReqCategory(const CLSID* pclsid,
CATID rgcatid, BOOL bRegister)
{
_ASSERTE(pclsid);
ICatRegister* pcr = NULL;
HRESULT hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr,
NULL, CLSCTX_INPROC_SERVER, IID_ICatRegister, (void**)&pcr);
if (FAILED(hr))
return hr;
return bRegister ? pcr->RegisterClassReqCategories(*pclsid, 1, &rgcatid)
: pcr->UnRegisterClassReqCategories(*pclsid, 1, &rgcatid);
}
HRESULT RegisterClassImplCategory(const CLSID* pclsid,
CATID rgcatid, BOOL bRegister)
{
_ASSERTE(pclsid);
ICatRegister* pcr = NULL;
HRESULT hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr,
NULL, CLSCTX_INPROC_SERVER, IID_ICatRegister, (void**)&pcr);
if (FAILED(hr))
return hr;
return bRegister ? pcr->RegisterClassImplCategories(*pclsid, 1, &rgcatid)
: pcr->UnRegisterClassImplCategories(*pclsid, 1, &rgcatid);
}
The autocorrect snap-in allows users to maintain a persistent list of commonly misspelled words (and their replacements, of course) by storing that information in a file. However, I had trouble deciding whether the file should be specific to each host application or shared among all of them. I finally decided to associate a wordlist with its host so each application using the autocorrect snap-in has its own file. I probably should have developed an additional interface that the application uses to tell the snap-in where to store the file, but I simply use the same path name as the host application and change its file extension, like so:
TCHAR sFileName[_MAX_PATH];
GetModuleFileName(AfxGetInstanceHandle(), sFileName,
_MAX_PATH);
TCHAR* pFileExt = _tcschr(sFileName, _T('.'));
_tcscpy(pFileExt, _T(".acf")); // auto-correct file
I’ve shown you how to use simple COM objects, called snap-ins, to extend the functionality of your application. I’ve discussed some of the features of an ideal snap-in and I showed you an implementation that uses my homegrown ISnapIn interface. Feel free to modify and improve that interface to meet your needs. Just remember to use a different CLSID in case someone else does the same thing. Hopefully, I’ve given you several ideas—and some nifty sample code—that will help you along the way. Incidentally, if you happen to develop a super-duper WordPad-compatible snap-in, I’d love to hear about it. u
To obtain complete source code listings, see page 5.