1998 г
COMponents
Написано и
разработано Стивом Робинсоном (Steve
Robinson) и Алексом Красселом (Alex Krassel),
Panther Software
Обзор
В отличие от
других отраслей, разработка
программного обеспечения требует,
чтобы прикладные модули были
скомпилированы и слинкованы с
другими зависимыми частями
приложения. Всякий раз, когда
разработчик хочет использовать в
приложении другую логику или новые
возможности, для того, чтобы эти
изменения вступили в силу, ему или
ей необходимо модифицировать и
перекомпилировать первичное
приложение.
В
производстве такое ограничение
недопустимо. Можете ли вы себе
представить, что вам пришлось бы
переделывать автомобильный
двигатель, если бы вы захотели
заменить ваши шины от изготовителя
на более совершенные? Это могло бы
пролиться золотым дождем на
механиков, но чрезмерные
эксплуатационные издержки
приведут к уменьшению спроса на
автомобили, а от этого пострадают
все: потребители, производители
автомобилей, те же механики.
Фактически, одним из основных
факторов успеха промышленной
революции стала способность
взаимозаменяемости деталей машин,
т.е. использование компонентов.
Сегодня мы не задумываясь заменяем
компоненты и добавляем новые
принадлежности в наши автомобили.
Автомобили
ничего "не знают" о шинах, которые
они используют. Шины имеют свойства
(ширина колеса и пр.). Если свойства
у различных шин совпадают, то эти
шины взаимозаменяемы. Светильник
ничего "не знает" о лампах,
которые в нем используются. Если
параметры ламп (диаметр
завинчивающейся части)
удовлетворяют требованиям
изготовителя осветительного
прибора, то эти лампы
взаимозаменяемы. Давно ли
индустрия программного
обеспечения стала догонять
остальную часть мира и строить
компоненты, которые понятия не
имеют о том, как они будут
использоваться ? Для отрасли,
которая считается передовой, мы
действительно плетемся в хвосте.
На первый
взгляд, динамически подключаемые
библиотеки (DLL) обеспечивают
решение указанных выше проблем.
Следующая выдуманная история
покажет, что это не так.
Предположим,
вам нужно разработать приложение
для компании Acme Gas Tanks. Приложение
будет показывать уровень бензина в
новом престижном топливном баке Acme
на 1000 галлонов. Во-первых, вы
создаете индикатор уровня на
основе ActiveX(tm), который имеет три
отметки: текущий уровень топлива в
баке, минимально возможный
безопасный уровень и максимально
возможный безопасный уровень. Вы
пишете DLL, назвав ее GasTankLevelGetterDLL, которая имеет
следующие функции:
- long GetLowestPossibleSafeLevel();
- long GetHighestPossibleSafeLevel ();
- long GetCurrentLevel();
Естественно, GasTankLevelGetterDLL поддерживает
возможность некоторого устройства
непрерывно считывать данные о
количестве топлива в новом
топливном баке Acme. Ваше приложение
работает превосходно и не
"глючит".
Пару недель
спустя, мистер Ричи Рич ( Richy Rich )
вызывает вас к себе и сообщает, что
ваш ActiveX для индикации уровня
является самой красивой вещью,
которую он когда-либо видeл в своей
жизни. Ричи говорит вам, что хочет
использовать его для контроля
уровня в своем аквариуме на 5000
галлонов. Он заявляет, что
индикатор должен показывать те же
три уровня, что и для топливного
бака. Вы говорите ему, что зайдете к
нему завтра, а пока подумаете над
его предложением.
На следующий
день вы приходите к мысли, называть
все DLL, которые реализуют те самые
три функции, хотя и с различной
внутренней обработкой, одинаково - LevelGetterDLL. Проблема контроля
уровня воды в аквариуме мистера
Ричи решена. Он проверяет ваше
приложение 24 часа в сутки, чтобы
убедиться, что его рыбки находятся
в полной безопасности. Вы также
передаете новую версию LevelGetterDLL Acme. Другие
компании связываются с вами на
предмет использования вашего ActiveX
индикатора уровня. Вы отвечаете им:
"Нет проблем! Возьмите эти три
функции, назовите вашу DLL LevelGetterDLL, и все готово." Вам
необходимо всего лишь один раз
перекомпилировать ваше приложение,
чтобы оно поддерживало новую
версию LevelGetterDLL, но поскольку во
всем мире все называют свои DLL
одинаково (LevelGetterDLL) и используют
одинаковые неизменные три метода,
то все работает превосходно, и вам
никогда не придется
перекомпилировать ваше приложение
снова. Вы возвращаетесь домой,
чувствуя себя немножко гением.
На следующий
день, открыв The Wall Street
Journal , вы
обнаруживаете, что Ричи Рич
разбился на своем вертолете. По
дороге в штаб-квартиру Rich Inc. ему не
хватило топлива. Похоже, Ричи был
клиентом Acme и запускал оба
приложения на своем компьютере
одновременно. Приложение 1 было то
самое, которое вы разработали с
использованием LevelGetterDLL для контроля
уровня в его аквариуме. Приложение 2
было сделано по заказу Acme для
контроля уровня топлива, в нем
использовалась та же версия LevelGetterDLL, которая была
установлена на вертолете Ричи. И
хотя Ричи запускал оба приложения,
Приложение 2 для топливных баков Acme
использовало DLL LevelGetterDLL для аквариума и
показывало уровни 5000-галлонного
аквариума вместо 1000-галлонного
топливного бака, поскольку версия
для аквариума была установлена на
компьютер последней. Ричи ничего не
знал о том, что его вертолету не
хватит топлива. Rich Inc. подает в суд
на Acme, которая, в свою очередь,
подает в суд на вас. Другие
компании, которым вы посоветовали
ваше решение, также подают на вас в
суд. Если бы вы использовали Component
Object Model (COM), Ричи Рич был бы
жив, и вам не пришлось бы садиться
на скамью подсудимых.
Правило
Если две или более DLL
предоставляют одинаковые
функции (immutability), вы можете
использовать любую из этих DLL.
Однако одно приложение не может
использовать сразу несколько DLL,
как и не могут одновременно
несколько таких DLL находиться на
одном и том же компьютере.
Технология COM решает эту
проблему. Два сервера COM с
идентичными интерфейсами (и
следовательно методами) могут
использоваться двумя различными
приложениями и могут находиться
на одном и том же компьютере,
поскольку они имеют различные
идентификаторы CLSID, и,
следовательно, различны на
бинарном уровне. Кроме того,
технически эти два сервера COM
взаимозаменяемы.
Отсутствие
"взаимозаменяемых деталей"
(компонентов) присуще индустрии
программных разработок в силу ее
относительно молодого возраста.
Однако, подобно индустриальной
революции, создавшей независимые
детали машин, технология COM
реализует это через программные
компоненты. Понимая смысл CLSID и
неизменности интерфейсов, можно
написать законченный plug-in без
какого-либо знания о клиенте. Это
означает, что Приложение 1 может
использовать или Plug-In1 или Plug-In2. Еще
лучше, чтобы Приложение 1 могло
динамически переключать Plug-In1 и
Plug-In2. Проектирование приложений,
использующих динамически
заменяемые вставки (plug-ins) сделает
для программной индустрии то же
самое, что сделали детали машин и
механизмов для промышленной
революции.
Восторгаясь
Active Template Library (ATL) и Distributed COM (DCOM), мы
постепенно забываем, что лежало в
основе появления COM. Способность DCOM
использовать удаленный вызов
процедур (remote procedure calls, RPC)
выстраивать данные (marshaling)
воодушевляет ( и, возможно, является
одной из причин роста популярности
COM за последние 12
месяцев), однако это не главное,
почему была разработана технология
COM. Главное, ради чего создавалась
COM, - предоставить производителям
программ возможность встраивать
новые функциональные части в
существующие приложения без
перестраивания этих приложений.
Компоненты COM должны быть
спроектированы как
взаимозаменяемые вставки (plug-ins),
независимо от того, является ли
компонент COM локально подключаемой
DLL или удаленно запускаемым
сервером.
Цель
Эта статья
продемонстрирует компоненты COM,
которые допускают многократное
использование аналогично
автомобильным шинам. Использование
COM позволит разработать серию
программных продуктов за более
короткий срок, чем без применения
этой технологии. Зная как создавать
COM объекты и интерфейсы, можно
разрабатывать взаимозаменяемые
компоненты.
Что потребуется
Для
построения примеров вам
потребуется Microsoft Visual C++(r) 5.0. Нет
необходимости в десятилетнем опыте
по Windows(r) и C, достаточно некоторого
знакомства с Visual C++, MFC,
наследованием и полиморфизмом.
Примеры будут построены и
выполнены под Windows NT(r) или Windows 95. Мы
будем использовать OLE/COM
Object Viewer -
удобную утилиту, поставляемую
вместе с Visual C++ 5.0 и Visual Basic(r) 5.0, а
также доступную для download на
http://www.microsoft.com/oledev.
Часть 1: Дублирование интерфейсов
В
вышеприведенном случае с Ричи
Ричем мы видели, что DLL для
аквариума и DLL для топливного бака
не могли находиться на одном и том
же компьютере, потому что ни
клиентское приложение, ни две DLL не
были COM компонентами. Какая бы DLL ни
была скопирована на компьютер,
только скопированная последней
будет использоваться клиентским
приложением. Как мы уже видели,
использование некорректной DLL
может привести к катастрофическим
результатам: вертолет разбился. Мы
предположили, что если бы
разработчик программы использовал
технологию COM, то он имел бы обе DLL на
машине. Поскольку две DLL были бы
различимы по их CLSID, они могли бы
использоваться в пределах одного
приложения. По технологии COM обе DLL
должны задействовать идентичные
методы через заменяемые
интерфейсы.
Чтобы это
доказать, мы собираемся создать
единственное GUI-приложение, которое
использует и показывает
информацию, получаемую от двух
серверов COM: GasTankLevelGetter DLL и FishTankLevelGetter DLL. Мы также
создадим одно приложение, которое
будет получать информацию от
каждой COM DLL и отображать их. Опрос
каждой DLL будет происходить
попеременно по четырехсекундному
таймеру. Чтобы подчеркнуть
неизменность интерфейсов и что COM
является двоичным стандартом, мы
собираемся написать GUI-приложение FishTankLevelGetter COM DLL исключительно на
основе информации о GasTankLevelGetter COM DLL. Однако мы не
собираемся предоставлять вам
исходный код GasTankLevelGetter COM DLL. Если вы
переписали пример, вы найдете GasTankLevelGetter COM DLL в папке Binaries. Мы
вам даже не скажем на чем написана GasTankLevelGetter: на Delphi, Visual C++, Java(tm), Cobol,
Turbo Pascal или Visual Basic. Вам, однако,
придется зарегистрировать GasTankLevelGetter DLL с помощью RegSvr32.
Как только вы
зарегистрировали GasTankLevelGetter DLL с помощью RegSvr32,
вы готовы начать, вооружившись OLE/COM Object Viewer. Если вы используете
Visual C++ 5.0, OLE/COM Object Viewer находится в
программной группе Visual C++ 5.0 при
навигации через Start |
Programs в
Explorer. Если у вас нет OLE/COM
Object Viewer,
спишите его из http://www.microsoft.com/oledev/ и
запустите приложение.
Запустив OLE/COM Object Viewer, выберите режим View | Expert для просмотра Type Libraries.
Пролистайте список и откройте
папку под названием Type
Libraries.
Пролистайте папку пока не найдете
GasTankLevelGetter 1.0 TypeLibrary (Ver 1.0). Выделите
этот элемент списка и вы увидите на
правой панели ID библиотеки типа и
ее полный путь, как показано на
рисунке.
Двойной
щелчок на GasTankLevelGetter откроет окно,
отображающее всю библиотеку типа.
Эта информация берется их данных
регистров, которые создаются при
регистрации DLL. Данные по TypeLib
хранятся в HKEY_CLASSES_ROOT \
TypeLib.
Раздел
coclass содержит список
поддерживаемых интерфейсов для
компонентного объекта. Объект
может иметь любое количество
интерфейсов, перечисляемых в его
теле и полностью описывающих тот
набор интерфейсов, которые этот
объект реализует, как входящих, так
и исходящих. Ниже приведены CLSID и
интерфейс, содержащиеся в coclass для данного COM объекта:
CLSID: 8A544DC6-F531-11D0-A980-0020182A7050
Interface Name: ILevelGetter
[
uuid(8A544DC6-F531-11D0-A980-0020182A7050),
helpstring("LevelGetter Class")
]
coclass LevelGetter {
[default] interface ILevelGetter;
};
Раскрывая
далее информацию по интерфейсу
наподобие coclass, мы можем определить:
- ID
интерфейса 8A544DC5-F531-11D0-A9 80-0020182A7050.
- Интерфейс
наследуется от IUnknown.
- Интерфейс
поддерживает методы. Первые
три метода возвращают значения
типа long, а четвертый -
указатель на BSTR.
[
odl,
uuid(8A544DC5-F531-11D0-A980-0020182A7050),
helpstring("ILevelGetter Interface")
]
interface ILevelGetter : IUnknown {
HRESULT _stdcall GetLowestPossibleSafeLevel([out, retval] long*
plLowestSafeLevel);
HRESULT _stdcall GetHighestPossibleSafeLevel([out, retval]
long* plHighestSafeLevel);
HRESULT _stdcall GetCurrentLevel([out, retval] long*
plCurrentLevel);
HRESULT _stdcall GetTextMessage([out, retval] BSTR*
ppbstrMessage);
};
Более
детальный взгляд на структуру type
library открывает нам методы и ID
интерфейсов.
Теперь,
поскольку мы знаем, как построить
интерфейс ILevelGetter, давайте создадим
наш собственный компонент COM на
основе этой информации. Если вы
решили работать с существующим
примером, все источники находятся в
папке LevelViewer. Запустите Visual C++
5.0 и создайте новый проект.
Определите тип ATLComAppWizard как проект и
"FishTankLevelGetter" как имя проекта. Мы
полагаем, что вы создали новую
папку проекта. Окно New Project Dialog
должно выглядеть как это показано
ниже.
В
AppWizard для Server Type укажите Dynamic Link Library
(DLL). Отметьте обе опции Allow merging of
proxy/stub code и Support MFC.
Когда
вы создали новый проект FishTankLevelGetter, выберите в меню Insert | New Class... для создания нового ATL
класса. Вы можете выбрать любое имя
класса, но убедитесь, что интерфейс
называется IlevelGetter, а его тип - Custom, что указывает на
наследование ILevelGetter от IUnknown. Если бы ILevelGetter в GasTankLevelGetter COM DLL
наследовалась от IDispatch, нам пришлось бы
выбрать тип интерфейса Dual, который указывал
бы на то, что новый интерфейс будет
производным от IDispatch. Если диалог New Class
выглядит как показано ниже, нажмите
OK, чтобы создать новый класс.
Следующий
шаг заключается в редактировании FishTankLevelGetter.IDL. В IDL файле вам нужно
иметь новый интерфейс ILevelGetter, наследуемый из IUnknown. Если вы работаете с
примерами, вы увидите следующий
код, который содержит четыре
одинаковых неизменяемых метода
интерфейса IlevelGetter, которые мы
видели в интерфейсе ILevelGetter
GasTankLevelGetter.
[
object,
uuid(7F0DFAA2-F56D-11D0-A980-0020182A7050),
helpstring("ILevelGetter Interface"),
pointer_default(unique)
]
interface ILevelGetter : IUnknown
{
HRESULT GetLowestPossibleSafeLevel([out, retval] long* plLowestSafeLevel);
HRESULT GetHighestPossibleSafeLevel([out, retval] long* plHighestSafeLevel);
HRESULT GetCurrentLevel([out, retval] long* plCurrentLevel);
HRESULT GetTextMessage([out, retval] BSTR* ppbstrMessage);
};
Если вы
пишите код, как и мы,
самостоятельно, вы захотите
добавить вышеуказанный код так, что
ваш интерфейс соответствовал
четырем идентичным неизменным
методам. Наиболее просто добавить
код с помощью "copy and paste"
непосредственно из окна ITypeLib Viewer. Ваш код должен
выглядеть точно также, как в
примере, за исключением ID
интерфейса.
Откройте LevelGetter.H и объявите методы в
классе. В вашем классе объявление
методов должно выглядеть как это
показано ниже:
class LevelGetter :
public ILevelGetter,
public CComObjectRoot,
public CComCoClass<LevelGetter,&CLSID_LevelGetter>
{
public:
LevelGetter(){}
BEGIN_COM_MAP(LevelGetter)
COM_INTERFACE_ENTRY(ILevelGetter)
END_COM_MAP()
//DECLARE_NOT_AGGREGATABLE(LevelGetter)
// Remove the comment from the line above if you don't want your object to
// support aggregation.
DECLARE_REGISTRY_RESOURCEID(IDR_LevelGetter)
// ILevelGetter
public: //THE FOUR NEW METHODS
STDMETHOD (GetLowestPossibleSafeLevel) (long* plLowestSafeLevel);
STDMETHOD (GetHighestPossibleSafeLevel) (long* plHighestSafeLevel);
STDMETHOD (GetCurrentLevel) (long* plCurrentLevel);
STDMETHOD (GetTextMessage) (BSTR* ppbstrMessage);
};
Вам теперь
нужно сделать четыре метода. Для
демонстрационных целей, давайте
оставим методы простыми.
Реализуйте их по вашему усмотрению
или скопируете следующий код из
образцов.
//---------------------------------------------------------------------------
STDMETHODIMP LevelGetter::GetLowestPossibleSafeLevel(long* plLowestSafeLevel)
{
*plLowestSafeLevel = 70;
return S_OK;
}
//---------------------------------------------------------------------------
STDMETHODIMP LevelGetter::GetHighestPossibleSafeLevel(long* plHighestSafeLevel)
{
*plHighestSafeLevel = 98;
return S_OK;
}
//---------------------------------------------------------------------------
STDMETHODIMP LevelGetter::GetCurrentLevel(long* plCurrentLevel)
{
*plCurrentLevel = 94;
return S_OK;
}
//---------------------------------------------------------------------------
STDMETHODIMP LevelGetter::GetTextMessage(BSTR* ppbstrMessage)
{
*ppbstrMessage = ::SysAllocString(L"All clear, water level is fine");
return S_OK;
}
Поскольку у
вас уже есть методы, скомпилируйте
и слинкуйте вашу COM DLL. Затем мы
начнем создавать клиентское
приложение.
Создание клиентского приложения для обоих COM объектов
Мы
собираемся создать клиентское
приложение, которое будет
поддерживать два COM объекта GasTankLevelGetter и FishTankLevelGetter. Используя AppWizard,
создайте MFC диалог приложения,
который бы поддерживал и
управляющие элементы Automation, и ActiveX
одновременно (укажите это в
соответствующих check box во время
работы с AppWizard).
Как только вы
создали приложение, отредактируйте
ваш основной диалог в редакторе
ресурсов, так чтобы он имел
сходство с следующим:
Замечание: Возможно вы
захотите просмотреть ID
элементов управления в
примере, поскольку мы
собираемся изменить эти
значения.
Следующий
шаг состоит в добавлении
указателей сообщений для двух
кнопок Gas Tank Level и Fish
Tank Level. В
примере эти методы называются OnGas
и OnFish соответственно
Если вы
создали класс диалога и добавили
указатели сообщений для кнопок, вам
необходимо открыть этот класс и
добавить несколько членов класса и
методов класса. Первое, что мы
сделаем, - это опишем далее
интерфейс ILevelGetter так, чтобы мы
могли добавлять члены класса (class
member) для этого типа интерфейса.
Во-вторых, добавим два
дополнительных метода класса (class
methods) ClearMembers и
SetNewData и
два члена класса m_pILevelGetter
и m_sLastCalled. Затем, используя Class
Wizard, добавим методы OnDestroy и OnTimer. Как только это
сделано, ваше описание класса
должно быть таким, как показано
ниже.
//forward declaration so for our class member
interface ILevelGetter;
class CLevelViewerDlg : public CDialog
{
DECLARE_DYNAMIC(CLevelViewerDlg);
friend class CLevelViewerDlgAutoProxy;
public:
CLevelViewerDlg(CWnd* pParent = NULL); // standard constructor
virtual ~CLevelViewerDlg();
//{{AFX_DATA(CLevelViewerDlg)
enum { IDD = IDD_LEVELVIEWER_DIALOG };
//}}AFX_DATA
//{{AFX_VIRTUAL(CLevelViewerDlg)
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support
//}}AFX_VIRTUAL
// Implementation
protected:
CLevelViewerDlgAutoProxy* m_pAutoProxy;
HICON m_hIcon;
BOOL CanExit();
//added by manually typing these into the class
void ClearMembers();
void SetNewData(const CLSID& clsid, const IID& iid);
ILevelGetter* m_pILevelGetter;
CString m_sLastCalled;
// Generated message map functions
//{{AFX_MSG(CLevelViewerDlg)
virtual BOOL OnInitDialog();
afx_msg void OnPaint();
afx_msg HCURSOR OnQueryDragIcon();
afx_msg void OnClose();
virtual void OnOK();
virtual void OnCancel();
//added by the Class Wizard
afx_msg void OnFish();
afx_msg void OnGas();
afx_msg void OnDestroy();
afx_msg void OnTimer(UINT nIDEvent);
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
Далее
изменим файл описания реализации
(implementation file). В конструкторе класса
проинициализируйте переменные
членов класса как это показано
ниже:
//--------------------------------------------------------------
CLevelViewerDlg::CLevelViewerDlg(CWnd* pParent /*=NULL*/)
: CDialog(CLevelViewerDlg::IDD, pParent)
{
//{{AFX_DATA_INIT(CLevelViewerDlg)
//}}AFX_DATA_INIT
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
m_pAutoProxy = NULL;
m_pILevelGetter = NULL;
m_sLastCalled = _T("CheckedGas");
}
Реализация
метода ClearMembers приводится далее.
Эта функция очищает элементы
управления диалога (dialog controls).
(Отметим, что мы использовали бы Dialog
Data exchange для членов класса.)
//--------------------------------------------------------------------
void CLevelViewerDlg::ClearMembers()
{
CWnd* pWnd = GetDlgItem(IDC_TANK_TYPE);
if(pWnd != NULL)
pWnd->SetWindowText("");
pWnd = GetDlgItem(IDC_LOWEST_SAFE);
if(pWnd != NULL)
pWnd->SetWindowText("");
pWnd = GetDlgItem(IDC_HIGHEST_SAFE);
if(pWnd != NULL)
pWnd->SetWindowText("");
pWnd = GetDlgItem(IDC_CURRENT);
if(pWnd != NULL)
pWnd->SetWindowText("");
pWnd = GetDlgItem(IDC_MESSAGE);
if(pWnd != NULL)
pWnd->SetWindowText("");
}
OnDestroy, показанный ниже,
используется для очистки при
закрытии диалога.
//--------------------------------------------------------------------
void CLevelViewerDlg::OnDestroy()
{
CDialog::OnDestroy();
KillTimer(1);
}
Данный класс
использует OnTimer для вызова
методов кнопок OnFish и OnGas так, что
пользователю не требуется нажимать
кнопки для обновления данных.
//--------------------------------------------------------------------
void CLevelViewerDlg::OnTimer(UINT nIDEvent)
{
if(m_sLastCalled == _T("CheckedFish"))
OnGas();
else
OnFish();
}
Замечание: В реальной жизни
предпочтительнее использовать
технологию с нажатием кнопок и
интерфейс IConnectionPoint. Законченный
пример такой реализации вы
найдете на
http://www.microsoft.com/workshop/prog/com/overview-f.htm.
Виртуальная
функция OnInitDialog используется в
основном для запуска таймера, хотя
она также возвращает данные из GasTankLevelGetter COM DLL.
//--------------------------------------------------------------------
BOOL CLevelViewerDlg::OnInitDialog()
{
CDialog::OnInitDialog();
SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon
OnGas(); //obtain data
SetTimer(1, 4000, NULL); //set timer for 4 seconds
return TRUE; // return TRUE unless you set the focus to a control
}
Теперь мы
готовы описать реализацию наших
методов кнопок OnFish и OnGas, которые
вызываются попеременно каждые 4
секунды. Обе эти функции идентичны
на процедурном уровне; они передают
CLSID и IID в SetNewData. Единственная
разница состоит в том, что CLSID
и IID, передаваемые методом OnGas,
используются в GasTankLevelGetter, а CLSID и IID передаваемые
методом OnFish, - в FishTankLevelGetter.
OnGas возвращает CLSID,
взятый из строки GUID, которая имеется
в данных coclass TypeLib. Таким же образом
возвращается IID и, кроме того, он
отображается в OLE/COM
Object Viewer.
Как только получены GUID, вызывается SetNewData.
//--------------------------------------------------------------------
void CLevelViewerDlg::OnGas()
{
m_sLastCalled = _T("CheckedGas");
CLSID clsid;
IID iid;
HRESULT hRes;
hRes = AfxGetClassIDFromString(
"{8A544DC6-F531-11D0-A980-0020182A7050}",
&clsid);
if(SUCCEEDED(hRes))
{
hRes = AfxGetClassIDFromString(
"{8A544DC5-F531-11D0-A980-0020182A7050}", &iid);
if(SUCCEEDED(hRes))
SetNewData(clsid, iid);
}
}
Метод SetNewData, показанный ниже,
создает instance в GasTankLevelGetter COM объекте или FishTankLevelGetter COM объекте в
зависимости от CLSID. После этого SetNewData вызывает методы
интерфейса ILevelGetter для получения
данных.
//--------------------------------------------------------------------
void CLevelViewerDlg::SetNewData(const CLSID& clsid, const IID& iid)
{
ClearMembers();
ASSERT(m_pILevelGetter == NULL);
HRESULT hRes = CoCreateInstance(clsid, NULL, CLSCTX_ALL,
iid, (void**)&m_pILevelGetter);
if(!SUCCEEDED(hRes))
{
m_pILevelGetter = NULL;
return;
}
long lLowestSafeLevel, lHighestSafeLevel, lCurrentLevel;
BSTR bstrMessage = NULL;
m_pILevelGetter->GetLowestPossibleSafeLevel(&lLowestSafeLevel);
m_pILevelGetter->GetHighestPossibleSafeLevel(&lHighestSafeLevel);
m_pILevelGetter->GetCurrentLevel(&lCurrentLevel);
m_pILevelGetter->GetTextMessage(&bstrMessage);
m_pILevelGetter->Release();
m_pILevelGetter = NULL;
CString sLowest, sHighest, sCurrent, sMessage;
sLowest.Format("%d",lLowestSafeLevel);
sHighest.Format("%d",lHighestSafeLevel);
sCurrent.Format("%d",lCurrentLevel);
sMessage = bstrMessage;
::SysFreeString(bstrMessage);
CString sItem;
if(m_sLastCalled == _T("CheckedFish"))
{
//we are checking the fish tank now
sItem = _T("Fish Tank");
}
else //m_sLastCalled == _T("CheckedGas")
{
//we are checking the fish tank now
sItem = _T("Gas Tank");
}
CWnd* pWnd = GetDlgItem(IDC_TANK_TYPE);
if(pWnd != NULL)
pWnd->SetWindowText(sItem);
pWnd = GetDlgItem(IDC_LOWEST_SAFE);
if(pWnd != NULL)
pWnd->SetWindowText(sLowest);
pWnd = GetDlgItem(IDC_HIGHEST_SAFE);
if(pWnd != NULL)
pWnd->SetWindowText(sHighest);
pWnd = GetDlgItem(IDC_CURRENT);
if(pWnd != NULL)
pWnd->SetWindowText(sCurrent);
pWnd = GetDlgItem(IDC_MESSAGE);
if(pWnd != NULL)
pWnd->SetWindowText(sMessage);
}
Поскольку
интерфейсы одинаковы, мы уверены,
что методы будут работать с обоими
COM объектами. Последние два шага
должны реализовать OnFish и включить
определение интерфейса.
//--------------------------------------------------------------------
void CLevelViewerDlg::OnFish()
{
m_sLastCalled = _T("CheckedFish");
CLSID clsid;
IID iid;
HRESULT hRes = AfxGetClassIDFromString(
"{7F0DFAA3-F56D-11D0-A980-0020182A7050}", &clsid);
if(SUCCEEDED(hRes))
hRes = AfxGetClassIDFromString(
"{7F0DFAA2-F56D-11D0-A980-0020182A7050}", &iid);
if(SUCCEEDED(hRes))
SetNewData(clsid, iid);
}
Определение
интерфейса, созданное чисто
виртуальными членами класса,
включается в верхнюю часть файла
описания реализации ( хотя его
можно поместить в описание класса
или отдельный .h файл), так что член
класса m_pILevelGetter типа ILevelGetter* "знает" свои
методы. Определение интерфейса
представлено ниже:
//------------------------------------------------------------------
interface ILevelGetter : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE GetLowestPossibleSafeLevel(long*
plLowestSafeLevel) = 0;
virtual HRESULT STDMETHODCALLTYPE GetHighestPossibleSafeLevel(long*
plLowestSafeLevel) = 0;
virtual HRESULT STDMETHODCALLTYPE GetCurrentLevel(long* plLowestSafeLevel) = 0;
virtual HRESULT STDMETHODCALLTYPE GetTextMessage(BSTR* pbstrMessage) = 0;
};
Теперь мы
готовы откомпилировать, слинковать
и запустить приложение. Если вы
запустили приложение, вы можете
щелкнуть по какой-либо кнопке,
чтобы переключить COM компоненты,
или позволить таймеру переключать
их автоматически каждые четыре
секунды. И только теперь Ричи Рич
сможет спокойно лететь на своем
вертолете и следить за уровнем воды в
своем аквариуме.
Часть 2: Наследование классов и наследование интерфейсов
В первой
части стать мы показали значимость
неизменности интерфейсов и
продемонстрировали, как
разработчик может построить
приложение, которое может легко
заменять компоненты, если
разработан интерфейс. А что, если
интерфейс существующего COM-сервера
имеет сотни методов? В примере из
первой части мы сделали это простым
клонированием интерфейса IlevelGetter, поскольку он содержал
только четыре метода. Попробуйте с
помощью OLE/COM Object Viewer просмотреть
некоторые другие библиотеки типов
на вашем компьютере. Как вы можете
убедиться, многие компоненты имеют
интерфейсы с весьма значительным
количеством методов. Клонирование
интерфейсов, которые реализуют
сотни методов, с целью изменить
всего лишь несколько из них было бы
весьма обременительно.
Правила COM
гласят, что если вы наследуете
интерфейс из существующего
интерфейса, вам необходимо
реализовывать все методы,
поскольку описания интерфейсов
содержат чисто виртуальные
функции. То же правило, которое
обеспечивает взаимозаменяемость
деталей машин, может обернуться для
разработчиков тяжелым, ненужным
бременем!
А что, если вы
смогли бы наследовать интерфейсы
без необходимости повторно
описывать реализацию всех методов?
Что если бы вы могли создать
компонент, унаследовать интерфейсы
и функциональное назначение и
переделать функциональность по
своему усмотрению? Сегодня это
нельзя сделать с помощью COM
объектов, разработанных вне вашей
организации. Однако, если
разработчики в вашей организации
используют язык программирования,
поддерживающий наследование и
полиморфизм, типа Visual C++, вы это
действительно сделаете. На самом
деле, как мы покажем, MFC позволяет
сделать это значительно легче.
В корне MFC
есть CCmdTarget. CCmdTarget - это не только
базовый класс для message-map
архитектуры, он также содержит Dispatch
планы, которые влияют на
интерфейсы, такие как IDispatch и IUnknown. Каждый прямой
потомок CCmdTarget, созданный с
помощью Class Wizard, содержит эти
интерфейсы со своими собственными
CLSID. CCmdTarget - один из
основных рабочих классов и базовый
класс для таких "повседневных" MFC
классов, как CView, CWinApp,
CDocument, CWnd и CFrameWnd. Соответственно,
каждый производный класс от CCmdTarget может реализовывать
собственные CLSID и интерфейсы.
Пример,
который мы собираемся рассмотреть,
покажет наследование
интерфейсов путем образования
новых C++ производных классов от CCmdTarget. В нашем базовом
классе мы реализуем интерфейс с
методами, которые вызывают
виртуальные функции членов C++
класса. Наш производный класс
заменит некоторые из отобранных
виртуальных функций. Что особенно
важно, вместо реализации
наследуемого класса в той же DLL, мы
создадим отдельную DLL со своим
собственным CLSID. Наиболее
эффективно наследовать реализацию
интерфейса от одного кода в другой
без переписывания исходного
интерфейса.
Давайте
начнем с просмотра кода в проекте BaseLevelGetterDLL. BaseLevelGetterDLL является
типичной MFC DLL. Она была создана с
помощью AppWizard как "regular DLL using the shared
MFC DLL". Она также поддерживает
автоматику (automation). Завершив работу
с AppWizard, получаем BaseLevelGetterExport.h, а BASE_LEVEL_GETTER_DLL
оказывается
включенной как preprocessor definition в Project | Settings | C++. BaseLevelGetterExport.H и диалог Project | Settings приведены ниже.
//BaseLevelGetterExport.h
#ifndef BASE_LEVEL_GETTER_EXPORT_DLL_H
#define BASE_LEVEL_GETTER_EXPORT_DLL_H
#if defined(BASE_LEVEL_GETTER_DLL)
#define BASE_LEVEL_GETTER_EXPORT __declspec(dllexport)
#else
#define BASE_LEVEL_GETTER_EXPORT __declspec(dllimport)
#endif
#endif //BASE_LEVEL_GETTER_EXPORT_DLL_H
Определив BASE_LEVEL_GETTER_DLL, мы можем создавать
классы и экспортировать их из нашей
DLL.
Следующим
шагом будет создание C++ класса,
который содержит наши интерфейсы. С
помощью Class Wizard несколькими
нажатиями кнопки мыши мы создадим
класс, наследованный от CCmdTarget. Выделив Createable by type ID в диалоге New Class, мы
создадим наш новый класс с макросом
IMPLEMENT_OLECREATE, присваивающем
классу его собственные CLSID и интерфейс IDispatch.
Обращаясь к BaseLevelGetter.CPP, мы видим CLSID:
//Here is our CLSID
// {C20EA055-F61C-11D0-A25F-000000000000}
IMPLEMENT_OLECREATE(BaseLevelGetter, "BaseLevelGetterDLL.BaseLevelGetter", 0xc20ea055, 0xf61c, 0x11d0, 0xa2, 0x5f, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0)
И интерфейс
под названием IbaseLevelGetter типа IDispatch:
// {C20EA054-F61C-11D0-A25F-000000000000}
static const IID IID_IBaseLevelGetter =
{ 0xc20ea054, 0xf61c, 0x11d0, { 0xa2, 0x5f, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 } };
BEGIN_INTERFACE_MAP(BaseLevelGetter, CCmdTarget)
INTERFACE_PART(BaseLevelGetter, IID_IBaseLevelGetter, Dispatch)
END_INTERFACE_MAP()
Вместо того,
чтобы работать с интерфейсом,
предоставляемым по умолчанию Class
Wizard, мы собираемся добавить наш
собственный интерфейс, чтобы
показать как легко добавлять
интерфейсы в классы-потомки от CCmdTarget. Первое, что мы должны
сделать, - это описать наши
интерфейсы. Определение интерфейса
всегда одинаково. Каждый интерфейс
должен иметь IID и IUnknown как основной
интерфейс где-нибудь в своей
иерархии. Также необходимо
реализовать три метода IUnknown. В ILevelGetter.H мы используем GUIDGEN.EXE ( находится в \Program
Files\DevStudio\VC\Bin) для генерации
уникального IID для нашего
интерфейса наследуем интерфейс от IUnknown. Дополнительно к трем
виртуальным функциям IUnknown мы добавили еще 4
виртуальные функции, которые будут
реализованы в нашем COM объекте. Ниже
приведен полный код ILevelGetter.H.
#ifndef ILEVELGETTER_H
#define ILEVELGETTER_H
// {BCB53641-F630-11d0-A25F-000000000000}
static const IID IID_ILevelGetter =
{ 0xbcb53641, 0xf630, 0x11d0, { 0xa2, 0x5f, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 } };
interface ILevelGetter : public IUnknown
{
//first add the three always required methods
virtual HRESULT STDMETHODCALLTYPE
QueryInterface(REFIID riid, LPVOID* ppvObj) = 0;
virtual ULONG STDMETHODCALLTYPE AddRef() = 0;
virtual ULONG STDMETHODCALLTYPE Release() = 0;
//now add methods for this custom interface
virtual HRESULT STDMETHODCALLTYPE
GetCurrentLevel(long* plCurrentLevel) = 0;
virtual HRESULT STDMETHODCALLTYPE
GetHighestPossibleSafeLevel(long* plHighestSafeLevel) = 0;
virtual HRESULT STDMETHODCALLTYPE
GetLowestPossibleSafeLevel(long* plLowestSafeLevel) = 0;
virtual HRESULT STDMETHODCALLTYPE
GetTextMessage(BSTR* ppbstrMessage) = 0;
};
Следующим
шагом будет определение методов
интерфейса в BaseLevelGetter.H. В верхней части BaseLevelGetter.H добавим директиву include
для описания нашего интерфейса как
это показано ниже:
#include "ILevelGetter.h"
Как только мы
включили ILevelGetter.H, мы можем
добавить наши методы интерфейса,
используя макрос BEGIN_INTERFACE_PART. В итоге BEGIN_INTERFACE_MACRO создает вложенный
класс типа XLevelGetter и член класса m_xLevelGetter в BaseLevelGetter. (Более подробное
описание макроса BEGIN_INTERFACE_PART смотри MFC Technical Note
38.) Каждый метод в интерфейсе
объявляется в макросе так же, как
если бы никакого макроса не было.
Можно убедиться, что объявления
метода в ILevelGetter.H такие же как и в
версии с использованием ATL.
BEGIN_INTERFACE_PART(LevelGetter, ILevelGetter)
STDMETHOD(GetCurrentLevel) (long* plCurrentLevel);
STDMETHOD(GetHighestPossibleSafeLevel) (long* plHighestSafeLevel);
STDMETHOD(GetLowestPossibleSafeLevel) (long* plLowestSafeLevel);
STDMETHOD(GetTextMessage) (BSTR* ppbstrMessage);
END_INTERFACE_PART(LevelGetter)
Поскольку
наша цель заключается в
эффективном наследовании
интерфейсов из одного источника в
другой без необходимости
повторного описания реализации
всех методов, мы собираемся
добавить четыре виртуальных
функции в наш класс. Каждая
виртуальная функция будет
соответствовать методу в
интерфейсе ILevelGetter. В примере эти
методы описаны в нижней части class
declaration сразу после макроса BEGIN_INTERFACE_PART.
//since the class can be dynamically created
//these virtual functions cannot be pure
virtual long GetCurrentLevel();
virtual long GetHighestSafeLevel();
virtual long GetLowestSafeLevel();
virtual CString GetMessage();
Отметим, что
поскольку наш класс-потомок от CCmdTarget использует DECLARE_DYNCREATE, эти функции не могут
быть чисто виртуальными.
Последнее,
что осталось сделать, - объявить наш
класс "exportable". Для этого нам
необходимо всего лишь включить
наше описание экспорта в описание
класса. Это выглядит так:
#include "BaseLevelGetterExport.h"
class BASE_LEVEL_GETTER_EXPORT BaseLevelGetter : public CCmdTarget
{
Реализация
нашего интерфейса также проста.
Первое, что нужно сделать, - это
добавить поддержку нашему новому
интерфейсу ILevelGetter. Общее правило
заключается в добавлении макроса INTERFACE_PART между BEGIN_INTERFACE_PART и END_INTERFACE_PART для каждого
поддерживаемого интерфейса. В BaseLevelGetter.CPP это делается
дополнением следующей строки:
INTERFACE_PART(BaseLevelGetter, IID_ILevelGetter, LevelGetter)
Так что
полное описание INTERFACE_PART выглядит
следующим образом:
BEGIN_INTERFACE_MAP(BaseLevelGetter, CCmdTarget)
INTERFACE_PART(BaseLevelGetter, IID_IBaseLevelGetter, Dispatch)
INTERFACE_PART(BaseLevelGetter, IID_ILevelGetter, LevelGetter)
END_INTERFACE_MAP()
Далее мы
описываем реализацию методов ILevelGetter. Первые три метода,
которые должны быть реализованы, -
это QueryInterface, AddRef и Release из IUnknown. Эти методы
показаны ниже.
//------------------------------------------------------------------------
HRESULT FAR EXPORT BaseLevelGetter::XLevelGetter::QueryInterface
(
REFIID iid,
LPVOID* ppvObj
)
{
METHOD_PROLOGUE_EX_(BaseLevelGetter, LevelGetter)
return (HRESULT) pThis->ExternalQueryInterface(&iid, ppvObj);
}
//-------------------------------------------------------------------------
ULONG FAR EXPORT BaseLevelGetter::XLevelGetter::AddRef()
{
METHOD_PROLOGUE_EX_(BaseLevelGetter, LevelGetter)
return (ULONG) pThis->ExternalAddRef();
}
//-------------------------------------------------------------------------
ULONG FAR EXPORT BaseLevelGetter::XLevelGetter::Release()
{
METHOD_PROLOGUE_EX_(BaseLevelGetter, LevelGetter)
return (ULONG) pThis->ExternalRelease();
}
Четыре
метода ILevelGetter реализуются
весьма просто. Вместо фактического
выполнения обработки, каждый метод
вызывает свою связанную функцию
через указатель pThis. На самом деле это
требует некоторых дополнительных
объяснений. Если вы посмотрите на
определение макроса BEGIN_INTERFACE_PART(...) (файл
...\MFC\include\AFXDISP.H), вы обратите
внимание, что этот макрос является
вложенным описанием класса. Макрос
делает вложенный класс (в
нашем случае, XLevelGetter) производным от
интерфейса (ILevelGetter в нашем примере) и
объявляет его в пределах
существующего класса (BaseLevelGetter).
Макрос END_INTERFACE_PART(...) завершает
"внутреннее" описание класса XLevelGetter и объявляет
переменную члена этого класса m_xLevelGetter. Поскольку m_xLevelGetter является членом
класса BaseLevelGetter, мы могли бы
некоторыми сложными
арифметическими операциями над
указателями передать от this объекта XLevelGetter в this объекта,
содержащего BaseLevelGetter. Однако
библиотека MFC содержит другой
макрос, выполняющий то же самое. Он
называется METHOD_PROLOGUE_EX_, и в нашем
конкретном случае он создаст
переменную BaseLevelGetter*
pThis. Вы
можете использовать pThis для доступа к public
членам и методам "внешнего"
класса BaseLevelGetter, включая
виртуальные (полиморфные) функции.
Вызов виртуальных функций во
"внешнем" классе, фактически,
приводит к наследованию
интерфейса. Обратите внимание, что
виртуальные функции BaseLevelGetter возвращают
бессмысленные значения и содержат
комментарии, чтобы позволить
разработчикам, создающим
производные классы, переписать эти
функции.
Другой
способ показать виртуальное
отношение, возможно значительно
более удобный для чтения, - это
"указать владельца объекта" (set an
owner object) в классе XLevelGetter (класс, созданный
макросом BEGIN_INTERFACE_PART). Внутри макроса BEGIN_INTERFACE_PART (BaseLevelGetter.H) мы добавляем две
функции, и член класса выглядит
следующим образом:
XLevelGetter() { m_pOwner = NULL; } //constructor sets member to NULL
void SetOwner( BaseLevelGetter* pOwner ) { m_pOwner = pOwner; } //set the member
BaseLevelGetter* m_pOwner; //class member
Внутри
конструктора BaseLevelGetter мы вызываем XLevelGetter::SetOwner. Как упоминалось выше,
макрос BEGIN_INTERFACE_PART добавляет в BaseLevelGetter член класса m_xLevelGetter, который представляет LevelGetter. В конструкторе BaseLevelGetter мы вызываем:
m_xLevelGetter.SetOwner( this );
который
присваивает m_pOnwer значение
действительного объекта.
Ниже
показана реализация четырех
методов ILevelGetter и четырех
ассоциированных виртуальных
функций BaseLevelGetter. Остальные два
метода (GetLowestPossibleSafeLevel и GetTextMessage) реализованы по
принципу использования "владельца
объекта".
//------------------------------------------------------------------------
STDMETHODIMP BaseLevelGetter::XLevelGetter::GetCurrentLevel
(
long* plCurrentLevel
)
{
METHOD_PROLOGUE_EX_(BaseLevelGetter, LevelGetter)
//call outer object's GetCurrentLevel
//whether this class or a derived class
*plCurrentLevel = pThis->GetCurrentLevel();
return S_OK;
}
//-------------------------------------------------------------------------
STDMETHODIMP BaseLevelGetter::XLevelGetter::GetHighestPossibleSafeLevel
(
long* plHighestSafeLevel
)
{
METHOD_PROLOGUE_EX_(BaseLevelGetter, LevelGetter)
//call outer object's GetHighestSafeLevel
//whether this class or a derived class
*plHighestSafeLevel = pThis->GetHighestSafeLevel();
return S_OK;
}
//-------------------------------------------------------------------------
STDMETHODIMP BaseLevelGetter::XLevelGetter::GetLowestPossibleSafeLevel
(
long* plLowestSafeLevel
)
{
METHOD_PROLOGUE_EX_(BaseLevelGetter, LevelGetter)
//call outer object's GetLowestSafeLevel
//whether this class or a derived class
if( m_pOnwer != NULL)
{
*plLowestSafeLevel = m_pOwner->GetHighestSafeLevel();
}
else
{
ASSERT(FALSE);
}
return S_OK;
}
//------------------------------------------------------------------------
STDMETHODIMP BaseLevelGetter::XLevelGetter::GetTextMessage
(
BSTR* ppbstrMessage
)
{
METHOD_PROLOGUE_EX_(BaseLevelGetter, LevelGetter)
//call outer object's GetMessage
//whether this class or a derived class
CString sMessage;
If( m_pOwner != NULL )
{
sMessage = m_pOwner->GetMessage();
}
else
{
ASSERT(FALSE);
}
*ppbstrMessage = sMessage.AllocSysString();
return S_OK;
}
//---------------------------------------------------------------------
long BaseLevelGetter::GetCurrentLevel()
{
TRACE("Derived classes should override!");
return -1;
}
//---------------------------------------------------------------------
long BaseLevelGetter::GetHighestSafeLevel()
{
TRACE("Derived classes should override!");
return -1;
}
//---------------------------------------------------------------------
long BaseLevelGetter::GetLowestSafeLevel()
{
TRACE("Derived classes should override!");
return -1;
}
//---------------------------------------------------------------------
CString BaseLevelGetter::GetMessage()
{
TRACE("Derived classes should override!");
return "BaseLevelGetter";
}
Скомпилируйте
и слинкуйте приложение. Как только
DLL создана, скопируйте ее в каталог
Windows\System (\WINNT\System32 для Windows NT).
Важно:
Поскольку мы будем
использовать интерфейс ILevelGetter из BaseLevelGetter, не
забудьте после помещения
этой DLL в соответствующий
каталог зарегистрировать
ее с помощью RegSvr32. Если бы
мы использовали BaseLevelGetter как
абстрактный базовый класс
(т.е. виртуальные функции BaseLevelGetter должны
были бы быть
переопределены) и при этом,
возможно, удалось бы
избежать ошибок в
реализации, тогда не было
бы необходимости
регистрировать COM объект с
помощью RegSvr32.
Чтобы
построить COM объект, который
реализует интерфейс ILevelGetter, но не требует
переопределения всех методов, мы
создаем COM DLL точно так же, как BaseLevelGetterDLL: мы создаем MFC AppWizard DLL,
которая поддерживает automation, и
добавляем класс, являющийся
потомком CCmdTarget. Пример содержит
проект HotTubLevelGetterDLL с классом HotTubLevelGetter - потомком от CmdTarget, который создается
через диалог New Class в Class Wizard, как
показано ниже.
Далее
добавляем BaseLevelGetterDLL в путь include,
указав его как каталог Additional Include на
закладке Project | Settings |
C/C++ , как
показано ниже.
И линкуем BaseLevelGetterDLL.lib, добавляя ее как Library
Module на закладке Project |
Settings | Link.
Завершив все
установки проекта, выполним
следующие пять шагов для полного
завершения создания COM DLL plug-in.
1. Открыть HotTubLevelGetter.H и заменить все instances из
CCmdTarget на BaseLevelGetter
(существует
единственная instance CCmdTarget в HotTubLevelGetter.H).
2. Добавить BaseLevelGetter.H как include:
#include <BaseLevelGetter.h>
class HotTubLevelGetter : public BaseLevelGetter
{
3. Переписать
виртуальные функции BaseLevelGetter как это
требуется. В примере объявляются
две следующие виртуальные функции:
virtual CString GetMessage( ) { return "HotTubLevelGetter"; }
virtual long GetCurrentLevel( ) { return -2; }
4. Открыть HotTubLevelGetter.CPP и заменить все instances из
CCmdTarget на BaseLevelGetter
(существует
пять instances CCmdTarget в HotTubLevelGetter.CPP).
5. Выполнить
компиляцию и линковку. Не забудьте
зарегистрировать вашу COM DLL через
RegSvr32.
Прежде чем
продемонстрировать работу COM plug-in
на клиенте, давайте посмотрим что
мы построили. Классы BaseLevelGetter и HotTubLevelGetter оба являются
потомками CCmdTarget. Когда мы
создавали HotTubLevelGetter, мы указали Class
Wizard наследовать его от CCmdTarget. Напомним, что
каждый класс, созданный Class Wizard как
прямой потомок CCmdTarget, поддерживает
собственные CLSID и интерфейс IDispatch. Когда мы изменяем
базовый класс HotTubLevelGetter с CCmdTarget на BaseLevelGetter,
HotTubLevelGetter наследует виртуальные
методы BaseLevelGetter.
Когда
клиенту необходим доступ к HotTubLevelGetter, он выполняет обычный CoCreateInstance(...) - передавая CLSID HotTubLevelGetter и IID_ILevelGetter, и вызывая методы ILevelGetter. Когда выполняется
какой-либо метод, например, GetCurrentLevel, METHOD_PROLOGUE_EX_ берет значение pThis
из offset table, и pThis действительно
указывает на instance HotTubLevelGetter. То же вещь
происходит, когда мы используем m_pOwner (он также указывает на
instance HotTubLevelGetter); это немного
легче для понимания из-за того, что
мы можем наблюдать как выполняется
метод m_xLevelGetter.SetOwner( this
).
Давайте посмотрим на клиентское
приложение и установим некоторые
точки прерывания.
Откройте LevelViewer в папке LevelViewer2. Этот проект
почти идентичен первому варианту LevelViewer. OnFish позиционирован в BaseLevelGetter, а OnGas - в HotTubLevelGetter, как показано
далее.
//-----------------------------------------------------------
void CLevelViewerDlg::OnFish() //mapped to BaseLevelGetter
{
m_sLastCalled = _T("CheckedFish");
CLSID clsid;
HRESULT hRes = AfxGetClassIDFromString("BaseLevelGetterDLL.BaseLevelGetter", &clsid);
if(SUCCEEDED(hRes))
SetNewData(clsid, IID_ILevelGetter);
}
//------------------------------------------------------------
void CLevelViewerDlg::OnGas() //mapped to HotTubLevelGetter
{
m_sLastCalled = _T("CheckedGas");
CLSID clsid;
HRESULT hRes = AfxGetClassIDFromString("HotTubLevelGetterDLL.HotTubLevelGetter", &clsid);
if(SUCCEEDED(hRes))
SetNewData(clsid, IID_ILevelGetter);
}
Обе функции
вызывают SetNewData, передавая CLSID, созданный Class Wizard, и IID_ILevelGetter, описанный в ILevelGetter.H и включенный вLevelViewerDlg.H.
Замечание:
на закладке C++ , категория Preprocessor, добавьте ..\BaseLevelGetterDLL в качестве
дополнительного include каталога.
SetNewData работает точно также,
как и раньше. Постройте и слинкуйте
приложение, но перед запуском
поставьте точки прерывания на
каком-нибудь одном или на всех
методах интерфейса как показано
ниже.
Когда
выполнение остановится на точке
прерывания, воспользуйтесь режимом
"Step Into" (по клавише F8 или F11 в
зависимости от установленной
системы) и пошаговым проходом (F10)
дойдите до строки с указателем
объекта pThis или m_pOwner. Проверьте
значение. В зависимости от того,
подключил таймер HotTubLevelGetter или BaseLevelGetter,
pThis (или m_pOnwer) будут указывать на
правильный объект.
Как вы
видели, COM plug-ins - весьма мощный метод
разработки приложений, который
может использоваться в реальных
ситуациях, например, такой как
описана ниже.
Медицинская
страховая компания, которая
занимается разработкой заказных
планов страхования для крупных
компаний, приходит к вам за советом
относительно разработки новой Windows
системы. Каждый раз, когда они
добавляют новый план, они должны
разработать новую логику обработки
или внести небольшое исправление в
обрабатывающую логику для
существующего плана, но не должны
пересматривать каждую часть
функционального назначения. Но
поскольку они все время добавляют
планы в свою систему, модификация,
переопределение реализации или
клонирование апробированной
исходной программы непрактично,
поскольку это чревато потерей
целостности работающего кода,
независимо от аккуратности
программистов.
Как
разработчик COM на C++, вы понимаете
потребность в заменяемых
компонентах, которые поддерживают
полиморфизм. Рассмотрим следующее:
интерфейс IBasePlan, входящий в класс BasePlan, реализует 100 методов
интерфейса. Требования плана ABC
включают модификацию реализации 50
методов в интерфейсе IBasePlan. Требования плана
XYZ включают модификацию реализации
51 метода в интерфейсе IBasePlan, но 50 из них точно
такие же, как для плана ABC. Вместо
полного определения реализации для
каждого COM объекта, вы назначаете в BasePlan 100 виртуальных функций
члена C++ класса, по одной для
каждого метода в интерфейсе IBasePlan , как вышеприведенном
примере.
Поскольку у
вас есть ассоциированные
виртуальные функции в классе BasePlan, иерархия класса для
плана XYZ такова:
1. class BASE_PLAN_EXPORT BasePlan : public
CCmdTarget
Реализует IBasePlan, 100 методов интерфейса
и 100 ассоциированных виртуальных
функций члена C++ класса.
2. class ABC_PLAN_EXPORT ABCPlan : public
BasePlan
Наследуется
от BasePlan, использует 50
виртуальных функций члена C++ класса
в BasePlan и замещает 50
виртуальных функций BasePlan.
3. class XYZPlan : public ABCPlan
Наследуется
от ABCPlan, использует 49
виртуальных функций члена C++ класса
в BasePlan, использует 50
виртуальных функций члена C++ класса
в ABCPlan и замещает 1
виртуальную функцию BasePlan.
Каждый
компонент создается как отдельный
binary и COM объект. Каждый из них имеет
отдельный CLSID и, благодаря
структуре наследования, реализует
интерфейс IBasePlan. Применяя AppWizard и
Class Wizard, вы можете завершить
реализацию плана XYZ в течение
нескольких минут без какого-либо
затрагивания COM компонентов
базового класса. Все библиотеки COM
DLL размещаются на том же компьютере,
и если вы используете компонентные
категории или другие аналогичные
методы регистрации, приложение
клиента найдет план XYZ, как только
он будет зарегистрирован RegSvr32.