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

Расширения программ MS Office

Автор: Сергей Выдров
Источник: RSDN Magazine #5-2003
Опубликовано: 15.05.2004
Исправлено: 10.12.2016
Версия текста: 1.0
Зачем
Где
Как
Визуальный эффект
Архитектура
В заключение

"Вам и во сне не довелось
Представить, как несчастен лось."
Андрей Палыч Шевелев,
начало «Песенки про лося».

Когда говорят о программировании для MS Office (или «офисном программировании»), чаще всего подразумевается, что речь идет о написании макросов на языке Visual Basic, или, в крайнем случае, о разработке механизма передачи данных из разрабатываемой программы в MS Word или Excel. Между тем, приложения MS Office, обладая открытой архитектурой, позволяют расширять свою функциональность с помощью специальных дополнений – add-ins, которые далее я буду именовать плагинами (транслитерация английского синонима plug-in все же более привычна русскому уху и глазу, чем «эд-ин»). В этой статье я ставлю перед собой задачу вкратце рассказать о том, как написать такое дополнение.

Зачем

В первую очередь, следует задаться вопросом: а для чего, собственно, стоит писать свой плагин? Попробую ответить, для чего это понадобилось мне. Не стану скрывать, что я принадлежу к числу людей, считающих, что при всех недостатках продукции Microsoft, решения этой компании являются, как минимум, наименьшим из всех зол. Попросту говоря, пока что никто, на мой взгляд, ничего лучше не предложил. Поэтому для редактирования текстов я использую Word, для подготовки презентаций – Power Point, для просмотра вариантов расстановки мебели или конструирования принципиальных схем – Visio, а для создания web-страниц – FrontPage, причем пользуюсь самыми последними доступными мне версиями этих замечательных программ. В то же время мне не хватает многих возможностей, которые, впрочем, отсутствуют и в продуктах конкурентов. Для вставки разных объектов в документ или назначения фрагменту текста атрибута заданного языка вполне хватает встроенного редактора макросов. Но для выполнения сколько-нибудь нетривиальной задачи в рамках VB мне становится тесно, начинает не хватать многочисленных удобств привычного языка и IDE.

Может ли написание плагина быть коммерчески оправдано? Вполне, хотя бы потому, что плагины позволяют устанавливать обратную связь с вашими собственными приложениями. Думается также, что они вполне могут быть проданы и как независимый товар. Здесь мне хочется процитировать Илью Биллига, менеджера по маркетингу компании Microsoft (статья Большой секрет, опубликованная в журнале Компьютерра еще в 1997 году):

"Так какова же наиболее ясная перспектива программирования в России? Если вы не имеете гениальной идеи, которой хотите потрясти мир, то, на мой взгляд, проще всего зарабатывать деньги, программируя в рамках Microsoft Office. Кто-то скажет, что не барское это дело – на Бейсике программировать (как мы уже условились, для нас это не проблема – С.В.), да еще в Офисе мелкософтовском. Однако подумайте сами:

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

Где

В первую очередь имеет смысл сходить на http://www.microsoft.ru/offext. В этом русскоязычном разделе сайта редмондской компании можно найти много интересного относительно политики Microsoft в области независимых дополнений. Там также есть несколько статей и форум, но большой активности я не заметил.

На сайте КомпьютерПресс лежит статья Анатолия Тенцера, посвященная написанию дополнений к MS Office на Delphi. Стоит заметить, что Тенцер является соавтором книги Delphi 6 и технология COM, вышедшей в серии «Мастер-класс» и, как рискну предположить, автором того раздела, который посвящен офисному программированию.

Раз уж речь зашла о Delphi, расскажу об интересном предложении, которое предназначается специально для написания плагинов к MS Office в этой среде. Речь идет о wizards, предлагаемых компанией Afalina. Напоминает персонажа Джека Лондона, продающего лодки плывущим на Клондайк, у которого «хватило ума понять, где он нашел свой собственный Клондайк».

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

Из англоязычных ресурсов хотелось бы отметить статью Building an Office2000 COM Addin with VC++/ATL Amit Dey, посвященную написанию плагина для MS Outlook, которая лежит на codeguru.com и codeproject.com и, разумеется, MSDN, который, однако, в данном случае весьма скуп на информацию.

Как

