Недокументированный Visual C++ 6.0

или спелеология в бесплодных землях MSDEV

Автор: Nick Hodapp
Перевод: [очень вольный]: Юрий Губанов ака Ёрик
Ланит-Терком

Источник: Undocumented Visual C++
Опубликовано: 26.03.2005
Версия текста: 1.1

Введение
Visual C++ - MFC приложение
Пакетные (.PKG) файлы
Расширения, использующие автоматизацию
Продвинутые расширения, использующие автоматизацию
Тупые фокусы с расширениями
Другие подсказки и трюки
Hooking и Subclassing
Панели инструментов
Заключение
От переводчика

Примеры к статье.

Введение

Microsoft Visual C++ версий 5 и 6 предоставляет интерфейс автоматизации, предназначенный для того, чтобы разработчики расширяли продукт. К сожалению, интерфейс этот – бесяще ограниченный и багливый – делает практически невозможным написание расширений, выполняющих что-либо более-менее нетривиальное.

Некоторые третьесторонние продукты, такие как Bounds Checker, бесшовно интегрируются с Visual C++ IDE. Однако они не являются расширениями, в буквальном смысле, и не ограничены карликовыми возможностями интерфейса автоматизации. Вместо этого подобные продукты реализованы как MFC-extension DLL (но имеют расширение файла .pkg). Разработчики этих продуктов посвящены в недокументированный интерфейс Visual C++, а конкретно – в интерфейс библиотеки DEVSHL, реализующей ядро Visual C++. DEVSHL.DLL – неотъемлемая часть Visual C++, и ее экспортируемые методы доступны только по номеру (никаких имен методов нет). Чтобы использовать DEVSHL.DLL, нужны соответствующие .h и .lib файлы. То ли разработчики продуктов наподобие Bounds Checker получили эти файлы от Microsoft, то ли восстановили их с нуля, я не в курсе. Если кто-нибудь пошлет мне (прим. переводчика: и мне! и мне!) DEVSHL.H и DEVSHL.LIB, я буду навечно у него в долгу.

Некоторые шареварные и коммерческие расширения добились такой интеграции с Visual C++, которая выглядит более продвинутой, чем мог бы позволить интерфейс автоматизации, без какого-либо сокровенного знания от Microsoft. Примеры включают в себя аддин WndTabs, написанный Соломоновичем (Oz Solomonovich), мой собственный продукт WorkspaceEx и RadVC. Эти расширения не только реализуют поразительно сложные новые возможности для пользователя, но также аккуратно интегрируются с пользовательским интерфейсом Visual C++. Цель этой статьи – дать представление о том, как подобной интеграции можно добиться.

Эта статья время от времени ссылается на мой продукт WorkspaceEx, который является шареварой и доступен на моем сайте CoDeveloper (прим переводчика: сейчас там, увы, ничего нет). Несмотря на то, что я не распространяю исходников этого продукта, в этой статье я опишу большинство основных техник, используемых WorkspaceEx для интеграции с Visual C++.

Visual C++ - MFC приложение

Это не должно вас удивить. Чтобы убедиться в том, что это правда, просто посмотрите на список зависимостей MSDEV.EXE. Он связан с MFC42.DLL (прим. переводчика: в оригинале «It is bound (pun intended) to load MFC42.DLL», т.е. не только связан, но и ограничен).

На самом деле MSDEV.EXE – основной пример того, на что может быть похоже типичное большое MFC-приложение. Если вы всмотритесь внутрь кода (мы сделаем это вскорости), то увидите, что Visual C++ интенсивно использует архитектуру document/view, наследует большинство классов от CObject, и опирается на основные классы MFC, такие как CString и CObList.

Интересная вещь, которую вы, возможно, заметили – многие возможности IDE реализованы «hard way». Например, Visual C++ повсюду использует метафору tab-контрола. Однако, ни один из его элементов управления, использующих tab-контрол, не является родным common tab-контролом Windows. Вместо этого используется собственный класс, реализованный специально для Visual C++. Если пытаться угадывать, почему это так, я бы сделал предположение, что IDE было написано до того, как tab-контрол был закончен или стал доступен для Windows 95, или что этот контрол был недоступен для Windows NT 3.5 (моя персональная история знаний о Windows начинается в основном с NT 4.0).

