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

Создание оснастки для консоли управления с ATL – легче легкого

Автор: Ray F. Djajadinata
The IntelliSys Corp.

Перевод: Алифанов Андрей
The RSDN Group

Введение
Что делает оснастку оснасткой?
И снова Документ/Вид!
Мастер
Что это за оснастка?
Еще один CoClass
Быстрый взгляд на реализацию Вида
Расширения, вносимые ATL в программную модель
Перечисление процессов
Добавление узлов в оснастку
Игра Мастера
Всегда считайте своих детей
Добавляем контекстное меню
Включение ATL волшебства: расширение возможностей обработки меню
Дефект волшебства: исправляем ошибку в ATL

Демонстрационный проект - 106KB

Введение

Я думаю, одной из причин успеха Microsoft является то, что эта фирма пытается сделать все легким в использовании. Например, Microsoft так разрабатывает свои продукты, что, получив представление об одном, Вы с легкостью можете применить полученные знания при работе с другим. Один из примеров такого подхода – интеграция Visual Basic и Office. Другой хороший пример – использование COM везде, начиная от API трехмерной графики и заканчивая доступом к базам данных. Еще один пример – встраивание Internet Explorer в Windows, что дает пользователю единую среду для работы, как с ресурсами собственного компьютера, так и для доступа к Сети.

Microsoft Management Console, в дальнейшем MMC – это как раз продукт вышеописанного подхода, позволяющий легко выполнять разнообразные задачи. Использование MMC для пользователей и администраторов означает, что им больше не нужно изучать различные пользовательские интерфейсы разных приложений. Вместо этого они должны знать только интерфейс MMC, который очень похож на интерфейс Windows Explorer. То есть слева имеется так называемая "панель разделов", а справа "панель результатов", в которой отображается результат выбора соответствующего пункта левой панели.

Для нас важно, что MMC предоставляет единую программную модель для написания управляющих и административных приложений. Эта модель задает правила взаимодействия между приложениями для MMC (известными как оснастки) и MMC как таковой. Она основана на COM-интерфейсах и потому легка для понимания. Библиотека ATL 3.0 добавляет уровень абстракции в эту модель, значительно облегчая написание оснасток. Знакомство с разработкой оснасток и с механизмами ATL позволит вам подготовиться к приходу Windows NT 5.0, оп, Windows 2000, в которой все управляющие/административные приложения работают внутри MMC.

ПРИМЕЧАНИЕ

Оригинал статьи вышел в январе 1999 года. Поэтому прошу не удивляться высказываниям автора.

Что делает оснастку оснасткой?

Оснастка – это не более чем COM-объект. Но какими свойствами должен обладать этот объект, чтобы стать оснасткой? Во-первых, он должен находиться во внутрипроцессном сервере. Во-вторых, он должен создать несколько записей в Реестре под следующими ключами:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MMC\NodeTypes

и

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MMC\SnapIns

кроме своих обычных записей. Самый простой путь для этого – добавить их в сценарий регистрации и позволить Регистратору сделать остальное. Мастер создания оснасток сделает это автоматически при генерации проекта, но вы можете (а иногда должны) добавить дополнительные записи.

Это все, что требуется для того, чтобы COM-объект считался оснасткой. Но чтобы он стал реальной оснасткой, еще нужно реализовать несколько интерфейсов. Их достаточно много, но наиболее важные – это IComponentData и IComponent. Для их реализации необходимо использование другой группы интерфейсов – реализованных собственно MMC. Наиболее важные – это IConsole и IConsoleNameSpace, используемые для управления панелями разделов и результатов.

И снова Документ/Вид!

Архитектура MMC во многом похожа на хорошо знакомую архитектуру Документ/Вид. Оснастка может выдавать множество представлений одних и тех же данных. Интерфейс IComponentData – это Документ, а IComponent – Вид. Каждый экземпляр оснастки может иметь любое количество компонентов IComponent, обычно по одному на каждое дочернее MDI окно (MMC – это многодокументное MFC приложение, и оснастки выполняются в его дочерних окнах), но только один компонент IComponentData (каждый раз, когда вы добавляете оснастку через диалог "Добавить/Удалить", вы получаете ее новый экземпляр). Методы интерфейса IComponentData взаимодействуют с элементами в панели разделов, перечисляют их, предоставляют информацию о них, и т.д. Методы интерфейса IComponent используются в панели результатов. Они определяют, какой Вид должен быть предоставлен пользователю, перечисляют элементы в панели и т.д.

