Як подолати відсутність множинного спадкоємства в Delphi.


Всі співтовариство програмістів розділяється по прихильності до тієї чи іншої
платформі і мови програмування. Один воліє Delphi для Windows,
іншому подобається асемблер для DOS, третій програмує на С + + для OS / 2.
Навики роботи для однієї платформи зовсім не обов'язково стануть корисними при
переході на іншу, а знання окремої мови програмування може навіть
утруднити вивчення іншого. Всі ці перешкоди можна було б подолати,
використовуючи межпроцессное взаємодія між програмами, однак тут виникає
нова проблема – різні форми внутрішнього подання даних у цих
програмах.


Однак є спосіб вирішення цих проблем: застосування єдиного стандарту для
організації зв'язку між об'єктами, який не залежить від використовуваної платформи
і мови. Саме така розроблена Microsoft компонентна модель об'єкта COM
(Component Object Model). Дана технологія вже отримала широке впровадження: адже
на її базі працюють механізми OLE і ActiveX.


На жаль, у виданій на поточний момент літературі недостатньо чітко
відображений той факт, що програмувати для COM-моделі можна на самих різних
мовах. У більшості прикладів, за дуже рідкісним винятком, використовується Сі + +.
Деякі приклади орієнтовані тільки на Сі + + і кошти цієї мови для
множинного успадкування. Інші приклади будуються на основі бібліотеки MFC,
причому в цьому випадку настільки інтенсивно використовуються її специфічні
макроконструкції для COM, що створюється враження, ніби це взагалі не Сі.
Висновок такий: якщо у вас немає досвіду роботи в Сі + +, то вам буде важко
розібратися, як програмувати для COM.


У цій і наступній за нею статтях ми розглянемо процес формування
COM-об'єктів у середовищі розробки Borland Delphi. У першій частині ми торкнемося
проблем організації COM-об'єктів в Delphi і покажемо кілька варіантів їх
рішення. У другій частині будуть наведені приклади п'яти типових об'єктів для
стандартних надбудов оболонки Windows 95. В окремих випадках COM-об'єкти
доцільно зберігати як EXE-файли. Проте в цій статті з метою простоти
викладу матеріалу будуть розглядатися лише COM-об'єкти, записані в
найбільш часто використовується для них формі DLL-модулів.


Основні поняття про COM-об'єктах


Що ж криється всередині COM-об'єкта? Нам абсолютно не потрібно вникати в це!
Весь обмін інформацією між COM-об'єктом і зовнішнім світом здійснюється через
конкретні інтерфейси. Кожен з них реалізує доступ до однієї або декількох
функцій, звернутися до яких може будь-який об'єкт або програма. Всі COM-об'єкти
повинні мати інтерфейс IUnknown з трьома його функціями – AddRef, Release і
QueryInterface. Функції AddRef і Release відповідають за звичайне завдання
супроводу життєвого циклу об'єкта. При кожному зверненні до Addref вміст
лічильника посилань даного об'єкта збільшується на одиницю, а при кожному зверненні
до Release – зменшується. Коли значення лічильника досягає нуля, об'єкт
знищується. Практичний інтерес представляє третя функція інтерфейсу
IUnknown – QueryInterface. Отримавши доступ до обов'язково присутнього
інтерфейсу IUnknown, програма або будь-який інший об'єкт відразу може звернутися до
функції QueryInterface і дізнатися про всі інші наявних у цього об'єкта
інтерфейсах. IUnknown знаходиться на вершині ієрархічного дерева всіх
COM-інтерфейсів. Будь-який інший інтерфейс фактично успадковується від IUnknown і
тому також повинен забезпечувати доступ до всіх трьох IUnknown-функцій.


Поняття об'єкта як у термінології COM-моделі, так і в Delphi або Сі + + має
практично однаковий зміст. А ось COM-інтерфейс більше нагадує Delphi-або
Сі + +-об'єкт, у якого відсутні public-змінні і є лише віртуальні
методи. Список функцій інтерфейсу відповідає віртуальної таблиці методів
Object Pascal або об'єкта Сі + +. Створити COM-інтерфейс можна засобами
практично будь-якої мови: досить лише оголосити об'єкт з необхідним списком
віртуальних методів. Само собою зрозуміло, що задаються визначення методів
повинні в точності відповідати визначень функцій у самих інтерфейсах.
Однак, крім того, необхідно дотримувати правильний порядок їх розміщення в
віртуальної таблиці. Сказане означає, що ці визначення слідують в заданому
порядку, а перед ними немає ніяких інших віртуальних методів.