Окончательное разоблачение тайны, что Visual C++ является MFC-приложением, заключено в том, что разработчикам собственных App Wizards (прим. переводчика: прилагательных колдовщиков) предлагают писать их в виде MFC extension DLL.

Пакетные (.PKG) файлы

Visual C++ реализован как исполняемый файл (MSDEV.EXE) и несколько MFC extension dll. Основная масса этих dll имеют расширение .pkg. и живет в поддиректории BIN\IDE. При старте Visual C++ загружает все .pkg файлы, которые он находит в этой поддиректории. Дополнительные возможности enterprise-версии продукта Microsoft поставляет в виде .pkg-файлов.

Несмотря на то, что .pkg-файлы обычно являются MFC-extension dll, это не является обязательным условием. Наоборот, .pkg-файл всего лишь должен быть dll, экспортирующей две функции, InitPackage() и ExitPackage(). Эти методы имеют следующие прототипы:

DWORD _stdcall InitPackage(HANDLE hInstPkg); // экспортируется под номером 2
DWORD _stdcall ExitPackage(HANDLE hInstPkg); // экспортируется под номером 1

Visual Studio загружает .pkg-файл и, как часть процесса его инициализации, вызывает метод InitPackage(). PKG-файл должен вернуть 1, чтобы сообщить об успешной инициализации. Аналогично, ExitPackage() вызывается при закрытии IDE.

Параметр, передаваемый в эти методы – это описатель экземпляра самого .pkg-файла.

В интересах тех читателей, которые подзабыли о специфике dll, используемых MFC, позволю себе освежить ваши воспоминания. Существует два основных типа dll: «обычные» dll, которые имеют собственный объект CWinApp, и «extension» dll, которые разделяют CWinApp с вызывающим приложением.

PKG-файл, реализованный в виде MFC-extenstion dll, имеет особое преимущество. Он имеет доступ к CWinApp и другим MFC-объектам, принадлежащим Visual C++.

Простейший способ создать собственный .pkg-файл – использовать MFC AppWizard (dll) и выбрать опцию extension dll. Затем добавить и экспортировать методы InitPackage() и ExitPackage() и подправить настройки проекта и .def-файл, чтобы порождать .pkg-файл вместо .dll.

Чтобы получить доступ к внутренним MFC-объектам Visual C++ из метода InitPackage(), реализованного внутри MFC extention .pkg, просто вызовите AfxGetApp() и используйте возвращенный указатель как точку отсчета.

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

Важно заметить, что MFC-extension .pkg-файлы должны быть слинкованы с release-версией библиотек времени исполнения MFC. Подробности этого требования будут описаны в секции "Продвинутые расширения, использующие автоматизацию".

Пакетные файлы представляют собой альтернативный метод написания расширений для Visual C+. IDE загружает .pkg файлы раньше, чем расширения, использующие автоматизацию, кроме того, расширения не обязаны быть реализованы с помощью COM. Однако, .pkg-расширения не получают прямого доступа к интерфейсам автоматизации, как им, возможно, хотелось бы. Для тех, кому интересно – WorkspaceEx на данный момент реализован и как .pkg-расширение и как расширение, использующее автоматизацию.

Расширения, использующие автоматизацию

Могу поспорить, что 95% всех расширений к Visual C++ начали свою жизнь как код, сгенерированный с помощью DevStudio Addin Wizard (прим. переводчика: колдовщика добавочных студийных дев :)).

Код, сгенерированный мастером расширений компилируется в COM dll, которая реализует объект, инстанциируемый Visual C++. Интересным аспектом этого кода является то, что он интенсивно использует и MFC и ATL - нечасто используемая техника. Незамедлительный вопрос – почему, ведь это всё могло легко быть закодировано полностью на ATL. Я думаю, это привело бы к гораздо более простому решению (Вероятно, ATL была слишком молода, когда писался мастер, в самом деле, это видно по коду, который генерируется).