Мастер

Настало время показать вам возможности Мастера создания оснасток. Прежде всего, создайте обычный ATL проект. Помните, что нужно выбрать Dynamic link Library для типа сервера. Затем выберите Insert, New ATL Object, и вы увидите знакомое диалоговое окно ATL Object Wizard. Выберите категорию Objects и тип объекта MMC Snapin.

Появится диалоговое окно подобное изображенному на рисунке 1. Я назвал эту оснастку VCDSnapin1. Теперь выберите закладку MMC Snapin, выключите галочку Supports Persistence, и включите IExtendContextMenu. Нажмите кнопку OK, и Мастер сгенерирует для вас четыре класса, как показано на рисунке 2.


Рисунок 1.


Рисунок 2.

Что это за оснастка?

Самый простой класс из четырех – CVCDSnap1About. Этот класс реализует интерфейс ISnapinAbout и является полноценным COM-объектом, который может создаваться извне. Реализация методов этого класса прямолинейна и их имена ясно описывают назначение. Я добавил значок, представляющий оснастку и изменил представление статической папки.

Чтобы использовать свой значок, используйте метод GetSnapinImage:

STDMETHOD(GetSnapinImage)(HICON *hAppIcon)
{
  if (hAppIcon == NULL)
    return E_POINTER;

  *hAppIcon = static_cast<HICON>(::LoadImage(_Module.GetResourceInstance(),
                                             MAKEINTRESOURCE(IDI_SNAPINIMAGE),
                                             IMAGE_ICON, 0, 0, LR_DEFAULTCOLOR));

  return S_OK;
}

А чтобы сменить изображение папки, используйте метод GetStaticFolderImage:

STDMETHOD(GetStaticFolderImage)(HBITMAP *hSmallImage,
                                HBITMAP *hSmallImageOpen,
                                HBITMAP *hLargeImage,
                                COLORREF *cMask)
{
  *hSmallImage = LoadBitmap(_Module.GetResourceInstance(),
                            MAKEINTRESOURCE(IDB_VCDSNAP1_16));
  *hSmallImageOpen = LoadBitmap(_Module.GetResourceInstance(),
                                MAKEINTRESOURCE(IDB_VCDSNAP1_16OPEN));
  *hLargeImage = LoadBitmap(_Module.GetResourceInstance(),
                            MAKEINTRESOURCE(IDB_VCDSNAP1_32));
  return S_OK;
}

Информация, предоставляемая MMC через эти методы, будет показана, когда вы выберете оснастку в окне Добавить/Удалить оснастку и нажмете кнопку О программе (эта кнопка будет доступна, только если оснастка поддерживает интерфейс ISnapinAbout). Для примера смотрите рисунок 3.


Рисунок 3.

Еще один CoClass

Если вы откроете файл VCDSnapin1.idl, то увидите, что он содержит только два создаваемых извне класса – VCDSnapin1About и VCDSnapin1. Это верно всегда, независимо от того, используете вы ATL или нет. Один из этих классов является необязательным и реализует интерфейс ISnapinAbout, а другой является основным классом оснастки и реализует интерфейс IComponentData.

Итак, в этом проекте класс CVCDSnap1 является основным и наследуется от IComponentDataImpl (ATL реализации интерфейса IComponentData) и IExtendContextMenu. Однако, в нем определено очень небольшое количество функций, так как большинство из них реализовано в классе IComponentDataImpl. ObjectMain() – это простая статическая функция, регистрирующая специфичный для MMC формат буфера обмена для дальнейшего использования в методе IDataObject::GetDataHere().

Быстрый взгляд на реализацию Вида

Как я сказал ранее, в MMC наш "класс Вид" – это класс, реализующий интерфейс IComponent. В моем проекте это класс CVCDSnap1Component, наследующийся от IComponentImpl. И снова, класс-предок выполняет за нас всю основную работу. Единственный переопределенный Мастером метод – это Notify(), хотя никто не мешает вам переопределить нужные методы самостоятельно.

Расширения, вносимые ATL в программную модель

При написании кода на уровне SDK вы сталкиваетесь с одной серьезной проблемой: приходится обрабатывать один объект в разных местах, что делает код трудным для чтения и подверженным ошибкам. Так происходит, потому что интерфейсы MMC разрабатывались без учета концепции "элемента".

