Sean Baxter

COM Automation:
Type Information (Part III)

1999

 

The Type Info Browser 

To really drive home the mechanics of the COM Type Information description interfaces, I've prepared a little browser for the world's greater benefit.   This program isn't perfect.  Oh certainly not.  However, keep in mind that Microsoft's OLEViewer is in version 57.  My viewer is in version 1  :)  You can download the source and the executable here.  Windows 98 users, beware.  The type browser is built from the common control tree view.  This tree view is subject to Windows 98's "Animate windows, menus and lists" enhancement.  With this setting enabled, the tree view will not be responsive when being expanded or collapsed.  You can turn this very annoying feature off through control panel-> display-> effects-> Animate windows, menus, and lists.

Recall from Part II that the type description structures FUNCDESC, VARDESC, TYPEATTR, and TLIBATTR (not covered in Part II) need to be freed with the appropriate ReleaseXXXX method in ITypeInfo or ITypeLib.  Because it's convienent to be able to break out of a stack frame without having to explicitly release all resources in that frame, I've defined some thin wrappers for these four structures.   Note that they throw an exception EVeryBadThing when their initialization function fails.  This saves us from having to check error codes.   However, note that I am not using any exception-throwing COM interface wrappers.  Those would clean the program up even more.  This will be left as an exercise to the reader (and if you do write a wrapper that throws exceptions on failed return codes, why don't you email it to me? :)

Enter the source code.  This first part includes definitions for the type description structures.  I'll break the source into chunks and make comments on each one.

#pragma comment(lib, "comctl32")

#include <windows.h>
#include <atlbase.h>
#include <commctrl.h>
#include <sstream>
#include <vector>

struct EVeryBadThing { };
struct CComTypeAttr {
    TYPEATTR* _typeAttr;
    CComPtr<ITypeInfo> _pTypeInfo;
    operator TYPEATTR*() { return _typeAttr; }
    TYPEATTR*
operator->() { return _typeAttr; }
    explicit CComTypeAttr(ITypeInfo* ti) throw(EVeryBadThing) : _pTypeInfo(ti) {
        HRESULT hr(_pTypeInfo->GetTypeAttr(&_typeAttr));
        if(hr)
throw EVeryBadThing();
    }
    ~CComTypeAttr() { _pTypeInfo->ReleaseTypeAttr(_typeAttr); }
};
struct CComFuncDesc {
    FUNCDESC* _funcDesc;
   
operator FUNCDESC*() { return _funcDesc; }
    FUNCDESC*
operator->() { return _funcDesc; }
    CComPtr<ITypeInfo> _pTypeInfo;
    CComFuncDesc(ITypeInfo* ti, int index)
throw(EVeryBadThing) : _pTypeInfo(ti) {
        HRESULT hr(_pTypeInfo->GetFuncDesc(index, &_funcDesc));
       
if(hr) throw EVeryBadThing();
    }
    ~CComFuncDesc() { _pTypeInfo->ReleaseFuncDesc(_funcDesc); }
};
struct CComVarDesc {
    VARDESC* _varDesc;
   
operator VARDESC*() { return _varDesc; }
    VARDESC*
operator->() { return _varDesc; }
    CComPtr<ITypeInfo> _pTypeInfo;
    CComVarDesc(ITypeInfo* ti, int index)
throw(EVeryBadThing) : _pTypeInfo(ti) {
        HRESULT hr(_pTypeInfo->GetVarDesc(index, &_varDesc));
       
if(hr) throw EVeryBadThing();
    }
    ~CComVarDesc() { _pTypeInfo->ReleaseVarDesc(_varDesc); }
};
struct CComLibAttr {
    TLIBATTR* _libAttr;
   
operator TLIBATTR*() { return _libAttr; }
    TLIBATTR*
operator->() { return _libAttr; }
    CComPtr<ITypeLib> _pTypeLib;
   
explicit CComLibAttr(ITypeLib* tlb) throw(EVeryBadThing) : _pTypeLib(tlb) {
        HRESULT hr(_pTypeLib->GetLibAttr(&_libAttr));
       
if(hr) throw EVeryBadThing();
    }
    ~CComLibAttr() { _pTypeLib->ReleaseTLibAttr(_libAttr); }
};

These are very simple wrappers.  When the appropriate initialization method fails, the constructor throws an EVeryBadThing exception to prevent the object from existing in an undefined state.  The next method, stringifyParameterAttributes, simply returns a string holding the parameter attributes gleaned from the PARAMDESC structure.

std::string stringifyParameterAttributes(PARAMDESC* paramDesc) {
    USHORT paramFlags = paramDesc->wParamFlags;
    int numFlags(0);
    for(DWORD bit(1); bit <= PARAMFLAG_FHASDEFAULT; bit<<=1)
        numFlags += (paramFlags & bit) ? 1 : 0;
    if(!numFlags) return "";
    std::ostringstream oss;
    oss<< '[';
    if(paramFlags & PARAMFLAG_FIN)
    { oss<< "in";
if(--numFlags) oss<< ", "; }
   
if(paramFlags & PARAMFLAG_FOUT)
    { oss<< "out";
if(--numFlags) oss<< ", "; }
   
if(paramFlags & PARAMFLAG_FLCID)
    { oss<< "lcid";
if(--numFlags) oss<< ", "; }
   
if(paramFlags & PARAMFLAG_FRETVAL)
    { oss<< "retval";
if(--numFlags) oss<< ", "; }
   
if(paramFlags & PARAMFLAG_FOPT)
    { oss<< "optional";
if(--numFlags) oss<< ", "; }
   
if(paramFlags & PARAMFLAG_FHASDEFAULT) {
        oss<< "defaultvalue";
       
if(paramDesc->pparamdescex) {
            oss<< '(';
            PARAMDESCEX& paramDescEx = *(paramDesc->pparamdescex);
            VARIANT defVal();
            CComBSTR bstrDefValue;
            CComVariant variant;
            HRESULT hr(VariantChangeType(&variant,
                &paramDescEx.varDefaultValue, 0, VT_BSTR));
           
if(hr) oss<< "???)";
           
else {
                char ansiDefValue[MAX_PATH];
                WideCharToMultiByte(CP_ACP, 0, variant.bstrVal,
                    SysStringLen(variant.bstrVal) + 1, ansiDefValue,
                    MAX_PATH, 0, 0);
               
if(paramDescEx.varDefaultValue.vt == VT_BSTR)
                    oss<< '\"'<< ansiDefValue<< '\"'<< ')';
                else oss<< ansiDefValue<< ')';
            }
        }
    }
    oss<< ']';
    return oss.str();
}

stringifyParameterAttributes iterates through each bit of PARAMDESC::wParamFlags (well, at least the bits we care about), and concatenates the parameter's attributes between two brackets - just like MIDL.  The PARAMFLAG_FHASDEFAULT bit should draw your attention.   Methods exposed through IDispatch::Invoke can have default values.   Because the parameters of a dispatch method are oleautomation compliant, the default values can be stored in VARIANTARG structures.   PARAMDESC::pparamdescex points to a PARAMDESCEX structure which holds the default value of the parameter.  stringifyParameterAttributes uses VariantChangeType to coerce the default parameter to a Basic String, which is then inserted into the character stream.

param1.gif (18041 bytes)

Figure 1

Figure 1 shows stringifyParameterAttributes at work.  Exhilarating, isn't it.

These next functions do most of the low-level type description interface work.  I'll forgo the comments, as these procedures have been explained in Part II:

std::string stringifyCustomType(HREFTYPE refType, ITypeInfo* pti) {
    CComPtr<ITypeInfo> pTypeInfo(pti);
    CComPtr<ITypeInfo> pCustTypeInfo;
    HRESULT hr(pTypeInfo->GetRefTypeInfo(refType, &pCustTypeInfo));
    if(hr) return "UnknownCustomType";
    CComBSTR bstrType;
    hr = pCustTypeInfo->GetDocumentation(-1, &bstrType, 0, 0, 0);
    if(hr) return "UnknownCustomType";
    char ansiType[MAX_PATH];
    WideCharToMultiByte(CP_ACP, 0, bstrType, bstrType.Length() + 1,

        ansiType, MAX_PATH, 0, 0);
    return ansiType;
}

std::string stringifyTypeDesc(TYPEDESC* typeDesc, ITypeInfo* pTypeInfo) {
    std::ostringstream oss;
    if(typeDesc->vt == VT_PTR) {
        oss<< stringifyTypeDesc(typeDesc->lptdesc, pTypeInfo)<< '*';
       
return oss.str();
    }
   
if(typeDesc->vt == VT_SAFEARRAY) {
        oss<< "SAFEARRAY("
            << stringifyTypeDesc(typeDesc->lptdesc, pTypeInfo)<< ')';
       
return oss.str();
    }
   
if(typeDesc->vt == VT_CARRAY) {
        oss<< stringifyTypeDesc(&typeDesc->lpadesc->tdescElem, pTypeInfo);
        for(int dim(0); typeDesc->lpadesc->cDims; ++dim)
            oss<< '['<< typeDesc->lpadesc->rgbounds[dim].lLbound<< "..."
                << (typeDesc->lpadesc->rgbounds[dim].cElements +
                typeDesc->lpadesc->rgbounds[dim].lLbound - 1)<< ']';
       
return oss.str();
    }
   
if(typeDesc->vt == VT_USERDEFINED) {
        oss<< stringifyCustomType(typeDesc->hreftype, pTypeInfo);
       
return oss.str();
    }
   
    switch(typeDesc->vt) {
        // VARIANT/VARIANTARG compatible types
    case VT_I2:
return "short";
   
case VT_I4: return "long";
   
case VT_R4: return "float";
   
case VT_R8: return "double";
   
case VT_CY: return "CY";
   
case VT_DATE: return "DATE";
   
case VT_BSTR: return "BSTR";
   
case VT_DISPATCH: return "IDispatch*";
   
case VT_ERROR: return "SCODE";
   
case VT_BOOL: return "VARIANT_BOOL";
   
case VT_VARIANT: return "VARIANT";
   
case VT_UNKNOWN: return "IUnknown*";
   
case VT_UI1: return "BYTE";
   
case VT_DECIMAL: return "DECIMAL";
   
case VT_I1: return "char";
   
case VT_UI2: return "USHORT";
   
case VT_UI4: return "ULONG";
   
case VT_I8: return "__int64";
   
case VT_UI8: return "unsigned __int64";
   
case VT_INT: return "int";
   
case VT_UINT: return "UINT";
   
case VT_HRESULT: return "HRESULT";
   
case VT_VOID: return "void";
   
case VT_LPSTR: return "char*";
   
case VT_LPWSTR: return "wchar_t*";
    }
    return "BIG ERROR!";
}

std::string stringifyVarDesc(VARDESC* varDesc, ITypeInfo* pti) {
    CComPtr<ITypeInfo> pTypeInfo(pti);
    std::ostringstream oss;
   
if(varDesc->varkind == VAR_CONST) oss<< "const ";
    oss<< stringifyTypeDesc(&varDesc->elemdescVar.tdesc, pTypeInfo);
    CComBSTR bstrName;
    HRESULT hr(pTypeInfo->GetDocumentation(varDesc->memid, &bstrName, 0, 0, 0));
   
if(hr) return "UnknownName";
    char ansiName[MAX_PATH];
    WideCharToMultiByte(CP_ACP, 0, bstrName, bstrName.Length() + 1, ansiName,
        MAX_PATH, 0, 0);
    oss<< ' '<< ansiName;
   
if(varDesc->varkind != VAR_CONST) return oss.str();
    oss<< " = ";
    CComVariant variant;
    hr = VariantChangeType(&variant, varDesc->lpvarValue, 0, VT_BSTR);
   
if(hr) oss<< "???";
    else {
        WideCharToMultiByte(CP_ACP, 0, variant.bstrVal,

            SysStringLen(variant.bstrVal) + 1, ansiName, MAX_PATH, 0, 0);
        oss<< ansiName;
    }
   
return oss.str();
}

Because stringifying an entire COM method prototype is one of the more complex tasks in the realm of type information, I've constructed two helper functions to make this simpler.  The first, stringifyFunctionArgument, concatenates the result of stringifyParameterAttributes with the result of stringifyTypeDescstringifyCOMMethod builds the entire prototype as seen in Figure 1.  It streams first the return type, then the method name, and finally the stringifyFunctionArgument result for each of the method's parameters.  These arguments are enclosed in parantheses (just like an IDL function prototype).

std::string stringifyFunctionArgument(ELEMDESC* elemDesc, ITypeInfo* pti) {
    CComPtr<ITypeInfo> pTypeInfo(pti);
    std::ostringstream oss;
    oss<< stringifyParameterAttributes(&elemDesc->paramdesc);
    if(oss.str().size()) oss<< ' ';
    oss<< stringifyTypeDesc(&elemDesc->tdesc, pti);
    return oss.str();
}

std::string stringifyCOMMethod(FUNCDESC* funcDesc, ITypeInfo* pti) {
    CComPtr<ITypeInfo> pTypeInfo(pti);
    std::ostringstream oss;
   
if(funcDesc->funckind == FUNC_DISPATCH)   
        oss<< "[id("<< (int)funcDesc->memid<< ')';
    else oss<< "[VOffset("<< funcDesc->oVft<< ')';
    switch(funcDesc->invkind) {
    case INVOKE_PROPERTYGET: oss<< ", propget] "; break;
   
case INVOKE_PROPERTYPUT: oss<< ", propput] "; break;
   
case INVOKE_PROPERTYPUTREF: oss<< ", propputref] "; break;
   
case INVOKE_FUNC: oss<< "] "; break;
    }
    oss<< stringifyTypeDesc(&funcDesc->elemdescFunc.tdesc, pTypeInfo);
    CComBSTR bstrName;
    pTypeInfo->GetDocumentation(funcDesc->memid, &bstrName, 0, 0, 0);
    char ansiName[MAX_PATH];
    WideCharToMultiByte(CP_ACP, 0, bstrName, bstrName.Length() + 1,
        ansiName, MAX_PATH, 0, 0);
    oss<< ' '<< ansiName<< '(';
    for(int curParam(0); curParam < funcDesc->cParams; ++curParam) {
        oss<< stringifyFunctionArgument(&funcDesc->lprgelemdescParam[curParam],
            pTypeInfo);
       
if(curParam < funcDesc->cParams - 1) oss<< ", ";
    }
    oss<< ')';
    return oss.str();
}

Each method is either invoked through a vtable interface or through IDispatch::Invoke.   The method's offset in the object's virtual table is displayed for interface methods, and the method's DISPID is displayed for dispinterface methods. 

With the six functions functions posted above, the core functionality of the type browser is complete.  What we now need to do is provide a way for the user to select a type library, and navigate the type library's contents through a tree view hierarchy.  Naturally, this is easier said than done.  I've whipped up a number of classes to accomplish this task.  Check out the class view:

clsview.gif (8122 bytes)

Figure 2

Looks nasty?  It isn't actually that bad - four of the classes are just our type description wrappers, and EVeryBadThing is the trivial exception structure.  To keep the app fast and prevent excessive memory usage, I've taken a "wait and find out" approach to expanding the items on the tree view.  The children for a node are constructed not when the parent node is constructed, but when the parent node is expanded.  The only problem with this is that the parent doesn't know how many children it has until it is actually expanded.  And to get expanded, it needs to have children.  All non-terminal nodes (like structures, interfaces, and containers of structures, interfaces, etc) add a single child node upon construction.  This is encapsulated in the CExpansionNode class.   When the parent node is expanded, the child node is deleted, and the legitimate child nodes are added.  Of course if there are no child nodes, then expanding the parent node will only cause the expansion button to disappear.  While this may seem problematic, it really isn't.  Microsoft's OLE Viewer (now in version 57) uses the same scheme.

template<typename T> class CExpansionNode {
    mutable HTREEITEM _expNode;
public:
    void BuildExpansionNode() {
        if(_expNode) return;
        TVINSERTSTRUCT tvInsertStruct;
        tvInsertStruct.hParent = static_cast<T*>(this)->treeItem();
        tvInsertStruct.hInsertAfter = TVI_FIRST;
        tvInsertStruct.item.mask = 0;
        _expNode = TreeView_InsertItem(
static_cast<T*>(this)->treeView(),
            &tvInsertStruct);
    }       
    void KillExpansionNode() {
        if(!_expNode)
return;
        TreeView_DeleteItem(static_cast<T*>(this)->treeView(), _expNode);
        _expNode = 0;
    }       
    CExpansionNode() : _expNode(0) { }
    CExpansionNode(const CExpansionNode& c) : _expNode(c._expNode) {
       c._expNode = 0;
    }
   CExpansionNode& operator=(const CExpansionNode& c) {
        KillExpansionNode();
        _expNode = c._expNode;
        c._expNode = 0;
        return *this;
    }
    ~CExpansionNode() {
        KillExpansionNode();
    }
};

CExpansionNode is a very simple class.  The template argument is the name of the derived class (which in this program will always be a node object).  T::treeItem returns the parent node's HTREEITEM.  T::treeView returns the HWND of the tree view.  The CExpansionNode's this pointer is static_casted to T* (which thunks if necessary) producing a pointer to the start of the derived object.  Notice that CExpansionNode's constructor does not insert the dummy node.  Why not?  The derived class T will also inherit CTypeBrowserBase.  The CTypeBrowserBase constructor must be invoked before the dummy node is built (to initialize the data for treeView and treeItem).  For this reason, construction of the dummy node has been relegated to CExpansionNode::BuildExpansionNode.  This method is called from the body of the node class's constructor.  This will all make more sense when you see CTypeBrowserBase, so here it is:

class CTypeBrowserBase {
   
mutable HTREEITEM _treeItem;
    HWND _treeView;
    std::string _displayName;
    HTREEITEM _parentTreeItem;
protected:
    CTypeBrowserBase* GetParentNode() {
       
if(_parentTreeItem == TVI_ROOT || !_parentTreeItem) return 0;
        TVITEM tvItem;
        tvItem.lParam = 0;
        tvItem.hItem = _parentTreeItem;
        tvItem.mask = TVIF_PARAM;
        TreeView_GetItem(_treeView, &tvItem);
       
return reinterpret_cast<CTypeBrowserBase*>(tvItem.lParam);
    }
public:
    CTypeBrowserBase() : _treeView(0) { }
    CTypeBrowserBase& operator=(const CTypeBrowserBase& c) {
       
if(_treeItem) TreeView_DeleteItem(treeView(), treeItem());
        _treeView = c._treeView;
        _displayName = c._displayName;
        _treeItem = c._treeItem;
        TVITEM tvItem;
        tvItem.mask = TVIF_HANDLE | TVIF_PARAM;
        tvItem.hItem = _treeItem;   
        TreeView_GetItem(treeView(), &tvItem);
        tvItem.lParam = reinterpret_cast<LPARAM>(this);
        TreeView_SetItem(treeView(), &tvItem);
        c._treeItem = 0;
       
return *this;
    }
   
    std::string displayName() {
return _displayName; }
    HWND treeView() {
return _treeView; }
    HTREEITEM treeItem() {
return _treeItem; }       
   
    CTypeBrowserBase(BSTR name, HWND tv, HTREEITEM parentTreeItem) :
        _treeView(tv), _parentTreeItem(parentTreeItem) {
        char ansiName[MAX_PATH];
        WideCharToMultiByte(CP_ACP, 0, name, SysStringLen(name) + 1,
            ansiName, MAX_PATH, 0, 0);
        _displayName = ansiName;
       
        TVINSERTSTRUCT tvInsertStruct;
        tvInsertStruct.hParent = parentTreeItem;
        tvInsertStruct.hInsertAfter = TVI_LAST;
        tvInsertStruct.item.mask = TVIF_TEXT | TVIF_PARAM;
        tvInsertStruct.item.pszText = LPSTR_TEXTCALLBACK;
        tvInsertStruct.item.cchTextMax = 0;
        tvInsertStruct.item.lParam = reinterpret_cast<LPARAM>(this);
       
        _treeItem = TreeView_InsertItem(treeView(), &tvInsertStruct);
    }   
    CTypeBrowserBase(const CTypeBrowserBase& c) : _treeView(c._treeView),
        _displayName(c._displayName), _treeItem(c._treeItem) {
        TVITEM tvItem;
        tvItem.mask = TVIF_HANDLE | TVIF_PARAM;
        tvItem.hItem = _treeItem;   
        TreeView_GetItem(treeView(), &tvItem);
        tvItem.lParam = reinterpret_cast<LPARAM>(this);
        TreeView_SetItem(treeView(), &tvItem);
        c._treeItem = 0;
    }   
    ~CTypeBrowserBase() {
       
if(treeItem()) {
            TreeView_DeleteItem(treeView(), treeItem());
            _treeItem = 0;
        }
    }
    virtual void collapse() = 0;
   
virtual void expand() = 0;   
   
virtual std::string editBoxText() {
        return std::string(" COM Owns You");
    }
};

CTypeBrowserBase is a class with a lot of functionality.  It is a base for all node class in the type browser program.  CTypeBrowserBase does two things: it manages node data and serves as a base class for the window procedure to send expand and collapse events.  CTypeBrowserBase::GetParentNode returns a pointer to the CTypeBrowserBase slice of the parent node.  This is a protected method so it can only be invoked by the derived class.  How does this function actually work?  It surely isn't returning any cached pointer: TVITEM::lParam is cast to CTypeBrowserBase* and returned to the caller.  Curious.  The lParam is four free bytes that the tree view control gives the programmer, to store any sort of information (akin to the GWL_USERDATA entry, but for individual nodes instead of windows).   Because the HTREEITEM handles are not nice indices into an array of nodes (as list view item handles would be) but pointers to opaque data, they aren't much help in associating the actual tree view item with corresponding type information object.  What we do is store the address of the type info object as the node's lParam.  Given an HTREEITEM handle to a node, the corresponding type info object can be accessed by dereferencing the pointer stored in TVITEM::lParam by calling the TreeView_GetItem macro.

The CTypeBrowserBase constructor creates the actual tree view node for the type information item.  The name of the node (the string that appears next to the node's button), the HWND of the tree view, and the HTREEITEM of the parent node are all passed as arguments.  The constructor initializes a TVINSERTSTRUCT structure using with the HWND and HTREEITEM parameters.  TVINSERTSTRUCT::item::pszText points to the node's display name.  When this value is LPSTR_TEXTCALLBACK, the tree view sends a TVN_GETDISPINFO message to its parent's window's procedure.  Our message handler casts the node's lParam to a CTypeBrowserBase pointer and invokes CTypeBrowserBase::displayName.  This displayName text is the CTypeBrowserBase constructor's first parameter.  The tree view can similarly retrieve an item's edit box text (displayed when the node is selected and the TVN_SELCHANGED message is fired).

Sounds like a great scheme, doesn't it!  Use the node's lParam to store the location of the corresponding type info object.  But wait...  What happens if the object changes location?  The pointer stored in the lParam then refers to garbage.  While an object obviously can't just pick up and move to a different place in memory, it can be copied.  These objects are often part of an STL vector, so they are created on the stack before being pushed to their container.  This sequence will invoke the object's copy constructor or assignment operator at least once.  So how do we handle this?  Recall the functionality of the Standard Library auto_ptr.  An auto_ptr has a single data member: a pointer to the data it wraps.  The auto_ptr's destructor deletes this memory if the pointer is non-zero.  Invoking the copy constructor or assignment operator on an auto_ptr will set the source's pointer to the argument object's pointer, and then clear the argument object's pointer.  This effectively transfers "ownership" of the memory to the most recently created auto_ptr.  We employ a similar strategy here. 

CTypeBrowserBase "owns" a tree view node.   Like auto_ptr, CTypeBrowserBase transfers ownership of its _treeItem member during copy construction and assignment operations.  The copy constructor grabs the TVITEM structure of the argument's node and writes its own pointer to TVITEM::lParam.  Here's the kicker: it sets the argument's _treeItem member to zero.  This means that the object has "lost ownership" over its node, and the object's death will not cause the destructor to remove its node from the tree.  To allow the copy constructor and assignment operator to fiddle with its argument's HTREEITEM, CTypeBrowserBase::_treeItem is defined as mutable.

CTypeBrowserBase also serves as the common base class for all nodes.  The virtual methods collapse, expand, and editBoxText are overridden by the derived class to perform operations specific to the type information object.  These methods, along with CTypeBrowserBase::displayName (which is non-virtual), are invoked from the parent window's message procedure.  The methods treeView and treeItem provide the derived class (and CExpansionNode) with access to the HWND of the tree view and the HTREEITEM of the node. 

Here come the first two node classes.

class CComEndNode : public CTypeBrowserBase {
public:
    CComEndNode(BSTR name, HWND tv, HTREEITEM parentTreeItem) :
        CTypeBrowserBase(name, tv, parentTreeItem) { }
   
virtual void collapse() { }
    virtual
void expand() { }
};

class CComMessageEndNode : public CComEndNode {
    std::string _message;
public:
    CComMessageEndNode(BSTR name, HWND tv, HTREEITEM parentTreeItem,
        std::string message) :
        CComEndNode(name, tv, parentTreeItem), _message(message) { }
   
virtual std::string editBoxText() { return _message; }
};

Shamefully simple.  These two classes are terminating nodes; they don't inherit CExpansionNode, so they can't be expanded.  CComEndNode's constructor merely passes its parameters to CTypeBrowserBase and provides trivial implementations of collapse and expand.  The class inherits CTypeBrowserBase's implemention of editBoxText, which returns the string " COM Owns You."  CComMessageEndNode is identical to its base, CComEndNode, except it lets you override the edit box message with whatever you want. 

Obviously these terminating nodes aren't too helpful by themselves.  These end nodes are children of "container nodes," that is, nodes that are expandable.  Examples of these nodes include structures, enums, unions, interfaces, dispinterfaces, and typedef "containers."  Typedefs have no child values, so they are actually end nodes.  The following template class serves as a container for union values, structure values, or enum values, depending on the template argument.

class CComVariableGroup : public CTypeBrowserBase,
   
public CExpansionNode<CComVariableGroup> {
    CComPtr<ITypeInfo> _pTypeInfo;
    std::vector<CComEndNode> _items;
public:
    CComVariableGroup(BSTR name, HWND tv, HTREEITEM parentTreeItem,
        ITypeInfo* ti) : CTypeBrowserBase(name, tv, parentTreeItem),
        _pTypeInfo(ti) {
        BuildExpansionNode();
    }
    virtual void collapse() {
        if(_items.size()) {
            _items.clear();
            BuildExpansionNode();
        }
    }
   
virtual void expand() {
        try {
            KillExpansionNode();
            CComTypeAttr typeAttr(_pTypeInfo);
            int totalVars(typeAttr->cVars);
            for(int var(0); var < totalVars; ++var) {
                try {
                    CComVarDesc varDesc(_pTypeInfo, var);
                    CComBSTR name(stringifyVarDesc(varDesc, _pTypeInfo).c_str());
                    _items.push_back(CComEndNode(name, treeView(), treeItem()));
                } catch (EVeryBadThing) { }
            }
        } catch (EVeryBadThing) { }
    }
};

CComVariableGroup contains any number of CComEndNode objects.  This class derives from CExpansionNode, meaning it is expandable.  The expand and collapse methods also have nontrivial implementations.  In addition to the parameters it sends to CTypeBrowserBase, CComVariableGroup's constructor is passed an ITypeInfo pointer.  This is a type description pointer for one of the type library's variable types, namely a structure, a union, or an enum.  The name of this variable type is passed to the constructor as the name parameter. TYPEATTR::cVars (initialized with the CComTypeAttr constructor) specifies the number of values for the type.  On expand, CComVariableGroup for loops through each of these variables and invokes stringifyVarDesc to produce a string version of the value.  Remember that for constants (i.e. enumerated values) stringifyVarDesc will append an equals sign and the constant value.  A CComEndNode is constructed from this string, and added to the variable group's vector.  Upon collapse, CComVariableGroup clears its vector, destroying all end nodes.  This invokes CTypeBrowserBase's destructor for each end node, which removes the node from the tree view.  Finally, CComVariableGroup's expansion node is built again to enable another expansion.

vargro1.gif (22555 bytes)

Figure 3

Figure 3 shows the relationships between the CComVariableGroup objects and their CComEndNodes.  In addition to the "Enums" node, expanding the "Structs" node and "Unions" node will expose more CComVariableGroups as well, each with a set of CComEndNodes.  So CComVariableGroup supplies functionality for three different types of variables.  That's a pretty flexible class.  CComInterfaceJoint is also a flexible class; it manages both interface and dispinterface nodes.

class CComInterfaceJoint : public CTypeBrowserBase,
   
public CExpansionNode<CComInterfaceJoint> {
    CComPtr<ITypeInfo> _pTypeInfo;
    std::vector<CComMessageEndNode> _items;
public:
    CComInterfaceJoint(BSTR name, HWND tv, HTREEITEM parentTreeItem,
        ITypeInfo* ti) : CTypeBrowserBase(name, tv, parentTreeItem),
        _pTypeInfo(ti) {
        BuildExpansionNode();
    }
    virtual void collapse() {
        if(_items.size()) {
            _items.clear();
            BuildExpansionNode();
        }
    }
   
virtual void expand() {
        try {
            KillExpansionNode();
            CComTypeAttr typeAttr(_pTypeInfo);
            int totalFuncs(typeAttr->cFuncs);
            for(
int curFunc(0); curFunc < totalFuncs; ++curFunc) {
               
try {
                    CComFuncDesc funcDesc(_pTypeInfo, curFunc);
                    CComBSTR name[1];
                    UINT cNames;
                    HRESULT hr(_pTypeInfo->GetNames(funcDesc->memid,
                        reinterpret_cast<BSTR*>(&name), 1, &cNames));
                   
if(hr) continue;
                    char ansiName[MAX_PATH];
                    WideCharToMultiByte(CP_ACP, 0, *name, name->Length() + 1,
                        ansiName, MAX_PATH, 0, 0);
                    std::ostringstream oss;
                    oss<< ansiName;
                    switch(funcDesc->invkind) {
                    case INVOKE_FUNC: oss<< " (method)"; break;
                   
case INVOKE_PROPERTYGET: oss<< " (prop get)"; break;
                   
case INVOKE_PROPERTYPUT: oss<< " (prop put)"; break;
                   
case INVOKE_PROPERTYPUTREF: oss<< " (prop put ref)"; break;
                    }
                    std::string methodMessage(stringifyCOMMethod(funcDesc,
                        _pTypeInfo));
                    _items.push_back(CComMessageEndNode(CComBSTR(oss.str().c_str()),
                        treeView(), treeItem(), methodMessage));
                } catch(EVeryBadThing) { }
            }
        }
catch(EVeryBadThing) { }
    }
   
virtual std::string editBoxText() {
       
try {
            CComTypeAttr typeAttr(_pTypeInfo);
            wchar_t stringClsid[39];
            StringFromGUID2(typeAttr->guid, stringClsid, 39);
            char ansiClsid[39];
            WideCharToMultiByte(CP_ACP, 0, stringClsid, 39, ansiClsid, 39, 0, 0);
            std::ostringstream oss;
            oss<< "    IID: "<< ansiClsid<< ' '<<displayName();
            return oss.str();
        }
catch(EVeryBadThing) { return std::string(""); }            
    }
};

Like CComVariableGroup, CComInterfaceJoint is a container for end nodes.  In addition to the CTypeBrowserBase constructor parameters, CComInterfaceJoint::CComInterfaceJoint is passed an ITypeInfo pointer.  This is a type description for the interface in question, be it dispatch or virtual.  TYPEATTR::cFuncs (retrieved with the CComTypeAttr constructor) specifies the number of functions of the interface.  expand enters a for loop which iterates through each function.  ITypeInfo::GetNames returns the function's name, and a FUNCDESC::invkind switch stringifies the invoke kind of the function.  Together, these strings comprise the end node display names, as shown in Figure 4

Notice that CComInterfaceJoint manages not a vector of CComEndNodes, but of CComMessageEndNodes.  The latter class allows overriding of the edit box text.  In the case of CComInterfaceJoint's end nodes, the edit box displays the full function prototype, procured from stringifyCOMMethod.   We certainly are getting a lot of mileage from the functions presented in Part II and early in Part III.  Another interesting aspect of CComInterfaceJoint is its overridden editBoxText method.  When selected, the interface joint displays its IID and interface name in the edit box.  It might loook something like this: "IID: {35053A20-8589-11D1-B16A-00C0F0283628} IProgressBar."  One nice thing about the edit control is that you can select an arbitrary portion of its contents and copy it to the clipboard through the popup menu.  Quite helpful for dropping IIDs into your own projects.

intgro1.gif (25461 bytes)

Figure 4

Figure 4 shows the relationship between the CComInterfaceJoint and its CComMessageEndNode children.  By relying on stringifyCOMMethod, the CComMessageEndNode edit box text displays the DISPID for dispatch methods and the vtable offsets for virtual methods.  Now onto the containers!

template<TYPEKIND tk> class CComVariableGroupContainer :
    public CTypeBrowserBase, public CExpansionNode<CComVariableGroupContainer> {
    CComPtr<ITypeLib> _pTypeLib;
    std::vector<CComVariableGroup> _variables;
public:    
    CComVariableGroupContainer(BSTR name, HWND tv, HTREEITEM parentTreeItem,
        ITypeLib* pTlb) : CTypeBrowserBase(name, tv, parentTreeItem),
        _pTypeLib(pTlb) {
        BuildExpansionNode();   
    }   
    virtual void expand() {
        KillExpansionNode();
        if(!_pTypeLib) return;
        UINT infoCount(_pTypeLib->GetTypeInfoCount());
       
if(!infoCount) return;
       
        for(int info(0); info < infoCount; ++info) {
            TYPEKIND typeKind;
            HRESULT hr(_pTypeLib->GetTypeInfoType(info, &typeKind));
           
if(hr || typeKind != tk) continue;
           
            CComBSTR name;
            hr = _pTypeLib->GetDocumentation(info, &name, 0, 0, 0);
           
if(hr) continue;
            CComPtr<ITypeInfo> pTypeInfo;
            hr = _pTypeLib->GetTypeInfo(info, &pTypeInfo);
           
if(hr) continue;
            _variables.push_back(CComVariableGroup(name, treeView(),
                treeItem(), pTypeInfo));
        }
    }   
   
virtual void collapse() {
       
if(_variables.size()) {
            _variables.clear();
            BuildExpansionNode();
        }
    }
   
virtual std::string editBoxText() { };
};

template<> std::string CComVariableGroupContainer<TKIND_ENUM>::editBoxText() {
    std::ostringstream oss;
    oss<< "    Enumerations in "<< GetParentNode()->displayName();
    return oss.str();
}

template<> std::string CComVariableGroupContainer<TKIND_UNION>::editBoxText() {
    std::ostringstream oss;
    oss<< "    Teamsters in "<< GetParentNode()->displayName();
   
return oss.str();
}

template<> std::string CComVariableGroupContainer<TKIND_RECORD>::editBoxText() {
    std::ostringstream oss;
    oss<< "    Structures in "<< GetParentNode()->displayName();
   
return oss.str();
}

CComVariableGroupContainer objects contain CComVariableGroups (how aptly named).  This is a templated class, where the template argument, tk, holds a value of the TYPEKIND enum.  Because the contained objects, CComVariableGroups only handle enums, unions, and structures, tk must only be TKIND_ENUM, TKIND_UNION, or TKIND_RECORD (why not TKIND_STRUCTURE I do not know).  The class's constructor takes a name parameter, which is "Enums," "Unions," or "Structs" depending on the TYPEKIND.  More importantly, the constructor is passed an ITypeLib pointer, which describes the type library in question. 

On expand, CComVariableGroupContainer retrieves the number of described types through ITypeLib::GetTypeInfoCount.   It then loops through each type, querying ITypeLib::GetTypeInfoType and comparing the returned TYPEKIND to the template argument TYPEKIND.  When the two match, the type's name is retrieved through ITypeLib::GetDocumentation and the ITypeInfo pointer describing the type is retrieved.  The type's name and type description pointer are passed to the CComVariableGroup constructor, which is pushed to the vector.  None of this is very complicated - with good design, building a type info browser isn't that painful.

I'm sure your eyes have wandered down to the editBoxText definition.  "What ht hell..." you must be thinking.  It returns no value.  In fact, it doesn't do anything.  CComVariableGroupContainer::editBoxText will not compile.  Why then did I define it this way?  To allow for specialization.  The three specializations that follow send customized messages to the edit box.  The name of the type library is glued to the stringified typekind ("Enumerations", "Structures", or "Teamsters" (Hoffa lives!)) and returned from editBoxText.  Yippie.  Download the executable and see for yourself.

template<TYPEKIND tk> class CComInterfaceGroupContainer :
    public CTypeBrowserBase, public CExpansionNode<CComInterfaceGroupContainer> {
    CComPtr<ITypeLib> _pTypeLib;
    std::vector<CComInterfaceJoint> _interfaces;
public:
    CComInterfaceGroupContainer(BSTR name, HWND tv, HTREEITEM parentTreeItem,
        ITypeLib* pTlb) : CTypeBrowserBase(name, tv, parentTreeItem),
        _pTypeLib(pTlb) {
        BuildExpansionNode();
    }
    virtual void expand() {
        KillExpansionNode();
        if(!_pTypeLib) return;
        UINT infoCount(_pTypeLib->GetTypeInfoCount());
       
if(!infoCount) return;
        for(int info(0); info < infoCount; ++info) {
            TYPEKIND typeKind;
            HRESULT hr(_pTypeLib->GetTypeInfoType(info, &typeKind));
           
if(hr || typeKind != tk) continue;
           
            CComBSTR name;
            hr = _pTypeLib->GetDocumentation(info, &name, 0, 0, 0);
           
if(hr) continue;
            CComPtr<ITypeInfo> pTypeInfo;
            hr = _pTypeLib->GetTypeInfo(info, &pTypeInfo);
           
if(hr) continue;
            _interfaces.push_back(CComInterfaceJoint(name, treeView(),
                treeItem(), pTypeInfo));
        }
    }
   
virtual void collapse() {
       
if(_interfaces.size()) {
            _interfaces.clear();
            BuildExpansionNode();
        }
    }
   
virtual std::string editBoxText() { }
};

template<> std::string CComInterfaceGroupContainer<TKIND_DISPATCH>::editBoxText() {
    std::ostringstream oss;
    oss<< "    Dispatch Interfaces in "<< GetParentNode()->displayName();
   
return oss.str();
}

template<> std::string CComInterfaceGroupContainer<TKIND_INTERFACE>::editBoxText() {
    std::ostringstream oss;
    oss<< "    VTable Interfaces in "<< GetParentNode()->displayName();
   
return oss.str();
}

CComInterfaceGroupContainer is very similar to CComVariableGroupContainer.  The TYPEKIND template argument specifies which sort of interface the object describes: TKIND_INTERFACE or TKIND_DISPATCH.  The name of the interface is passed to the class's constructor, as is the type library pointer.  The expand method iterates through each of the library's types, comparing ITypeLib::GetTypeInfoType to the template argument tk.  On a match, the interface's name and ITypeInfo description pointer are passed to the CComInterfaceJoint constructor.  The resulting object is pushed to the vector.  The editBoxText specializations send a helpful messages to the application's edit control.

If you were really paying attention in Part II, you'd already have identified a problem with CComInterfaceGroupContainer::expand.  So let me clue you in: dual interfaces are always exposed as type TKIND_DISPATCH.  To quote the Automation Programmer's Reference, "For dual interfaces, ITypeLib::GetTypeInfo returns only the TKIND_DISPATCH type information.  To get the TKIND_INTERFACE type information, ITypeInfo::GetRefTypeOfImplType can be called on the TKIND_DISPATCH type information, passing an index of –1.   Then, the returned type information handle can be passed to ITypeInfo::GetRefTypeInfo."  This is easily taken care of with an expand specialization:

template<> void CComInterfaceGroupContainer<TKIND_INTERFACE>::expand() {
    KillExpansionNode();
    if(!_pTypeLib) return;
    UINT infoCount(_pTypeLib->GetTypeInfoCount());
    if(!infoCount)
return;
    for(int info(0); info < infoCount; ++info) {
        try {
            TYPEKIND typeKind;
            HRESULT hr(_pTypeLib->GetTypeInfoType(info, &typeKind));
           
if(hr) continue;
           
if(typeKind == TKIND_DISPATCH) {
                CComPtr<ITypeInfo> pDispatchInfo;
                hr = _pTypeLib->GetTypeInfo(info, &pDispatchInfo);
               
if(hr) continue;
                HREFTYPE interfaceRefType;
                hr = pDispatchInfo->GetRefTypeOfImplType(-1, &interfaceRefType);
               
if(hr) continue;
                CComPtr<ITypeInfo> pInterfaceInfo;
                hr = pDispatchInfo->GetRefTypeInfo(interfaceRefType,
                    &pInterfaceInfo);
               
if(hr) continue;
                CComTypeAttr typeAttr(pInterfaceInfo);
               
if(typeAttr->typekind != TKIND_INTERFACE) continue;
                CComBSTR name;
                hr = _pTypeLib->GetDocumentation(info, &name, 0, 0, 0);
               
if(hr) continue;
                _interfaces.push_back(CComInterfaceJoint(name, treeView(),
                    treeItem(), pInterfaceInfo));
               
continue;
            }
           
if(typeKind != TKIND_INTERFACE) continue;
            CComBSTR name;
            hr = _pTypeLib->GetDocumentation(info, &name, 0, 0, 0);
           
if(hr) continue;
            CComPtr<ITypeInfo> pTypeInfo;
            hr = _pTypeLib->GetTypeInfo(info, &pTypeInfo);
           
if(hr) continue;
            _interfaces.push_back(CComInterfaceJoint(name, treeView(),
                treeItem(), pTypeInfo));
        } catch(EVeryBadThing) { }
    }
}

Fairly beefy function; I'm getting paid by the line.  On second consideration, I'm not getting paid at all.  Anyways, this specialization has two cases: TKIND_INTERFACE (vtable interface) and TKIND_DISPATCH (dual or dispatch).  If ITypeLib::GetTypeInfoType returns TKIND_DISPATCH, GetRefTypeOfImplType is invoked on the corresponding ITypeInfo pointer to procure the vtable interface's HREFTYPE (if there is one).  ITypeInfo::GetRefTypeInfo produces the ITypeInfo pointer describing the vtable interface.  ITypeInfo::GetDocumentation produces the interface's name.  We ship these two pieces of informations off to CComInterfaceJoint's constructor, and add the resulting object to the vector.  If the initial ITypeLib::GetTypeInfoType call produces a TKIND_INTERFACE, we can skip all this nonsense and just take the straightforward approach.

We've reached the homestretch (well, in the same sense that a marathon runner who enters the fourteenth mile is in the homestretch).  How are typedefs added to the browser?  Check out CComTypedefContainer:

class CComTypedefContainer : public CTypeBrowserBase,
    CExpansionNode<CComTypedefContainer> {
    CComPtr<ITypeLib> _pTypeLib;
    std::vector<CComEndNode> _typedefs;
public:
    CComTypedefContainer(HWND tv, HTREEITEM parentTreeItem, ITypeLib* pTlb) :
        CTypeBrowserBase(CComBSTR("Typedefs"), tv, parentTreeItem),
        _pTypeLib(pTlb) {
        BuildExpansionNode();
    }
    virtual void expand() {
        KillExpansionNode();
        if(!_pTypeLib) return;
        UINT count(_pTypeLib->GetTypeInfoCount());
        for(int curAlias(0); curAlias < count; ++curAlias) {
            try {
                TYPEKIND typeKind;
                HRESULT hr(_pTypeLib->GetTypeInfoType(curAlias, &typeKind));
               
if(hr || typeKind != TKIND_ALIAS) continue;
                CComBSTR name;
                hr = _pTypeLib->GetDocumentation(curAlias, &name, 0, 0, 0);
               
if(hr) continue;
                CComPtr<ITypeInfo> pTypeInfo;
                hr = _pTypeLib->GetTypeInfo(curAlias, &pTypeInfo);
               
if(hr) continue;
                CComTypeAttr typeAttr(pTypeInfo);
                               
                std::ostringstream typeDefString;
                typeDefString<< "typedef "<<
                    stringifyTypeDesc(&typeAttr->tdescAlias, pTypeInfo);
                char ansiName[MAX_PATH];
                WideCharToMultiByte(CP_ACP, 0, name, name.Length() + 1,
                    ansiName, MAX_PATH, 0, 0);
                typeDefString<< ' '<< ansiName;
                _typedefs.push_back(CComEndNode(CComBSTR(
                    typeDefString.str().c_str()), treeView(), treeItem()));
            } catch(EVeryBadThing) { }
        }
    }
   
virtual void collapse() {
       
if(_typedefs.size()) {
            _typedefs.clear();
            BuildExpansionNode();
        }
    }
   
virtual std::string editBoxText() {
        std::ostringstream oss;
        oss<< "     Type Definitions in "<< GetParentNode()->displayName();
       
return oss.str();
    }       
};

CComTypedefContainer manages a vector of CComEndNodes.  Each end node's display value is a typedef statement.  This statement is the concatenation of "typedef", stringifyTypeDesc, and the type's name.  CComTypedefContainer adds typedefs to our type browser.  The only type left is the coclass (I am neglecting TKIND_MODULE types, as they are evil). 

class CComCoClassNode : public CTypeBrowserBase,
    public CExpansionNode<CComCoClassNode> {
    CComPtr<ITypeInfo> _pTypeInfo;
    std::vector<CComEndNode> _interfaces;
public:
    CComCoClassNode(BSTR name, HWND tv, HTREEITEM parentTreeItem,
        ITypeInfo* ti) : CTypeBrowserBase(name, tv, parentTreeItem),
        _pTypeInfo(ti) {
        BuildExpansionNode();
    }
    virtual void expand() {
        try {
            KillExpansionNode();
           
if(!_pTypeInfo) return;
            CComTypeAttr typeAttr(_pTypeInfo);
            int implInterfaces(typeAttr->cImplTypes);
            for(int curInterface(0); curInterface < implInterfaces;
                ++curInterface) {
                HREFTYPE hRefType;
                HRESULT hr(_pTypeInfo->GetRefTypeOfImplType(curInterface,
                    &hRefType));
                CComPtr<ITypeInfo> interfaceTypeInfo;
                hr = _pTypeInfo->GetRefTypeInfo(hRefType, &interfaceTypeInfo);
               
if(hr) continue;
                CComBSTR interfaceName;
                hr = interfaceTypeInfo->GetDocumentation(-1, &interfaceName,
                    0, 0, 0);
                if(hr)
continue;
                _interfaces.push_back(CComEndNode(interfaceName, treeView(),
                    treeItem()));
            }
        } catch(EVeryBadThing) { }
    }
   
virtual void collapse() {
        BuildExpansionNode();
    }
   
virtual std::string editBoxText() {
        try {
            CComTypeAttr typeAttr(_pTypeInfo);
            wchar_t stringClsid[39];
            StringFromGUID2(typeAttr->guid, stringClsid, 39);
            char ansiClsid[39];
            WideCharToMultiByte(CP_ACP, 0, stringClsid, 39, ansiClsid, 39, 0, 0);
            std::ostringstream oss;
            oss<< " CLSID: "<< ansiClsid<< ' '<<displayName();
            return oss.str();
        } catch(EVeryBadThing) { return std::string(""); }           
    }
};

Expanding a coclass node displays a list of the interfaces implemented by the coclass.  These terminating nodes are managed by CComEndNode objects.  For a breakdown of the interface's methods, the user can look in the "Interfaces" and "Dispinterfaces" containers.  CComCoClassNode::editBoxText provides some useful information to the user.  A CComTypeAttr object is created, and the CLSID is gleaned from it.  This CLSID plus the name of the coclass is displayed in the edit control when the coclass node is selected.  The CComCoClassNode object is managed by (yes, you guessed it) CComCoClassContainer.  And here it is:

class CComCoClassContainer : public CTypeBrowserBase,
   
public CExpansionNode<CComCoClassContainer> {
    CComPtr<ITypeLib> _pTypeLib;
    std::vector<CComCoClassNode> _coClasses;
public:
    CComCoClassContainer(HWND tv, HTREEITEM parentTreeItem, ITypeLib* pTlb) :
        CTypeBrowserBase(CComBSTR("CoClasses"), tv, parentTreeItem),
        _pTypeLib(pTlb) {
        BuildExpansionNode();
    }
    virtual
void expand() {
        KillExpansionNode();
        if(!_pTypeLib) return;
        UINT count(_pTypeLib->GetTypeInfoCount());
        for(int curCoClass(0); curCoClass < count; ++curCoClass) {
            try {
                TYPEKIND typeKind;
                HRESULT hr(_pTypeLib->GetTypeInfoType(curCoClass, &typeKind));
               
if(hr || typeKind != TKIND_COCLASS) continue;
                CComBSTR name;
                hr = _pTypeLib->GetDocumentation(curCoClass, &name, 0, 0, 0);
               
if(hr) continue;
                char ansiName[MAX_PATH];
                WideCharToMultiByte(CP_ACP, 0, name, name.Length() + 1,
                    ansiName, MAX_PATH, 0, 0);
                CComPtr<ITypeInfo> pTypeInfo;
                hr = _pTypeLib->GetTypeInfo(curCoClass, &pTypeInfo);
               
if(hr) continue;
                _coClasses.push_back(CComCoClassNode(name, treeView(),
                    treeItem(), pTypeInfo));
            } catch(EVeryBadThing) { }
        }
    }
   
virtual void collapse() {
       
if(_coClasses.size()) {
            _coClasses.clear();
            BuildExpansionNode();
        }
    }
   
virtual std::string editBoxText() {
        std::ostringstream oss;
        oss<< "     CoClasses in "<< GetParentNode()->displayName();
       
return oss.str();
    }
};

CComCoClassContainer behaves in the spirit of the other containers.  It constructs CTypeBrowserBase by passing the string "CoClasses," which sets "CoClasses" as the display name.   On expand, each type in the library is tested against TKIND_COCLASS.  For those that match, the name of the coclass is retrieved with ITypeLib::GetDocumentationCComCoClassNode objects are created for each coclass, and are initialized with the name of the coclass.  It's very consistent with the classes we've already covered.

Guess what?  There are only two classes left to cover.   But before we work on those, take a look at Figure 5 and make sure you understand where each class fits into the tree view.

brkdown1.gif (35997 bytes)

Figure 5

This program looks a lot simpler after seeing Figure 5, doesn't it!  Or maybe not.  There are two classes remaining: CComTypeLibrary manages the actual type library pointer and serves as the parent node for the six container classes.  From Figure 5, "Microsoft XML 1.0 <1.0>," "Active Setup Control Library <1.0>," and "PStore 1.0 Type Library <1.0>" are CComTypeLibrary objects.  All of these type library objects are children of the root object, CComTypeLibraryContainer.  While CComTypeLibrary is a very simple class (it just has a few auto_ptrs and statements to initialize them), CComTypeLibraryContainer does some nontrivial work.  CComTypeLibraryContainer::expand iterates through all the keys in HKEY_CLASSES_ROOT\TypeLib and pulls out the type libraries.  It then instantiates CComTypeLibrary objects from this registry information. 

class CComTypeLibrary : public CTypeBrowserBase,
   
public CExpansionNode<CComTypeLibrary> {
    std::auto_ptr<CComVariableGroupContainer<TKIND_ENUM> > _enumContainer;
    std::auto_ptr<CComVariableGroupContainer<TKIND_RECORD> > _structContainer;
    std::auto_ptr<CComVariableGroupContainer<TKIND_UNION> > _unionContainer;
    std::auto_ptr<CComTypedefContainer> _typedefContainer;
    std::auto_ptr<CComInterfaceGroupContainer<TKIND_INTERFACE> >
        _interfaceContainer;
    std::auto_ptr<CComInterfaceGroupContainer<TKIND_DISPATCH> >
        _dispatchContainer;
    std::auto_ptr<CComCoClassContainer> _coClassContainer;
    std::string _editBoxText;
    CComBSTR _filename;
public:
    CComTypeLibrary(BSTR name, HWND tv, HTREEITEM parentTreeItem,
        CComBSTR filename, std::string editBoxInfo) : CTypeBrowserBase(name,
        tv, parentTreeItem), _editBoxText(editBoxInfo), _filename(filename) {
        BuildExpansionNode();
    }   
    virtual void expand() {
        KillExpansionNode();
        CComPtr<ITypeLib> pTypeLib;
        HRESULT hr(LoadTypeLib(_filename, &pTypeLib));
        if(hr) return;
        _enumContainer = std::auto_ptr<CComVariableGroupContainer<TKIND_ENUM> >(
            new CComVariableGroupContainer<TKIND_ENUM>(CComBSTR(L"Enums"),
            treeView(), treeItem(), pTypeLib));
        _structContainer = std::auto_ptr<CComVariableGroupContainer<TKIND_RECORD> >(
            new CComVariableGroupContainer<TKIND_RECORD>(CComBSTR(L"Structs"),
            treeView(), treeItem(), pTypeLib));
        _unionContainer = std::auto_ptr<CComVariableGroupContainer<TKIND_UNION> >(
            new CComVariableGroupContainer<TKIND_UNION>(CComBSTR(L"Unions"),
            treeView(), treeItem(), pTypeLib));
        _typedefContainer = std::auto_ptr<CComTypedefContainer>(new
            CComTypedefContainer(treeView(), treeItem(), pTypeLib));
        _interfaceContainer =
            std::auto_ptr<CComInterfaceGroupContainer<TKIND_INTERFACE> >(
            new CComInterfaceGroupContainer<TKIND_INTERFACE>(
            CComBSTR(L"Interfaces"), treeView(), treeItem(), pTypeLib));
        _dispatchContainer =
            std::auto_ptr<CComInterfaceGroupContainer<TKIND_DISPATCH> >(
            new CComInterfaceGroupContainer<TKIND_DISPATCH>(
            CComBSTR(L"Dispinterfaces"), treeView(), treeItem(), pTypeLib));
        _coClassContainer = std::auto_ptr<CComCoClassContainer>(
            new CComCoClassContainer(treeView(), treeItem(), pTypeLib));
    }   
   
virtual void collapse() {
        _enumContainer =
            std::auto_ptr<CComVariableGroupContainer<TKIND_ENUM> >(0);
        _structContainer =
            std::auto_ptr<CComVariableGroupContainer<TKIND_RECORD> >(0);
        _unionContainer =
            std::auto_ptr<CComVariableGroupContainer<TKIND_UNION> >(0);
        _typedefContainer = std::auto_ptr<CComTypedefContainer>(0);
        _interfaceContainer =
            std::auto_ptr<CComInterfaceGroupContainer<TKIND_INTERFACE> >(0);
        _dispatchContainer =
            std::auto_ptr<CComInterfaceGroupContainer<TKIND_DISPATCH> >(0);
        _coClassContainer = std::auto_ptr<CComCoClassContainer>(0);
        BuildExpansionNode();
    }   
    ~CComTypeLibrary() {
        KillExpansionNode();
    }
    virtual std::string editBoxText() {
return _editBoxText; }
};

CComTypeLibrary include six auto_ptrs, _enumContainer, _structContainer, _unionContainer, _typedefContainer, _interfaceContainer, _dispatchContainer, and _coClassContainer, which control the lifetimes of the container nodes.  On CComTypeLibrary::expand, each of these auto_ptrs are intialized with a new object.  collapse deletes each object.  CComTypeLibrary::editBoxText returns the member _editBoxText, which is a string cached from the constructor.  CComTypeLibraryContainer constructs the CComTypeLibrary objects, and initializes them with the display name (the type library name appended with version number), the filepath of the type library, and the edit control text (the LIBID, locale ID, and filepath).  CComTypeLibraryContainer is the root of our tree view; here it comes:

class CComTypeLibraryContainer : public CTypeBrowserBase,
    public CExpansionNode<CComTypeLibraryContainer> {
    std::vector<CComTypeLibrary> _libraries;
public:
    CComTypeLibraryContainer(BSTR name, HWND tv, HTREEITEM parentTreeItem) :
        CTypeBrowserBase(name, tv, parentTreeItem) {
        BuildExpansionNode();        
    }
    virtual void expand() {
        KillExpansionNode();
        CRegKey typeLibKey;
        typeLibKey.Open(HKEY_CLASSES_ROOT, "TypeLib", KEY_ENUMERATE_SUB_KEYS
            | KEY_QUERY_VALUE);
        DWORD totalLibs;
        RegQueryInfoKey(typeLibKey, 0, 0, 0, &totalLibs, 0, 0, 0, 0, 0, 0, 0);
        _libraries.reserve(totalLibs * 1.5);
        for(int curLib(0); curLib < totalLibs; ++curLib) {
            char ansiKeyName[39];
            DWORD cchAnsiKeyName(39);
            RegEnumKeyEx(typeLibKey, curLib, ansiKeyName, &cchAnsiKeyName,
                0, 0, 0, 0);
            if(cchAnsiKeyName != 38) continue;
            GUID libGUID;
            wchar_t wideKeyName[39];
            MultiByteToWideChar(CP_ACP, 0, ansiKeyName, 39, wideKeyName, 39);
            HRESULT hr(CLSIDFromString(wideKeyName, &libGUID));
            if(hr) continue;
           
            CRegKey libKey;
           
if(libKey.Open(typeLibKey, ansiKeyName, KEY_ENUMERATE_SUB_KEYS
                | KEY_QUERY_VALUE))
continue;
            DWORD totalVersions;
            RegQueryInfoKey(libKey, 0, 0, 0, &totalVersions, 0, 0, 0,
                0, 0, 0, 0);
            for(int curVersion(0); curVersion < totalVersions; ++curVersion) {
                char ansiVersion[16];
                DWORD cchAnsiVersion(16);
                RegEnumKeyEx(libKey, curVersion, ansiVersion, &cchAnsiVersion,
                    0, 0, 0, 0);
                DWORD verMajor, verMinor;
               
if(sscanf(ansiVersion, "%u.%u", &verMajor, &verMinor) != 2)
                    continue;
               
                CRegKey versionKey;
               
if(versionKey.Open(libKey, ansiVersion, KEY_ENUMERATE_SUB_KEYS
                    | KEY_QUERY_VALUE))
                   
continue;
                char typeLibName[MAX_PATH];
                DWORD cchTypeLibName(MAX_PATH);
                std::ostringstream ossTypeLibName;
               
if(!versionKey.QueryValue(typeLibName, 0, &cchTypeLibName)
                    && lstrlen(typeLibName)) ossTypeLibName<< typeLibName;
                else ossTypeLibName<< ansiKeyName;
                ossTypeLibName<< " <"<< ansiVersion<< '>';
               
                DWORD totalLcids;
                RegQueryInfoKey(versionKey, 0, 0, 0, &totalLcids, 0, 0,
                    0, 0, 0, 0, 0);
               
for(int curLcid(0); curLcid < totalLcids; ++curLcid) {
                   
char stringLcid[16];
                    DWORD cchStringLcid(16);
                    RegEnumKeyEx(versionKey, curLcid, stringLcid,
                        &cchStringLcid, 0, 0, 0, 0);
                   
if(!strcmpi("helpdir", stringLcid)) continue;
                   
if(!strcmpi("flags", stringLcid)) continue;
                   
                    LCID lcidVal;
                   
if(!sscanf(stringLcid, "%x", &lcidVal)) continue;
                    strncat(stringLcid, "\\win32", 6);
                    CRegKey locationKey;
                   
if(locationKey.Open(versionKey, stringLcid, KEY_QUERY_VALUE))
                       
continue;
                   
                    char ansiPath[MAX_PATH];
                    DWORD cchAnsiPath(MAX_PATH);
                   
if(locationKey.QueryValue(ansiPath, 0, &cchAnsiPath))
                       
continue;
                   
                    std::ostringstream editText;
                    editText<< "     "<< ansiKeyName<< "     ";
                    editText.flags(std::ios_base::hex);
                    editText<< "LCID: "<< lcidVal;
                    editText<< "     "<< ansiPath;
                    _libraries.push_back(CComTypeLibrary(
                        CComBSTR(ossTypeLibName.str().c_str()), treeView(),
                        treeItem(), CComBSTR(ansiPath), editText.str()));
                }
            }
        }
        _libraries.reserve(_libraries.size());
    }
    virtual void collapse() {
        _libraries.clear();
        BuildExpansionNode();
    }
    ~CComTypeLibraryContainer() {
        KillExpansionNode();
    }
};

Yikes!  CComTypeLibraryContainer::expand is huge.  Note that the constructor does not take any type information pointers.  It is the responsibility of expand to explore the registry and pluck out the type information entries.  So how do we go about doing this?

reg1.gif (14192 bytes)

Figure 6

Figure 6 shows how the type library filepaths are nested deep inside the confines of the HKEY_CLASSES_ROOT hive.  The CRegKey object from <statreg.h> is used for expand's registry work.  The typeLibKey object is opened at HKEY_CLASSES_ROOT\TypeLib.  The number of subkeys under \TypeLib is queried, and a for loop is entered to iterate through each key.  The name of each subkey is read and converted to a LIBID with CLSIDFromString.  If this call fails, the key is not a LIBID so it cannot describe a type library, and the next entry is processed.  If the key is a valid LIBID, the key is opened and its number of subkeys is queried.  From Figure 6 you can see that the immediate subkeys of the LIBID key are the version keys.  The program enters another for loop, this one to process each verison key.  The version key's default value is the name of the type library.  A string representation of the version (such as "<1.0>") is appended to the type library name.  When the time comes to produce the CTypeLibrary objects, this library name/library version string is fed to the constructor as the node's display name.

The number of version subkeys is queried, and a third for loop is entered.  The example in Figure 6 has three subkeys under the version key: 0, FLAGS, and HELPDIR.  FLAGS and HELPDIR are OLE standard registry keys.   HELPDIR identifies the directory of the type library's help files and FLAGS is a bitfield storing a few important attributes of the type library.  Neither of these keys hold the filepath of the type library.  We therefore compare the subkey name to both of these identifiers and continue the for loop on a match.   What's left should be the locale ID of the type library.  This key's name is read and pushed through sprintf, which tests the key as a hexadecimal number (locale IDs are just hex numbers).  If the key is indeed a hexadecimal number, the LCID's subkey "win32" is opened.  This key's default value - the filepath of the type library - is extracted.  Hoorah.

When a filepath is procured, the CComTypeLibrary object is created.  The object's edit control text is pieced together inside CComTypeLibraryContainer::expand from the LIBID, the locale ID, and the filepath.  You may have noticed the statement _libraries.reserve(totalLibs * 1.5); at the start of the expand method.  This is a small optimization - vectors resize often when undergoing a large number of push_backs.  Each time the _libraries vector resizes, all elements in it must be copied to new memory space.  This invokes the copy constructor or assignment operator of CTypeBrowserBase, which has to make a call to the tree view control to request the TVITEMs information of the node, change the lParam to reflect the correct this pointer, and commit the change.  By reserving a sufficient amount of space prior to pushing any objects to the _libraries vector (even if the average number of versions per type library is greater than 1.5, the code still works fine.  It just may have to resize itself), we can prevent this sluggish sequence of events.

Well, that's the extent of the class framework for this application (*gasp*).  All we need now is a parent window message procedure and the WinMain entry point:

struct SAppWindows {
    HWND mainWindow, treeView, editBox;
    int editBoxHeight;
};

long _stdcall mainProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
    SAppWindows& windows = *reinterpret_cast<SAppWindows*>(
        GetWindowLong(hwnd, GWL_USERDATA));
    NMHDR* nmhdr;
    switch(message) {
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    case WM_SIZE:
        SetWindowPos(windows.treeView, 0, 0, 0, LOWORD(lparam), HIWORD(lparam) -
            windows.editBoxHeight, SWP_NOZORDER);
        SetWindowPos(windows.editBox, 0, 0, HIWORD(lparam) -
            windows.editBoxHeight, LOWORD(lparam), windows.editBoxHeight,
            SWP_NOZORDER);
       
return 0;
   
case WM_NOTIFY:
        nmhdr = reinterpret_cast<NMHDR*>(lparam);
       
switch(nmhdr->code) {
       
case TVN_GETDISPINFO:
            if(nmhdr->hwndFrom == windows.treeView) {
                NMTVDISPINFO* tvDispInfo =
                   
reinterpret_cast<NMTVDISPINFO*>(lparam);
                CTypeBrowserBase* browserBase =
                   
reinterpret_cast<CTypeBrowserBase*>(tvDispInfo->item.lParam);
                tvDispInfo->item.pszText =
                    const_cast<char*>(browserBase->displayName().c_str());
                tvDispInfo->item.cchTextMax = browserBase->displayName().size();
               
return 0;
            }
            break;
       
case TVN_SELCHANGED:
           
if(nmhdr->hwndFrom == windows.treeView) {
                NMTREEVIEW* changedStruct =
reinterpret_cast<NMTREEVIEW*>(lparam);
                CTypeBrowserBase& browseBase =
                    *
reinterpret_cast<CTypeBrowserBase*>(
                    changedStruct->itemNew.lParam);
                static std::string editBoxText;
                editBoxText = browseBase.editBoxText();
                SetWindowText(windows.editBox, editBoxText.c_str());                
               
return 0;
            }
           
break;
       
case TVN_ITEMEXPANDING:
           
if(nmhdr->hwndFrom == windows.treeView) {
                NMTREEVIEW* nmTreeView =
reinterpret_cast<NMTREEVIEW*>(lparam);
                TreeView_SelectItem(nmhdr->hwndFrom, nmTreeView->itemNew.hItem);
                CTypeBrowserBase* browserBase =
                   
reinterpret_cast<CTypeBrowserBase*>(
                    nmTreeView->itemNew.lParam);
               
if(nmTreeView->action & TVE_EXPAND) browserBase->expand();
               
return 0;
            }
           
break;
       
case TVN_ITEMEXPANDED:
           
if(nmhdr->hwndFrom == windows.treeView) {
                NMTREEVIEW* nmTreeView =
reinterpret_cast<NMTREEVIEW*>(lparam);
                CTypeBrowserBase* browserBase =
                   
reinterpret_cast<CTypeBrowserBase*>(
                    nmTreeView->itemNew.lParam);
               
if(nmTreeView->action & TVE_COLLAPSE) browserBase->collapse();
               
return 0;
            }
           
break;
        }
    }
   
return DefWindowProc(hwnd, message, wparam, lparam);
}

This is a pretty simple message procedure.  We're just handling WM_SIZE, WM_DESTROY, and WM_NOTIFY (for tree view notifications).  The SAppWindows struct holds the handles to our three windows (the parent window, the tree view, and the edit control) and the height of the edit control.  This height is used to align the windows in the WM_SIZE handler.  The application's SAppWindows is constructed on WinMain's stack, and its address is saved as the parent window's GWL_USERDATA entry.  The message procedure's first statement extracts the GWL_USERDATA entry and casts it to an SAppWindows reference.   WM_SIZE is a simple handler - the edit control is placed at the bottom and the tree view fills the remaining portion of the client space. 

WM_NOTIFY traps four notifications from the tree view.   TVN_GETDISPINFO is a request for the node's display name.  The handler casts the node's lParam to CTypeBrowserBase* and invokes displayName on the pointer to retrieve the display name.  The TVN_ITEMEXPANDING and TVN_ITEMEXPANDED handlers are very simple.  The TVN_ITEMEXPANDING message is sent when a node is about to expand or collapse.  The handler casts lParam to the CTypeBrowserBase pointer and calls the expand virtual method.  Similarly, TVN_ITEMEXPANDED is sent when a node just was expanded or collapsed.  When the collapsed action flag is set, we invoke collapse on the browser base slice.  The last message, TVN_SELCHANGED, is fired when a node is selected.  We catch this message in order to keep the edit control displaying current information.  The CTypeBrowserBase::editBoxText is invoked from the pointer stored in lParam, and the returned string is sent to the edit control as its window text.  There you have it - the window procedure.

int _stdcall WinMain(HINSTANCE h, HINSTANCE, char*, int s) {
    CoInitialize(0);
    WNDCLASSEX wc;
    ZeroMemory(&wc, sizeof(wc));
    wc.cbSize = sizeof(wc);
    wc.hIcon = wc.hIconSm = LoadIcon(0, IDI_WINLOGO);
    wc.hCursor = LoadCursor(0, IDC_ARROW);
    wc.lpszClassName = "Cornholio";
    wc.lpfnWndProc = mainProc;
    wc.hInstance = h;
    RegisterClassEx(&wc);
   
    SAppWindows appWindows;
    appWindows.mainWindow = CreateWindow(wc.lpszClassName,
        "COM Type Info Browser", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, h, 0);
   
    InitCommonControls();
    appWindows.treeView = CreateWindow(WC_TREEVIEW, 0, TVS_HASBUTTONS
        | TVS_HASLINES | TVS_LINESATROOT | TVS_DISABLEDRAGDROP | WS_CHILD
        | WS_VISIBLE, 0, 0, 0, 0, appWindows.mainWindow, 0, h, 0);
   
    appWindows.editBox = CreateWindow("EDIT", 0, WS_VISIBLE | WS_CHILD | ES_LEFT
        | ES_READONLY | ES_AUTOHSCROLL, 0, 0, 0, 0,
        appWindows.mainWindow, 0, h, 0);
   
    HFONT font(CreateFont(16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, "Arial"));
    SendMessage(appWindows.editBox, WM_SETFONT,
        reinterpret_cast<WPARAM>(font), 0);
    TEXTMETRIC textMetric;
    HDC hdc(GetDC(0));
    HFONT standardFont(reinterpret_cast<HFONT>(SelectObject(hdc, font)));
    GetTextMetrics(hdc, &textMetric);
    SelectObject(hdc, standardFont);
    ReleaseDC(0, hdc);
    appWindows.editBoxHeight = textMetric.tmHeight;
    SetWindowText(appWindows.editBox, " COM Owns You");
   
    SetWindowLong(appWindows.mainWindow, GWL_USERDATA,
        reinterpret_cast<long>(&appWindows));
   
    CComTypeLibraryContainer typeLibraryContainer(
        CComBSTR("HKEY_CLASSES_ROOT\\TypeLib"), appWindows.treeView, TVI_ROOT);
    ShowWindow(appWindows.mainWindow, s);
    MSG msg;
    while(GetMessage(&msg, 0, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return 0;
}

Here we go.  You've now seen all the code.  WinMain creates the parent window, the edit control, and the tree view.  These window handles are stored in the SAppWindows instance, and the address of this object is saved as the parent window's GWL_USERDATA entry.  The default edit control font (it's the hideous system font) is quite unattractive.  A lovely Arial 16 created and selected into the edit control.  The height of this font is then saved as the height of the edit control.  Finally: the moment we've all been waiting for.

CComTypeLibraryContainer is constructed on WinMain's stack.  The first parameter is the display name of the name, "HKEY_CLASSES_ROOT\TypeLib," the second parameter is the handle to the tree view control, and the third parameter is the HTREEITEM of the parent node.  Because this is the tree's root node, TVI_ROOT is passed.  That's it!  You now have a fully functioning COM Type Information browser.  Sure, it's far inferior to version 57 of the Microsoft OLE Viewer, but you wrote it.  Actually, I wrote it.  But you suffered through three long, dry, stale articles to get to this point.  Kudos!

Boy this was a fun experience.  Thanks for your support and interest, and I hope you'll read all my other COM articles as they are posted (yes, "you" is in the singular).  Until next time, try and use a little COMmon sense.

- Sean Baxter