Сообщений 0    Оценка 156        Оценить  
Система Orphus

Использование ATL для автоматизации MFC приложений

Автор: Nick Hodapp
Перевод: Игорь Ткачёв
Источник: CodeGuru::Using ATL to Automate an MFC Application
Опубликовано: 15.07.2001
Исправлено: 21.05.2007

Download source - 40 KB

Текущая версия продукта, который я помогаю разрабатывать, предоставляет программный интерфейс автоматизации (Automation interface). Для кодировании клиентской части и открытых (exposed) COM объектов используется MFC. Как и любые COM объекты, базирующиеся на MFC, наши объекты зависят от поддержки COM объектов, реализованной внутри класса CCmdTarget. Когда я впервые реализовал эти объекты, я был обескуражен объёмом кода, который я должен был написать для поддержки довольно стандартных интерфейсов. Было проделано много работы для реализации открытых объектов/интерфейсов, подобных стандартной модели Application-Documents-Document, используемой в Microsoft Office и других продуктах. В следующих версиях нашего продукта мы будем использовать ATL для реализации объектов автоматизации. В этой статье я рассмотрю возможность использования ATL для реализации COM интерфейсов на классах производных от классов MFC.

Среди многих других проблем использования MFC для реализации объектов автоматизации, существует тот факт, что поддержка создания источников событий реализована в MFC в классе COleControl, а не в CCmdTarget. Поэтому, если Вашему объекту автоматизации, производному от CCmdTarget, нужен источник или приёмник событий, Вы должны реализовать требуемые интерфейсы самостоятельно. (На самом деле, можно позаимствовать MFC код, реализованный в COleControl, и добавить его к Вашему собственному классу, производному от CCmdTarget. Возможно, я расскажу как это сделать в другой статье, если это будет интересно.) В свою очередь, библиотека ATL обеспечивает готовую, повторно используемую реализацию многих основных интерфейсов, включая те, что связаны с источниками и приёмниками событий.

Что касается меня, то мне тоже не нравится подход вложенных классов, применяющийся в MFC для реализации множественных интерфейсов COM объектов. Из-за этого страдает повторное использование кода, и, я чувствую, что это более трудно поддерживать, чем при множественном наследовании (которое используется в ATL). Отсутствие поддержки в MFC dual-интерфейов также, мягко говоря, неприятно.

MSVC 6.0 предоставляет возможность легко добавлять поддержку ATL объектов в существующее MFC приложение. Использование ATL позволяет кодировать COM объекты более легко и быстро, это даёт массу преимуществ по сравнению с MFC. Но, к сожалению, MSVC IDE генерирует эти ATL COM объекты так, что их крайне трудно использовать из самого MFC приложения. Это результат соглашения, применяемого IDE при генерации заглушек (stubs) методов объектов. Когда создаётся заглушка, макрос AFX_MANAGE_STATE(AfxGetStaticModuleState()) подключает информацию о внутреннем состоянии MFC, которая может отличаться от необходимой для выполняемого в данный момент модуля. (Более подробную информацию об MFC state-handling, см. в Tech Note 58.) Таким образом, вызов функции AfxGetApp() из метода ATL COM объекта не возвращает "реальное" приложение, которое выполняется в данном модуле (фактически, не возвращается никакого CWinApp объекта). Причина такого соглашения - возможные проблемы в многозадачной среде.

Объекты, производные от классов MFC (COM и другие) могут быть потокобезопасными (thread safe) только в том потоке, в котором они были созданы. То есть, Вы не можете создать CView в одном потоке, а затем использовать его в другом. ATL COM объекты потокобезопасны настолько насколько об этом позаботится программист, который их разрабатывает. Всё зависит от цели использования объекта, программист может захотеть, а может и нет, тратить усилия для написания объектов, безопасно вызываемых из нескольких потоков.