Для создания собственного MS Office add-in понадобятся Visual Studio 6 или 7 и, собственно, MS Office 2000 или XP. Что же такое add-in? Это специальным образом зарегистрированный COM-сервер. Программисты, часто использующие COM, наверняка не раз сталкивались с задачей создать и обособить единую группу COM-объектов, реализующих один и тот же интерфейс. В рамках самой технологии COM специальных средств для этого не предусмотрено, а потому можно завести специальный объект-«регистратор» или же создать отдельный раздел в реестре. Архитекторы MS Office пошли по второму пути – вот как должен выглядеть раздел, создаваемый плагином:

HKEY_CURRENT_USER\Software\Microsoft\Office\<App>\AddIns\<Project.Class>.

Здесь <App> – это имя приложения, для которого регистрируется плагин, Project – имя сервера и Class – имя кокласса, реализующего нужный интерфейс. В качестве имени кокласса для написания офисных расширений традиционно выбирают Connect:


Рисунок 1. Регистрация расширений в реестре

В этом разделе должны быть созданы два параметра: строковый FriendlyName – имя расширения, которое увидит конечный пользователь, и DWORD LoadBehavior – способ запуска расширения. Ниже представлен список возможных значений параметра LoadBehavior:

На практике, однако, чаще всего используются значения 3 и 9, как булевы слияния Bootload и Connected или DemandLoad и Connected. Еще один параметр – DWORD CommandLineSafe – является опциональным и содержит флаг (0x00 – False, 0x01 – True) на разрешение запуска плагина, если основное приложение запущено без оконного интерфейса. Очевидно, речь идет о печати и тому подобных вещах.

ПРИМЕЧАНИЕ

Если вы хотите, чтобы ваш плагин был доступен всем пользователям компьютера, аналогичная информация должна быть внесена в ветку HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office\.

Второе – и последнее – требование к COM-серверу, который должен выступать как плагин к MS Office – это корректная реализация коклассом интерфейса _IDTExtensibility2. Здесь будет уместно вставить пару слов о расширяемой архитектуре приложений Microsoft. На радость независимым разработчикам эта фирма унифицировала интерфейсы (в том числе и интерфейсы подключения) для всех программ MS Office, Visio, FrontPage, Project и даже Studio.NET. Будучи зарегистрированным расширением для разных приложений, один и тот же плагин за счет позднего связывания способен добавлять функциональность в любое из них. Эта унификация безусловно облегчает работу программиста.

Приступим к реализации. Visual Studio.NET содержит wizard, облегчающий написание каркаса плагина. На выбор предлагаются C#, C++(ATL) и VB. По ряду причин я остановился на варианте с ATL. На самом деле этот wizard довольно бесполезен (так же, как и, например, wizard для написания Direct3D-приложений), а кое в чем даже вреден, о чем речь пойдет далее. Все, что он делает – это создает проект внутрипроцессного COM-сервера на базе ATL, который реализует _IDTExtensibility2 и регистрирует сервер в реестре, как это было описано выше.

Само собой разумеется, что для того, чтобы реализовать интерфейс, его надо описать самому или импортировать из бинарной библиотеки типов. Вот здесь и скрывается вредное последствие автоматической генерации кода. Рассмотрим кусок файла stdafx.h, который был сгенерирован wizard’ом:

      #pragma warning( disable : 4278 )
#pragma warning( disable : 4146 )
  // The following #import imports the _IDTExtensibility2 interface based on // it's LIBID#import"libid:AC0714F2-3D04-11D1-AE7D-00A0C90F26F4"
               version("1.0") lcid("0")  raw_interfaces_only named_guids

	//The following #import imports MSO based on it's LIBID#import"libid:2DF8D04C-5BFA-101B-BDE5-00AA0044DE52"
              version("2.2") lcid("0") raw_interfaces_only named_guids
#pragma warning( default : 4146 )
#pragma warning( default : 4278 )

В первой директиве #import импортируется библиотека типов, в которую включено описание _IDTExtensibility2. А вот вторая директива импортирует многочисленные типы, которые являются общими для всех приложений MS Office (она хранится в файле mso.dll). Все бы ничего, но вот авторы wizard’а, видимо, решили, что хороший программист пишет с применением голых интерфейсов и заботливо вставили параметр raw_interfaces_only. Нет уж, с меня довольно и того, что я знаю, что происходит в результате вызова AddRef() и Release(), вызывать их вручную я не намерен, а пользоваться свойствами я хочу именно как свойствами, а не через put и get. Так что я вычистил вторую директиву (тем более, что с точки зрения педанта она содержала ошибки) и завел себе вот такой файл, который и предлагаю вашему вниманию.

      #pragma once