У файлі OLE2.PAS, що входить в комплект Delphi 2.0, показано, як давати
визначення типу інтерфейсного об'єкта для IUnknown і для декількох десятків
інших, похідних від IUnknown інтерфейсів, наприклад IClassFactory, IMarshal і
IMalloc. Кожному методу, що входить до складу цих інтерфейсних об'єктів, дається
таке визначення, як virtual, stdcall або abstract. Пояснення, навіщо
вказується virtual, вже було дано. Ключове слово stdcall повідомляє компілятор,
що виклик даного методу слід проводити за стандартними правилами. Слово
abstract вказує, що функціональна частина даного методу в поточному об'єкті
відсутня, але вона має бути присутньою у деякої дочірнього об'єкта, для
якого буде створюватися його примірник. У файлі OLE2.PAS дається визначення
для більш ніж 50 інтерфейсів, безпосередньо успадкованих від IUnknown, причому
кожен з них надає як власний інтерфейс, так і IUnknown.


Однак через необхідність мати для COM-об'єкта два або більш інтерфейсу, не
вважаючи IUnknown, виникає одна проблема. У Сі + + достатньо дати визначення
COM-об'єкту як багаторазово успадкованого від тих об'єктів, де необхідні
інтерфейси містяться. Однак для об'єктів Delphi можливість множинного
спадкування не допускається. Тому доводиться шукати інше рішення. (До уваги
програмістів на Сі + +: при створенні COM-об'єктів на базі MFC застосовується
технологія, аналогічна описуваної тут для Delphi. Ця особливість залишається
непоміченою на тлі безлічі макроконструкції, які використовуються
при визначенні COM-об'єкта засобами MFC.)


Сателіти та контейнери


Ключовий чинник створення в Delphi COM-об'єкта з декількома інтерфейсами
полягає в тому, що об'єкт розглядається як передавальний контейнер цих
інтерфейсів. Зовсім не обов'язково мати їх усередині даного COM-об'єкта.
Необхідно лише при запиті, коли викликається метод QueryInterface його
інтерфейсу IUnknown надавати доступ до потрібного інтерфейсу. Такий COM-об'єкт,
створений в Delphi, може лише безпосередньо обслуговувати три свої функції
IUnknown, а при запиті через QueryInterface інтерфейсу IUnknown, передавати
покажчик на самого себе. Він діє як передавальний механізм і розпорядник
інших об'єктів, що мають свої інтерфейси. Такі інтерфейсні об'єкти-сателіти
відображають свої три IUnknown-методу на загальний об'єкт-контейнер. Якщо приходить
запит на один з сателітних інтерфейсів (як правило, через метод
QueryInterface), контейнер передає покажчик на відповідний
об'єкт-сателіт. На лістингу показаний приклад, як засобами Delphi можна створити
такі інтерфейсні об'єкти з типами сателіт і контейнер, а також як
підготувати відповідний інтерфейс IClassFactory.


Лістинг. За допомогою цих узагальнених об'єктів з описом інтерфейсів можна
створювати в середовищі Delphi COM-об'єкти з декількома інтерфейсами.

unit DelphCom;

/ / "Узагальнені" об'єкти. Призначені для створення COM-об'єктів
/ / У Delphi. ISatelliteUnknown – інтерфейсний об'єкт, який
/ / Буде обслуговуватися через IContainerUnknown. Будь-який реальний
/ / COM-об'єкт з декількома інтерфейсами
/ / Буде успадковуватися з IContainerUnknown і містити
/ / Функцію QueryInterface.


interface
uses Windows, Ole2, Classes, SysUtils, ShellApi, ShlObj;

var DllRefCount : Integer;
type
IContainerUnknown = class;

ISattelliteUnknown = class(IUnknown)

/ / Цей інтерфейс буде обслуговуватися через IContainerUnknown.
/ / Відображає три IUnknown-функції на свій об'єкт-контейнер.


protected
fContainer : IContainerUnknown;
public
constructor Create(vContainer : IContainerUnknown);
function QueryInterface(const WantIID: TIID;
var ReturnedObject): HResult; override;
function AddRef: Longint; override;
function Release: Longint; override;
end;

IContainerUnknown = class (IUnknown)
protected
FRefCount : Integer;
public
сonstructor Create;
destructor Destroy; override;

(IUnknown-функції)


function QueryInterface(const WantIID: TIID;
var ReturnedObject): HResult; override;
function AddRef: LongInt; override;
function Release: LongInt; override;
end;

IMyClassFactory = сlass(IClassFactory)
private
FRefcount : Integer;
public
constructor Create;
destructor Destroy; override;
function QueryInterface(const WantIID: TIID;
var ReturnedObject): HResult; override;
function AddRef: LongInt; override;
function Release: LongInt; override;

/ / У дочірньому об'єкті повинно бути дано визначення
/ / Для функції CreateInstance


function LockServer(fLock: BOOL):
HResult; override;
end;

function DLLCanUnloadNow : HResult; StdCall; Export;
implementation

(****** ISatelliteUnknown *****)


constructor ISatelliteUnknown.Create(vContainer:
IContainerUnknown);
begin fContainer := vContainer; end;

function ISatelliteUnknown.QueryInterface(const WantIID: TIID;
var ReturnedObject): HResult;
begin
Result := fContainer.QueryInterface(WantIid,
ReturnedObject);
end;

function ISatelliteUnknown.AddRef: LongInt;
begin Result := fContainer.AddRef; end;

function ISatelliteUnknown.Release: LongInt;
begin Result := fContainer.Release; end;

(****** IContainerUnknown ******)


constructor IContainerUnknown.Create;
begin
inherited Create;
FRefCount := 0;
Inc(DllRefCount);
end;

destructor IContainerUnknown.Destroy;
begin
Dec(DllRefCount);
inherited Destroy;
end;

function IContainerUnknown.QueryInterface(const WantIID: TIID;
var ReturnedObject): HResult;
var P : IUnknown;
begin
if IsEqualIID(WantIID, IID_IUnknown) then P := Self
else P:= nil;
Pointer(ReturnedObject) := P;
if P = nil then Result := E_NOINTERFACE
else begin
P.AddRef;
Result := S_OK;
end;
end;

function IContainerUnknown.AddRef: LongInt;
begin Inc(FRefCount); Result := FRefCount; end;

function IContainerUnknown.Release: LongInt;
begin
Dec(FRefCount);
Result := FRefCount;
if FRefCount = 0 then Free;
end;

(****** IMyClassFactory ******)


constructor IMyClassFactory.Create;
begin
inherited Create;
Inc(DllRefCount);
FRefCount := 0;
end;

destructor IMyClassFactory.Destroy;
begin
Dec(DllRefCount);
inherited Destroy;
end;

function IMyClassFactory.QueryInterface(const WantIID: TIID;
var ReturnedObject): HResult;
begin
if IsEqualIID(WantIiD, IID_IUnknown) or
IsEqualIID(WantIiD, IID_IClassFactory) then
begin
Pointer(ReturnedObject) := Self;
AddRef;
Result := S_OK;
end
else begin
Pointer(ReturnedObject) := NIL;
Result := E_NOINTERFACE;
end
end;

function IMyClassFactory.AddRef: LongInt;
begin
Inc(FRefCount);
Result := FRefCount;
end;

function IMyClassFactory.Release: LongInt;
begin
Dec(FRefCount);
Result := FRefCount;
if FRefCount = 0 then Free;
end;

function IMyClassFactory.LockServer(fLock: Bool):HResult;
begin Result := E_NOTIMPL; end;

(****** Експортована функція ******)


function DLLCanUnloadNow: hResult; StdCall; Export;
begin
if DllRefCount = 0 then Result := S_OK
else Result := S_FALSE;
end;

initialization
DllRefCount := 0;
end.

Об'єкти-сателіти


Об'єктний тип ISatelliteUnknown безпосередньо успадковується від робочого типу
IUnknown, причому всі його три абстрактних методу обов'язково перевизначаються.
ISatelliteUnknown містить єдине поле protected-змінної з ім'ям
FContainer і типом IContainerUnknown (його визначення дається пізніше); початкове
значення для даної змінної присвоюється в його конструктора Create.
Призначення трьох його IUnknown-функцій полягає лише в тому, щоб передати
результат, отриманий після виклику відповідного методу об'єкта-контейнера. У
Залежно від того, який інтерфейс запитує викликає програма, вона
отримує доступ до методів QueryInterface, AddRef і Release або безпосередньо
через об'єкт-контейнер, або через будь-який з його об'єктів-сателітів


Якщо вам вже доводилося вивчати літературу за технологією OLE, то ви
напевно звернули увагу, що в модулі DelphCOM, наведеному в лістингу,
використовуються нестандартні імена для параметрів QueryInterface. Зазвичай для
позначення ідентифікатора ID потрібного інтерфейсу використовується ім'я riid, а
переданому програмі об'єкту призначається ім'я ppv. Оскільки імена параметрів
мають сенс тільки в межах даного об'єкта, я вирішив замінити зашифровані
стандартні імена на більш зрозумілі WantIID і ReturnedObject.


Об'єкти-контейнери


Об'єктний тип IContainerUnknown також безпосередньо успадковується від
IUnknown. Він містить власний лічильник кількості посилань, записується у полі
protected-змінної з ім'ям FRefCount; його функція AddRef забезпечує
прирощення лічильника FRefCount, а Release – його зменшення. Обидві функції – AddRef
і Release – передають у програму нове значення лічильника. Якщо воно стає
рівним 0, функція Release додатково виробляє вивільнення об'єкта.


Крім цього, в модулі DelphCOM дається визначення глобального лічильнику посилань
для всієї DLL, через який відстежуються всі об'єкти, похідні від цих
узагальнених COM-об'єктів. Його збільшення й зменшення виробляються при роботі
відповідно конструктора і деструктора цього об'єкта-контейнера. Будь-яка DLL,
де містяться COM-об'єкти, повинна виконувати дві спеціальні функції –
DLLCanUnloadNow і DLLGetClassObject. У модулі DelphCOM присутня функція
DLLCanUnloadNow, яка буде приймати значення False до тих пір, поки
значення згаданого глобального лічильника DLL не стане рівним 0. Що ж
стосується функції DLLGetClassObject, то її зміст специфічно для кожної
конкретної DLL, що використовує DelphCOM. Тому її не можна буде записати до тих
пір, поки не будуть задані самі COM-об'єкти (які є похідними від
ISatelliteUnknown і IContainerUnknown).


