Продолжается подписка на наши издания! Вы не забыли подписаться?

Волшебство ActiveX : Пример ActiveX Control и DCOM, использующих ATL

Май, 1997

Написано и распространено Стивом Робинсоном и Алексом Красильщиковым

Panther Software

Часть вторая: Создание клиентского приложения ThePusher (Pusher)

Первым клиентским приложением, которое мы создадим, будет ThePusher . Напомним , что в нашем серверном приложении есть функции под названием HelloWorld и AcceptNewValue. Приложение ThePusher - простое диалоговое приложение, задача которого состоит в обеспечении связи и передаче данных с приложением TheServer. При помощи ThePusher вы убедитесь, насколько просто создавать клиентские COM приложения, а порой даже и DCOM серверы, и вызывать функции в COM интерфейсах, поскольку все описанное имеет много общего.

Давайте начнем с создания нового приложения с помощью Visual C++ . Для удобства создадим ThePusher в папке, находящейся рядом с приложением TheServer. Начните новый проект и заполните диалоговое окно New Project Workspace следующим образом:

Щелкните кнопку Create и в Step 1 (первом шаге) установите "dialog-based application."(диалоговое приложение).

Щелкните Next, чтобы перейти к Step 2 (второму шагу) и отметьте OLE Automation checkbox. Выбор OLE automation добавляет в функцию CThePusherApp::InitInstance следующий код:

   //Инициализация OLE библиотек
   if (!AfxOleInit())
   {
      AfxMessageBox(IDP_OLE_INIT_FAILED);
      return FALSE;
   }

AfxOleInit предоставляет весь требуемый начальный OLE код, а также включает файлы <afxdisp.h> и stdafx.h (которые содержат все нужные OLE файлы).

Наше приложение должно работать с MFC через динамически подключаемую библиотеку, что предварительно устанавливается AppWizard Step 3 (третьим шагом). Предустановленные имена, выбранные AppWizard Step 4 (четвертым шагом), полностью подходят для нашего примера. В связи с этим, мы не будем менять больше никаких установок, и после завершения AppWizard Step 4 (четвертого шага) щелкнем кнопку Finish.

После того, как вы щелкните кнопку Finish, выберите Build | Rebuild All, чтобы быть уверенными, что все сделано правильно.

Результатом запуска исполняемого файла будет простое диалоговое окно, которое выглядит следующим образом:

Первым пунктом при создании приложения ThePusher, будет добавление необходимых переменных и элементов пользовательского интерфейса, чтобы при соединении с TheServer'ом мы могли посылать и получать данные. Интерфейс TheServer'а - ItheServerComObject - содержит метод под названием AcceptNewValue. Этот метод использует длинное значение - lNewValue и указатель на длинное значение lplFormerValue. Так, как все COM методы возвращают HRESULTs, то в действительности в качестве возвращаемого значения мы используем lpFormerValue. Если вы откроете файл TheServer.idl (расположенный в ..\TheServer\), вы начнете понимать почему мы инициализировали значение объявлением [out]. Давайте воспользуемся ClassWizard для создания в клиентском приложении ThePusher пары переменных, соответствующих аналогичным значениям в TheServer .

Начните с выбора закладки ResourceView в окне Project Workspace. Откройте папку диалога и дважды щелкните ресурс IDD_THEPUSHER_DIALOG, чтобы отредактировать его. Добавьте кнопку и edit control (элемент редактирования), как показано ниже на рисунке.

Если ваш диалог выглядит так же, как изображенный выше, запустите ClassWizard и щелкните закладку the Member Variables. Дважды щелкните IDC_EDIT1 (если это тот ID ресурса, который вы присвоили элементу редактирования в вашем диалоговом окне) и введите имя переменной "m_lValueToPush".

Установите значение поля Category в Value (не control), а значение в Variable Type на long (длинное). Щелкните кнопку OK, чтобы вернуться в диалоговое окно ClassWizard. Чтобы быть последовательными при обращении с величинами, которые мы добавим в приложение TheServer, введите 0 в качестве минимального значения и 2 в качестве максимального.

Щелкните закладку Message Maps и выберите идентификатор (control ID), который соответствует кнопке Push Value. В нашем примере - это IDC_BUTTON1. Дважды щелкните пункт BN_CLICKED в списке Messages и измените название функции на OnPushValue. Дважды щелкните OK, чтобы выйти из ClassWizard. Перекомпилируйте и запустите приложение, чтобы убедиться, что все установки сделаны правильно. Если вы все сделали правильно, ваше приложение должно выглядеть точно также, как и диалоговое окно, которое вы создали в редакторе ресурсов.

Перед тем, как мы обеспечим соединение с TheServer, давайте добавим другую переменную, содержащую предшествующее значение, переданное серверу. В файле ThePusherDlg.h добавьте переменную типа long под названием m_lFormerValue, и коли уж мы учимся безопасному программированию, убедитесь в том, что проинициализировали ее в конструкторе значением - 0. Теперь мы готовы создать соединение с TheServer.

Откройте файл stdafx.cpp и добавьте строчку #include <atlimpl.cpp> сразу после #include "stdafx.h". В atlimpl.cpp реализована большая часть ATL COM библиотеки. К этому моменту становится ясно, что Microsoft сделала большую часть работы за вас. Откройте файл stdafx.h и в нижней части файла добавьте следующие строчки:

//файлы объявления для базовых классов ATL
#include <atlbase.h>
//внешняя ссылка на CComModule
extern CComModule _Module;
//Подключение ActiveX Template Library
#include <atlcom.h>
//файл объявления idl для сервера
#include "..\TheServer\TheServer.h"