// Этот файл импортирует типы данных  MS OfficeXP и MS FrontPage2002.// Он должен быть включён в любой файл, в котором используются эти типы.// LibIDs используется вместо имён файлов для того, чтобы решить проблемы // с переносимостью. Импортируется tlb из Program Files\Common Files\Microsoft// Shared\Office10\mso.dll#import"libid:2DF8D04C-5BFA-101B-BDE5-00AA0044DE52"\
  rename("RGB","_RGB")\
  rename("DocumentProperties","_DocumentProperties")\
  rename("FindText","_FindText")
usingnamespace Office;

// Импортируется tlb из Program Files\Microsoft Office\Office10\FPEDITAX.DLL#import"libid:7577AE81-4416-11CE-9C34-524153480000" rename("FindText","_FindText") exclude("tagREADYSTATE")
usingnamespace FrontPageEditor;

// Импортируется tlb из // Program Files\Common Files\Microsoft Shared\VBA\VBA6\VBE6EXT.OLB#import"libid:0002E157-0000-0000-C000-000000000046"usingnamespace VBIDE;

// импортируется tlb из Program Files\Microsoft Office\Office10\FRONTPG.EXE#import"libid:3824BCD5-7042-11CE-8E17-0020AF9F9648"usingnamespace FrontPage;

Этот файл использовался для написания плагина к FrontPage2002. Обратите внимание, что после импорта происходит объединение импортируемого пространства имен с глобальным. Это необходимо, потому что часть файлов ссылается на типы, определенные в других библиотеках.

СОВЕТ

Как можно заметить, для импортирования файлов я (вслед за авторами wizard’а) выбрал не очень распространенный способ с применением libid. Что это дает? При переносе проекта на другую машину у меня не будет проблем с тем, что файлы, возможно, находятся в другом каталоге. Очевидные минусы: невозможно использовать __uuidof() и поэтому приходится таскать неудобоваримого вида GUID’ы. Как бы то ни было, такая возможность может оказаться полезной для вас.

Теперь рассмотрим сам интерфейс _IDTExtensibility2 на примере файлов connect.h и connect.cpp, которые создает wizard:

      // Connect.h
      //_IDTExtensibility2 implementation:
STDMETHOD(OnConnection)(IDispatch * Application,
                        AddInDesignerObjects::ext_ConnectMode ConnectMode,
                        IDispatch *AddInInst,
                        SAFEARRAY **custom);
STDMETHOD(OnDisconnection)(AddInDesignerObjects::ext_DisconnectMode RemoveMode,
                           SAFEARRAY **custom );
STDMETHOD(OnAddInsUpdate)(SAFEARRAY **custom );
STDMETHOD(OnStartupComplete)(SAFEARRAY **custom );
STDMETHOD(OnBeginShutdown)(SAFEARRAY **custom );

CComPtr<IDispatch> m_pApplication;
CComPtr<IDispatch> m_pAddInInstance;

// Connect.cpp
STDMETHODIMP CConnect::OnConnection(IDispatch *pApplication,
                           AddInDesignerObjects::ext_ConnectMode ConnectMode,
                           IDispatch *pAddInInst, SAFEARRAY ** /*custom*/ )
{
  pApplication->QueryInterface(__uuidof(IDispatch), (LPVOID*)&m_pApplication);
  pAddInInst->QueryInterface(__uuidof(IDispatch), (LPVOID*)&m_pAddInInstance);
  return S_OK;
}

STDMETHODIMP CConnect::OnDisconnection(AddInDesignerObjects::ext_DisconnectMode,
                                       SAFEARRAY **)
{
  m_pApplication = NULL;
  return S_OK;
}

STDMETHODIMP CConnect::OnAddInsUpdate (SAFEARRAY ** /*custom*/ )
{
  return S_OK;
}

STDMETHODIMP CConnect::OnStartupComplete (SAFEARRAY ** /*custom*/ )
{
  return S_OK;
}

STDMETHODIMP CConnect::OnBeginShutdown (SAFEARRAY ** /*custom*/ )
{
  return S_OK;
}