Об'єкт IContainerUnknown реагує на запити інтерфейсу IUnknown,
надходять через QueryInterface, і передає покажчик на самого себе. При
запиті іншого інтерфейсу передається код помилки E_NOINTERFACE. Коли ж ця
ситуація виникає в похідному від IContainerUnknown об'єкті, то функція
QueryInterface спочатку звертається до цієї, успадковане від батьківського об'єкта
функції. Якщо у відповідь передається значення E_NOINTERFACE, тоді перевіряється
збіг ідентифікатора запитуваної інтерфейсу з ідентифікаторами його
інших інтерфейсів. При збігу в програму передається покажчик цього
об'єкта-сателіта.


Генератор класу


COM-об'єкти можуть створюватися при видачі відповідної команди від системи
або від деякої програми. Цей процес створення управляється особливим типом
COM-об'єкта, що має назву генератором класу (class factory); він також виходить
прямим наслідуванням від IUnknown. Наявний в модулі DelphCOM об'єкт
IMyClassFactory, як і об'єкт IContainerUnknown, містить методи AddRef і
Release. Якщо через QueryInterface надходить запит на IUnknown або
IClassFactory, то він передає покажчик на самого себе. Крім названих трьох
функцій в інтерфейсі IClassFactory додатково з'являються дві нові –
CreateInstance і LockServer. Зазвичай функція LockServer не потрібно, і в цьому
випадку вона приймає особливе значення E_NOTIMPL – ознака того, що дана
функція не задіяна.