Visual C++ использует крайне подозрительную технику для идентификации и инстанциации расширений. Основной вопрос – как IDE узнает CLSID и ProgID расширения, чтобы его было возможно инстанциировать?

Рассмотрим события, которые происходят при инсталляции и использовании нового расширения. Обычно пользователь открывает диалог Tools/Customize и выбирает закладку “Add-ins and Macro File’. Предположим, что файл расширения не находится в поддиректории Addins, а пользователь укажет путь к нему вручную. Идентифицировав расширение, Visual Studio загружает его и делает доступным для использования.

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

Расширение – это просто COM-объект с уникальным CLSID. Он должен реализовывать известный и документированный интерфейс IDSAddin, чтобы, будучи инстанциированным Visual C++, последний мог вызвать известные методы объекта, а именно OnConnection() и OnDisconnection(). В обязанности аддина входит сделать обратный вызов IDE из OnConnection() и передать dispatch interface (обычно) второго объекта, который реализует собственно набор команд расширения. Так как CLSID объекта расширения уникален и не зарегистрирован в системе никоим образом, как Visual C++ узнает, что надо создать именно этот объект?

Я подозреваю, что когда IDE Visual C++ идентифицирует dll, потенциально содержащую расширение, она загружает ее и вызывает DllRegisterServer(), общеизвестный и обязательный метод, находящийся во всех in-process COM-библиотеках. Dll расширения делает системный вызов, чтобы добавить регистрационную информацию о себе в системный реестр. Я полагаю, что IDE следит за подобными вызовами, чтобы обнаружить CLSID регистрируемых объектов, а затем создает эти объекты и запрашивает у них интерфейс IDSAddin.

Альтернативой этой невероятной теории может служить другая – что Visual C++ загружает библиотеку типов dll аддина и перебирает объекты коклассов. Однако я не верю в это, так как расширения работают, даже если коклассы для объектов расширения не присутствуют в библиотеке типов.

Гораздо более простая и понятная архитектура имеет место в случае, когда расширение саморегистрируется как принадлежащее к отдельной категории компонентов. Visual C++ может в этом случае перебрать все объекты в этой категории и инстанциировать их. Но стоп, именно так я бы и сделал!

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

HKEY_CURRENT_USER\Software\Microsoft\DevStudio\6.0\AddIns

Расширение вполне может самостоятельно установить себя, создавая соответствующие записи под этим ключом. Эти записи содержат подключ с ProgID расширения, который должен содержать «1» как значение по умолчанию для того, чтобы быть загруженным IDE (это значение соответствует выбранному состоянию в списке расширений на вкладке “Addins and Macro Files“). Три требуемых строковых значения этого ключа: Description, DisplayName и Filename.


Мои собственные расширения устанавливают себя самостоятельно, создавая эти записи в реестре во время обычной COM-регистрации, выполняемой с помощью DllRegisterServer(). Это избавляет от необходимости писать отдельный код в инсталляционной программе и, в большинстве случаев является чистым решением. Инсталлятор просто регистрирует dll, и при следующем запуске Visual C++ моё расширение уже доступно.

К сожалению, расширения, которые генерируются мастером DevStudio, не используют файлы .rgs (регистрационные скрипты), вместо этого предлагая делать обновление реестра в коде. Я советую переделать подобные существующие расширения на использование .rgs-скриптов и использовать мой собственный Addin Wizard, обсуждаемый ниже, для будущих расширений. Используйте фичи, камрады, не переизобретайте лисапед.

Ниже приведен .rgs-скрипт, похожий на используемый в WorkspaceEx. Первая секция – это самоинсталляция, вторая производит регистрацию объекта.

HKCU
{
  NoRemove Software
  {
    NoRemove Microsoft
    {
      NoRemove DevStudio
      {
        NoRemove '6.0'
        {
          NoRemove AddIns
          {
            ForceRemove 'WorkspaceEx.DSAddin.1' = s '1'
            {
              val Description = s 'Extends the capabilities of the Workspace window.'
              val DisplayName = s 'WorkspaceEx'
              val Filename    = s '%MODULE%'
            }
          }
        }
      }
    }
  }
} 