Итак, интерфейс содержит 5 методов. OnConnection вызывается приложением в момент загрузки плагина. Посмотрим, какие параметры за что отвечают.

Первый параметр – IDispatch* Application – является ссылкой на экземпляр приложения, в которое был загружен плагин. Как можно видеть, по умолчанию предлагается сохранить эту ссылку в виде указателя на dispinterface. С точки зрения объектно-ориентированного дизайна такое решение, пожалуй, не совсем корректно. В самом деле, для кокласса Connect это совершенно избыточная информация. Имеет смысл использовать эту ссылку для инициализации объектов, обеспечивающих функциональность.

Вторым параметром идет AddInDesignerObjects::ext_ConnectMode ConnectMode. AddInDesignerObjects – это пространство имен, в котором находится описание самого интерфейса _IDTExtensibility2. ConnectMode может принимать следующие значения:

Первое используется, когда пользователь подключил плагин вручную, второе – когда плагин был автоматически загружен приложением. Прочие значения из этого перечисления, если верить MSDN, не используются. Однако мне попадалась статья, где упоминалось, что этот аргумент может принимать значение ext_cm_External, если расширение было запущено извне MS Office. Кроме того, помните параметр SafeCommandLine в реестре? Логично было бы предположить, что в таких случаях приложения MS Office выставляют этот параметр в ext_cm_CommandLine. К сожалению, я не нашел времени на такие эксперименты (в основном потому, что не видел в этом практической необходимости).

Следующим параметром идет IDispatch* AddInInst, который представляет собой указатель на этот экземпляр плагина в коллекции COMAddIns приложения. Для чего его можно было бы применить, я пока не придумал.

Последний параметр – SAFEARRAY** custom – массив произвольных значений – может быть использован для «передачи пользовательских данных». Очевидно, речь здесь идет о сложной системе взаимно ссылающихся плагинов. Впрочем, в некоторых статьях из MSDN указано, что этот параметр не должен использоваться в рамках MS Office. То же самое касается аргумента custom в остальных методах.

В статье COM Add-ins in Detail by Peter Vogel указывается, что для метода OnConnection() первый элемент этого массива указывает на способ загрузки приложения:

1. Приложение запущено самостоятельно.

2. Приложение запущено как внедренное в другой документ

3. Приложение запущено через OLE Automation (например, с помощью CreateObject)

Метод OnDisconnection вызывается перед выгрузкой плагина. Параметр RemoveMode может принимать следующие значения:

Первое значение используется, когда приложение заканчивает свою работу, второе – когда пользователь выгружает плагин вручную.

Пару слов о загрузке: если во время вызова методов этого интерфейса происходит сбой, приложение автоматически отключает такой плагин, предварительно запросив разрешение пользователя при следующем запуске. Заново включить его можно в окне About (кнопка «Откл. объекты…» в приложениях MS Office XP). Я думаю, что при отладке эта информация может пригодиться. Следующий метод, OnStartupComplete, вызывается, когда все объекты, включая прочие плагины, уже загружены в память. В этот момент можно проверить состояние коллекции COMAddIns. Также, если ваше расширение предполагает открытие окон при загрузке (например, Tip Of the Day или мастер создания нового документа), то в этом методе должен быть активизирован соответствующий объект.

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

Наконец, метод OnAddInsUpdate вызывается, когда состояние коллекции COMAddIns было изменено, например, в случае загрузки или выгрузки прочих расширений. Как видим, интерфейс _IDTExtensibility2 предоставляет максимум возможностей по управлению взаимодействием расширений.

Визуальный эффект

Давайте создадим какой-нибудь наглядный эффект, например, добавим кнопку на панель инструментов. Для этого напишите такую реализацию метода OnConnection:

_ApplicationPtr pApp = NULL;
try
{

  // Запрос интерфейса _Application.
  pApp = pApplication;
}
catch (...)
{
  return E_INVALIDARG;
}


// Попытаемся получить доступ к нужной панели инструментов
CommandBarPtr pCommandBar = pApp->CommandBars->GetItem(_T("Formatting"));
if (NULL == pCommandBar) 
  return;


// Если кнопка уже создана, получаем указатель на нее, иначе создаем новую
m_pButton = pCommandBar->FindControl(msoControlButton,
                                vtMissing,
                                _T("{0BC9C674-86F5-40f8-BC67-526D0AA0B935}"),
                                vtMissing,
                                msoFalse);

