IRegistrar, поиск подменю и другая информация

Автор:

  • Пол Диласиа (Paul DiLascia)

Опубликовано: октябрь 2006 года

 Оригинал статьи (EN)
 Загрузить примеры кода из этой статьи: C++atWork2006_10.exe (174KB)

Вопрос

Когда я создаю DLL с поддержкой автоматизации, используя Visual C++® 6.0, то функции для регистрации генерируются, а функции для отмены регистрации моей DLL – нет. Я написал код DllUnregisterServer, который выглядит следующим образом:

STDAPI DllUnregisterServer(void) 
{ 
   AFX_MANAGE_STATE(AfxGetStaticModuleState()); 
   if (!COleObjectFactory::UnregisterAll()) 
      return ResultFromScode(SELFREG_E_CLASS); 
   return NOERROR; 
}

COleObjectFactory::UnregisterAll возвращает TRUE, но моя DLL остается зарегистрированной. Как нужно писать подобный код?

Иван Павлик

Ответ

Увы, вы попали в небольшую ловушку во вселенной MFC. Действительно, кажется, что UnregisterAll является той функцией, которую нужно вызвать, чтобы отменить регистрацию своей DLL. Если заглянуть внутрь olefact.cpp, где реализована UnregisterAll, то можно увидеть, что она выглядит примерно так:

for (/* pFactory = all factories */) {
  pFactory->Unregister();
}

То есть, она охватывает все фабрики вашего модуля и для каждой вызывает команду Unregister. Пока все идет нормально. Посмотрим, что же делает команда COleObjectFactory::Unregister? Это нам скажет код:

BOOL COleObjectFactory::Unregister()
{
   return TRUE;
}

Вот это да! Мы видим, что COleObjectFactory::Unregister не делает ничего – она просто возвращает значение TRUE. Это особенно примечательно, учитывая, что COleObjectFactory::Register и в самом деле регистрирует ваш COM-класс, вызывая::CoRegisterClassObject. Однако, есть другая функция, COleObjectFactory::Revoke, которая вызывает ::CoRevokeClassObject, чтобы отменить регистрацию COM-класса. MFC вызывает Revoke автоматически из деструктора COleObjectFactory. Итак, в чем же здесь загвоздка?

Проблема заключается в неудачной путанице в названиях, которая произошла из-за различных способов регистрации COM-классов для библиотек (DLL) и программ (EXE). В случае DLL вы регистрируете класс, добавляя ключи в реестр Windows (CLSID, ProgID, и т.д.). В случае с EXE-программами вы должны в процессе работы вызвать CoRegisterClassObject, чтобы зарегистрировать класс в системе COM. Этот вопрос еще более запутывается из-за того, что для EXE-модулей функцией, противоположной Register, является не Unregister, а Revoke (CoRevokeClassObject).

Когда в Microsoft добавляли поддержку COM для MFC, они старались максимально облегчить жизнь, но им это не всегда удавалось сделать в совершенстве. Функция COleObjectFactory::Register выглядит следующим образом:

// in olefact.cpp
BOOL COleObjectFactory::Register()
{
   if (!afxContextIsDLL) {
      ::CoRegisterClassObject(..., 
         CLSCTX_LOCAL_SERVER, ..);
   }
}

Сразу видно, что она ничего не делает для DLL; она только регистрирует EXE-модули, используя CLSCTX_LOCAL_SERVER (context = local server, т.е. EXE выполняется на локальной машине). Следуя базовой реализации в C API, программисты Майкрософт использовали то же самое имя Revoke для функции, которая отменяет регистрацию EXE: COleObjectFactory::Revoke (и которая вызывается автоматически из деструктора фабрики классов).