HKCR
{
  WorkspaceEx.DSAddin.1 = s 'DSAddin Class'
  {
    CLSID = s '{4674EF43-FAA0-11D3-84A4-00A0C9E52DCB}'
  }
  WorkspaceEx.DSAddin = s 'DSAddin Class'
  {
    CLSID = s '{4674EF43-FAA0-11D3-84A4-00A0C9E52DCB}'
    CurVer = s 'WorkspaceEx.DSAddin.1'
  }
  NoRemove CLSID
  {
    ForceRemove {4674EF43-FAA0-11D3-84A4-00A0C9E52DCB} = s 'WorkspaceEx'
    {
      Description = s 'Extends the capabilities of the Workspace window.'
      ProgID = s 'WorkspaceEx.DSAddin.1'
      VersionIndependentProgID = s 'WorkspaceEx.DSAddin'
      InprocServer32 = s '%MODULE%'
      {
        val ThreadingModel = s 'both'
      }
    }
  }
}

Продвинутые расширения, использующие автоматизацию

Сгенерированные стандартным мастером расширения представляют из себя обычные MFC-dll, то есть, имеющие собственный объект CWinApp. Этот подход препятствует доступу расширения к объекту CWinApp вызывающего его приложения Visual C++, как это возможно из модуля расширения .pkg.

Вы можете создать расширение, которое являлось бы одновременно и in-process COM-сервером (для обслуживания собственно объекта расширения) и MFC-extension dll (что позволило бы ему получить доступ к кишочкам Visual C++). Сразу предупреждаю, что стандартного мастера от Microsoft, который порождал бы такого зверя, не существует. Чтобы исправить ситуацию, я написал такого мастера, и он приложен к этой статье.

CoDeveloper’s Extension Addin Wizard прост в использовании. Запустите его, измените имя и описание расширения по умолчанию и сгенерируйте код.


Если кто-нибудь захочет расширить мастера, чтобы тот генерировал .pkg-файлы или предоставлял дополнительную функциональность – пожалуйте в гости. Я с радостью внесу изменения, пока сгенерированный код не превратится в (прим. переводчика: э-э-э…, ну, вы поняли).

CoDeveloper Wizard генерирует обновленную версию кода, порождаемого мастером от Microsoft. Главные отличия в том, что целевая dll – MFC-extension DLL, и COM-объекты регистрируются с помощью .rgs-скриптов. По моему мнению, такой код гораздо понятнее и поддерживаемее.

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

Несколько предостережений, о которых надо знать, когда вы собираете MFC-extension dll, загружаемую MSDEV.EXE. Они относятся и к .pkg-файлам, обсуждаемым в предыдущей секции, и к расширениям, сгенерированным с помощью CoDeveloper Wizard. Прежде всего, вы не сможете успешно слинковаться с отладочной версией runtime-библиотек MFC, так как MSDEV.EXE загружает только MFC42.DLL, а отладочная MFC42D.DLL имеет несколько другую раскладку своих структур в памяти.