if (NULL == m_pButton)
{
  m_pButton = pCommandBar->Controls->Add(msoControlButton,
                                         vtMissing,
                                         vtMissing,
                                         1,
                                         vtMissing);

  // Устанавливаем атрибуты
  m_pButton->Style = msoButtonCaption;
  m_pButton->Caption = _T("CaptionText");
  m_pButton->TooltipText = _T("TooltipText");
  m_pButton->Tag = _T("{0BC9C674-86F5-40f8-BC67-526D0AA0B935}");
}

// m_Handler.Advise(m_pButton);

При этом потребуется добавить поле

_CommandBarButtonPtr m_pButton;

в класс Connect. Пока не обращайте внимания на закомментированную последнюю строку. В методе OnDisconnection напишите:

      // m_Handler.Unadvise(m_pButton);
m_pButton = NULL;

Теперь при следующем запуске приложения на панели инструментов Formatting (или Форматирование, в зависимости от локализации) появится новая кнопка без рисунка с надписью CaptionText. При наведении на нее курсора будет появляться подсказка TooltipText. Разберем, как работает этот фрагмент.

Сначала мы получаем smart pointer на экземпляр приложения. Далее, из коллекции панелей инструментов извлекаем панель инструментов с заголовком Formatting. Если такая панель недоступна, метод прекращает свою работу. Если же доступ к панели разрешен, мы пытаемся найти созданную во время прошлой сессии кнопку по ее тегу. Если такая кнопка не найдена, мы создаем ее заново, после чего выставляем атрибуты: стиль, текст, подсказку, а также тот самый тег, по которому она может быть найдена при следующем запуске. Обратите внимание, что при отключении кнопка не уничтожается, а при подключении атрибуты задаются только один раз, хотя в большинстве примеров, которые я видел, рекомендуется уничтожать свои элементы управления. Дело в том, что пользователь может сменить заголовок у кнопки или назначить ей рисунок.

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

Чтобы уверенно владеть материалом, вам надо быть знакомым с Connection Point, реализацией диспинтерфейса, а также шаблоном ATL IDispEventSimpleImpl. Впрочем, здесь я не буду забираться в такие дебри, а покажу заведомо работоспособный, но не совсем корректный способ обработки событий.

Здесь потребуется реализация диспинтерфейса _CommandBarButtonEvents:

      struct
      __declspec(uuid("000c0351-0000-0000-c000-000000000046"))
_CommandBarButtonEvents : IDispatch
{
  // Методы:
  HRESULT Click
  (
    struct _CommandBarButton * Ctrl,
    VARIANT_BOOL * CancelDefault );
};

Создайте класс EventsHandler:

      class EventsHandler : _CommandBarButtonEvents
{
public:

  EventsHandler(){}

private:
  // Методы IUnknown и IDispatch.virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppvObj)
  {
    if (iid == __uuidof(IUnknown) ||
                 iid == __uuidof(IDispatch) ||
                 iid == __uuidof(_CommandBarButtonEvents))
    {
      *ppvObj = this;
      return S_OK;
    }
    return E_NOINTERFACE;
  }

  virtual ULONG STDMETHODCALLTYPE AddRef(){return 0;}
  virtual ULONG STDMETHODCALLTYPE Release(){return 0;}
  virtual HRESULT STDMETHODCALLTYPE GetTypeInfoCount(UINT*){return S_OK;}
  virtual HRESULT STDMETHODCALLTYPE GetTypeInfo(UINT, LCID, ITypeInfo**)
  {
    return E_NOTIMPL;
  }
  virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames(REFIID,
                                                  LPOLESTR*,
                                                  UINT,
                                                  LCID,
                                                  DISPID*)
  {return E_NOTIMPL;}

  virtual HRESULT STDMETHODCALLTYPE Invoke(DISPID dispIdMember,
                                           REFIID,
                                           LCID,
                                           WORD,
                                           DISPPARAMS *pDispParams,
                                           VARIANT* pVarResult,
                                           EXCEPINFO* pExcepInfo,
                                           UINT* puArgErr);
    // Connection cookie.
    DWORD m_dwCookie;

  public:

  // Вызывается для подключения к событиямvoid Advise(_CommandBarButtonPtr pButton);
  // Вызывается для отключения от событийvoid Unadvise(_CommandBarButtonPtr pButton);
};