Поскольку вы, скорее всего, уже видели заголовочные (header) файлы раньше, то единственной незнакомой строчкой является CСomModule. CСomModule реализует модуль COM сервера, тем самым позволяя клиенту получать доступ к компонентам сервера. CСomModule поддерживает как серверы внутри процесса (.DLL файлы), так и серверы вне процесса (.EXE файлы), независимо от того, локальные они или удаленные.

Если вы программируете в Windows NT 4.0 или более поздней версии и инсталлировали Visual C++ Patch 4.2b, убедитесь в том, что вы добавили строчку #define _WIN32_WINNT 0x0400 в верхней части файла stdafx.h. Ваш stdafx.h файл должен выглядеть также, как и данный в примере.

Откройте файл ThePusher.cpp, реализующий класс нашего приложения. Добавьте следующие строчки сразу после определений _DEBUG:

#define IID_DEFINED
#include "..\TheServer\TheServer_i.c"
CComModule _Module;

Начало файла ThePusher.cpp должно выглядеть так:

#include "stdafx.h"
#include "ThePusher.h"
#include "ThePusherDlg.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
#define IID_DEFINED
#include "..\TheServer\TheServer_i.c"
CComModule _Module;

TheServer_i.c определяет IID и константы, которые используются для доступа к нашему интерфейсу. Если вы обратите внимание на IID, то обнаружите, что он имеет тот же формат, что и его аналог из реестра. На самом деле, CLSID определен как тип IID, который вы можете видеть в реестре. Если вы когда-либо задумывались, что представляет из себя формат реестра, то ответ вы найдете здесь! К примеру, {0x5E603BF3,0x9823,0x11D0,{0xA4,0xF8,0x00,0x00,0xB4,0x53,0x3E,0xC9}} – это IID нашего ItheServerComObject интерфейса. Это выражение состоит из длинного (long) значения, после которого идут два коротких (short) значения и массив символов. Если вы хотите, то можете ввести первое длинное значение или два коротких в калькулятор и пересчитать их в десятичный формат, чтобы убедиться в том, что они соответствуют действительным значениям.

К этому моменту вы должны увидеть новые файлы в окне project workspace. Среди них: atlbase.h, atlcom.h, atlimpl.cpp, TheServer.h, and TheServer_i.c. Ваше окно Project Workspace должно выглядеть теперь следующим образом:

Перекомпилируйте свой проект и убедитесь, что все в порядке. Теперь добавим в TheServer код для соединения с COM объектом.

Прежде всего мы должны создать переменную, содержащую указатель на интерфейс в COM объекте TheServer. Откройте файл ThePusherDlg.h и добавьте следующую строчку:

CComPtr<ITheServerComObject> m_pITheServerObject;

CComPtr является шаблонным классом (тип: параметризированный) для управления указателями COM интерфейса. CcomPtr автоматически производит подсчет ссылок и использует перегрузку операторов для контроля над соответствующими операциями, тем самым освобождая вас от лишней работы. Мы используем ItheServerComObject в качестве шаблонного типа потому, что именно так называется интерфейс, который будет создан. ItheServerComObject объявляется в TheServer.h, который, в свою очередь, содержится в stdafx.h, поэтому ни в этом файле, ни в файле реализации ThePusherDlg.cpp не требуется добавления других файлов или предварительных объявлений классов. Откройте ThePusherDlg.cpp и, коли уж мы учимся безопасной разработке программного обеспечения, в конструкторе проинициализируйте переменную, которую мы только что добавили к файлу объявления класса значением NULL. Ваш конструктор должен выглядеть так:

CThePusherDlg::CThePusherDlg(CWnd* pParent /*=NULL*/)
   : CDialog(CThePusherDlg::IDD, pParent)
{
   //{{AFX_DATA_INIT(CThePusherDlg)
   m_lValueToPush = 0;
   //}}AFX_DATA_INIT
   //Заметьте, что LoadIcon не требует результата  DestroyIcon in Win32
   m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
   m_lFormerValue = 0;
   m_pITheServerObject = NULL;
}

В функции CThePusherDlg::OnInitDialog, мы проинициализируем наш указатель на интерфейс. В начале функции, после вызова CDialog::OnInitDialog() добавьте следующие строчки:

//вызовите всем известный CoCreateInstance…
HRESULT hRes = ::CoCreateInstance(CLSID_TheServerComObject, NULL, CLSCTX_ALL,
                       IID_ITheServerComObject, (void**)&m_pITheServerObject);
_ASSERT(hRes == S_OK);
_ASSERTE(m_pITheServerObject != NULL);
,

CoCreateInstance является Win32 функцией, которая создает не проинициализированный объект определенного типа. Она содержит две основные функции – CoGetClassObject, которая инициализирует фабрику класса (IClassFactory), и CreateInstance метод фабрики класса. Если вы хотите создать только одну реализацию вашего интерфейса, воспользуйтесь CoCreateInstance или CoCreateInstanceEx, которая является старшим братом CoCreateInstance. Если вам надо создать несколько интерфейсов определенного типа, воспользуйтесь CoGetClassObject и CreateInstance. Параметрами CoCreateInstance являются:

Если CoCreateInstance создается успешно, она возвращает S_OK и устанавливает наш указатель. Если что-то не так, то она возвращает что-нибудь отличное от S_OK (большая часть значений ошибок определена в winerror.h). К тому же, в этом случае CoCreateInstance устанавливает наш указатель на член интерфейса в NULL. Если вы не выбрали OLE Automation в AppWizard, когда создавали проект, возвращаемое значение HRESULT будет 800401F0L, что соответствует CO_E_NOTINITIALIZED. Это означает, что OLE библиотеки не были загружены. Это довольно распространенный случай, если вы быстро проноситесь по примерам.

Для того, чтобы удостовериться, что наш интерфейс существует и правильно работает, давайте немедленно добавим две строчки сразу после наших заявленных функций.