Мой совет по преодолению этой потенциальной головной боли заключается в том, чтобы немедленно удалить Debug-конфигурацию из проекта вашего аддина и добавить ее же, но основанную на Release-конфигурации. Затем вернуть отладочные опции компиляции и линковки, не добавляя только флагов, указывающих на линковку с отладочными версиями runtime-библиотек. (и #define _DEBUG). Эта псевдо-debug конфигурация сборки отлично работает – вы сможете отлаживать милый вашему сердцу код, вы только не сможете зайти внутрь runtime-библиотек. Кстати, я поленился реализовать в CoDeveloper Wizard’е создание проекта с псевдо-debug конфигурацией. Я окинул взглядом душераздирающие способы создания новой конфигурации через интерфейс автоматизации и сказал себе: «У тебя есть дела поважнее».

Вы также можете пересобрать MFC42.DLL чтобы она содержала отладочную информацию. Однако я выяснил, что на моей системе makefile для MFC некорректно собирается с 4-м сервис-паком VC++.

Тупые фокусы с расширениями

Возможно, вы удивились, какого черта я так сильно стремлюсь получить доступ к внутренним MFC-объектам IDE. Для ответа на этот вопрос я написал расширение под названием OpenVC. OpenVC реализует две команды: DeleteNCB и ShowInnards.

Команда DeleteNCB очень похожа на команду NukeNCB из моего шареварного продукта WorkspaceEx. Она дает пользователю возможность автоматически удалить испорченный .ncb файл рабочего пространства (workspace). NCB-файлы периодически стухают, и когда они это делают, функции IDE наподобие Intellisense и ClassView перестают работать. Решение исторически заключалось в том, чтобы закрыть рабочее пространство, открыть оболочку для работы с файлами, зайти в каталог проекта, удалить .ncb-файл и, наконец, заново открыть рабочее пространство. DeleteNCB и NukeNCB делают эти действия по единственному нажатию клавиши. DeleteNCB немного менее удобна, чем NukeNCB, из-за выскакивающего диалога Visual C++, который спрашивает пользователя, хочет ли тот закрыть все открытые документы.

Проблема реализации DeleteNCB заключалась в том, что расширение должно знать путь к .ncb-файлу, чтобы иметь возможность удалить его. Было бы даже достаточно знать путь к файлу рабочего пространства (.dsw), так как эти два файла живут в одном каталоге. Однако в то время как интерфейс автоматизации предоставляет способы перечислить проекты в рабочем пространстве, он не предоставляет атрибутов самого рабочего пространства.

DeleteNCB применяет остроумный подход к решению проблемы. Недолгие эксперименты вскрывают тот факт, что в иерерхии классов MSDEV класс, соответствующий Workspace, реализован как наследник CDocument. DeleteNCB попросту перебирает все открытые экземпляры CDocument, пока не находит класс CProjectWorkspaceDocTemplate. Затем он закрывает workspace, удаляет .ncb-файл и переоткрывает рабочее пространство.

STDMETHODIMP CCommands::DeleteNCB()
{
   // save-all
   m_piApplication->ExecuteCommand( _bstr_t( "FileSaveAll" ) );

   // Получаем объект CWinApp MSDEV’а:
   // (тут скрывается "Магия")
   CWinApp* pApp = AfxGetApp();

   if ( NULL == pApp )
      return E_FAIL;

   // перебор шаблонов документов, поиск шаблона документа, являющегося пространством:
   POSITION posdt = pApp->GetFirstDocTemplatePosition();

   while ( NULL != posdt )
   {
      CDocTemplate * pdt = pApp->GetNextDocTemplate( posdt );

      if ( 0 == strcmp( "CProjectWorkspaceDocTemplate", pdt->GetRuntimeClass() ->m_lpszClassName ) )
      {
         // нашли, берем первый (и единственный) документ:
         POSITION posdoc = pdt->GetFirstDocPosition();

         if ( NULL == posdoc )
            break;

         CDocument* pdoc = pdt->GetNextDoc( posdoc );

         if ( NULL == pdoc )
            break;

         // сохраняем путь+файл пространства:
         CString strWorkspace = pdoc->GetPathName();

         if ( 0 == strWorkspace.GetLength() )
            break;

         // закрываем пространство
         pApp->GetMainWnd() ->SendMessage( WM_COMMAND, 36633 ); // волшебное число для "File/Close Workspace"

         // составляем имя ncb файла
         CString strNCB = strWorkspace.Left( strWorkspace.GetLength() - 4 );

         strNCB += ".ncb";

         // удаляем ncb
         if ( FALSE == ::DeleteFile( strNCB ) )
            AfxMessageBox( CString( "Error deleting NCB file:\n\n" ) + strNCB );

         // снова открываем пространство
         pApp->OpenDocumentFile( strWorkspace );
      }
   }

   return S_OK;
}

Я слышу, как вы говорите: «это прекрасно, но откуда я узнаю, что мне надо искать класс CProjectWorkspaceDocTemplate

Команда ShowInnards определяет информацию о CRuntimeClass для MFC-классов в процессе Visual C++, используя для этого некоторые обсуждаемые ниже техники. С помощью ShowInnards мне удалось идентифицировать более 200 классов, и я подозреваю, что, возможно, удалось бы определить несколько оставшихся, потратив совсем немного усилий.

ShowInnards определяет имя класса, размер, предков и версию схемы (последняя, правда, является редко используемой и нечасто бывает важной). Она также пытается определить раскладку экземпляра класса в памяти, т.е. она пытается определить переменные-члены класса. Это делается путем исследования экземпляра класса в памяти, байт за байтом. Используя чутка хитрости, она определяет, является ли набор байтов указателем на класс-наследник CObject'а или самим встроенным классом-наследником CObject’а. Это делается путем исследования потенциальной таблицы виртуальных функций объекта-претендента. Если проверяемые байты совместимы с таблицей виртуальных функций CObject, вызывается метод GetRuntimeClass() идентифицируемого объекта.

bool CDlgClasses::GetRuntimeInfo( void* pvObj, CRuntimeClass** pprc )
{
   CObject * pObj = ( CObject* ) pvObj;

   // Мы хотим получить информацию о рантайм-типе MFC из данного потенциально указывающего на CObject указателя.
   // Проблема в том, что этот указатель может на самом деле указывать не на CObject.
   // Если мы слепо преобразуем тип и вызовем pObj->GetRuntimeClass(),
   // то скорее всего мы попытаемся выполнить не-код, или, возможно, мы вызовем некую
   // другую виртуальную функцию, например, деструктор.
   // Так как это будет нихарашо, мы попытаемся проверить сигнатуру функции, чтобы
   // увидеть, «похожа» ли она на GetRuntimeClass(), которая, как я выяснил, содержит просто
   // инструкцию mov с последующей инструкцией ret.

   // Я не гарантирую, что этот код будет работать всегда. Если он перестанет работать, исправьте его.
   __try // надеюсь, что SEH работает, и что нам он не понадобится!
   {
      // проверяем адрес:
      if ( FALSE == AfxIsValidAddress( pObj, sizeof( CObject ), FALSE ) )
         return false;

      // удостовериваемся, что указатель на vtable корректен
      void** vfptr = ( void** ) * ( void** ) pObj;

      if ( !AfxIsValidAddress( vfptr, sizeof( void* ), FALSE ) )
         return false;

      // проверяем первую запись vtable
      void* pvtf0 = vfptr[ 0 ];

      if ( IsBadCodePtr( ( FARPROC ) pvtf0 ) )
         return false;

      // смотрим на код этой функции. Проверяем, что это mov и ret
      BYTE arrOpcodes[ 6 ];

      memcpy( arrOpcodes, pvtf0, 6 );

      // приготовьтесь, это будет безобразно.
      // если вы не понимаете, что тут происходит, забейте
      // и не расстраивайтесь
      if ( arrOpcodes[ 0 ] == 0xFF && arrOpcodes[ 1 ] == 0x25 )  // jmp
      {
         void** pvAddr = *( void*** ) & ( arrOpcodes[ 2 ] );

         if ( IsBadCodePtr( ( FARPROC ) * pvAddr ) )
            return false;

         memcpy( arrOpcodes, *pvAddr, 6 );
      }

      if ( arrOpcodes[ 0 ] != 0xB8 || arrOpcodes[ 5 ] != 0xC3 )  // mov, ret
         return false;

      // отлично, выглядит как возможный кандидат на «настоящий» GetRuntimeClass().
      // идем дальше и вызываем его.
      *pprc = pObj->GetRuntimeClass();

      ASSERT( AfxIsValidAddress( ( *pprc ) ->m_lpszClassName, sizeof( char* ), FALSE ) );
      ASSERT( ( *pprc ) ->m_lpszClassName[ 0 ] == 'C' ); // криво, но большинство классов начинаются с ‘C’
   }
   __except ( 1 )
   {
      return false;
   }

   return true;
}

Эта процедура опасна, и возможно, не 100% корректна. В моем коде используется структурная обработка исключений на тот случай, если по ошибке будет исполнена память, не содержащая код. Но самая реальная опасность этой процедуры кроется в возможной неправильной идентификации виртуальной функции, которая не является GetRuntimeClass(). Вместо вызова GetRuntimeClass алгоритм может на самом деле вызвать какую-либо другую функцию, возможно, виртуальный деструктор исследуемого объекта. Это будет нехорошо, потому что состояние объекта, скорее всего, изменится, что повлечет за собой суровые ошибки в дальнейшем. Однако, для целей OpenVC это процедура, как оказалось, работает отлично.

Наконец, ShowInnards имеет два режима отображения. Режим отчетов использует grid-контрол Криса Маундера (Chris Maunder) (с расширением grid-tree от Кена Бертельзона (Ken Bertelson)), чтобы отображать отчет об идентифицированных классах. Для классов, чья внутренняя структура была определена, справа в edit control'е показывается структурное представление раскладки объекта в стиле C. Графический режим показывает всю иерархию классов внутри CScrollView. Я извиняюсь за паршивую раскладку дерева, но я считаю ее достаточной на данный момент.



Знание внутренней раскладки некоторых классов в VC++ является ключом к реализации многих возможностей WorkspaceEx. Например, WorkspaceEx прослеживает и обновляет переменную-член класса CWorkspaceView, который хранит номер выбранной закладки. Большинству расширений не понадобится доступ или необходимость манипуляции этими структурами, кроме тех, которые пытаются изменить существующее поведение IDE. Я мог бы удвоить длину этой статьи обсуждениями всяческих тонких моментов, которые я открыл, но по правде это не то, что может возбуждать. ShowInnards обеспечит любого достаточной информацией для того, чтобы начать понимать, какие биты что делают. Далее я рекомендую вам поудобнее устроиться в вашем отладчике.

Другие подсказки и трюки

Hooking и Subclassing

Вопрос, который мне задают чаще всего о WorkspaceEx, звучит «как так же аккуратно проинтегрироваться с пользовательским интерфейсом VC++?» Ответ на миллион долларов (и я надеюсь, он принесет мне миллион долларов (прим. переводчика: и мне! и мне!)) может быть сформулирован в четырех словах: хуки и сабклассинг окон.

WorkspaceEx, WndTabs и другие расширения используют хуки или сабклассинг окон, созданных Vuisual C++, поэтому код расширения может обрабатывать сообщения, посланные окну, до того, как это сделает IDE. Эти техники настолько мощны, что вполне позволяют изменять приложение так, что оно будет выглядеть совершенно иначе. Если вы не верите мне, посмотрите на RadVC. В комбинации с возможностью доступа к внутренним структурам MFC-приложения и манипулирования ими, расширение может делать почти все, что ему заблагорассудится.

Позвольте мне показать вам пример, как это работает. WorkspaceEx ставит хук на окно типа CWorkspaceView. Чтобы найти правильное окно, на которое будет поставлен хук, код WorkspaceEx перебирает все окна процесса MSDEV. Функция перебора вызывает функцию примерно такого вида:

BOOL CALLBACK FindWorkspaceProc( HWND hwnd, LPARAM lParam )
{
   CWnd * pWnd = CWnd::FromHandle( hwnd );
   CString strClass = pWnd->GetRuntimeClass() ->m_lpszClassName;

   if ( strClass == "CWorkspaceView" )
      g_wndWorkspace.HookWindow( hwnd );

   return TRUE;
}

Заметьте, как функция определяет MFC-класс исследуемого окна. Это довольно простой способ идентифицировать окно. Можно также идентифицировать окно другими способами (по заголовку, ID, классу окна и т.п.), но этот метод прост и хорошо работает.

Глобальная переменная g_wndWorkspace – наследник CSubclassWnd, класса, разработанного Полом Дилация (Paul DiLascia). Я рекомендую использовать этот или подобный класс, т.к. он упрощает работу процедуры.

В некоторых сценариях полезно использовать настоящий хук на окно для перехватывания оконных сообщений. Пример того, как это используется в WorkspaceEx – диалог VC++ Options, куда WorkspaceEx вставляет собственную вкладку. Чтобы сделать это, WorkspaceEx отлавливает создание диалога Options, используя хук WM_CBT, который вызывается, когда в процессе создается новое окно. В этом случае процедура хука смотрит, совпадают ли некоторые атрибуты окна с атрибутами диалога опций, которые я выявил (а именно, название, размер и стиль). Когда совпадение определено, она сабклассит это окно, используя технику сабклассинга, описанную выше.

Панели инструментов

Недостаток интерфейса автоматизации VC++, о котором я слышал немало жалоб, заключается в невозможности задать имя панели инструментов расширения. Вспомните, что VC++ позволяет расширениям создать единственную панель инструментов с кнопками, вызывающими команды расширения.

Эта проблема может быть решена использованием CBT хука окна. Прежде чем вызывать AddCommandBarButton(), установите хук с процедурой, которая отслеживает создание окон с заголовком, содержащим строчку «Toolbar». Этот хук будет вызван при создании нового окна панели инструментов. Перед выходом из хука поименуйте заголовок окна именем на ваш выбор путем модификации имени окна, которое вы можете найти в структуре создания окна, передаваемой в хук.

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

Новое имя должно быть по длине таким же или короче, чем замещаемое имя – обычно восемь символов.

Процедура хука может выглядеть примерно так:

LRESULT CALLBACK FindToolbarProc( int nCode, WPARAM wParam, LPARAM lParam )
{
   // отслеживаем создание окна:
   if ( nCode == HCBT_CREATEWND )
   {
      CBT_CREATEWND * pcw = ( CBT_CREATEWND* ) lParam;

      // отслеживаем создание тулбара, устанавливаем имя тулбара
      if ( pcw->lpcs->lpszName && 0 != strstr( pcw->lpcs->lpszName, "Toolbar" ) )
      {
         strcpy( ( char* ) pcw->lpcs->lpszName, "NukeNCB" );
      }
   }

   return CallNextHookEx( g_hkCBT, nCode, wParam, lParam );
}

Есть и вторая проблема со спецификацией панели инструментов. Документация VC++ подразумевает, что метод AddCommandBarButton(), должен быть вызван из метода расширения OnConnection(), но только когда параметр OnConnection() bFirstTime установлен в VARIANT_TRUE. Однако если ваше расширение является самоустанавливающимся (как описано ранее), OnConnection никогда не будет вызван с первым параметром установленным в VARIANT_TRUE. Если вы попытаетесь вызвать AddCommandBarButton() из OnConnection(), в то время как bFirstTime установлен в VARIANT_FALSE, этот вызов закончится неудачей.

Эта проблема имеет простое решение. Оказывается, процедура AddCommandBarButton() не закончится неудачей, если она вызвана после выхода из OnConnection() даже если bFirstTime никогда не устанавливается в VARIANT_TRUE. Вы просто должны сделать так, чтобы ваше расширение создавало свою панель инструментов после OnConnection(), и это может быть сделано различными способами. Я предлагаю такой: создать невидимое окно в OnConnection() (или постановой хука на существующее окно) и послать ему сообщение перед выходом из OnConnection(). Когда сообщение будет обработано, OnConnection() уже завершит выполнение и расширение сможет вызывать AddCommandBarButton(). Если вы используете эту технику, в вашей ответственности будет знать о том, добавляли ли вы ваш тулбар ранее - добавление дважды и более свидетельствует о плохих манерах.

Заключение

Впрочем, хватит уже об этом. Использование этих техник на пользу Добра оставляю в качестве упражнения читателю (вы ведь ненавидите это выражение, не правда ли?).

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

От переводчика

Ник Ходапп – человек непростой судьбы. Забавно, что через некоторое время после опубликования этой статьи его мечта сбылась. Ник проработал некоторое время в Microsoft в качестве менеджера проекта по созданию Managed C++. Однако сейчас в Microsoft он не работает. Интересно, почему? ;-)


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