Хитрость состоит в том, что наш обработчик будет являться членом класса, а потому за подсчетом ссылок можно не следить. Кроме того, приложения Office не вызывают других методов dispinterface, кроме Invoke. Поэтому надо будет реализовать всего два метода: IUnknown::QueryInterface (как показано выше) и IDispatch::Invoke (как это будет показано ниже).

      void EventsHandler::Advise(_CommandBarButtonPtr pButton)
{
ATL::AtlAdvise(pButton,
               (LPUNKNOWN)this,
               __uuidof(_CommandBarButtonEvents),
               &m_dwCookie);
}

void ButtonFunctionalityEventsHandler::Unadvise(_CommandBarButtonPtr pButton)
{
  ATL::AtlUnadvise(pButton, __uuidof(_CommandBarButtonEvents), m_dwCookie);
}

HRESULT EventsHandler::Invoke(DISPID dispIdMember,
                              REFIID,
                              LCID,
                              WORD,
                              DISPPARAMS *pDispParams,
                              VARIANT* pVarResult,
                              EXCEPINFO* pExcepInfo,
                              UINT* puArgErr)
{
  // No errors expected
  pExcepInfo = NULL;
  puArgErr	 = NULL;

  if (dispIdMember == 1)
  {
    ::MessageBox(NULL, _T("Привет из обработчика ошибок!"), _T("Привет!"), 0);
      return S_OK;
  }
  elsereturn DISP_E_MEMBERNOTFOUND;
}

Подключение к событиям происходит с помощью функций ATL Advise() и Unadvise(), инкапсулированных в наш класс (и в шаблон IDispEventSimpleImpl). Объявите в классе Connect экземпляр этого типа, раскомментируйте вызовы Advise() и Unadvise() – и все должно заработать. Разумеется, в любой реализации при обработке события лучше всего воспользоваться делегатами.

Архитектура

Переходим к самому, на мой взгляд, интересному: дизайну кода. Какая архитектура должна лежать в основе расширения? Я ответил для себя на этот вопрос так. Если проанализировать многочисленные плагины, начиная с VisualAssist’а для Visual Studio 6 и заканчивая RSDN Authoring Pack, с помощью которого я сейчас верстаю эту статью, окажется, что все они добавляют к базовым функциям приложения некоторое количество своих. Другими словами, почти очевидно, что имеет место быть класс Feature. Feature – это в данном случае некое взаимодействие с пользователем, точнее, некоторая возможность, предоставляемая пользователю. К способам такого взаимодействия относятся: взаимодействие через сопоставленные элементы управления (кнопка, выпадающий список, пункт меню) и редактирование документа, то есть реагирование на порождаемое при этом событие.

Простейшая иерархия классов, следовательно, выглядит так: во главе иерархии лежит абстрактный базовый класс Feature. От него наследуются два класса: Event и Control. Как очевидно из названия, первый обеспечивает обработку событий редактирования, второй – создание элемента управления и обработку его событий. Логично унаследовать от второго класса классы Button, MenuItem, ComboBox. Хорошей гарантией соблюдения подстановочного критерия Лисков послужила бы гомоморфность этой иерархии: все открытые (public) члены любого производного класса должны быть переопределениями виртуальных методов базового класса Feature, а добавление новых открытых членов запрещается. Вот как выглядит этот класс у меня:

      public:

  // Вызывается оболочкой, когда происходит подключение к приложениюvirtualvoid Create(_ApplicationPtr pApp) = 0;

  // Вызывается оболочкой, когда происходит отключение от приложенияvirtualvoid Disconnect() = 0;

Далее, все алгоритмы, которые обеспечивают функциональность (сортировка, поиск и замена, взаимодействие с ОС и т.п.) должны быть выстроены в параллельную иерархию классов, никак не пересекающуюся с первой. Например, если при щелчке по одной кнопке должен вызываться упрощенный алгоритм, а при щелчке по второй – расширенный, то класс расширенного алгоритма должен быть унаследован от упрощенного, а вот классы обеих кнопок должны быть равноправными наследниками класса Button.

Вот одно из ранних описаний класса Button (в этом варианте он напрямую наследовался от Feature):

      private:

  // Ссылка на приложение
  _ApplicationPtr m_pApplication;

  // Обработчик событий
  EventsHandler m_Handler;

  // Интерфейс объекта «Офисная кнопка»
  _CommandBarButtonPtr m_pButton;
 