Было бы нежелательно для многопоточных ATL объектов иметь доступ и манипулировать MFC объектами из чужого потока. А чужим будет любой поток, кроме того, в котором были созданы MFC объекты. Есть только один путь безопасно интегрировать два таких объекта - гарантировать, что они всегда разговаривают друг с другом в одном потоке. Это может быть достигнуто при использовании STA (Single Threaded Apartment) модели выполняемого модуля. Фактически, это единственный поддерживаемый режим работы для MFC COM объекта.

В исполняемом файле модель потока задаётся при начальном вызове CoInitialize() или CoInitializeEx(). MFC использует функцию AfxOleInit(), которая, в конце концов, вызывает CoInitializeEx() с параметром COINIT_APARTMENTTHREADED. До тех пор, пока мы гарантируем, что наши ATL объекты работают внутри STA (и гарантируем то же самое для MFC приложения), мы можем работать и с ними и с MFC объектами, работающими в данном модуле. Если исхитриться, то можно создать гибридный (half-breed) объект, который был бы очень полезен для автоматизации наших приложений. Например, было бы здорово создать ATL COM интерфейс прямо из MFC CWinApp объекта, или из CDocument или CView объекта.

Если вы начинаете зевать от всей этой прозы, вставайте. Пришло время заняться экспериментом. Целью будет преобразование MFC приложения, не поддерживающего в данный момент автоматизацию, так, чтобы оно смогло предоставить объект Application. Объект Application будет таким же, как объект производный от CWinApp, но мы будем использовать ATL вместо CCmdTarget для реализации COM интерфейса. Это не будет так лёгко сделать, как кажется... (Если Вам не интересно следовать этим шагам, готовый проект распространяется вместе с этой статьёй.)

  1. Сгенерируйте новое MDI приложение с помощью MFC AppWizard. Назовите проект AutoATL, и согласитесь со всеми умолчаниями визарда (то есть, не устанавливайте флажок "Automation"). Откомпилируйте проект.

  2. Выберите пункт "New ATL Object" в меню Insert. Разрешите IDE добавить требуемую поддержку ATL.

  3. В ATL Object Wizard выберите объект "Simple Object" и дайте ему имя "Application". На странице атрибутов оставьте "Apartment threading model", выберите "Dual interface" и установите "No Aggregation" и "Support Connection Points".

    Теперь мы имеем всё, что MSVC смог нам дать для дальнейшей работы: MFC приложение и COM объект, который находится в том же выполняемом модуле. Попытаемся переписать производный от CWinApp class, CAutoATLApp.

  4. Скопируйте следующую часть заголовочного файла класса CApplication в заголовочный файл CAutoATLApp и замените "CApplication" на "CAutoATLApp" в аргументах шаблонов и макросов, как показано ниже:

    // CApplication
    class ATL_NO_VTABLE CApplication :
        public CComObjectRootEx<CCOMSINGLETHREADMODEL>,
        public CComCoClass<CAPPLICATION, &CLSID_Application>,
        public IConnectionPointContainerImpl<CAPPLICATION>,
        public IDispatchImpl<IAPPLICATION, &IID_IApplication, &LIBID_AutoATLLib>
    {
    public:
        CApplication()
        {
        }
    
    DECLARE_REGISTRY_RESOURCEID(IDR_APPLICATION)
    DECLARE_NOT_AGGREGATABLE(CApplication)
    
    DECLARE_PROTECT_FINAL_CONSTRUCT()
    
    BEGIN_COM_MAP(CApplication)
        COM_INTERFACE_ENTRY(IApplication)
        COM_INTERFACE_ENTRY(IDispatch)
        COM_INTERFACE_ENTRY(IConnectionPointContainer)
    END_COM_MAP()
    
    BEGIN_CONNECTION_POINT_MAP(CApplication)
    END_CONNECTION_POINT_MAP()
    
    // IApplication
    public:
    };
    
    // CAutoATLApp:
    class CAutoATLApp : public CWinApp,
        public CComObjectRootEx<CCOMSINGLETHREADMODEL>,
        public CComCoClass<CAUTOATLAPP, &CLSID_Application>,
        public IConnectionPointContainerImpl<CAUTOATLAPP>,
        public IDispatchImpl<IAPPLICATION, &IID_IApplication, &LIBID_AutoATLLib>
    {
    public:
        CAutoATLApp();
    
    DECLARE_REGISTRY_RESOURCEID(IDR_APPLICATION)
    DECLARE_NOT_AGGREGATABLE(CAutoATLApp)
    
    DECLARE_PROTECT_FINAL_CONSTRUCT()
    
    BEGIN_COM_MAP(CAutoATLApp)
        COM_INTERFACE_ENTRY(IApplication)
        COM_INTERFACE_ENTRY(IDispatch)
        COM_INTERFACE_ENTRY(IConnectionPointContainer)
    END_COM_MAP()
    
    BEGIN_CONNECTION_POINT_MAP(CAutoATLApp)
    END_CONNECTION_POINT_MAP()
    
    // Overrides
        //{{AFX_VIRTUAL(CAutoATLApp)
        public:
        virtual BOOL InitInstance();
        virtual int ExitInstance();
        //}}AFX_VIRTUAL
    
    // Implementation
        //{{AFX_MSG(CAutoATLApp)
        afx_msg void OnAppAbout();
        //}}AFX_MSG
        DECLARE_MESSAGE_MAP()
    
    private:
        BOOL m_bATLInited;
    
    private:
        BOOL InitATL();
    };
    

  5. Класс CApplication нам больше не нужен, и необходимо удалить его файлы (Application.cpp, Application.h) из проекта. Это можно сделать на закладке File View окна Project Workspace. Далее удалите строчку #include для "Application.h" в файле AutoATL.cpp и подкорректируйте карту объектов (object map):

    BEGIN_OBJECT_MAP(ObjectMap)
    OBJECT_ENTRY(CLSID_Application, CAutoATLApp)
    END_OBJECT_MAP()
    

  6. Пробуем откомпилировать, получаем ошибку...

    Компилятор выдал 22 ошибки и 47 предупреждений потому, что мы сделали то, о чём никак не могли предположить. Наш код пытается объявить CWinApp с использованием группы ATL классов путём грубого вмешательства. Но, к сожалению, класс CWinApp, производный от MFC CCmdTarget не совместим с ATL CComObjectRootEx<>, в частности, имеется явный конфликт имён. Исследуем первую ошибку:

    : error C2385: 'CAutoATLApp::InternalAddRef' is ambiguous
    
    : warning C4385: could be the 'InternalAddRef' in base 
      'CCmdTarget' of base 'CWinThread' of base 'CWinApp' of 
      class 'CAutoATLApp'
    
    : warning C4385: or the 'InternalAddRef' in base 
      'CComObjectRootEx<class ATL::CComSingleThreadModel>' of 
      class 'CAutoATLApp'
    

    Какое счастье!! Разработчикам ATL из Рэдмонда (Redmond) так сильно понравилась реализация MFC, что они приняли ту же самую схему имён. Сейчас я их назову, существует пять конфликтов имён, которые возникают в нашем случае. Кроме функций InternalAddRef(), InternalRelease() и InternalQueryInterface(), также происходит конфликт имён переменных m_dwRef и m_pOuterUnknown.

    Обычно конфликт имён разрешается путём использования пространства имён (namespaces), но у меня имеется другой трюк.

  7. Откройте файл проекта stdafx.h и добавьте следующие пять строчек перед строкой, которая подключает atlbase.h:

    #define m_dwRef                m_dwRefAtl
    #define m_pOuterUnknown        m_pOuterUnknownAtl
    #define InternalQueryInterface InternalQueryInterfaceAtl
    #define InternalAddRef         InternalAddRefAtl
    #define InternalRelease        InternalReleaseAtl
    #include <atlbase.h>
    

    ATL представляет собой библиотеку шаблонов, поэтому вся её реализация находится в заголовочных файлах, подключаемых модулями, которые её используют. Мы можем использовать препроцессор, чтобы переименовать идентификаторы внутри заголовочных ATL файлов до того как ATL код будет скомпилирован. Что мы в результате получили: наши имена переименованы и теперь не будут конфликтовать.

  8. Пытаемся откомпилировать ещё раз. Снова получаем ошибку, но...

    Заметьте, что на этот раз наш конфликт имён исчез. Вместо них 2 ошибки и 12 предупреждений и все сосредоточены на одной строке в AutoATLApp.cpp:

    CAutoATLApp theApp;
    
    : error C2259: 'CAutoATLApp' : cannot instantiate abstract 
      class due to following members:
    

    Если Вы раньше писали ATL объекты, то Вы поймёте что произошло. Наш компонент неполон; его экземпляр может быть создан только с использованием класса оболочки CComObject<> (или одного из родственных), потому что методы интерфейса IUnknown QueryInterface(), AddRef() и Release() виртуальные (видимо автор имел ввиду абстрактные, прим. переводчика) и они реализуются семейством классов CComObject<>. Так как этот объект глобальный, мы будем использовать CComObjectGlobal<>.

  9. Измените глобальное объявление theApp в соответствии со строкой ниже, затем откопилируйте проект:

    CComObjectGlobal<CAutoATLApp> theApp;
    

    Ура, ошибок нет! Для полного счастья, запустите приложение и убедитесь, что оно работает нормально.

    К сожалению, мы ещё не всё сделали. Отсутствие ошибок компиляции не обозначает отсутствие проблем (как часто вам хотелось, чтобы это было не так?) Проблема (и это, возможно, не так очевидно...) в том, что если бы даже клиентское приложение (имеется ввиду то приложение, которое будет запускать наш сервер автоматизации, прим. переводчика) смогло создать экземпляры нашего объекта Application, то ни один из них не был бы тем же самым глобальным объектом "theApp", который объявлен в нашем модуле AutoATL.cpp. То, что мы ищем, в данном случае, - это единственный экземпляр объекта (singleton), который уникален для процесса. Позволение двум или более различным объектам CAutoATLApp жить вместе недопустимо.

    Приношу извинения, но шаги по устранению это маленькой проблемы слегка сложноваты. Пожалуйста, следуйте за мной, и я сделаю всё возможное, чтобы прояснить ситуацию.

  10. То, что нам нужно - это единственный экземпляр объекта Application. ATL разрешает объекту иметь единственный экземпляр, если добавить одну строчку в объявление класса:

    class CAutoATLApp : public CWinApp,
        public CComObjectRootEx<CComSingleThreadModel>,
        public CComCoClass<CAutoATLApp, &CLSID_Application>,
        public IConnectionPointContainerImpl<CAutoATLApp>,
        public IDispatchImpl<IApplication, &IID_IApplication, &LIBID_AutoATLLib>
    {
    public:
        CAutoATLApp();
    
    DECLARE_CLASSFACTORY_SINGLETON(CAutoATLApp)
    

    Эта строка говорит ATL реализовать фабрику классов для нашего объекта таким образом, что бы объект создавался только один раз, а в дальнейшем возвращались ссылки на него.

  11. Однако, объект, который мы указали в качестве нашего приложения (шаг 9), не создаётся фабрикой классов. Поэтому, это должно быть отменено: удалите строчку, указанную в шаге 9.

    Теперь перед нами стоит трудная задача. До этого COM и ATL инициализировались в функции OnInitInstance() нашего объекта CAutoATLApp, которая больше не будет вызываться, потому что мы не имеем экземпляра объекта CAutoATLApp. Часть инициализации COM отвечала за регистрацию нашей фабрики классов и создание экземпляра объекта. Нам как-то нужно вызвать эти стандартные операции в стартовом модуле, чтобы экземпляр CAutoATLApp мог быть создан.

    Когда IDE добавлял поддержку ATL в наш проект (шаг 2), был добавлен глобальный объект _Module. _Module имеет тип CAutoATLModule, производный от CComModule, и предназначен в основном для поддержки фабрики классов нашего модуля. Он и выполнит нужную нам инициализацию COM и ATL. Мы внесём изменения так, чтобы всё необходимое инициализировалось автоматически.

  12. Измените объявление класса CAutoATLModule в stdafx.h как показано ниже:

    class CAutoATLModule : public CComModule
    {
    public:
        bool m_bATLInited;
        CAutoATLModule();
        ~CAutoATLModule();
    
        LONG Unlock();
        LONG Lock();
        LPCTSTR FindOneOf(LPCTSTR p1, LPCTSTR p2);
        DWORD dwThreadID;
    };
    

    Затем, в файле AutoATL.cpp, добавьте код нового конструктора и деструктора: Вы можете скопировать большую часть кода прямо из функций InitATL() и ExitInstance() класса CAutoATLApp, но не забудьте учесть мои изменения:

    CAutoATLModule::CAutoATLModule()
    {
        m_bATLInited = TRUE;
    
        // STA apartment
        HRESULT hRes = CoInitialize(NULL);
        if (FAILED(hRes))
            exit(1);
    
        Init(ObjectMap, GetModuleHandle(NULL));
        dwThreadID = GetCurrentThreadId();
    
        LPTSTR lpCmdLine = GetCommandLine(); //this line necessary for _ATL_MIN_CRT
        TCHAR szTokens[] = _T("-/");
    
        BOOL bRun = TRUE;
        LPCTSTR lpszToken = FindOneOf(lpCmdLine, szTokens);
        while (lpszToken != NULL)
        {
            if (lstrcmpi(lpszToken, _T("UnregServer"))==0)
            {
                UpdateRegistryFromResource(IDR_AUTO3, FALSE);
                UnregisterServer(TRUE); //TRUE means typelib is unreg'd
                bRun = FALSE;
                break;
            }
            if (lstrcmpi(lpszToken, _T("RegServer"))==0)
            {
                UpdateRegistryFromResource(IDR_AUTO3, TRUE);
                RegisterServer(TRUE);
                bRun = FALSE;
                break;
            }
            lpszToken = FindOneOf(lpszToken, szTokens);
        }
    
        if (!bRun)
        {
            Term();
            CoUninitialize();
            exit(0);
        }
    
        hRes = RegisterClassObjects(CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE);
        if (FAILED(hRes))
        {
            CoUninitialize();
            exit(1);
        }
    }
    
    CAutoATLModule::~CAutoATLModule()
    {
        if (m_bATLInited)
        {
            RevokeClassObjects();
            Term();
            CoUninitialize();
        }
    }
    

    Эти изменения позволяют выполнять инициализацию непосредственно при создании объекта _Module. Наша фабрика класса Application будет создана и в свою очередь создаст и зарегистрирует наш экземпляр объекта Application. Если Вам интересно, продолжим и постигнем магию, которую MFC использует для определения местоположения объекта CWinApp, в независимости от того, как этот объект был создан. Хотя, как и большинство вещей в MFC, это не так магично после того, как Вы увидите в действии...

    Тепер _Module само-регистрируемый и нам нужно сделать незначительные изменения в CAutoATLApp.

  13. Удалите InitATL(), ExitInstance() и m_bATLInited члены CAutoATLApp (не забудьте удалить их и из файла заголовка тоже!) Затем удалите вызов InitATL() из InitInstance() и замените его следующим кодом:

    BOOL CAutoATLApp::InitInstance()
    {
        if (false == _Module.m_bATLInited)
            return FALSE;
    
        _Module.UpdateRegistryFromResource(IDR_AUTOATL, TRUE);
        _Module.RegisterServer(TRUE);
    
        AfxEnableControlContainer();
    

    Готово! Чтобы протестировать наши усилия, давайте добавим метод, который мы сможем вызвать из клиентского приложения.

  14. Раскройте класс CAutoATLApp в дереве Class View. Нажмите правой кнопкой мышки на иконке и выберите пункт "Add Method". Задайте имя метода "Beep", затем жмите OK. Раскройте иконку и дважды кликните на методе "Beep", чтобы перейти к заготовке функции, созданной для нас IDE. Добавьте вызов функции для выдачи сигнала:

    STDMETHODIMP CAutoATLApp::Beep()
    {
        AFX_MANAGE_STATE(AfxGetStaticModuleState())
    
        ::Beep(1000,1000);
    
        return S_OK;
    }
    
  15. Откомпилируйте и выполните приложение. В результате выполнения приложения в реестре будет зарегистрирован сервер автоматизации и его объект Application.

  16. Используя Visual Basic или любое другое средство, которое Вам больше нравится, напишите простого клиента, чтобы вызвать метод Beep(). VB код мог бы быть таким:

    Dim a As AutoATLLib.Application
    Set a = New AutoATLLib.Application
    a.Beep
    

    Шестнадцать шагов, не слабо! Но теперь мы имеем программу, с которой можем поиграть. Вперёд, запустите своего клиента и убедитесь, что программа подаёт звуковой сигнал, как и ожидалось.

    Однако есть ещё несколько довольно серьёзных вопросов, с которыми нам нужно разобраться. Прежде всего, мы рассмотрим природу макроса AFX_MANAGE_STATE(AfxGetStaticModuleState()) в методе Beep(). Если Вы добавите строчку кода, следующую за макросом, чтобы получить указатель на CWinApp, Вы увидите в отладчике, что возвращаемое значение равно NULL.

    STDMETHODIMP CAutoATLApp::Beep()
    {
        AFX_MANAGE_STATE(AfxGetStaticModuleState())
    
        CWinApp* pApp = AfxGetApp();
        // pApp is NULL!
    
        ::Beep(1000,1000);
        return S_OK;
    }
    

    Конечно, в контексте Beep(), это указатель на CWinApp, но это неверная точка зрения. Дело в том, что макрос аннулировал нашу способность добираться до информации о состоянии, которое использует приложение, когда оно не в контексте метода COM объекта. Простое удаление вызова этого макроса тоже не даёт ответа, так как многим модулям MFC нужна корректная информация, чтобы работать правильно.

    Прежде всего, причина того, что контекст MFC не в порядке немного сложна. Очень упрощённое объяснение этому то, что COM создал скрытое окно, которое получает сообщения из очереди сообщений потока, точно так же как это делают окна MFC. Когда это окно получает сообщение от COM, запрашивающего создание вызова метода объекта, весь MFC код находится вне области видимости (т.к. не-MFC окно обслуживает очередь сообщений) и контекст MFC не определён.

    К счастью, многие MFC объекты сохраняют указатель на информацию о контексте, которая относится к этому объекту. Так как наш вызов метода является членом MFC класса CWinApp, нам нужно просто попросить MFC использовать информацию о состоянии из нашей переменной класса m_pModuleState:

    STDMETHODIMP CAutoATLApp::Beep()
    {
        AFX_MANAGE_STATE(m_pModuleState)
    

    Чрезвычайно важно, чтобы MFC COM объекты тоже делали это, даже если они используют специальный макрос METHOD_PROLOGUE, который тоже устанавливает указатель pThis. Я предлагаю определить и использовать следующий макрос:

    #define METHOD_PROLOGUE_ATL AFX_MANAGE_STATE(m_pModuleState);
    

    Если Вы определите такой макрос в stdafx.h (или в любом другом глобальном заголовочном файле), то это сделает Вашу жизнь намного проще. Только не забывайте использовать этот макрос в начале любых методов смешанных MFC/ATL объектов.

    Следующий вопрос, который нам нужно разобрать касается создания объекта и его времени жизни. В нашем эксперименте единственный объект Application будет жить до тех пор, пока модуль загружен. И модуль будет оставаться загруженным, пока существуют клиенты, удерживающие интерфейс объекта Application. К счастью MFC никогда не пытается удалять объект, так как считает его глобальным объектом создаваемым в стеке (на самом деле в стандартном MFC приложении объект theApp создаётся в сегменте данных, прим. переводчика). Заметьте, что нам тоже удалось создать объект Application через нашу фабрику классов.

    Но что случится, если мы попытаемся автоматизировать MFC класс, у которого менее стабильный жизненный цикл? Возьмём, например, CDocument. CDocument создаётся в недрах MFC и разрушает себя в методе OnCloseDocument(). К счастью, MFC достаточно гибок, чтобы позволить поработать нам над этими проблемами. Давайте попробуем дать нашему классу CAutoATLDoc COM интерфейс, используя ATL.

    Следуйте тем же шагам, которые мы проделали для добавления ATL кода объекта CAutoATLApp:

    Когда Вы попытаетесь откомпилировать результирующий CAutoATLDoc класс, Вы получите похожие ошибки, которые мы видели раньше на шаге 8. MFC макрос IMPLEMENT_DYNCREATE формирует код, который пытается создать объект CAutoATLDoc непосредственно, без обёртки CComObject<>. Отсюда ошибки "cannot instantiate abstract class".

    Но, подождите! Эти ошибки показывают возможное решение нашей дилеммы, как правильно создать и инициализировать наш ATL COM объект. MFC объекты производные от CObject обычно создаются вызовом CObject::CreateObject(). Метод CreateObject() виртуальный и может быть перекрыт так, чтобы программист мог контролировать процесс создания объекта. Например, Вы можете перекрыть CreateObject(), если Вы хотите разместить объект в альтернативной куче. Мы можем перекрыть CreateObject(), чтобы правильно создать и инициализировать ATL COM объект.

  17. Простым способом это сделать будет создание и использование новых макросов вместо DECLARE_DYNCREATE и IMPLEMENT_DYNCREATE. Добавьте следующие макросы в файл stdafx.h:

    #define DECLARE_DYNCREATE_ATL(class_name) \
        DECLARE_DYNCREATE(class_name)
    
    #define IMPLEMENT_DYNCREATE_ATL(class_name, base_class_name) \
        CObject* PASCAL class_name::CreateObject() \
            { CComObject<class_name>* pObj = NULL; \
              HRESULT hr = CComObject<class_name>::CreateInstance(&pObj); \
              if (FAILED(hr)) { AfxThrowOleException(hr); } \
              pObj->InternalAddRef(); \
              return pObj; } \
        IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, 0xFFFF, \
            class_name::CreateObject)
    
  18. Замените макросы DECLARE_DYNCREATE и IMPLEMENT_DYNCREATE в файлах AutoATLDoc.h и AutoATLDoc.cpp на их новые аналоги. Теперь проект должен компилироваться без ошибок.

    То, что мы сделали довольно круто, действительно. Когда MFC нужно будет создать новый документ, будет использоваться наш метод CreateObject(), который правильно создаст и проинициализирует ATL COM часть объекта. Заметьте, что метод CreateObject() увеличивает счётчик ссылок объекта - это важно, так как MFC не знает, что объект удалит себя сам, когда счётчик ссылок достигнет нуля. Правда, это приводит к другому расхождению относительно времени жизни объекта. Но, мы это рассмотрим позже.

    Несмотря на то, что у нас теперь есть внешне создаваемый объект Document, возможно клиентское приложение хотело бы иметь доступ к уже существующему объекту Document. Давайте добавим эту возможность, реализовав свойство ActiveDocument объекта Application.

  19. Так же, как мы добавляли метод Beep() на шаге 14, добавьте свойство ActiveDocument к объекту Application. На этот раз выберите "Add Property", задайте "IDocument*" в качестве типа свойства и "ActiveDocument" в качестве имени. Снимите отметку возле "Put Function", так как это будет свойство read-only. Дважды кликните на методе "get_ActiveDocument" и внесите изменения в заготовку функции как показано ниже:

    STDMETHODIMP CAutoATLApp::get_ActiveDocument(IDocument **pVal)
    {
        METHOD_PROLOGUE_ATL
    
        CMDIChildWnd* pChild = ((CMDIFrameWnd*)m_pMainWnd)->MDIGetActive();
        if (!pChild) 
            return E_FAIL;
        
        ASSERT_KINDOF(CMDIChildWnd, pChild);
        CComObject<CAutoATLDoc>* pDoc = 
            dynamic_cast<CComObject<CAutoATLDoc>*>(pChild->GetActiveDocument());
        return pDoc->QueryInterface(IID_IDocument, reinterpret_cast<void**>(pVal));
    }
    

    Теперь выполним ещё два вспомогательных действия:

    import "oaidl.idl";
    import "ocidl.idl";
    
        interface IDocument;
    
  20. Для того чтобы мы могли протестировать эти новые функции и увидеть результат, давайте добавим метод в объект Document. Мы могли бы опять добавить "Beep", но лучше, наверное, добавить Message Box:

    STDMETHODIMP CAutoATLDoc::Hello()
    {
        METHOD_PROLOGUE_ATL
    
        ::MessageBox(NULL, GetTitle(), "Hello World", MB_OK | MB_ICONEXCLAMATION);
    
        return S_OK;
    }
    
  21. Чтобы вызвать этот метод, Вам нужно модифицировать Вашего клиента. Этот VB код прекрасно работает:

    Set a = New AutoATLLib.Application
    a.ActiveDocument.Hello
    

    Чтобы этот код работал, приложение должно выполнятся и документ должен быть открыт. В противном случае свойство ActiveDocument не будет работать, так как не существует активного документа!

И последний оставшийся вопрос - как должным образом гарантировать, что объект проживает свою жизнь соответствующей протяженности. COM объекты обычно разрушаются самостоятельно, когда счётчик ссылок достигает нуля. Но наш гибридный MFC/ATL объект не следует всем правилам COM. Например, MFC свободно манипулирует объектом и может разрушить его, используя указатель, полученный при создании объекта. В нашем случае автоматизации класса CDocument, мы можем вздохнуть немного легче, так как существует только одно место, где MFC постоянно удаляет CDocument - это метод OnCloseDocument() класса CDocument. (Если я не прав в этом утверждении, я уверен, кто-нибудь даст мне знать!) Так как мы можем перекрыть OnCloseDocument(), мы имеем возможность держать объект живым в случае, если есть внешние ссылки, даже если MFC приняла решение, что документ должен быть ликвидирован. Другая возможность - установка переменной m_bAutoDelete класса CDocument в FALSE. Эта недокументированная переменная обычно используется, чтобы держать документ неподалеку, даже если все его представления были закрыты.

Конечно, если Вы решите, что OnCloseDocument() вызывается слишком поздно в процессе уничтожения объекта, Вы можете перекрыть метод CanCloseFrame(). CanCloseFrame() вызывается, когда пользователь пытается интерактивно закрыть документ. Третьим вариантом может быть перегрузка оператора 'delete' класса.

Проблема, которую пытается разрешить вся эта дискуссия, в том, что пользователь приложения или сама MFC могут решить удалить объект в то время когда ещё остались внешние ссылки на объект Document через автоматизацию. Это потому, что объект не имеет полных полномочий принимать решение когда он будет удалён. Вы получите подобный результат, если попытаетесь автоматизировать CView таким же способом.

Довольно интересно, что этот результат тоже будет получен при использовании старого доброго MFC для автоматизации приложений. Я решил эту проблему в нашем текущем приложении путём создания COM прокси-объектов, которые имеют продолжительность жизни отдельную от MDI объектов, которые они представляют. Это решение работает, но оно более трудное в сопровождении, чем набор MDI объектов, которые предоставляют свой собственный COM интерфейс.

Пара идей, что ещё можно было бы сделать в этом примере:

Я не собираюсь рано убегать с этой вечеринки, но, я думаю, я дал Вам достаточно, чтобы возбудить Ваш аппетит. Мне было бы интересно услышать Ваше мнение и Ваш взгляд на этот способ.

Пожалуйста, шлите мне письма на следующий адрес:

nhodapp@nmt.com

Материал опубликован с разрешения автора.
    Сообщений 0    Оценка 156        Оценить