กก

กก

Using Windows95 Shell and COM -- A. K. A. OLE

COM programming is so difficult that you shouldn't even try it without MFC. Right or wrong? Absolutely wrong!. Granted, OLE and its successor COM have the elegance of a figure-skating hippopotamus. But putting MFC on top of COM is like dressing the hippo in an oversized clown suit.
 
Download Download the source of a sample application, TreeSizer (zipped 12K) (courtesy Laszlo Radanyi), that calculates the total size of all files in a given directory and all its subdirectories. It uses the Win95 shell browser to browse directories.

So what is a programmer to do when faced with the necessity to use some of the Windows 95 (and Windows NT 4.0) shell features that are only accessible through COM interfaces? Read on...

To begin with, whenever you are planning to use COM, you have to tell the system to initialize the COM subsystem. Similarly, whenever you're done using COM, you should tell the system to uninitialize it. The simplest thing to do, is to define an object whose constructor initializes COM and whose destructor deinitializes it. The best place to place it is to embed it in the Controller object (see the Generic Windows program), like this.


class Controller
{
public:
    Controller (HWND hwnd, CREATESTRUCT * pCreate);
    ~Controller ();
    // ...
private:
    UseCom    _comUser; // I'm a COM user
	 
    Model       _model;
    View        _view;
    HINSTANCE   _hInst;
};


This way we are guaranteed that the COM subsystem will be initialized before any calls are made to it and that it will be deinitialized after the program has done its tear-down (i.e., after the View and the Model have been destroyed).

The UseCom class is very simple.


class UseCom
{
public:
    UseCom ()
    {
        HRESULT err = CoInitialize (0);
        if (err != S_OK)
            throw "Couldn't initialize COM";
    }
    ~UseCom ()
    {
        CoUninitialize ();
    }
};

So far it hasn't been too difficult, has it? That's because we haven't touched the bane of COM programming--reference counting. You see, every time you obtain an interface, its reference count is incremented, every time you're done with it, you are supposed to explicitly decrement it. Oh, and it gets even weirder when you start querying, cloning, passing interfaces around, etc. But wait a moment, we know how to manage such problems! It's called Resource Management. We should never touch COM interfaces without encapsulating them in smart interface pointers. Here's how it works.



template <class T>class SIfacePtr
{
public:
    ~SIfacePtr ()
    {
        Free ();
    }
    T * operator->() { return _p; }
    T const * operator->() const { return _p; }
    operator T const * () const { return _p; }
    T const & GetAccess () const { return *_p; }

protected:
    SIfacePtr () : _p (0) {}
    void Free ()
    {
        if (_p != 0)
            _p->Release ();
        _p = 0;
    }

    T * _p;
private:
    SIfacePtr (SIfacePtr const & p) {}
    void operator = (SIfacePtr const & p) {}
};

Don't worry that this class looks unusable (because it has a protected constructor). We'll never use it directly--we'll inherit from it. By the way, this is a great trick: create a class with a protected do-nothing constructor and keep deriving from it. All the derived classes will have to provide their own specialized public constructors. As you might have guessed, various classes derived from SIfacePtr will differ in the way they obtain, in their constructors, the interface in question.

Private dummy copy constructor and operator= are not strictly necessary, but they will save you hours of debugging if, by mistake, you'll pass a smart interface by value rather than by reference. That's a big no-no. You'll end up releasing the same interface twice and that'll screw the COM's reference counting. Believe me, I've been there. As it is, the compiler will refuse to pass by value an object that has a private copy constructor (dummy or not). It will also issue an error when you try to assign an object that has a private assignment operator.

To wrap this short introduction up, let me present one more variation on the theme of smart pointers. Shell API's often allocate memory using their own special allocator. That wouldn't be so bad, if it weren't for the fact that they expect you to release this memory using the same allocator. So, whenever the shell hands us such an embarassing package, we'll wrap it up in a special smart pointer.


template <class T>
class SShellPtr
{
public:
    ~SShellPtr ()
    {
        Free ();
        _malloc->Release ();
    }
    T * weak operator->() { return _p; }
    T const * operator->() const { return _p; }
    operator T const * () const { return _p; }
    T const & GetAccess () const { return *_p; }

protected:
    SShellPtr () : _p (0) 
    {
        // Obtain malloc here, rather than
        // in the destructor. 
        // Destructor must be fail-proof.
        // Revisit: Would static IMalloc * _shellMalloc work?
        if (SHGetMalloc (& _malloc) == E_FAIL)
            throw Exception "Couldn't obtain Shell Malloc"; 
    }
    void Free ()
    {
        if (_p != 0)
            _malloc->Free (_p);
        _p = 0;
    }

    T * _p;
    IMalloc *  _malloc;
private:
    SShellPtr (SShellPtr const & p) {}
    void operator = (SShellPtr const & p) {}
};

Notice the same trick as before: the class SShellPtr is not directly usable. You have to subclass it and provide the appropriate constructor.