Например, возьмем элемент в панели разделов. У него есть разные "свойства", такие как текстовый заголовок, тип узла (GUID, задающий определенный вид узла), указатель на IDataObject, контекстные меню и т.д. Одни из них видимы, другие – нет. К этим свойствам нельзя получить доступ в едином, согласованном стиле. Чтобы управляться с элементами, приходится дробить код на методы. Большинство этих методов принимает специальный параметр, уникальным образом идентифицирующий элемент (cookie), который может использоваться для определения обрабатываемого в данный момент элемента.

Например, для того, чтобы предоставить MMC указатель на интерфейс IDataObject для данного элемента, вы реализуете метод QueryDataObject(), принимающий cookie и возвращающий класс, реализующий интерфейс IDataObject (а именно метод GetDataHere()) для данного элемента. Для установки его заголовка вы реализуете метод GetDisplayInfo(), который принимает структуру, опять же содержащую cookie. А как насчет обработки событий? Обработка сообщений окном – достаточно прямолинейный процесс, проходящий в его собственной оконной процедуре. Но для элементов или узлов MMC обработчик можно найти в двух местах: IComponentData::Notify() и IComponent::Notify().

ATL решает эту проблему, помещая всю эту кашу из обработчиков в C++ классы. Как обычно, имеется абстрактный базовый класс <X>, и класс <X>Impl, предоставляющий реализацию по умолчанию. Эти классы называются ISnapInItem и ISnapInItemImpl, соответственно. Использование классов, наследующих от ISnapInItemImpl, позволяет поместить обработчики уведомлений в "оконную процедуру" класса-элемента, сохранить информацию об элементе в переменных-членах класса и т.д. В моем проекте этот класс называется CVCDSnap1Data и представляет корневой узел оснастки.

Трюк, стоящий за всем этим волшебством, достаточно прост. Как я уже говорил, код, обрабатывающий каждый элемент, разбросан по нескольким интерфейсам с несколькими методами в каждом. Все, что делает ATL - направляет вызовы этих методов в сам элемент. Давайте рассмотрим пример перенаправления вызова метода интерфейса IComponentData. Этот метод принимает один аргумент, указатель на структуру SCOPEDATAITEM:

STDMETHOD(GetDisplayInfo)(SCOPEDATAITEM *pScopeDataItem);

В MMC есть два типа элементов: элементы панели разделов и элементы панели результатов. Первые представлены структурой SCOPEDATAITEM, вторые – структурой RESULTDATAITEM. Каждая из структур имеет поле lParam, хранящее cookie для данного элемента. Сущность магии ATL в том, что в этом поле хранится указатель на сам объект – элемент. Давайте посмотрим, что ATL делает в этом методе (взят из ATLSNAP.H, с некоторыми изменениями):

STDMETHOD(GetDisplayInfo)(SCOPEDATAITEM *pScopeDataItem)
{
  // Здесь отладочный код
  if (pScopeDataItem == NULL)
    // предупреждение
  else
  {
    // Преобразуем cookie к указателю на объект
    CSnapInItem* pItem = (CSnapInItem*)pScopeDataItem->lParam;
    if (pItem == NULL)
      pItem = m_pNode;
        .            
        .
    if (pItem != NULL)
      hr = pItem->GetScopePaneInfo(pScopeDataItem);
  }

  return hr;
}

Как видите, вместо того, чтобы определять порядок действий на основе значения cookie, ATL просто передает ответственность за обработку вызова самому элементу через преобразованный cookie. Если вы посмотрите на реализацию многих других методов в файле ATLSNAP.H, увидите, что они поступают так же. Такой подход позволяет собрать код, ранее размазанный по разным интерфейсам, в одном месте: в классе – наследнике ISnapInItemImpl.

Перечисление процессов

Я написал демонстрационную оснастку, которая перечисляет все запущенные в системе процессы вместе с используемыми DLL. Я использовал возможности, предоставляемые Windows NT, так как полагаю, что большинство из читающих эту статью, работают в этой ОС (я также использовал Unicode-версии некоторых функций, так что пример в любом случае не будет работать в Win9x). Чтобы скомпилировать этот код, нужно иметь файлы PSAPI.H и PSAPI.LIB, которые находятся на CD с Visual C++. Также нужна библиотека PSAPI.DLL, которая обычно находится в каталоге %WINDOWS%\SYSTEM32.

Добавление узлов в оснастку