hRes = m_pITheServerObject->HelloWorld();
VERIFY(SUCCEEDED(hRes));

Вызовем метод HelloWorld напрямую из указателя на наш интерфейс. В этом примере показано, насколько просто вызывать методы из указателей на интерфейсы. Так как HelloWorld не делает ничего, кроме возвращения значения S_OK, у нас появляется возможность воспользоваться макросом SUCCEEDED. Этот макрос определяется в winerrors.h следующим образом:

#define SUCCEEDED(Status) ((HRESULT)(Status) >= 0)

Всегда пользуйтесь макросом SUCCEEDED.

Теперь мы готовы к серьезной работе с нашим интерфейсом. В методе OnPushValue введите следующий код:

UpdateData();
ASSERT(m_pITheServerObject != NULL);
if(m_pITheServerObject != NULL)
{
   HRESULT hRes;
   hRes = m_pITheServerObject->AcceptNewValue(m_lValueToPush, &m_lFormerValue);
   if(SUCCEEDED(hRes))
   {
      TRACE(“Текущее значение:         %d\n”,m_lValueToPush);
      TRACE(“Предыдущее значение было: %d\n”,m_lFormerValue);
   }
   else
   {
      ASSERT(FALSE);
   }
}

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

Часть 3: Добавление Точки Соединения к Серверу

ThePusher – первое созданное нами DCOM клиентское приложение. При его создании вы узнали, насколько просто производить соединение с сервером вне процесса и передавать в него (получать от него) данные. Следующим шагом является получение данных от сервера и передача их в другое клиентское приложение. В среде Windows-разработчиков всегда было принято, что если у вас имеется несколько окон просмотра одного документа, то одно из окон передает данные в документ и посылает сообщение остальным окнам о том, что им необходимо получить новые данные из документа. Несколько привлекательнее вариант, когда документ является более “умным” и автоматически посылает другим окнам просмотра данные при условии, что они нуждается в обновлении. Этот вариант мы и будем использовать с нашим DCOM сервером. В те моменты, когда TheServer получает новые данные, мы будем автоматически передавать информацию подключенным клиентам. Однако, Windows сообщения не работают через COM. Вы не можете так просто вызвать старую добрую API функцию ::SendMessage. Тем не менее, существует прекрасное решение этой проблемы для COM и DCOM, под названием – точки соединения.

Использование точек соединения является одной из наиболее мощных возможностей COM и DCOM . В сущности, их использование заложено в основе одного из наиболее популярных COM объектов – элементе управления ActiveX. В терминологии COM, точки соединения являются механизмом, при помощи которого устанавливается обратное соединение между исходным объектом и внешним (объектом, с которым устанавливается соединение).

Следующим шагом мы возьмем указатель на интерфейс из нашего клиента и передадим его серверу. Интерфейс нашего клиента будет иметь определенные методы. В связи с этим, когда мы передадим указатель на клиентский интерфейс серверу, сервер получит возможность вызывать методы клиента. Выражаясь языком COM, мы собираемся создать IConnectionPoint и поместить его в IСonnectionPointContainer. Это очень напоминает функции обратного вызова (callback) в С. Каждый IConnectionPoint в сервере является оберткой к соответственному интерфейсу клиента. Интерфейс клиента, предназначенный для обратного вызова, называется - sink. sink должен быть реализован каждым клиентом, который предполагает получение данных от сервера через этот интерфейс. Давайте посмотрим на IConnectionPoint и IСonnectionPointContainer.

Интерфейс IСonnectionPointContainer реализован в сервере. Его наличие говорит о существовании выходных (обратно вызываемых) интерфейсов. Он предоставляет доступ к точкам подключения обратно вызываемых интерфейсов, называемых IConnectionPoint. sink (обратно вызываемый) интерфейс соответствующего IConnectionPoint объявляется в сервере, но в действительности реализуется в клиенте. Клиентское приложение выполняет QueryInterface с IID сервера для получения Iunknown интерфейса сервера (строка 03). Как только клиент получает интерфейс сервера, он запрашивает у сервера интерфейс IсonnectionPointContainer (строка 07). Следующим шагом клиент ищет требуемый интерфейс IСonnectionPoint (строка 11), и если такой интерфейс обнаружен, использует его для вызова метода Advise. Получив в качестве параметра sink-интерфейс, Advise производит подключение этого интерфейса (строка 17). Основная хитрость заключается в том, что сервер знает описание подключаемого интерфейса, так как оно объявлено в сервере. Это дает серверу возможность выполнять вызовы методов подключенного объекта (это и есть обратный вызов, ведь реализация подключенного объекта находится в клиенте). (Примечание редакции: Вторая хитрость, которую обычно недооценивают люди, объясняющие этот материал, заключается в том что Advise не запоминает переданный ему указателя на интерфейс. Он использует его для получения (с помощью QueryInterface) указателя на интерфейс с IID, который поддерживается данным IСonnectionPoint. Собственно в качестве параметра (строка 17 - pSinkAsUnknown) в Advise передается не указатель на sink интерфейс, а указатель на Iunknown объекта, где реализован sink интерфейс. И уже в глубинах самого Advise происходит создание и подключение sink интерфейса.). Реализация точек соединения без ATL представляется достаточно простой, и выглядит приблизительно следующим образом:

/*01*/IUnknown* pUnknown != NULL;
/*02*/ISomeObject* pISomeObject;
/*03*/SCODE sc = pUnknown->QueryInterface(IID_ISomeObject, (void**)&pISomeObject);
/*04*/if (SUCCEEDED(sc) && pISomeObject != NULL)
/*05*/{
/*06*/   IConnectionPointContainer* pIConnectionPointContainer = NULL;
/*07*/   sc = pISomeObject->QueryInterface(IID_IConnectionPointContainer, 
/*08*/                                         (void**)&pIConnectionPointContainer);
/*09*/   if (SUCCEEDED(sc) && pIConnectionPointContainer != NULL)
/*10*/   {     
/*11*/      sc = pIConnectionPointContainer->FindConnectionPoint(
/*12*/         IID_ISomeConnectionSink, &m_pIConnectionPoint);
/*13*/      pIConnectionPointContainer->Release();
/*14*/      if (SUCCEEDED(sc) && pIConnectionPoint != NULL);
/*15*/      {
/*16*/         IUnknown* pSinkAsUnknown = (IUnknown*)* m_pIOurSink;
/*17*/         sc = m_pIConnectionPoint->Advise(pSinkAsUnknown,
/*18*/                                      &m_dwConnectionCookie);
/*19*/         if (SUCCEEDED(sc))
/*20*/            return TRUE;
/*21*/      }
/*22*/   }
/*23*/}
/*24*/return FALSE;

Когда клиенту надо отключиться, он вызывает метод UnAdvise, антипод Advise. Если вам нужно написать функцию UnAdvise без ATL, то она выглядит приблизительно так:

m_pIConnectionPoint->Unadvise(m_dwConnectionCookie);
m_pIConnectionPoint->Release();

ATL реализует точки соединения аналогичным образом. Однако, в Microsoft все облекли в удобную оболочку. Так как ATL COM объекты генерируются из IDL файла, то код маршалинга создается за вас. Метод ATLAdvise реализуется следующим образом:

HRESULT hRes = AtlAdvise(pISomeObjectWithConnectionPointContainer, 
            pUnknownOfOurSinkObject,  //получает Iunknown sink обекта
            IID_IOfSink,              //id  sink интерфейса
            &dwConnectionCookie);     /*значение содержащееся в dwConnectionCookie  
                                        после вызова AtlAdvise понадобится вам
                                        когда вы захотите отключиться 
                                      */

AtlUnadvise, который схож с IConnectionPoint::Unadvise, работает следующим образом:

HRESULT hRes = AtlUnadvise(pISomeObjectWithConnectionPointContainer, 
                  IID_IOfSink, 
                  &dwConnectionCookie);

Теперь мы готовы реализовать IСonnectionPointContainer в COM объекте TheServer, и объявить IСonnectionPoint интерфейс в TheServer IDL файле. Откройте проект TheServer, а также файл TheServerComObject.h. Добавьте в свой базовый класс следующие строчки:

public IConnectionPointImpl<CTheServerComObject, &IID_IMySinkID>,

чтобы ваше объявление класса выглядело примерно так:

class /* ATL_NO_VTABLE */ CTheServerComObject : 
   public CComObjectRootEx<CComObjectThreadModel>,
   public CComCoClass<CTheServerComObject, &CLSID_TheServerComObject>,
   public ISupportErrorInfo,
   public IConnectionPointContainerImpl<CTheServerComObject>,
       public IConnectionPointImpl<CTheServerComObject, &IID_IMySinkID>,
