SoftCraft
разноликое программирование

Top.Mail.Ru

Оболочка из классов для COM


Использование оболочки Windows совместно с COM

Перевод А. И. Легалова

Англоязычный оригинал находится на сервере компании Reliable Software


Программировать с использованием COM настолько трудно, что Вы не должны даже пробовать это без MFC. Правильно или неправильно? Абсолютная чушь! Рекламируемые OLE и его преемник COM имеют элегантность гиппопотама, занимающегося фигурным катанием. Но размещение MFC на вершине COM подобно одеванию гиппопотама в клоунский костюм еще больших размеров.

Загрузите исходник примера, TreeSizer (zip архив 12 кб, любезность Laszlo Radanyi), в котором вычисляется суммарный размер всех файлов в некотором каталоге и всех его подкаталогах. Он, для просмотра каталогов, использует окно просмотра оболочки Windows.

Итак, что делать программисту, когда он столкнется с потребностью использовать возможности оболочки Windows, которые являются доступными только через интерфейсы COM? Читайте ...

Для начала, всякий раз, когда Вы планируете использовать COM, Вы должны сообщить система, чтобы она инициализировала COM подсистему. Точно так же всякий раз, когда вы заканчиваете работу, Вы должны сообщить системе, чтобы она выгрузила COM. Самая простой способ это сделать заключается в определении объекта, конструктор которого инициализирует COM, а деструктор выгрожает ее. Самое лучшее место, для недрения данного механизма - это объект Controller (см. Windows программу Generic), подобный этому.


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

Этот способ гарантируют, что COM подсистема будет проинициализирована прежде, чем к ней будут сделаны любые обращения и что она будет освобождена после того, как программа осуществит свои разрушения (то есть, после того, как "Вид" и "Модель" будут разрушены).

Класс UseCom очень прост.


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

Пока не было слишком трудно, не так ли? Дело в том, что мы не коснулись главной мерзости COM программирования - подсчета ссылок. Вам должно быть известно, что каждый раз, когда что Вы получаете интерфейс, его счетчик ссылок увеличивается. И Вам необходимо явно уменьшать его. И это становится более чем ужастным тогда, когда Вы начинаете запрашивать интерфейсы, копировать их, передавать другим и т.д. Но ловите момент: мы знаем, как управляться с такими проблемами! Это называется управлением ресурсами. Мы никогда не должны касаться интерфейсов COM без инкапсуляции их в интеллектуальных указателях на интерфейсы. Ниже показано, как это работает.

Примечание. В настоящий момент данная тема достаточно широко рассмотрена в литературе, переведенной на русский язык. В частности, можно отметить книги:

  • Джефф Элджер. "C++: библиотека программиста";
  • Скотт Мейерс. "Эффективное программирование на С++".

А.Л.



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) {}
};

Не волнуйте, что этот класс выглядит непригодным (потому что имеет защищенный конструктор). Мы никогда не будем использовать его непосредственно. Мы наследуем от него. Между прочим, это удобный прием: создайте класс с защищенным пустым конструктором, и осуществите наследование от него. Все наследующие классы должны обеспечивать их внутреннюю реализацию своими открытыми конструкторами. Поскольку Вы можете иметь различные классы, наследующие от SIfacePtr, они будут отличаться по способу получения, по их конструкторам, рассматриваемым интерфейсам.

Закрытые фиктивный копирующий конструктор и оператор "=" не всегда необходимы, но они сохранят Вам время, потраченное на отладку, если по ошибке вы передадите интеллектуальный интерфейс по значению вместо передачи по ссылке. Это больше невозможно. Вы можете освободить один и тот же интерфейс, дважды и это будет круто отражаться на подсчете ссылок COM. Верьте мне, у меня это случалось. Как это часто бывает, компилятор откажется передавать по значению объект, который имеет закрытую копию конструктора. Он также выдаст ошибку, когда Вы пробуете присвоить объект, который имеет закрытый оператор присваивания.