В моей оснастке перечисляются процессы, и, для каждого процесса, использующиеся ими DLL. Таким образом, эта оснастка имеет, по крайней мере, два типа узлов. Чтобы добавить узлы, я использовал простейший способ:

  1. Скопируйте объявление класса элемента (в моем проекте CVCDSnap1Data) и вставьте его в тот же файл, ниже исходного класса.
  2. Замените все имена CVCDSnap1Data на выбранное мной для элемента имя – CDllNode.
  3. Теперь загляните в .cpp файл (в моем проекте это VCDSnap1.cpp). В самом начале этого файла вы найдете код инициализации нескольких статических переменных. Скопируйте этот код и просто измените имена. Не забудьте также изменить GUIDы, потому что теперь мы работаем с другим узлом. Мои объявления выглядят следующим образом:
static const GUID CDllNodeGUID_NODETYPE =
  { 0x5af13f00, 0x6144, 0x11d2, { 0xa8, 0xd4, 0xe8, 0xba, 0x5d, 0x0, 0x0, 0x0 } };
const GUID*  CDllNode::m_NODETYPE = &CDllNodeGUID_NODETYPE;
const OLECHAR* CDllNode::m_SZNODETYPE = OLESTR("5AF13F00-6144-11d2-A8D4-E8BA5D000000");
const OLECHAR* CDllNode::m_SZDISPLAY_NAME = OLESTR("VCDSnap1");

const CLSID* CDllNode::m_SNAPIN_CLASSID = &CLSID_VCDSnap1;
  1. Скопируйте методы класса CVCDSnap1Data и измените имя класса.
  2. Проделайте то же для второго узла, который будет представлять запущенные процессы. Я назвал его CProcessNode.

Не забудьте добавить код в .rgs файл, чтобы зарегистрировать новые узлы в Реестре. Необходимо добавить GUIDы новых узлов под записями, сделанными Мастером. Добавьте их в ключи NodeTypes и NoRemove NodeTypes. Это должно выглядеть примерно так:

NodeTypes
{
   <Entry made by the Wizard>
   // Добавленные нами записи
   {9F5493AB-5C5D-11D2-A8C5-2450A8000000} 
   {5AF13F00-6144-11d2-A8D4-E8BA5D000000} 
}

NoRemove NodeTypes
{
   <Entry made by the Wizard>
   // Добавленные нами записи
   ForceRemove {9F5493AB-5C5D-11D2-A8C5-2450A8000000}
   {
   }
   ForceRemove {5AF13F00-6144-11d2-A8D4-E8BA5D000000}
   {
   }
}

Каждый класс в примере, представляющий узел, имеет закрытые члены, хранящие информацию о данном узле. CVCDSnap1Data, например, имеет переменную типа std::vector, в которой хранятся "умные указатели" (я использовал std::auto_ptr) на CProcessNode. Каждый объект типа CProcessNode, в свою очередь, хранит вектор из std::auto_ptr<CDllNode>.

ПРИМЕЧАНИЕ

По причине использования конструкций типа std::vector<str::auto_ptr<T> > проект не будет компилироваться, если используется современная версия STL. Кроме того, в примере используется новая версия библиотеки ввода-вывода Microsoft, несовместимая с STL от SGI.

Игра Мастера

Вы ведь знаете, этим Мастерам никогда нельзя полностью доверять. И "Мастер ATL оснасток" – не исключение. Кто-то из вас наверняка уже заметил, что в .rgs файле кое-чего не хватает. А где же код регистрации кокласса CVCDSnap1About? Оказывается, Мастер использовал для него макрос DECLARE_REGISTRY() вместо обычного DECLARE_REGISTRY_RESOURCEID():

DECLARE_REGISTRY(CVCDSnap1About, _T("VCDSnap1About.1"),
                 _T("VCDSnap1About.1"), IDS_VCDSNAP1_DESC, THREADFLAGS_BOTH);

Ну и что здесь такого, спросите вы? Что здесь такого? А то, что использование макроса DECLARE_REGISTRY() может заставить отладчик пропускать расставленные вами точки прерывания в процессе отладки, и вы долго будете раздумывать, как MMC может вызывать ваши методы без срабатывания отладчика на точках прерывания. Это одна из ошибок, которая может стоить вам многих часов мучений при отладке, поскольку проявляется эта ошибка только в проектах, содержащих файлы с длинными именами. Так, когда я проверял пример с названием mmctest, размещенный в папке c:\test, все работало отлично. Более подробную информацию о данной ошибке вы можете найти по следующему адресу.