Так для чего же используется функция COleObjectFactory::Unregister, которая ничего не делает? Возможно, в Майкрософт когда-то планировали, чтобы функция Register/Unregister работала также и для DLL. Но, по ситуации на сегодняшний день, они этого не сделали. Чтобы зарегистрировать DLL, нужно использовать совершенно другую функцию: COleObjectFactory::UpdateRegistry. В эту функцию передается логический аргумент, который указывает MFC, что вы хотите сделать: зарегистрировать или отменить регистрацию COM-класса. Есть также UpdateRegistryAll, которая охватывает все фабрики классов, вызывая для каждой UpdateRegistry. Поэтому правильный вариант кода для DllUnregisterServer:: будет таким:

STDAPI DllUnregisterServer(void)
{
   AFX_MANAGE_STATE(AfxGetStaticModuleState());
   return COleObjectFactory::UpdateRegistryAll(FALSE)
      ? S_OK : SELFREG_E_CLASS;
}

Сервер DllRegisterServer должен выглядеть точно так же, за исключением того, что в функцию UpdateRegistryAll следует передать значение TRUE вместе FALSE.

Стандартная реализация UpdateRegistry работает прекрасно, когда нужно добавить или удалить необходимые ключи реестра в HKCR/CLSID–InprocServer32, ProgID, Insertable, ThreadingModel, и т.д., но иногда могут потребоваться дополнительные ключи, относящиеся к вашему COM-классу. Например, может понадобиться зарегистрировать категории, которые являются расширением старого ключа "Insertable"(что означает, что COM-класс можно поместить на форму в режиме разработки программы). В этом случае нужно переопределить функцию UpdateRegistry, чтобы добавить и удалить дополнительные ключи.

Несколько лет назад, в ноябрьском и декабрьском номерах журнала Microsoft Systems Journal (сейчас MSDN®Magazine)я показывал, как создать Band Object для Internet Explorer, используя интерфейс IRegistrar библиотеки Active Template Library (ATL). (Для объектов Band нужно зарегистрировать специальную категорию CATID_DeskBand.) IRegistrar является действительно прекрасным инструментом, который позволяет написать сценарий регистрации (файл .RGS) и добавить его к элементам реестра, вместо того, чтобы вызывать функции работы с реестром RegOpenKey, RegSetValue и др. Типовой сценарий показан в листинге 1.

Листинг 1.

HKCR
{
    NoRemove CLSID
    {
        ForceRemove %CLSID% = s ‘%ClassName%’
        {
            InprocServer32 = s ‘%MODULE%’
            {
                val ThreadingModel = s ‘%ThreadingModel%’
            }
            ProgID = s ‘%ProgID%’
        }
    }
}

Как можно видеть, IRegistrar позволяет определить переменные, например %ClassName% и %ThreadingModel%, и затем во время выполнения заменять их реальными значениями. При использовании IRegistrar, вам не придется вызывать API реестра вновь. Можно написать сценарий, который больше похож на реальные элементы реестра, и можно использовать один и тот же сценарий, чтобы зарегистрировать или удалить регистрацию DLL. Это правда, IRegistrar действительно может отменить регистрацию. Мне не удалось найти официального описания функции IRegistrar, и тем не менее она существует (см. интерфейс в atliface.h). Если вы не используете IRegistrar для регистрации и отмены регистрации библиотек, работающих с COM, настоятельно советую попробовать — это сэкономит кучу работы. Подробности см. в моей статье (EN) за ноябрь 1999 г.

Вопрос

В моем приложении есть обыкновенное меню, в котором есть подменю Edit с командами (Cut, Copy, Paste, и т.д.). Я бы хотел показать подменю Edit, как контекстное меню, которое появляется, когда пользователь щелкает правой кнопкой мыши на главном окне. Проблема в том, как достать это подменю из главного меню. Кажется, что нет идентификатора команды, связанного с подменю. Я мог бы использовать индекс, начиная с нуля, но из-за настроек пользователя подменю Edit может не всегда быть вторым по счету. Также я не могу сделать поиск слова "Edit", так как мы поддерживаем несколько языков, и реальное слово может быть другим. Как мне найти подменю Edit во всех этих случаях?

Брайан Мэнлин