public IDispatchImpl<ITheServerComObject, &IID_ITheServerComObject, &LIBID_THESERVERLib>
{

IConnectionPointContainerImpl реализует контейнер точек соединения и управляет списком IConnectionPointImpl. Мы предположили, что Interface ID (IID) выходного интерфейса будет IID_ImySinkID, который в данный момент передан шаблонному классу IСonnectionPointImpl.

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

BEGIN_CONNECTION_POINT_MAP(CTheServerComObject)
    CONNECTION_POINT_ENTRY(IID_IMySinkID)
END_CONNECTION_POINT_MAP()

Входы точек соединения в карте используются IСonnectionPointContainerImpl. Класс, содержащий карту точек соединения, должен наследоваться от IСonnectionPointContainerImpl .

Теперь все готово для объявления в нашем IDL файле sink интерфейса. Сразу после объявления ItheServerComObject добавьте следующий код:

//sink интерфейс
[
  object,
  //используйте guidgen для генерации уникального ID
  uuid(869703A1-9824-11d0-A4F8-0000B4533EC9),
  dual,
  helpstring("MySink Interface"),
  pointer_default(unique)
]
interface IMySinkID : IDispatch
{
};

Как было упомянуто в комментарии, мы использовали guidgen.exe, который, в свою очередь, поставляется с Visual C++, для генерирования ID. Guidgen.exe очень прост в использовании. Он расположен в \msdev\bin .

Вам также придется добавить информацию к CTheServerComObject coclass объявлению, в нижней части определения библиотеки, следующим образом:

// эта строчка должна располагаться непосредственно за – “[default] interface ITheServerComObj;”

[default, source] interface IMySinkID;

Как только это будет сделано, щелкните Rebuild | All, и проект должен скомпилироваться, скомпоноваться и зарегистрировать ваш объект вместе с его интерфейсами.

Давайте добавим метод, который будет передавать данные в sink интерфейсы, поддерживаемые в контейнере точек соединения, в класс нашего COM объекта. Этот метод надо описать в файле описания классов и реализовать в TheServerComObject.cpp следующим образом:(Комментарии объясняют, что происходит в момент вызова метода.)

// Этот метод вызывается три смене значения m_lCurrentValue
// его задача вызвать метод SinkReceiveData всех подключенных sink интерфейсов
void CTheServerComObject::PushDataIntoSink(long lNewValue)
{
    HRESULT hr = S_OK;
    Lock(); //заблокировать владение критическим участка
//указатель на обратновызываемый интерфейс
    IMySinkID* pSink = NULL;

    //m_vec  принадлежит к типу CComDynamicUnkArray, который содержит
    //динамический массив точек соединения
    //в виде IUnknown
    IUnknown** pp = m_vec.begin();
    while(pp < m_vec.end() && hr == S_OK)
    {
        pSink = (IMySinkID*) *pp;
        hr = S_FALSE;
        if(pSink != NULL)
        {
            //вызов sink функции
            hr = pSink->SinkReceiveData(lNewValue);
            _ASSERTE(SUCCEEDED(hr));
            pp++;
        }
    }
    Unlock();//освобождение критического участка
}

Метод SinkReceiveData вызывается из CtheServerComObject для sink объекта, который в действительности создается в клиенте. Так как CTheServerComObject нуждается в сведениях об этой функции для компиляции, то мы должны объявить sink интерфейс в IDL файле проекта TheServer. После объявления в IDL файле sink интерфейса, компилятор MIDL получает возможность сгенерировать его в TheServer.h, который добавляется в верхней части TheServerComObject.cpp. Поэтому реализация CTheServerComObject будет иметь информацию о методе SinkReceiveData и, в связи с этим, спокойно компилироваться.

Sink интерфейс теперь должен выглядеть следующим образом:

//sink интерфейс
[
   object,
   //используйте guidgen для генерации уникального id
   uuid(869703A1-9824-11d0-A4F8-0000B4533EC9),
   dual,
   helpstring("MySink Interface"),
   pointer_default(unique)
]
interface IMySinkID : IDispatch
{
   HRESULT SinkReceiveData([in] long lValue);
};

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

Перед тем, как изменить AcceptNewValue, мы добавим некоторую дополнительную логику к приложению TheServer. добавим массив к CcomModule, чтобы получить возможность перебирать все наши sink интерфейсы в момент передачи данных подсоединенным выходным интерфейсам, подобно тому, как документ перебирает свои окна просмотра.

Для этого откройте файл stdafx.h, и внесите следующие строчки в верхней части файла. помните, что сейчас вы добавляете MFC файл, и вам нужно изменить опции компиляции (Build Settings), чтобы состыковаться с MFC. Их добавление даст более полную поддержку MFC (эти строчки были скопированы прямо из стандартного, сгенерированного AppWizard файла stdafx.h):

//добавьте стандартные MFC компоненты
#define _MT // for _beginthreadex and _endthreadex
#include <afxwin.h>             // MFC ядро и стандартные компоненты
#include <afxext.h>             // MFC расширения
#include <afxdisp.h>            // MFC классы OLE автоматизации
#ifndef _AFX_NO_AFXCMN_SUPPORT
#include <afxcmn.h>             // MFC поддержка для Windows Common Controls
#endif                          // _AFX_NO_AFXCMN_SUPPORT

В классе CExeModule, объявленном в этом файле, добавьте следующую строчку, содержащую переменную m_PtrArray:

CPtrArray m_PtrArray;

Добавьте в Сonstructor строчку _Module.m_PtrArray.Add(this); для того, чтобы вновь создаваемые клиентские объекты добавлялись в массив. (Заметьте, что в реальной ситуации вы предпочли бы, чтобы клиент сам добавлял и уничтожал себя из массива сервера).

CTheServerComObject::CTheServerComObject()
{
    m_pUnkMarshaler = NULL;
    m_lCurrentValue = 0;
    _Module.m_PtrArray.Add(this);
}

После добавления метода для поддержки массива, вы уже можете представить себе требования функции AcceptNewValue:

Функция AcceptNewValue :

STDMETHODIMP CTheServerComObject::AcceptNewValue
(
    long lNewValue,             //in  новое значение
    long FAR* lpFormerValue     //out предыдущее значение
)
{
    if(lNewValue >= 0 && lNewValue <= 2)
    {
        CTheServerComObject* pObj = NULL;
        int iSize = _Module.m_PtrArray.Add(this);
        for(int iIndex = 0; iIndex < iSize; iIndex++)
        {
            pObj = (CTheServerComObject*) _Module.m_PtrArray.GetAt(iIndex);
            if(pObj != NULL)
            {
                pObj->PushDataIntoSink(lNewValue);
            }
        }
        //теперь обновим наши переменные 
        *lpFormerValue = m_lCurrentValue;
        m_lCurrentValue = lNewValue;
        return S_OK;
    }
    return S_FALSE; //возвращение S_FALSE для значения недопустимо!
}

Перекомпилируйте проект. Теперь все готово для реализации sink объекта в ActiveX control клиенте.

Часть 4: Разработка ActiveX Control

Как любой хороший рассказчик, мы припасли самое лучшее напоследок, что в данном случае оправдано. Наш следующий шаг будет наиболее интересным, и мы увидим плоды нашего труда. Мы вложим встроенный интерфейс в компонент ActiveX (договоримся наш компонент называть TheControl), разместим TheControl в Internet Explorer и дадим ему автоматически подключиться к TheServer. Когда ThePusher засылает новое значение в TheServer, TheControl будет менять цвета прямо на ваших глазах. Но давайте не будем затягивать и приступим.

В предыдущем разделе, объекте в TheServer мы объявили интерфейс IConnectionPoint и реализовали IConnectionPointContainer. Итак, давайте вспомним: IConnectionPointContainer объявлен и реализован в TheServer. IConnectionPoint объявлен в IDL файле сервера, так что IConnectionPointContainer сервера имеет представление о типе точек соединения, которые он может содержать. Однако в действительности интерфейс обратной связи , оболочкой для которого служит IconnectionPoint, реализуется клиентом. Чтобы было понятнее: IConnectionPointContainer имеет возможность сохранять указатель на внешнем объекте. В терминологии СОМ это называется Sink (обратная связь). TheControl будет являться компонентом ActiveX с sink (обратной связью).

Создайте в Visual C++ New Project Workspace. В списке Type выберите OLE ControlWizard и назовите проект именем TheControl. Перед нажатием на кнопку Create просмотрите диалоговое окно, чтобы удостовериться в правильности выбранных установок. Как и в предыдущем случае, размещаем первичную папку этого проекта за первичной папкой TheServer, чтобы облегчить ссылку на используемые файлы. Теперь нажмите на Create и перейдите к 1 шагу в OLE ControlWizard.

  1. В 1 шаге мы будем использовать значения AppWizard, поэтому продолжим и нажмем на кнопку Next.
  2. Значения 2 шага по умолчанию также подходят этому примеру, поэтому во 2 шаге нажмем на кнопку Finish. Если хотите, можно просмотреть созданные имена, но ничего изменять не следует.
  3. Когда AppWizard сгенерирует все коды, выберите Rebuild | All , чтобы удостовериться в корректности всех построений. После линковки TheControl должна произойти автоматическая регистрация.

Первое, что мы должны сделать с TheControl - это добавить необходимые файлы. Как и проект ThePusher, проект TheControl нуждается во включении объявления ATL и ряда объявлений с TheServer.

Откройте stdafx.h и добавьте следующие строки:

// заголовочные файлы для базовых классов ATL
#include <atlbase.h>
//внешнее объявление CComModule
extern CComModule _Module;
//заголовочный файл ActiveX Template Library (ATL)
#include <atlcom.h>
// Заголовочный файл для idl TheServer
#include "..\TheServer\TheServer.h"

Аналогично ThePusher, TheControl будет иметь экземпляр CComModule, как внешнюю связь. Вызов CComModule реализует модуль СОМ-сервера, позволяющего клиенту получить доступ к компонентам сервера.CComModule поддерживает как серверы, размещенные в процессах (.DLL-файлы), так и серверы вне процессов (.EXE-файлы) причем и на локальных, и на удаленных серверах.

Если вы программируете в WindowsNT версии 4.0 или выше, и у вас установлен VisualC++ Patch 4.2b, не забудьте добавить строку #define_WIN32_WINNT 0x0400 в начале stdafx.h. Ваш файл stdafx.h должен быть похож на файл stdafx.h, который приходит с кодом примера.

Откройте stdafx.cpp и добавьте строку #include<atlimpl.cpp> сразу же за #include "stdafx.h". Это даст вам возможность использовать большую часть ATL COM библиотеки. После этого откройте TheControl.cpp, и сразу же за определением _DEBUG добавьте следующие строки:

#define IID_DEFINED
//idl implementation file
#include "..\TheServer\TheServer_i.c"
//ваш COM модуль обявленный в stdafx.h
CComModule _Module;

Этот код, как вы видели в ThePusher, определяет IID_DEFINED, и включает в себя CLSIDs, объявленные в TheServer_i.c, который был сгенерирован компилятором MIDL при построении TheServer, и, тем самым, объявляет наш CComModule. Если вы обновите все зависимости, то увидите, что к разделу Dependencies окна Project Workspace добавлены следующие файлы:

 

Пока открыт файл TheControl.cpp, давайте проинициализируем _Module (который объявлен в stdafx.h) вызовом его функции Init. Ваша функция InitInstance должна выглядеть следующим образом:

BOOL CTheControlApp::InitInstance()
{
   BOOL bInit = COleControlModule::InitInstance();

   if (bInit)
   {
      // TODO: Add your own module 
      //initialization code here.
      _Module.Init(NULL, NULL);
   }
   return bInit;
}

Выберите Rebuild | All и удостоверьтесь, что TheControl создан и зарегистрирован корректно.

Следующим пунктом в последовательности наших действий будет создание класса, который будет реализацией интерфейса обратной связи в TheControl. Как говорилось, указатель на интерфейс обратной связи передается в контейнер точек соединения сервера. Сервер, имеющий контейнер точек соединения, может использовать этот исходящий интерфейс подобно callback вызову в С.

Создайте новый файл под названием MySink.h, или скопируйте MySink.h из примера. В любом случае, давайте посмотрим на код в этом новом классе, который показан ниже:

#ifndef MYSINK_H
#define MYSINK_H

//предварительное объявление класса элемента 
//управления
class CTheControlCtrl;

class CMySink :
public IDispatchImpl<IMySinkID, &IID_IMySinkID, &LIBID_THESERVERLib>, public CComObjectRoot
{
  public: 
   BEGIN_COM_MAP(CMySink)
      COM_INTERFACE_ENTRY(IDispatch)
      COM_INTERFACE_ENTRY(IMySinkID)
   END_COM_MAP()

   STDMETHOD(SinkReceiveData)(long lValue);
   void SetOwner(CTheControlCtrl* pTheControl);

  private:
   CTheControlCtrl* m_pTheControl;
};
#endif

Первый пункт, на который вы обратили внимание - это объявление CTheControlCtrl, наш класс компонента ActiveX. Этот класс объявляется здесь, поэтому мы можем использовать его в классе CMySink. член класса, объявленный в конце CMySink являющийся указателем на CTheControlCtrl.

Размещая указатель на CTheControlCtrl, мы можем использовать класс нашего компонента в качестве собственника этого класса после создания объекта CMySink, который является целью функции SetOwner.

CMySink унаследован из двух базовых классов. IDispatchImpl, как было замечено ранее, обеспечивает реализацию Idispatch по умолчанию. CComObjectRoot, также обсуждавшийся ранее, организует управление подсчетом ссылок на объекты для конкретного COM-объекта. Макрос BEGIN_COM_MAP ставит метку на начале массива точек входов интерфейса СОМ и экспортирует интерфейсы в этом СОМ объекте через функцию QueryInterface. В этом массиве есть два входа СОМ интерфейса, один для IDispatch, а второй - для IMySinkID. IMySinkID был объявлен в TheServer.idl для того, чтобы IConnectionPointContainer, принадлежащий TheServer, имел представление о типе обратной связи точки соединения. Определение IID_IMySinkID находится в TheServer_i.c. (Этот файл включен в TheControl.cpp). В этом файле также объявлена функция SinkReceiveData, и сейчас мы реализуем этот метода в TheControl.

Откройте файл TheControlCtl.h и добавьте следующие строки сразу после объявления класса, чтобы начало вашего класса выглядело следующим образом:

class CTheControlCtrl : public COleControl
{
  private:
   //brush для изменения цвета компонента
   CBrush  m_BkgBrush;
   //advise holder
   DWORD m_dwAdvise;
   //указатель на интерфейс сервера
   CComPtr<ITheServerComObject> m_pITheServerObject;
   void ConnectToTheServer();
   void CreateSinkAdvise();
   BOOL UnAdviseSink();
  public:
   //публичный метод для изменения цвета
   void ChangeColor(long lValue);

давайте поближе рассмотрим некоторые объявления из приведенного текста:

Откройте TheControlCtrl.cpp и переместитесь вниз до класса конструктора. Там, где в конструкторе вы увидите традиционное TODO, добавьте следующие строки:

m_dwAdvise = 0;
m_pITheServerObject = NULL;
ConnectToTheServer();
if(m_pITheServerObject != NULL)
   CreateSinkAdvise();

Как всегда, начните с инициализации переменных членов, не пренебрегая рекомендациями по безопасному программированию. После этого следует вызвать ConnectToServer и, если указатель интерфейса на СОМ-объекте TheServer доступен, мы можем создать обратную связь Advise. (Буквально через минуту мы обеспечим выполнение всех этих функций.)

В объявлении класса имеется объект CBrush, который следует проинициализировать. Чтобы добавить функцию члена OnCreate, воспользуйтесь ClassWizard и добавьте следующую строку сразу же после To Do... для создания красного цвета.

// TODO: Add your specialized creation code here
m_BkgBrush.CreateSolidBrush(RGB(192,0,0));

В уже объявленной функции OnDraw замените функцию рисования по умолчанию одной следующей строкой. Вследствие этого при высвечивании первый компонент будет отображен красным цветом.

pdc->FillRect(rcBounds, &m_BkgBrush);

 Теперь давайте поработаем над методами, объявленными в нашем классе. Первый метод, который мы выполним - ConnectToTheServer. ConnectionToTheServer будет использовать CoCreateInstanceEx, побуждая наш m_pTheServerObject сохранять указатель интерфейса на СОМ-объекте в приложении TheServer. CoCreateInstanceEx является старшим братом CoCreateInstance и позволяет последнему динамически устанавливать местоположение удаленного сервера, минуя реестр (CoCreateInstance берет информацию о местоположении сервера из реестра). Например, вы можете запросить пользователя об IP-адресе сервера перед установкой соединения. код, приведенный ниже демонстрирует использование CoCreateInstanceEx с NULL в качестве имени удаленного сервера. В этом случае будет использоваться информация из реестра. Код для ConnectToTheServer приведен ниже с объяснением новых переменных CoCreateInstanceEx:

void CTheControlCtrl::ConnectToTheServer()
{
    USES_CONVERSION;
    //определение структуры QI для того чтобы, можно было бы 
    //получать более одного интерфейса за раз, если необходимо 
    //иметь несколько интерфейсов для одного id класса
    MULTI_QI QI[1];
    //IID первого интерфеса
    QI[0].pIID = &IID_ITheServerComObject;
    QI[0].pItf = NULL;
    QI[0].hr   = S_OK;
    //создание COSERVERINFO struct для серверной машины
    COSERVERINFO si;
    si.dwReserved1 = 0; //не используется
    //pass a url here such as 1.0.0.1 or UNC name
    si.pwszName = T2W(“P200sr”); //имя серверной машины
    si.pAuthInfo = NULL; //не используется
    si.dwReserved2 = 0; //не используется

    HRESULT hRes = S_FALSE;
    hRes = ::CoCreateInstanceEx(
       //class id как в CoCreateInstance
       CLSID_TheServerComObject, 
        //The controlling unknown 
        NULL, 
        //CLSCTX контекст
        CLSCTX_ALL, 
        //CoServerInformation или NULL если локальная машина
        NULL, //&si, 
        //необходимое количество интерфейсов
        1,   //1 интерфейс в нашей структуру MULTI_QI
        //массив в который необходимо вернуть указатели на интерфейсы
        QI);
    //проверяем возвращаемое значение
    ASSERT(hRes == S_OK);
    if(hRes != S_OK)
        return;
    //вынимаем необходимый нам указатель на интерфейс
    m_pITheServerObject = (ITheServerComObject*) QI[0].pItf;
    ASSERT(m_pITheServerObject != NULL);
}

Следующей функцией, выполнение которой мы осуществим, будет CreateSinkAdvise. CreateSinkAdvise вызывается из нашего конструктора после возвращения ConnectToTheServer, если в m_pITheServerObject находится правильное значение. CreateSinkAdvise использует ComObject template class для создания COM класса на базе нашего класса CMySink. Назовем эту переменную pSink, поскольку это будет исходящий sink интерфейс обратной связи в CConnectionPointContainer нашего TheServer. После того, как sink интерфейс создан, его необходимо отправить в TheServer с помощью метода AtlAdvise. Кроме того, после создания обратной связи мы вызовем ее функцию SetOwner, передавая указатель на объект (this) как владельца (owner). Приведем код для метода CreateSinkAdvise и SetOwner, принадлежащего CMySink:

void CTheControlCtrl::CreateSinkAdvise() 
{
   ASSERT(m_pITheServerObject != NULL);

   CComObject<CMySink>* pSink;  //создаем sink
   CComObject<CMySink>::CreateInstance(&pSink);  //производим регистрацию

   ASSERT(pSink != NULL);
   pSink->SetOwner(this);
   HRESULT hRes = AtlAdvise(m_pITheServerObject,   //объект TheServer
                      pSink->GetUnknown(),   //наш sinks Iunknown
                      IID_IMySinkID,         //iid
                      &m_dwAdvise);      //cookie
   if (FAILED(hRes))
   {
       ASSERT(FALSE);
   }
   TRACE(“подключение обратно вызываемого интерфейса прошло успешно\n”);
}

void CMySink::SetOwner(CTheControlCtrl* pTheControl)
{
    m_pTheControl = pTheControl;
}

UnAdviseSink - метод, используемый CTheControlCtrl для разъединения с TheServer, вызывает AtlUnadvise и уменьшает значение счетчика на подключение. Он выглядит следующим образом:

BOOL CTheControlCtrl::UnAdviseSink()
{
   HRESULT hRes = AtlUnadvise(m_pITheServerObject, 
         IID_IMySinkID, 
         m_dwAdvise);

   BOOL bRet = SUCCEEDED(hRes);
   if(!bRet) 
      AfxMessageBox(
         “Произошел сбой при отключение обратно вызываемого интерфейса”);

   return bRet;
 }

В этом примере в качестве времени на отключение TheControl от TheServer является разрушение TheControl (например, вы закрыли Internet Explorer или выгрузили страницу из браузера). Кроме того, мы будем использовать OnDestroy для ряда выбираемых нами действий, подобных удалению нашей кисти (m_BkgBrush). используйте ClassWizard для создания OnDestroy и внесите следующие изменения:

void CTheControlCtrl::OnDestroy() 
{
   UnAdviseSink();
   COleControl::OnDestroy();
   m_BkgBrush.DeleteObject();
}

Мы почти закончили. Вызовите метод, существующий в CMySink, называемый SinkReceiveData. После этого вызовите CTheServerComObject, принадлежащий TheServer и содержащий метод, названный PushDataIntoSink. PushDataIntoSink, в свою очередь, повторно просмотрит все обратные связи в списке, вызывая каждый метод обратной связи SinkReceiveData. Причем pSink из CTheServerComObject, к которому мы обращаемся - это обратная связь, принадлежащая TheControl! В результате, нам не только не нужно описывать его, поскольку он уже объявлен в CMySink, но и задавать этому методу значения, которые он принимает от владельца обратной связи. Рассмотрите следующий код, который вам следует добавить в конец TheControlCtrl.cpp:

STDMETHODIMP CMySink::SinkReceiveData(long lValue)
{   
   if (m_pTheControl != NULL)
   {
      m_pTheControl->ChangeColor(lValue);
      return (HRESULT)S_OK;
   }
   else
      return (HRESULT)S_FALSE;
}

После создания обратной связи в CreateSinkAdvise мы присвоили принадлежащий ему переменной m_pTheControl значение созданной нами копии элемента управления. При вызове SinkReceiveData из TheServer, метод присвоит значение его владельцу, CTheControlCtrl. Все, что нам нужно - это сделать доступной функцию с названием ChangeColor. Это займет много места. Мы уже объявляли ChangeColor в файле описаний CTheControlCtrl.h. Для того, чтобы функция ChangeColor меняла цвет TheControl, опишем ее следующим образом:

void CTheControlCtrl::ChangeColor(long lValue)
{
    COLORREF color;
    switch(lValue)
    {
        case 2:
            color = RGB(192,0,192);
            break;
        case 1:
            color = RGB(192,192,0);
            break;
        default:
            color = RGB(0,192,0);
    }

    m_BkgBrush.DeleteObject();
    m_BkgBrush.CreateSolidBrush(color);
    InvalidateControl();
}

 До компиляции нам нужно добавить #include "MySink.h" в начало файла для того, чтобы ему было известно о нашем объекте обратной связи. Выберите Rebuild|All и удостоверьтесь, что ваш элемент управления зарегистрирован. Теперь мы можем использовать его в Internet Explorer.

Internet Explorer и Ваши Компоненты DCOM в действии

Вы готовы увидеть работу вашего компонента? Запустите Microsoft ActiveX Control Pad. Если вы устанавливали его в каталог по умолчанию, то он лежит в каталоге “C:\Program Files\ActiveX Control Pad\PED.EXE”. В меню выберите Edit | Insert ActiveX Control. Появится диалоговое окно со списком всех компонентов, зарегистрированных на вашем компьютере. Перемещая список в окне, разыщите пункт с названием TheControl Control. Дважды щелкните по этому пункту, чтобы он создал компонент в умолчальной HTML-странице, созданной при запуске. Сохраните файл под именем page1.htm так (например на вашем рабочем столе), чтобы вы могли без затруднений переместить его в Internet Explorer. Запустите Internet Explorer и перетащите только что созданный вами с помощью панели управления ActiveX файл page1.htm в область клиента Internet Explorer. При загрузке компонента он сообщит вам информацию, касающуюся безопасности (что описано во многих других статьях). После того как вы выберете Yes to All, страница должна загрузить TheControl. Вы увидите красный прямоугольник. (Если вы не увидели элемента управления, уменьшите уровень безопасности Internet Explorer до среднего и перезагрузите его.)

Следующим действием будет запуск ThePusher. После запуска ThePusher, установите значения между 0 и 2 и нажмите на кнопку Push Value для того, чтобы послать его в TheServer. Необходимости запускать TheServer нет. Он запустится сам, как только произойдет обращение к CoCreateInstance со стороны TheControl или ThePusher. В среде Internet Explorer элемент управления ActiveX будет менять цвета, как только вы будете менять значение в ThePusher. И именно в этот момент, быстро глянув в зеркало, вы увидите улыбку на своем лице, которая подскажет вам, что вы работаете со своей первой DCOM-программой.


Copyright © 1994-2016 ООО "К-Пресс"