Всегда считайте своих детей

А теперь заставим нашу оснастку работать. В примере все запущенные в системе процессы будут добавляться в качестве дочерних узлов корневого узла оснастки, а загруженные DLL – в качестве дочерних узлов соответствующих процессов. Чтобы сделать это, я обрабатываю уведомление MMCN_EXPAND как для элементов CVCDSnap1Data, так и для CProcessNode. CProcessNode::Notify() добавляет элемент для каждой DLL, найденной в переменной m_vDlls класса CProcessNode, используя метод InsertItem интерфейса IConsoleNameSpace. CVCDSnap1Data очень похож, кроме того, что я заменил переменную m_vDlls на m_vProcesses. В листинге ниже показаны детали:

case MMCN_EXPAND:
  {
    CComQIPtr<IConsoleNameSpace, &IID_IConsoleNameSpace> spConsoleNameSpace(spConsole);
    // TODO : Перечислить элементы панели разделов
    // Если узел нужно раскрыть
    if (arg) {
      for (int ctr = 0; ctr < m_vDlls.size(); ctr++) {
        SCOPEDATAITEM sdi;
        ::ZeroMemory(&sdi, sizeof(SCOPEDATAITEM));
        sdi.mask = SDI_STR | SDI_PARAM | SDI_IMAGE | SDI_OPENIMAGE | SDI_PARENT;
        m_vDlls[ctr]->GetScopePaneInfo(&sdi);
        sdi.displayname = MMC_CALLBACK;
        sdi.relativeID = param;
        hr = spConsoleNameSpace->InsertItem(&sdi);
        assert(SUCCEEDED(S_OK));
      }
    }
    break;
  }

Теперь можно раскрыть корневой узел, показать работающие процессы, и, раскрыв каждый процесс, показать используемые им динамические библиотеки. Но обычно хочется использовать панель результатов, чтобы показать нечто более полезное. Прежде всего, вам нужно несколько столбцов с соответствующими заголовками, в которых будет показана информация о процессах и библиотеках. Чтобы добиться этого, нужно обрабатывать уведомление MMCN_SHOW в элементах CProcessNode и CVCDSnap1Data. Код показан в следующем листинге:

case MMCN_SHOW:
  {
    CComQIPtr<IResultData, &IID_IResultData> spResultData(spConsole);
    // TODO : Перечислить элементы панели результатов
    // Для узлов процессов добавляем четыре столбца,
    // чтобы показать данные, относящиеся к DLL
    // Если arg == TRUE – необходимо задать столбцы для элемента
   if (arg) {
      hr = spHeader->InsertColumn(0, L"Module's Full Path", LVCFMT_LEFT, MMCLV_AUTO); 
      hr = spHeader->InsertColumn(1, L"Base Address", LVCFMT_LEFT, MMCLV_AUTO); 
        .
        .
    }
    else {
      // Сначала необходимо удалить элементы
      hr = spResultData->DeleteAllRsltItems();
    }
    break;
  }

Как видите, для добавления столбцов с соответствующими заголовками в панель результатов используется интерфейс IHeaderCtrl (spHeader – это "умный указатель"). Но это еще не все. Нам нужно решить, что мы будем показывать в каждом столбце. ATL предоставляет нам одну функцию: GetResultPaneColInfo(). Она принимает один параметр типа int, показывающий, для какого столбца запрашивается информация, и возвращает LPOLESTR, указывающий на строку, которую нужно показать в данном столбце. В следующем листинге показан код для элементов CDllNode:

LPOLESTR CDllNode::GetResultPaneColInfo(int nCol)
{
  // TODO : Вернуть строки для других столбцов
  switch(nCol) {
    case 0:
      return const_cast<wchar_t*>(m_wstrFullPath.c_str());
    case 1:
      return const_cast<wchar_t*>(m_wstrBaseAddress.c_str());
         .
         .
    default:
      // Сюда мы попасть не должны!
      assert(false);
      return NULL;
  }
}

То есть для столбца 0 – показывается полный путь к исполняемому файлу, для столбца 1 – базовый адрес в памяти, и т.д.

Добавляем контекстное меню