Ответ

Для начала, позвольте мне сказать, что меню Edit должно быть вторым по счету подменю, если оно у вас есть. Официальное руководство по интерфейсу Windows® призывает к тому, чтобы первыми тремя меню были File, Edit, View, в этом порядке — если вы поддерживаете их. См. Руководство Windows по интерфейсу пользовательских приложений (The Windows Interface Guidelines for Software Design (EN) (Microsoft Press®, 1995). То есть, я могу помочь вам найти ваше подменю Edit или другое подменю, при соблюдении этих требований.

Вы действительно правы, для подменю не существует идентификатора команды. Это потому, что в Windows поле идентификатора команды используется для хранения описателя HMENU для подменю, если это подменю является элементом меню. Но беспокоиться не стоит: учитывая, что подменю всегда содержит определенную команду (например, Edit | Cut), и что у команды всегда один идентификатор (например ID_EDIT_CUT), можно достаточно легко написать функцию, которая находит все подменю, которые содержат эту команду. Пример программы приведен в листинге 2. Класс CSubmenuFinder является пространством имен для статической функции FindCommandID, которая вызывает себя рекурсивно. Она осуществляет глубокий поиск по всем меню и подменю, находя тот элемент меню, чей идентификатор команды совпадает с искомым.

Листинг 2.

SubmenuFinder.h

#pragma once

//////////////////
// Class to find a submenu that contains a given command ID.
// This class is really just a namespace for the function FindCommandID.
//
class CSubmenuFinder
{
public:
   CSubmenuFinder() { }
   ~CSubmenuFinder() { }
   static CMenu* FindCommandID(CMenu* pMenu, UINT nID);
};


SubmenuFinder.cpp

#include "StdAfx.h"
#include "SubmenuFinder.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

//////////////////
// Find the submenu within a given menu that contains a given command // ID.
// Iterate the menu items looking for the command ID. Recurse 
// into submenus if necessary.
//
CMenu* CSubmenuFinder::FindCommandID(CMenu* pMenu, UINT nID)
{
   if (pMenu) {
      for (UINT i=0; i<pMenu->GetMenuItemCount(); i++) {
         if (pMenu->GetMenuItemID(i)==nID)
            return pMenu; // found command--return menu

         // Recurse! Note this returns NULL if item is not a submenu.
         CMenu* pSubMenu = CSubmenuFinder::FindCommandID(
             pMenu->GetSubMenu(i),nID);

         if (pSubMenu) return pSubMenu; // found submenu -- return it!
      }
   }
   return NULL;
}
You can use CSubmenuFinder to find your edit menu, like so:

void CMainFrame::OnContextMenu(..)
{
   CMenu* pMenu = CSubmenuFinder::FindCommandID(
      GetMenu(),ID_EDIT_CUT);
   if (pMenu) pMenu->TrackPopupMenu(...);
}

В примере выполняется поиск в главном меню: ищется подменю, команда которого имеет идентификатор ID_EDIT_CUT, и затем возвращается название подменю, если таковое найдено. Я написал маленькую программку под названием EdMenu, чтобы проверить, как работает CSubmenuFinder. В EdMenu этот код используется при обработке сообщения WM_CONTEXTMENU, которое возникает, когда пользователь щелкает правой кнопкой мыши в главном окне. После чего на экране появляется то же самое подменю Edit, которое содержится в главном меню. И я проверил ее, временно поместив меню Edit перед меню View в одной из своих сборок. Чтобы проверить ее самостоятельно, скачайте исходный код.

Вопрос

Как можно конвертировать MFC CString в класс String в управляемом C++? Например, есть следующий код на С++:

void GetString(CString& msg)
{ 
   msg = // build a string
}

Как можно переписать эту функцию, используя управляемый C++ и заменяя параметр CString управляемым классом String? Проблема в том, что GetString изменяет объект CString вызвавшего его метода, и я хотел бы сделать то же самое, используя управляемый класс String.

Сумит Пракаш

Ответ

Это зависит от того, используете ли вы новый синтаксис C++/CLI, или старый Managed Extensions. В обоих случаях могут появиться вопросы, но я здесь, чтобы помочь вам их решить.

Давайте начнем с нового синтаксиса, так как на дворе 2006 год. Чтобы правильно понять этот синтаксис, важно помнить две вещи. Во-первых, в C++/CLI, управляемые объекты «носят шляпы». Поэтому CString становится String^. Во-вторых, помните, что управляемым эквивалентом для ссылки "&" является ссылка с отслеживанием (tracking reference), котрорая в C++/CLI обозначается "%". Уверен, что вы не сломаете голову над "^", но, при наличии достаточной практики, "%" тоже скоро покажется нормальным. Итак, новая функция будет выглядеть следующим образом:

// using C++/CLI
void GetString(String^% msg)
{ 
   msg = // build a string
}

Здесь все понятно? Чтобы показать, что она на самом деле работает, я написал маленькую программу strnet.cpp (см. листинг 3). В ней реализованы два класса, CFoo1 и CFoo2, каждый с членом GetName. В первом используется CString&, а во втором – String^%. Если вы скомпилируете и запустите эту программу (с ключом /clr, конечно), вы увидите, что в обоих случаях — как с CString так и так и с String — переданный объект (собственный или управляемый) модифицирован функцией-членом.

Листинг 3.

#include "stdafx.h"
#include "resource.h"

using namespace System;

//////////////////
// Passing a reference to a native MFC CString
//
class CFoo1 {
public:
   void GetName(CString& name)
   {
      name = _T("Rumplestilskin");
   }
};

//////////////////
// Passing a reference to a managed String^
//
class CFoo2 {
public:
   void GetName(String^% name)
   {
      name = _T("Rumplestilskin");
   }
};

int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
{
   CFoo1 foo1;
   CFoo2 foo2;

   CString name;
   foo1.GetName(name);
   tprintf(_T("GetName returns %s\n"), name);

   String^ s = gcnew String("");
   foo2.GetName(s);
   Console::WriteLine("GetName returns {0}\n",s);
}

Между прочим, для управляемых объектов всегда лучше использовать ссылку с отслеживанием "%", чем прямую (native) ссылку "&" (которая тоже будет компилироваться). Cсылка с отслеживанием всегда указывает на объект, даже если механизм сборки мусора перемещает его в пределах управляемой "кучи".

Если же вы используете старый синтаксис (/clr:oldSyntax), то как случилось так, что вы все еще живете в средневековье? Впрочем, ничего страшного, ссылки можно использовать, даже если вы приверженец ретро. Если вы работаете с Managed Extensions, помните, что управляемые объекты имеют суффикс __gc и являются указателями, поэтому CString становится String __gc *. А так как компилятор уже знает, что String является управляемым типом, вам даже не нужен __gc; достаточно обычного указателя "*". Ссылка будет такой же. Поэтому измененный код в том старом стиле выглядит так:

// __gc omitted
void GetString(String*& msg)
{ 
   msg = // build a string
}

Понятно? Вот так это можно сделать. Если же вы испытываете трудности с синтаксисом C++ , то вам следует вернуться к основам.

Вопрос

Недавно я разработал консольное CLR-приложение в Visual Studio® 2005. Я заметил, что приложение Visual Studio создало главную функцию, которая выглядит так:

int main(array<System::String ^> ^args)
{
   ...
   return 0;
}

Кажется, это отличается от старых argc/argv, которые я знаю в C/C++. Когда я попытался получить доступ к args[0], думая, что это будет имя файла (как в C/C++), я обнаружил, что args[0] является не именем файла, а первым параметром командной строки. А что произошло с именем файла? И не могли бы вы объяснить причину такого изменения?

Джейсон Лэндрю

Ответ

Да, в современном мире все быстро меняется. И все уже не так как прежде, да? Мы жили с функцией argc/argv с 1972 года, когда был разработан язык C, а теперь программисты из Редмонда это изменили. Возможно, они хотели, чтобы все как следует выучили новый синтаксис массивов.

Одной из причин для перехода на новый синтаксис является необходимость компилировать программу с ключом /clr:safe, что является предельным уровнем безопасности кода. Это как будто вы запираете свою программу в "чистую" комнату, типа тех стерильных помещений с хромированной мебелью, куда все входят через шлюз и без шапок и обуви. Если задан ключ /clr:safe, компилятор налагает все возможные ограничения. Нельзя использовать машинные (native) типы. Нельзя использовать глобальные переменные. Нельзя вызывать неуправляемые функции. Короче говоря, вы не можете использовать многие преимущества. И почему же вы подвергли себя таким ограничениям? Чтобы быть по-настоящему уверенным, что ваш код является безопасным. Говоря техническим языком, режим /clr:safe генерирует «проверяемый» код, что означает, что CLR может проверить, что ваш код не нарушает настроек безопасности, например, не старается получить доступ к файлу, когда настройки безопасности для пользователя этого не разрешают. Одними из членов длинного списка запретов для /clr:safe являютсяnative указатели — или просто, указатели. (Слово «native» является здесь избыточным, поскольку не может быть указателя на управляемый тип). Так как указатели в режиме /clr:safe запрещены, старая декларация argv просто не работает. Поэтому разработчики из Редмонда изменили декларацию и используют массив String^. А так как массивы знают свою длину, нет необходимости в argc. Стартовый код для создания и инициализации массива arg, который вызывается перед тем, как передать управление в вашу главную функцию, содержится в runtime-библиотеке. Если использовать /clr:safe не планируется, всегда используйте в главной функции можно использовать старый список параметров argc/argv.

Это объясняет причину использования управляемого массива, но остается вопрос с args[0]: почему он является первым параметром команды, а не именем файла? Я не могу говорить за программистов Microsoft, но возможно, они подумали, что имеет смысл опустить имя файла, может быть потому, что оно вам не нужно. В любом случае, это не имеет большого значения, так как получить имя файла легко. Я уверен, что есть много способов сделать это, но самым явным и ориентированным на CLR (совместимым с /clr:safe) способом, я думаю, является использование класса System::Environment. В нем имеется следующий метод:

static array<String^>^ GetCommandLineArgs ()

В отличие от параметра args, передаваемого в main, массив, возвращаемый из Environment::GetCommandLineArgs, на самом деле начинается с имени исполняемого файла.

И, наконец, зачем может понадобиться узнать имя собственного EXE-файла? Ведь это вы пишете программу, и предполагается, что вы знаете, как она называется. Одной из причин для этого может быть генерация сообщения системы справки, в котором содержится имя программы, без необходимости заносить это имя в код. Справочное сообщение может выглядеть так:

FooFile -- Turns every word in your file to "foo"
usage:
   FooFile [/n:<number>] filespec
   filespec = the files you want to change
   <number> = change only the first <number> occurrences

Здесь FooFile является именем программы. Я бы мог просто написать «FooFile» в тексте сообщения, но я давно уже знаю, что часто переименовываю программы или копирую код из одной программы в другую. Если же имя программы будет получено в самой программе (через argv или Environment::GetCommandLineArgs), то в справочном сообщении всегда будет правильное имя, даже если я переименую файл или скопирую код в другую программу.

Счастливого вам программирования!

 Вопросы и комментарии Полу Диласиа можно отправлять по адресу cppqa@microsoft.com.

Пол Диласиа (Paul DiLascia) является консультантом по программированию, а также разработчиком интерфейсов Windows- и веб-приложений. Он автор книги Windows++: Writing Reusable Windows Code in C++ (Addison-Wesley, 1992). В свое свободное время Пол разрабатывает PixieLib, библиотеку классов MFC, которая доступна на веб-сайте: http://www.dilascia.com/ (EN).