protected:

  // Вызывается, чтобы определить название кнопкиvirtual LPCTSTR GetCaption() = 0;

  // Вызывается, чтобы определить текст подсказки для кнопкиvirtual LPCTSTR GetTooltipText() = 0;

  // Вызывается, чтобы определить изображение для кнопкиvirtual HBITMAP GetFace() = 0;

  // Вызывается, чтобы определить стиль кнопкиvirtual MsoButtonStyle GetButtonStyle() = 0;

  // Вызывается, чтобы определить название панели инструментов,// на которой создается кнопка.virtual LPCTSTR GetHostToolbarCaption() = 0;

  // Вызывается, чтобы определить строковый идентификатор кнопкиvirtual LPCTSTR GetID() = 0;

  // Вызывается при щелчке по кнопке. Метод должен вернут TRUE, если обработчик// был завершен или FALSE, если прерван.virtual BOOL OnClick(_ApplicationPtr pApp) = 0;

  friendclass EventsHandler;

Обратите внимание, что ссылка на экземпляр кнопки приложения сделана защищенным полем и передается обработчиком событий в качестве параметра методу OnClick() (именно поэтому обработчик был сделан дружественным классом).

Как же управляться со всеми этими наследниками Feature? Я проштудировал «энциклопедию паттернов» «Банды Четырех» в надежде найти подходящее решение. Увы, но ни один порождающий паттерн, как я понял позже, не подходит (сначала я реализовал абстрактную фабрику классов). Давайте сформулируем, как говаривали большевики в пересказе моей школьной учительницы истории, «программу-максимум». Независимые друг от друга возможности программы должны компилироваться независимо друг от друга. При добавлении новых возможностей ни один из классов, обеспечивающих прежнюю функциональность, не должен быть перекомпилирован. И, желательно, не должен перекомпилироваться кокласс Connect и другие классы подключения. Именно выполнение последнего требования абстрактная фабрика классов обеспечить не смогла. Наметанный глаз уже увидел, что здесь необходим механизм регистрации. Идеальным вариантом послужили бы независимые COM-серверы, но ведь плагин сам является COM-сервером, следовательно, такое решение отсекается бритвой Оккама.

В результате я завел класс, служащий пространством имен, для статического массива features. Для каждого наследника feature я создал с помощью макроса вспомогательный класс-регистратор, который объявляет статический экземпляр feature и передает указатель на него в массив. На OnConnection() у каждого экземпляра кокласс Connect вызывал метод Create(). Не будучи особо изящной, эта схема в данный момент кажется мне и моим коллегам единственно возможной. Если вы сумеете опровергнуть эту теорему несуществования, автор будет благодарен вам от всей души.

ПРЕДУПРЕЖДЕНИЕ

В данной архитектуре заложена одна широко распространенная проблема. Порядок создания объектов, увы, недетерминирован, а, следовательно, мы оказываемся в ситуации с «гонками порождения». Имеется два типа конкуренции – массива с объектами и объектов между собой. Обе этих проблемы я решил, но по-разному. Массив я убрал из объявления класса, введя статическую функцию GetArray():

private:

static FeaturesArray& GetArray()

{

static FeaturesArray Features;

return Features;

}

Доступ к этой функции извне я запретил, обеспечив вместо этого интерфейс регистрации. Конкуренцию же объектов между собой я оставил «на откуп» самим объектам, введя понятие приоритета в базовые классы, от которых наследуются конкретные функциональности.

В заключение

"Что лось? Вот, скажем, дикобраз!
Но это в следующий раз..."
Андрей Палыч Шевелев,
окончание «Песенки про лося».

Время, отпущенное мною самому себе на эту статью, закончилось, а нерассмотренными остались ничуть не менее интересные вещи:

...и многое, многое другое. Я очень надеюсь, что этот материал пригодится вам в написании расширений для MS Office, когда они вам понадобятся, а также, что кто-нибудь продолжит заявленную тему. Засим прощаюсь, и, отложив неловкое перо, снова берусь за компилятор и дебаггер. Связаться со мной вы можете по e-mail: mailto:msnork@sibniinp.ru.


Эта статья опубликована в журнале RSDN Magazine #5-2003. Информацию о журнале можно найти здесь
    Сообщений 0    Оценка 420        Оценить