Контекстные меню MMC с помощью ATL обрабатываются очень легко. Благодаря волшебству ATL мы можем работать с контекстными меню в привычной манере – используя ресурсы меню и макросы. Мастер ATL автоматически генерирует ресурс меню в соответствии с требованиями MMC, как показано на рисунках 4 и 5.


Рисунок 4.


Рисунок 5.

В следующем листинге показана карта команд типичного контекстного меню оснастки MMC:

SNAPINMENUID( <menu resource id> )
BEGIN_SNAPINCOMMAND_MAP( <Class representing the item>, FALSE )
  SNAPINCOMMAND_ENTRY(<menu item id>, <handler>)
  SNAPINCOMMAND_RANGE_ENTRY(<id1>, <id2>, <handler>)
    .
END_SNAPINCOMMAND_MAP()

Аргумент макроса SNAPINMENUID задает идентификатор используемого ресурса, так как обычно для различных типов элементов используются свои контекстные меню. Макрос SNAPINCOMMAND_ENTRY связывает элементы меню с функциями-обработчиками, а его RANGE вариантнесколько элементов с одной функцией. Обработчик должен иметь следующую сигнатуру:

HRESULT Func(bool& bHandled, CSnapInObjectRootBase* pObj);

а для RANGE варианта:

HRESULT Func(UINT nId, bool& bHandled, CSnapInObjectRootBase* pObj);

Итак, как видите, это очень легко. В своем примере я добавил несколько пунктов в контекстное меню узла-процесса для изменения класса приоритета. Как это сделать: Скопируйте сгенерированный ресурс меню и переименуйте. Я выбрал имя IDR_PROC_MENU. Поместите свои элементы туда, где бы вы хотели их видеть. Я добавил одно подменю в меню ЗадачаSetPriority, и три пункта в него – Hi, Norm и Lo. Добавьте соответствующий код в карту команд. В этом примере я использовал вариант RANGE, так как это более удобно, чем связывать элементы меню с обработчиками один за другим. Карта команд показана в листинге ниже:

SNAPINMENUID(IDR_PROC_MENU)
BEGIN_SNAPINCOMMAND_MAP(CProcessNode, FALSE)
   SNAPINCOMMAND_RANGE_ENTRY(ID_TASK_SET_HI, ID_TASK_SET_LO, OnSetPriority)
END_SNAPINCOMMAND_MAP()

//Обработчик выглядит так: 

STDMETHODIMP CProcessNode::OnSetPriority(UINT nId,
                                         bool& bHandled,
                                         CSnapInObjectRootBase* pObj)
{
   switch (nId) {
      // здесь реальная работа...
   }   
}

Красота! Я думаю, вы согласитесь, что это отличный способ работы с меню. Больше не нужно возиться с AddMenuItems, Command, IContextMenuCallback и т.д. Но, как всегда, с удобством теряется гибкость. Если вы скомпилируете и запустите оснастку, то получите предупреждение, и контекстное меню не появится. Нужно сделать что-то еще с волшебной силой Мастера.

Включение ATL волшебства: расширение возможностей обработки меню

Один из явных недостатков работы с меню в стиле ATL: невозможность обрабатывать подменю. Проблема кроется в реализации метода CSnapInItemImpl::AddMenuItems() (этот метод обрабатывает вызовы IExtendContextMenu::AddMenuItems). Откройте файл ATLSNAP.H и убедитесь сами: код построен на предположении, что вы никогда не добавляете подменю в меню верхнего уровня. Вот почему вызов AddItem() заканчивается неудачей.

Чтобы устранить эту проблему, я немного изменил реализацию метода CSnapInItemImpl::AddMenuItems(). Как вы можете увидеть, моя реализация проверяет каждый элемент меню на вложенность. Таким образом, теперь можно создавать меню с какими угодно уровнями вложенности. Замените следующий кусок кода (найдите его в CSnapInItemImpl::AddMenuItems()):

menuItemInfo.fMask = MIIM_TYPE | MIIM_STATE | MIIM_ID;
menuItemInfo.fType = MFT_STRING;
TCHAR szMenuText[128];
for (int j = 0; 1; j++)
{
   .
   .
}

на

HRESULT hr = TraverseSubMenus(hSubMenu, piCallback, insertionID);
assert(SUCCEEDED(hr));