Найбільш важлива функція генератора класу, заради якої він створюється, – це
CreateInstance. З її допомогою викликає програма створює екземпляр необхідного
об'єкта. У модулі DelphCOM, правда, ще немає яких-небудь "закінчених" об'єктів;
тут містяться лише узагальнені об'єкти сателіта і контейнера. Коли ми даємо
визначення COM-об'єкту як успадкованого від IContainerUnknown, нам також
доводиться давати визначення об'єкту, похідному від IMyClassFactory, функція
якого – CreateInstance – буде передавати в програму новий екземпляр цього
COM-об'єкта.


Тепер, ввівши IMyClassFactory, ми отримали повний комплект узагальненого
COM-об'єкта для роботи в Delphi. Ця система з об'єктів сателіта і контейнера
може використовуватися в будь-якому об'єктно-орієнтованої мови програмування;
адже, дійсно, COM-об'єкти, створювані засобами MFC, використовують
аналогічну систему. У другій частині цієї статті ми перейдемо від теорії до
практиці. Можливості розглянутих тут узагальнених об'єктів будуть істотно
розширені, що дозволить як приклад створити п'ять різних типових
надбудов для оболонки Windows 95 – для обслуговування операцій з контекстним
меню, діалоговим вікном Property, перетягування об'єктів з допомогою правої
клавіші миші, маніпуляцій з піктограмами та операцій копіювання.


Розібравшись з прикладами, ви відчуєте повну готовність до створення
власних, реально діючих надбудов для оболонки Windows 95.


Ідентифікатори GUID, CLSID і IID