Notice also that I wasn't sure if _shellMalloc couldn't be made a static member of SShellPtr. The problem is that static members are initialized before WinMain. At that time, the whole COM system might be unstable. On the other hand, the documentation says that you can safely call another API, CoGetMalloc, before the call to CoInitialize. It doesn't say whether SHGetMalloc, which does almost the same thing, can also be called anytime in your program. Like in many other cases with badly designed/documented systems, only experiment can answer such questions. Feedback will be very welcome.

By the way, if you need a smart pointer that uses COM-specific malloc, the one obtained by calling CoGetMalloc, you can safely make its _malloc a static member and initialize it once in your program (here, SComMalloc::GetMalloc is static, too):


IMalloc * SComMalloc::_malloc = SComMalloc::GetMalloc ();

IMalloc * SComMalloc::GetMalloc ()
{
    IMalloc * malloc = 0;
    if (CoGetMalloc (1, & malloc) == S_OK)
        return malloc;
    else
        return 0;
}

That's all there is to know in order to start using Windows95 shell and its COM interfaces. Here's an example. Windows95 shell has this notion of a Desktop being the root of the "file" system. Did you notice how Windows95 applications let the user browse the file system starting at the desktop? This way you can, for instance, create files directly on your desktop, move between drives, browse a network drive, etc. It is, in effect, a poor man's Distributed File System (PMDFS). How can your application get access to PMDFS? Easy. For instance, let's write some code that will let the user pick a folder by browsing PMDFS. All we need to do is to get hold of the desktop, position ourselves with respect to it, start the built-in browser and retrieve the path that the user has chosen.


char path [MAX_PATH];
path [0] = '\0';
Desktop desktop;
ShPath browseRoot (desktop, unicodePath);
if (browseRoot.IsOK ())
{
    FolderBrowser browser (hwnd,
                          browseRoot,
                          BIF_RETURNONLYFSDIRS,
                          "Select folder of your choice");
    if (browser.IsOK ())
    {
        strcpy (path, browser.GetPath ());
    }
}

Let's start with the desktop object. It envelops the interface called IShellFolder. Notice how we conform to the First Rule of Acquisition--we allocate resources in the constructor by calling the API SHGetDesktopFolder. The smart interface pointer will take care of resource management (reference counting).



class Desktop: public SIfacePtr<IShellFolder>
{
public:
    Desktop ()
    {
        if (SHGetDesktopFolder (& _p) != NOERROR)
            throw "HGetDesktopFolder failed";
    }
};

Once we have the desktop, we have to create a special kind of path that is used by PMDFS. The class ShPath encapsulates this "path." It is constructed from a regular Unicode path (use mbstowcs to convert ASCII path to Unicode: int mbstowcs(wchar_t *wchar, const char *mbchar, size_t count)). The result of the conversion is a generalized path relative to the desktop. Notice that memory for the new path is allocated by the shell--we encapsulate it in SShellPtr to make sure it is correctly deallocated.



class ShPath: public SShellPtr<ITEMIDLIST>
{
public:
    ShPath (SIfacePtr<IShellFolder> & folder, wchar_t * path)
    {
        ULONG lenParsed = 0;
        _hresult = folder->ParseDisplayName (0, 
                                             0, 
                                             path, 
                                             & lenParsed, 
                                             & _p, 
                                             0);
    }
    bool IsOK () const { return SUCCEEDED (_hresult); }
private:
    HRESULT _hresult;
};

This shell path will become the root from which the browser will start its interaction with the user.

From the client code's point of view, the browser is the path chosen by the user. That's why it inherits from SShellPtr<ITEMIDLIST>.

By the way, ITEMIDLIST is the official name for this generalized path. It is a list of, and I'm not making this up, SHITEMID's. I shall refrain from making any further comments or jokes about shell naming conventions.



class FolderBrowser: public SShellPtr<ITEMIDLIST>
{
public:
    FolderBrowser (
        HWND hwndOwner,
        SShellPtr<ITEMIDLIST> & root,
        UINT browseForWhat,
        char const *title);

    char const * GetDisplayName () { return _displayName; }
    char const * GetPath ()        { return _fullPath; }
    bool IsOK() const              { return _p != 0; };

private:
    char       _displayName [MAX_PATH];
    char       _fullPath [MAX_PATH];
    BROWSEINFO _browseInfo;
};

FolderBrowser::FolderBrowser (
    HWND hwndOwner,
    SShellPtr<ITEMIDLIST> & root,
    UINT browseForWhat,
    char const *title)
{
    _displayName [0] = '\0';
    _fullPath [0] = '\0';
    _browseInfo.hwndOwner = hwndOwner;
    _browseInfo.pidlRoot = root; 
    _browseInfo.pszDisplayName = _displayName;
    _browseInfo.lpszTitle = title; 
    _browseInfo.ulFlags = browseForWhat; 
    _browseInfo.lpfn = 0; 
    _browseInfo.lParam = 0;
    _browseInfo.iImage = 0;
	 
    // Let the user do the browsing
    _p = SHBrowseForFolder (& _browseInfo);
	 
    if (_p != 0)
        SHGetPathFromIDList (_p, _fullPath);
}


That's it! Isn't it simple?

Now, would you like to hear what I think about OLE?