Трюк заключается в рекурсивном вызове функции TraverseSubMenus. Эта функция делает то, что делала AddMenuItems: обрабатывает каждый элемент меню. Отличие в том, что если она находит элемент, являющийся подменю, она не останавливается с выдачей предупреждения, как исходная функция. Вместо этого она вызывает сама себя, передавая подменю в качестве аргумента. Этот процесс продолжается, пока не останется ни одного подменю. В этом случае она просто добавляет элемент меню и возвращает управление. Вот это я и называю "Включение волшебства"! В листинге ниже показан исходный код функции TraverseSubMenus, которую я поместил в закрытую секцию класса CSnapInItemImpl.

STDMETHOD(TraverseSubMenus)(const HMENU hMenu,
                            LPCONTEXTMENUCALLBACK piCallback,
                            LONG lInsertionPointID)
{
  MENUITEMINFO menuItemInfo;
  ::ZeroMemory(&menuItemInfo, sizeof(MENUITEMINFO));
  menuItemInfo.cbSize = sizeof(menuItemInfo);
  HRESULT hr = S_OK;
  TCHAR szMenuText[128];
  for (int j = 0; 1; j++) {
    menuItemInfo.fMask = MIIM_TYPE | MIIM_STATE | MIIM_ID | MIIM_SUBMENU;
    menuItemInfo.fType = MFT_STRING;
    menuItemInfo.cch = 128;
    menuItemInfo.dwTypeData = szMenuText;
    TCHAR szStatusBar[256];

    if (!GetMenuItemInfo(hMenu, j, TRUE, &menuItemInfo))
      break;

    if (menuItemInfo.fType != MFT_STRING)
      continue; 

    this->UpdateMenuState(menuItemInfo.wID, szMenuText,
                          &menuItemInfo.fState);

    LoadString(_Module.GetResourceInstance(), menuItemInfo.wID,
               szStatusBar, 256);

    OLECHAR wszStatusBar[256];
    OLECHAR wszMenuText[128];
    USES_CONVERSION;
    ocscpy(wszMenuText, T2OLE(szMenuText));
    ocscpy(wszStatusBar, T2OLE(szStatusBar));
    CONTEXTMENUITEM contextMenuItem;
    contextMenuItem.strName = wszMenuText;
    contextMenuItem.strStatusBarText = wszStatusBar;
    contextMenuItem.lCommandID = menuItemInfo.wID;
    contextMenuItem.lInsertionPointID = lInsertionPointID;
    contextMenuItem.fFlags = menuItemInfo.fState;
    contextMenuItem.fSpecialFlags = 0;
    // Если это обычный элемент...
    if (menuItemInfo.hSubMenu == NULL) {
      hr = piCallback->AddItem(&contextMenuItem);
    }
    else {
      // Если это подменю – вызываем сами себя
      contextMenuItem.lCommandID = (WORD)menuItemInfo.wID;
      contextMenuItem.fFlags |= MF_POPUP;
      contextMenuItem.fSpecialFlags = CCM_SPECIAL_SUBMENU;
      hr = piCallback->AddItem(&contextMenuItem);
      assert(SUCCEEDED(hr));
         
      hr = TraverseSubMenus(menuItemInfo.hSubMenu, piCallback,
                            contextMenuItem.lCommandID);
    }

    assert(SUCCEEDED(hr));
  }

  return hr;
}

Думаю, вы не забудете сделать резервную копию файла ATLSNAP.H перед внесением изменений.

Дефект волшебства: исправляем ошибку в ATL

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

#define SNAPINCOMMAND_RANGE_ENTRY(id1, id2, func) \
  if (id1 >= nID && nID <= id2) \
  { \
    hr = func(nID, bHandled, pObj); \
    if (bHandled) \
      return hr; \
  }

Видите ли вы ошибку в коде? Нет? Тогда сравните с этим фрагментом:

#define SNAPINCOMMAND_RANGE_ENTRY(id1, id2, func) \
  if (nID >= id1 && nID <= id2) \
  { \
    hr = func(nID, bHandled, pObj); \
    if (bHandled) \
      return hr; \
  }

А теперь увидели? Я советую вам внимательно посмотреть файл ATLSNAP.H, чтобы понять, как ATL реализует свою магию. Кто знает, вдруг вы обнаружите другие ошибки.

ПРИМЕЧАНИЕ

Как ни странно, эта ошибка не была устранена в ATL версии 7.0, поставляющейся с Visual Studio.NET.


Впервые статья была опубликована в январе 1999 года в журнале "Visual C++ Developer"
    Сообщений 2    Оценка 20        Оценить