|
Delphi
Создание COM-объектов средствами Delphi
Часть I Часть II
Примеры
создания четырех COM объектов -
расширений оболочки Windows 95.
В технологиях создания COM
объектов в среде Delphi и в среде
Си++ наблюдаются существенные
различия, хотя, конечно, есть в
них и некоторое сходство: у
таких объектов обычно один или
несколько интерфейсов, а у
объекта в Delphi и у объекта в C++
может быть один и тот же
COM-интерфейс. Однако в Си++
задача обеспечения COM объекта
несколькими интерфейсами
решается с помощью механизма
множественного наследования,
т. е. порождаемый объект
наследует функции от всех
требующихся интерфейсов. В Delphi
подобной возможности нет,
поэтому необходим другой
подход.
В Delphi COM объект с несколькими
интерфейсами приходится
формировать из нескольких
отдельных объектов. Каждый из
требующихся COM-интерфейсов
предоставляется
объектом-сателлитом - потомком
имеющегося в Delphi объекта типа
IUnknown. Такой объект-саттелит
реализует интерфейс IUnknown. Сам
же COM объект представляет собой
объект-контейнер, тоже
производный от IUnknown.
Объект-контейнер, содержащий
экземпляры
объектов-сателлитов в виде
полей данных, в ответ на запрос
к своему методу QueryInterface
передает указатель на
упомянутый в нем интерфейс. Эти
приемы и их реализацию на
примере объектов ISatelliteUnknown и
IContainerUnknown мы рассмотрели в
первой части данной статьи. А
теперь с помощью этих объектов
мы попробуем подготовить
специальные COM объекты -
расширения оболочки Windows 95.
Мы продемонстрируем процедуры
создания средствами Delphi
четырех расширений Windows95:
обработчика контекстного меню,
обработчика списка параметров,
обработчика для механизма
drag-and-drop и обработчика
пиктограмм. Они выполняют
операции с некоторым
воображаемым типом файлов
DelShellFile с расширением DEL. Строка
текста такого файла
представляет собой целое
число; в настоящей программе
его заменит какой-то более
сложный атрибут файла.
Названный "магический
номер" используется всеми
четырьмя расширениями.
Среди прилагаемых к статье
исходных текстов вы обнаружите
и еще одно расширение - для
обслуживания операции
копирования. Но, поскольку для
его реализации не требовалась
связка контейнер/сателлит, мы
не уделили ему внимания в
статье.
Все упомянутые в статье
программы можно загрузить из
службы PC Magazine Online.
Подготовка вспомогательных
интерфейсов
На рис. 1 представлена иерархия
создаваемых нами
вспомогательных объектов.
Сплошными линиями обозначены
стандартные иерархические
связи между объектами; на
вершине этого дерева вы видите
объект IUnknown, описанный на языке
Delphi. Под именем каждого объекта
перечисляются все его
интерфейсы, за исключением
обязательного для всех
интерфейса IUnknown. Пунктирными
линиями показаны связи
контейнер/сателлит, которые
служат основой всей системы.
Инициализаций расширений,
предназначенных для
обслуживания контекстного
меню, списка параметров и
работы механизма drag-and-drop,
выполняется с помощью
интерфейса IShellExtInit.
Аналогичная операция для
расширения - обработка
пиктограмм осуществляется
через интерфейс IPersistFile. На
лист. 2 приведены описания
объектов-сателлитов,
реализующих два названных
вспомогательных интерфейса, и
объектов-контейнеров, заранее
подготовленных для управления
этими объектами-сателлитами.
Дополнительный метод Initialize
объекта IMyShellExtInit служит
функцией Initialize интерфейса
IShellExtInit. Данный объект
наследует функции объекта
ISatelliteUnknown: его методы QueryInterface,
AddRef и Release. В результате таблица
виртуальных методов объекта
IMyShellExtInit полность совпадает с
набором функций интерфейса
IShellExtInit. Метод Initialize извлекает
из передаваемых вызывающей
программой данных список
файлов и сохраняет его в
отдельном поле данных своего
объекта-контейнера, тип
которого обязательно должен
быть ISEIContainer.
ISEIContainer наследует методы AddRef и
Release контейнера IContainerUnknown.
Имеющий собственную
реализацию метода QueryInterface
объект ISEIContainer сначала
вызывает вариант QueryInterface,
унаследованный от IContainerUnknown.
Если полученное в ответ
значение не равно S_OK, тогда с
помощью его собственного
метода QueryInterface проверяется,
есть ли обращение к интерфейсу
IShellExtInit. Если ответ
положительный, этот метод
передает указатель на свое
поле типа protected FShellExtInit,
являющееся объектом типа
IMyShellExtInit. Кроме этого, в ISEIContainer
описываются поля для хранения
списка файлов, их числа и
маршруты к ним. Имеющийся у
него конструктор Create
инициализирует список файлов и
объекты FShellExtInit, а деструктор
Destroy высвобождает память,
отведенную для этих двух
объектов.
Описание объекта IMyPersistFile
кажется более сложным, чем у
IMyShellExtInit. Однако в
действительности пять из шести
его методов, реализующих
функции интерфейса IPersistFile, в
качестве результата передают
значение E_FAIL. Метод Load объекта
IMyPersistFile получает имя файла в
формате Unicode, преобразует его в
строку ANSI и записывает в
соответствующее поле своего
объекта-контейнера, тип
которого обязательно IPFContainer.
Так же как у ISEIContainer, метод
QueryInterface объекта IPFContainer имеет
свои особенности. Сначала
выполняется обращение к
унаследованному варианту
QueryInterface. Если в ответ получено
значение ошибки, то с помощью
собственного метода QueryInterface
проверяется, есть ли обращения
к интерфейсу IPersistFile. Если да,
передается указатель на
protected-поле FPersistFile - объект типа
IMyPersistFile. За создание и удаление
объекта FPersistFile отвечают
специальные методы
объекта-контейнера -
конструктор и деструктор.
Теперь все готово и можно
приступать к подготовке наших
расширений оболочки Windows95.
Рис. 1. Иерархия объектов -
расширений оболочки Windows
| -------- |
| |
IContainerUnknown ISatelliteUnknown
| |
|-> IPFContainer -----------> IMyPersistFile |
| IPersistFile IPersistFile |
| | | |
| ->IDSExtraction -------> IMyExtraction
ISEIContainer -----------> IMyShellExtInit
IShellExtInit |
| | -> |
| | || |
|-> IDSContextMenu ----||> IMyContextMenu
IDSDragDrop -|-------> IMyDragDrop IDSPropSheet
--------> IMyPropSheet
Лист. 1. Два объекта-сателлита
реализуют вспомогательные
интерфейсы, необходимые для
работы
таких расширений оболочки Windows
95, как обработчики
контекстного меню, списка
параметров, для механизма
drag-and-drop и пиктограмм.
type
IMyShellExtInit = class(ISatelliteUnknown)
public
function Initialize(pidlFolder:PItemIDlist; lpdobj:
IDataObject;hKeyProgID:HKEY) :HResult; virtual; stdcall;
end;
IMyPersistFile = class(ISatelliteUnknown)
public
function GetClassID(var classID: TCLSID): HResult;
virtual; stdcall;
function IsDirty: HResult; virtual; stdcall;
function Load(pszFileName: POleStr; dwMode: Longint):
HResult; virtual; stdcall;
function Save(pszFileName: POleStr; fRemember: BOOL):
HResult; virtual; stdcall;
function SaveCompleted(pszFileName: POleStr): HResult;
virtual; stdcall;
function GetCurFile(var pszFileName: POleStr): HResult;
virtual; stdcall;
end;
ISEIContainer = class(IContainerUnknown)
protected
FShellExtInit : IMyShellExtInit; // Интерфейс
объекта-сателлита
public
FNumFiles : Integer;
FInitFiles : TStringList;
FIDPath : String;
Constructor Create;
destructor Destroy; override;
function QueryInterface(const WantIID: TIID);
var ReturnedObject): HResult; override;
end;
IPFContainer = class(IContainerUnknown)
protected
FPersistFile : IMyPersistFile: //Интерфейс
объекта-сателлита
public
FPFFileName : String;
Constructor Create;
destructor Destroy; override;
function QueryInterface(const WantIID: TIID;
var ReturnedObject): HResult; override;
end;
Обработчик контекстного меню
Щелчок правой клавишей мыши на
каком-то файле, в среде Windows 95
Explorer приводит к тому, что
система предпринимает попытку
выяснить, задан ли для такого
типа файлов обработчик
контекстного меню. Если
таковой имеется, система
создает экземпляр COM-объекта -
обработчика контекстного меню
и передает список выделенных
файлов функции Initialize
интерфейса IShellExtInit этого
объекта. Затем обращается к
методу QueryContextMenu интерфейса
IContextMenu. В работе этой функции
используются стандартные
функции Windows API; например, для
вставки дополнительных
элементов меню или
разделителей вызывается
функция InsertMenu, которая
передает в качестве
return-значения число добавленных
элементов, не считая
разделителей. Если же
пользователь выбрал один из
этих внесенных элементов меню,
то происходит вызов функции
InvokeCommand интерфейса IContextMenu.
Чтобы предоставить
комментарий к данному элементу
меню в строке состояний
программы Explorer, вызывается
функция GetCommandString.
Для определения и
инициализации обработчика
контекстного меню
используются следующие
Delphi-объекты: IMyContextMenu, IDSContextMenu и
ICMClassFactory. Объект IMyContextMenu
является потомком ISatelliteUnknown;
его интерфейс IContextMenu реализует
три функции. Объект IDSContextMenu -
потомок ISEIContainer, поэтому
снабжен интерфейсом IShellExtInit. В
IDSContextMenu имеется
дополнительное protected-поле
FContextMenu с типом IMyContextMenu. И в этом
случае конструктор и
деструктор объекта IDSContextMenu
ответственны за создание и
удаление объекта-сателлита;
при обращении к интерфейсу
IContextMenu метод QueryInterface данного
объекта передает в вызывающую
программу указатель на объект
FContextMenu.
Эта программа содержит также
описание объекта ICMClassFactory -
потомка IMyClassFactory, специально
предназначенного для
получения экземпляра IDSContextMenu.
Метод CreateInstance создает
запрашиваемый экземпляр и
обеспечивает к нему доступ, но
только если среди интерфейсов
объекта IDSContextMenu имеется
запрашиваемый. Для каждого из
наших расширений оболочки
потребуется почти такой же
вариант потомка IMyClassFactory.
Метод QueryContextMenu предназначен
для проверки того, сколько
файлов выбирается: один или
несколько. Если только один, в
меню добавляется элемент под
именем Magic Number (магический
номер); если же их несколько -
элемент Average Magic Number
(усредненный магический номер).
Метод InvokeCommand проверяет
правильность переданных ему
аргументов и выводит в окне
сообщений запрошенный номер.
Метод GetCommandString в соответствии
с тем, что было запрошено,
передает либо отдельное слово -
наименование элемента меню,
либо пояснительную строку.
Обработчик для механизма
drag-and-drop
Обработчик для механизма
drag-and-drop практически не
отличается от обработчика
контекстного меню - в них
используется даже один и тот же
интерфейс IContextMenu. Однако
имеются некоторые отличия:
во-первых, активизация
расширения, предназначенного
для обслуживания механизма
drag-and-drop происходит при
переносе файла в какую-то папку
правой клавишей мыши;
во-вторых, это расширение
вносится в список файлов того
типа, которые помещены в данную
папку, а не к тому типу файлов, к
которому относится
перемещенный файл.
Объект-сателлит IMyDragDrop
содержит следующие методы:
QueryContextMenu, InvokeCommand и GetCommandString.
Сначала метод QueryContextMenu
выполняет просмотр
переданного ему системой
списка файлов с целью проверки,
все ли относятся к типу DelShellFile.
Если это так, данный метод
добавляет в меню новый элемент
Count Files (Подсчет файлов),
разделитель и передает в
качестве return-значение 1. Если же
результат отрицательный,
никаких действий не
производится и передается
значение 0. При выборе
добавленного элемента меню
метод InvokeCommand подсчитывает
количество файлов в
папке-получателе и добавляет
это число к "магическому
номеру" каждого из
выделенных DelShellFile-файлов.
Поскольку этот номер и
пиктограмма такого файла
взаимосвязаны, обращение к
функции API, SHChangeNotify осведомит
систему о необходимости
обновить пиктограммы каждого
из этих файлов.
В функциональном отношении
объект-контейнер IDSDragDrop
идентичен объекту IDSContextMenu.
Разница лишь в том, что тип его
объекта-сателлита - IMyDragDrop, а не
IMyContextMenu.
Обработчик списка параметров
Когда пользователь, выделив
один или несколько файлов,
выбирает в контекстном меню
команду Properties (Параметры),
система сначала пытается
определить, предусмотрен ли
специальный обработчик списка
параметров для данного типа
файлов. Если да, система
создает экземпляр
соответствующего расширения
оболочки и инициализирует,
передав функции Initialize его
интерфейса IShellExtInit список
выделенных файлов. Система
также обращается к функции
AddPages интерфейса IShellPropSheetExt, с
тем чтобы дать возможность
обработчику списка параметров
добавить к нему одну или
несколько страниц. Другая
функция интерфейса IShellPropSheetExt -
ReplacePages - обычно не
используется.
Однако, когда дело доходит до
реализации метода AddPages,
программисты, работающие с Delphi,
внезапно оказываются в полной
растерянности. Для создания
страницы списка параметров
необходим такой ресурс, как
шаблон диалогового окна, и
функция для его обработки. Лишь
бывалые Windows-программисты,
возможно, еще помнят о
старинных предшественниках
нынешних средств визуального
программирования. Для
подготовки шаблона
диалогового окна можно
воспользоваться инструментом
для генерации ресурсов, таким,
как Resource Workshop фирмы Borland или
составить сценарий ресурса и
откомпилировать его с помощью
компилятора ресурсов BRCC.EXE,
входящего в комплект Delphi.
Вместе с исходными текстами
для этой статьи можно
загрузить и сценарий ресурса,
описывающий список параметров
для файлов типа DelShellFile.
Этот сценарий дает определения
двух статических полей с
текстом, окна списка и кнопки. В
общем подключаемом файле SHEET.INC
объявлены константы IDC_Static,
IDC_ListBox и IDC_Button, используемые в
качестве идентификаторов для
управления диалоговым окном.
При исполнении метода AddPages
происходит инициализация
различных полей структуры
TPropSheetPage, в том числе шаблона
диалогового окна, процедуры
управления им и параметра lParam,
описанного в программе. Здесь
lParam содержит список файлов,
переданных из оболочки Windows.
Использование функции
обратного вызова гарантирует
освобождение памяти,
выделенной под этот список. При
обращении к функции
CreatePropertySheetPage она создает
страницу на основании данных
структуры TPropSheetPage, а при вызове
предусмотренной в оболочке
функции lpfnAddPage к диалоговому
окну Properties будет добавлена эта
страница.
Процедура управления
диалоговым окном обрабатывает
два конкретных сообщения. Если
поступает сообщение WM_INITDIALOG,
окно списка дополняется
перечнем файлов, указанным в
поле параметра lParam данной
страницы списка параметров.
Перед каждым именем
проставляется соответствующий
"магический номер". Затем
процедура формирует
статический элемент
управления, отображающий
количество выбранных в данный
момент файлов. Список файлов
удаляется, а поле, где прежде
находился данный список
файлов, обнуляется.
Если же пользователь щелкнет
на кнопке Zero Out (Очистить),
процедура управления
диалоговым окном получает
сообщение WM_COMMAND, где в младшем
слове wParam указывается
идентификатор данной кнопки.
Процедура просматривает весь
список файлов и делает нулевым
"магический номер"
каждого из них, затем
обращается к функции API -
SHChangeNotify, чтобы сообщить
системе о необходимости
перерисовать пиктограммы
файлов. Фактически любая
процедура управления
диалоговым окном списка
параметров должна иметь
средства для реакции на
сообщение WM_INITDIALOG, чтобы
выполнить инициализацию своих
управляющих элементов. Если же
она предназначена не только
для отображения информации,
тогда в ней должны быть
средства, обеспечивающие
реакцию на сообщения WM_COMMAND,
поступающие от конкретных
управляющих элементов.
Обработчик пиктограмм
В большинстве случаев средства
оболочки Windows 95 просто выбирают
для файла ту пиктограмму,
которая указана для такого
типа файлов в разделе DefaultIcon
системного реестра. Однако,
если в разделе DefaultIcon задано
значение %1, тогда происходит
обращение к некоторому
расширению оболочки, которое
выполняет роль обработчика
пиктограмм для данного файла.
Система обращается к функции
Load интерфейса IPersistFile этого
расширения, передавая ей в
качестве параметра имя файла.
Обработчик пиктограмм
обеспечивает соответствующую
пиктограмму через функции
GetIconLocation и Extract своего
интерфейса IExtractIcon. Эта
информация представляет собой
либо имя файла и порядковый
номер конкретной пиктограммы,
либо созданную при поступлении
запроса пиктограмму.
Наш пример объекта-сателлита
IMyExtractIcon реализует оба
варианта. Если задана
директива условной компиляции
UseResource, метод GetIconLocation
присваивает аргументу szIconFile в
качестве значения имя
DLL-модуля, содержащего объект
IMyExtractIcon, затем на основании
"магического номера"
файла вычисляет значение
аргумента piIndex. Данный метод
включает в значение аргумента
pwFlags флажок GIL_PERINSTANCE, наличие
которого означает, что каждый
файл может иметь свою
отдельную пиктограмму и флажок
GIL_DONTCACHE - знак того, что система
не должна сохранять эту
пиктограмму в памяти для
последующих применений. Метод
Extract в этом случае не
используется; его return-значение
будет S_FALSE.
Если же директива условной
компиляции UseResource не задана,
тогда объект-сателлит IMyExtractIcon
формирует пиктограмму для
каждого файла. Метод GetIconLocation
заносит "магический
номер" данного файла в
аргумент piIndex и помимо
упомянутых выше флажков
использует флажок GIL_NOTFILENAME. Из
оболочки вызывается метод Extract,
который создает для данного
файла пиктограммы двух
размеров - крупную и маленькую.
Высота красной полоски в
прямоугольнике пиктограммы
определяется "магическим
номером" файла. В исходных
текстах, прилагаемых к этой
статье, представлена процедура
создания пиктограммы на ходу.
Однако, поскольку она имеет
лишь косвенное отношение к
тематике этой статьи, ее
подробности здесь не
обсуждаются.
Компоновка программы
Для того чтобы все
перечисленные расширения
оболочки работали, нужно
скомпилировать их в DLL-модуль,
содержащий стандартные
функции DllGetClassObject и DllCanUnloadNow. В
числе исходных текстов,
прилагающихся к этой статье,
имеется и программа,
описывающая такой DLL-модуль.
Функция DllGetClassObject выполняет
следующие операции: выясняет, к
какому объекту поступил
запрос, формирует
соответствующую фабрику
классов (class factory) и передает в
качестве результата объект,
созданный этой фабрикой. Среди
упомянутых исходных текстов вы
найдете также программу,
описывающую DLL-модуль
несложной консольной
процедуры, управляющей
операциями внесения и удаления
из системного реестра
информации обо всех
перечисленных здесь образцах
расширений оболочки.
Теперь, изучив приведенные
примеры, можно приступать к
созданию собственных
расширений оболочки. Только не
забудьте заменить имеющиеся в
текстах программ значения
глобально уникальных
идентификаторов GUID (Globally Unique
Identifiers) новыми. В этом вам
поможет программа генерации,
GUIDS, представленная в первой
части этой статьи.
Средства для отладки COM
объектов
Большинство современных
пакетов для разработки
программ содержат встроенные
средства отладки,
обеспечивающие возможность
выполнения в пошаговом режиме,
трассировки кода, установки
точек прерывания и просмотра
значений переменных. Все они
пригодны для отладки
исполнимых EXE-модулей. Однако
если программа оформлена в
виде DLL-модуля, то
интегрированные средства
отладки оказываются
бесполезными. Даже при
использовании 32-разрядного
автономного отладчика не
так-то просто добраться до COM
объектов, поскольку они
выполняются в адресном
пространстве обратившегося к
ним объекта или программы.
Например, COM объекты,
являющиеся расширениями
оболочки Windows 95, исполняются в
адресном пространстве
программы Windows Explorer.
Однако чаще всего разработчика
интересуют достаточно простые
вопросы о работе COM объектов:
Был ли загружен DLL-модуль
вообще? Производилась ли
попытка создать экземпляр
конкретного COM объекта? Какой
интерфейс запрашивался?
Выяснить все это можно с
помощью простого механизма
регистрации сообщений: COM
объект отправляет сообщения о
своем состоянии, которые
принимает и регистрирует
предназначенная для этого
самостоятельная программа. Из
службы PC Magazine Online вы можете
загрузить специальный модуль
DllDebug, который обеспечивает
механизм передачи таких
сообщений.
Раздел этого модуля, который
выполняет инициализацию,
присваивает переменной WM_LOGGIT
уникальное значение
идентификатора сообщений,
полученное от функции
RegisterWindowMessage в результате
передачи ей строковой
переменной Debugging Status Message. При
первом обращении к функции
RegisterWindowMessage с использованием
этой строки она передает
уникальный номер сообщения, а
при последующих вызовах с ней в
качестве результата будет
получен тот же номер.
Поскольку 32-разрядные
программы выполняются в
отдельном адресном
пространстве, функция Loggit не
может так просто передать
указатель на свою строку с
сообщением о состоянии. В
адресном пространстве
принимающей программы этот
указатель будет
недействителен. Поэтому
функция Loggit вносит это
сообщение в таблицу глобальных
элементов системы Windows (global atom
table). После этого она обращается
к функции SendMessage, передавая ей
следующие параметры: значение
-1 для дескриптора окна, WM_LOGGIT в
качестве номера сообщения и
элемент для wParam. Функция SendMessage
сохраняет за собой управление
до тех пор, пока действующие в
системе окна верхнего уровня
не обработают это сообщение.
Теперь этот элемент можно
безболезненно удалить.
При подготовке сообщений о
состоянии очень кстати
придется функция NameOfIID,
предусмотренная в модуле DllDebug.
Согласно документации, она
передает идентификаторы
интерфейсов IIDs, реализуемых
расширениями оболочки. Однако
к ним можно добавить любые
значения системных IID,
необходимых для вашего
проекта. Например, в тело
метода QueryInterface можно было бы
вставить следующую строку:
Loggit(Format('QueryInterface: %s requested',
[NameOfIID(WantIID)]));
Организовать передачу
сообщения WM_LOGGIT - это еще
полдела. Нужна программа,
которая будет принимать и
регистрировать сообщения о
производимых операциях.
Утилита Logger, предлагаемая
службой PC Magazine Online, - один из
возможных вариантов решения
этой задачи.
Поскольку значение, имеющееся
в сообщении WM_LOGGIT, становится
известным только в процессе
исполнения, нет возможности
задать стандартный метод
обработки сообщения. Поэтому в
программе Logger
переопределяется интерфейсный
метод DefaultHandler. При прохождении
сообщения WM_LOGGIT этот метод
извлекает сообщение о
состоянии из передаваемого
элемента и добавляет его в
имеющийся список окна
просмотра. Помимо этой
основной функции она
обслуживает три рабочие кнопки
- для вставки комментария
пользователя, для очистки окна
списка и для сохранения
зарегистрированных сообщений
в файле. На рис. А вы видите
момент выполнения программы
Logger.
В приведенном диалоговом окне
представлены методы QueryInterface
нескольких COM объектов,
подготовленных в среде Delphi,
инструментированные строкой, в
которой регистрируется имя
запрашиваемого интерфейса.
Перед вами список запросов,
отправленных, когда Explorer
извлек пиктограмму для
некоторого файла, затем
пользователь щелкнул на ней
правой клавишей мыши и
просмотрел его параметры. Все
работает правильно. Если же
наша утилита вдруг выводит на
экран неожиданные результаты,
тогда в сомнительный фрагмент
своей программы можно добавить
новые обращения к функции Loggit и
повторять эксперимент до тех
пор, пока не удастся найти
ошибку.
|