Для завершения этого короткого введения, позвольте мне представить еще одну вариацию на тему интеллектуальных указателей. Оболочки API часто распределяет память, используя свои собственные специальные программы распределения. Это не было бы настолько плохо, если бы не предположение, что они ожидают от вас освобождения памяти с использованием той же самой программы распределения. Так, всякий раз, когда оболочка вручает нам такой сомнительный пакет, мы обворачиваем его в специальный интеллектуальный указатель.


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) {}
};

Обратите внимание на использование ранее показанного приема: класс SShellPtr непосредственно не пригоден для использования. Вы должны наследовать от него подкласс и реализовать в нем соответствующий конструктор.

Обратите также внимание, что я не уверен, может ли _shellMalloc быть статическим элементом SShellPtr. Проблема состоит в том, что статические элементы инициализируются перед WinMain. Из-за этого вся COM система может оказаться неустойчивой. С другой стороны, документация говорит, что Вы можете безопасно вызывать из другой API функции CoGetMalloc перед обращением к CoInitialize. Это не говорит о том, может ли SHGetMalloc, который делает почти то же самое, также вызываться в любое время в вашей программе. Подобно многим других случаям, когда система ужасно разработана или задокументирована, только эксперимент может ответить на такие вопросы. Добро пожаловать к нам с такими ответами.

Между прочим, если Вы нуждаетесь в интеллектуальном указателе, который использует специфическое распределение памяти для COM, то получите его, вызывая CoGetMalloc. Вы можете без опаски сделать этот _malloc статическим элементом и инициализировать его только один раз в вашей программе (ниже SComMalloc:: GetMalloc тоже статический):


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

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

Это - все, что надо знать, чтобы начать использовать оболочку Windows и ее COM интерфейсы. Ниже приводится пример. Оболочка Windows имеет понятие Рабочего стола, являющегося корнем "файловой" системы. Вы обращали внимание, как Windows приложения допускают пользователя, просматривают файловую систему, начинающуюся на рабочем столе? Этим способом Вы можете, например, создавать файлы непосредственно на вашем рабочем столе, двигаться между дисководами, просматривать сетевой дисковод, и т.д. Это, в действительности, Распределенная Файловая система (PMDFS - poor man's Distributed File System) ограниченного человека (?). Как ваше приложение может получить доступ к PMDFS? Просто. В качестве примера напишем код, который позволит пользователю, выбирать папку, просматривая PMDFS. Все, что мы должны сделать - это овладеть рабочим столом, позиционироваться относительно его, запустить встроенное окно просмотра и сформировать путь, который выбрал пользователь.


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 (folder.IsOK ())
    {
        strcpy (path, browser.GetPath ());
    }
}

Давайте, запустим объект desktop. Он использует интерфейс по имени IShellFolder. Обратите внимание, как мы приходим к Первому Правилу Захвата. Мы распределяем ресурсы в конструкторе, вызывая функцию API SHGetDesktopFolder. Интеллектуальный указатель интерфейса будет заботиться об управлении ресурсами (подсчет ссылок).


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

Как только мы получили рабочий стол, мы должны создать специальный вид пути, который используется PMDFS. Класс ShPath инкапсулирует этот "путь". Он создан из правильного Unicode пути (используйте mbstowcs, чтобы преобразовать путь ASCII в Unicode: int mbstowcs(wchar_t *wchar, const char *mbchar, size_t count)). Результат преобразования - обобщенный путь относительно рабочего стола. Обратите внимание, что память для нового пути распределена оболочкой - мы инкапсулируем это в SShellPtr, чтобы быть уверенными в правильном освобождении.


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;
};

Этот путь оболочки станет корнем, из которого окно просмотра начнет его взаимодействие с пользователем.

С точки зрения клиентского кода, окно просмотра - путь, выбранный пользователем. Именно поэтому он наследуется от SShellPtr<ITEMIDLIST>.

Между прочим, ITEMIDLIST - официальное имя для этого обобщенного пути. Это - список, и я не назвал его, SHITEMID'ом. Впредь я буду воздерживаться от любых дальнейших комментариев или шуток относительно соглашений, связанных с именами, используемыми в оболочке.


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);
}

Вот так! Разве это не просто?

Далее: Вам, наверное, будет приятно услышать то, что я думаю об OLE?