При створенні і роботі COM-об'єктів інтенсивно використовуються ідентифікатори,
іменовані як Globally Unique Identifiers (глобально унікальні ідентифікатори),
або, коротко, GUIDs (вимовляється "GOO-ids"). Цей параметр є
деякий 128-розрядне число, що генерується функцією CoCreateGUID, що входить до
склад Windows API. Значення GUID повинні бути унікальні в глобальних масштабах:
передане функцією CoCreateGUID значення ніколи не повинно повторюватися. Крейг
Брокшмідт (Kraig Brockschmidt), фахівець з OLE (з групи розробників OLE в
Microsoft), як-то заявив, що ймовірність збігу результатів двох різних
звернень до CoCreateGUID дорівнює того, що "два випадково блукаючих по
всесвіту атома раптом раптово зіткнуться й утворять гібрид маленького
каліфорнійського авокадо з каналізаційної щуром з Нью-Йорка ".


Справа в тому, що у кожного інтерфейсу повинен бути свій ідентифікатор IID
(Interface ID), що є тим же самим GUID. У файлі OLE2.PAS, що входить до
комплект Delphi, дається визначення десяткам таких параметрів. Приклад програми
з даної статті містить посилання на ідентифікатори інтерфейсів IUnknown і
IClassFactory; а у файлі OLE2.PAS міститься безліч інших подібних
параметрів. Крім того, будь-об'єктний клас, зареєстрований в системі,
повинен мати свій ідентифікатор класу Class ID (CLSID). Якщо вам коли-небудь
доводилося за допомогою програми RegEdit переглядати ключ
HKEY_CLASSES_ROOTCLSID системного реєстру Windows, ви напевно звертали
увагу на десятки, а іноді й сотні незрозумілих рядків із записаними в них
цифрами. Все це – ідентифікатори класів для всіх COM-об'єктів,
зареєстрованих на вашому комп'ютері. Не будемо вдаватися в подробиці; скажімо
лише, що при програмуванні COM-об'єктів слід використовувати наявні
параметри GUID, а також створювати нові, специфічні для вашої конкретної
програми.


Існує ряд безкоштовних утиліт, наприклад UUIDGEN.EXE, що дозволяють
генерувати нові значення GUID. Проте після її виконання доведеться займатися
рутинної завданням – акуратно переписувати отримані значення на місце констант
Delphi. Натомість UUIDGEN.EXE служба PC Magazine Online пропонує іншу
"Консольну" програму з текстовим висновком. Її можна або завантажити в
інтегроване середовище Delphi і зробити компіляцію там, або обробити
компілятором Delphi, ввівши через командний рядок DCC32 GUIDS.DPR. Тепер
запустіть отриману програму, і ви отримаєте абсолютно нове, не зустрічалося
раніше значення GUID – у вигляді рядка і у вигляді типової константи Delphi.


Відтепер, починаючи роботу над новим проектом, уважно підрахуйте
необхідну кількість окремих параметрів GUID. Про всяк випадок додайте ще
кілька. Тепер вкажіть це число як параметр для програми GUIDS.EXE і
перенаправьте її висновок в окремий файл. Там будуть записані вказану кількість
ідентифікаторів GUID, причому, як правило, вони будуть являти собою блок
безперервно зростаючих чисел. Справа в тому, що коли використовувані у вашому
проекті параметри GUID відрізняються між собою лише цифрою в окремій позиції,
легше розбиратися, який ідентифікатор до чого відноситься. Тепер ви можете
вирізати ці значення з текстового файлу і вставити в потрібні місця свого
проекту.


Частина 2

Приклади створення чотирьох 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

           <——— IUnknown ———–>
| ——– |
| |
IContainerUnknown ISatelliteUnknown
| |
|-> IPFContainer ———–> IMyPersistFile |
| IPersistFile IPersistFile <—|
| | ——–> |
| | | |
| ->IDSExtraction ——-> IMyExtraction <—|
| IPersistFile IExtractIcon |
| IExtractIcon |
| |
| |
–>ISEIContainer ———–> IMyShellExtInit<—|
IShellExtInit ——-> IShellExtInit |
| | -> |
| | || |
|-> IDSContextMenu —-||> IMyContextMenu <—|
| IShellExtInit || IContextMenu |
| IContextMenu || |
| ——– | |
| | —— |
|-> IDSDragDrop -|——-> IMyDragDrop <—|
| IShellExtInit| IContextMenu |
| IContextMenu | |
| | |
|-> IDSPropSheet ——–> IMyPropSheet <—|
IShellExtInit IShellPropSheetExt
IShellPropSheetExt

Ліст. 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 і повторювати експеримент до тих пір,
поки не вдасться знайти помилку.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*