Object — Ключевое слово Delphi

Содержание

Object — Ключевое слово Delphi

Нил Дж. Рубенкинг

Как преодолеть отсутствие множественного наследования в 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-объекты должны иметь интерфейс 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-объекты с несколькими интерфейсами.

// «Обобщенные» объекты. Предназначены для создания 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 ;

function QueryInterface(const WantIID: TIID; var ReturnedObject): HResult; override ; function AddRef: LongInt; override ; function Release: LongInt; override ; end ; IMy >с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

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;

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;

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.

При создании и работе 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_ROOT\CLSID системного реестра 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 отличаются между собой лишь цифрой в отдельной позиции, легче разбираться, какой идентификатор к чему относится. Теперь вы можете вырезать эти значения из текстового файла и вставить в нужные места своего проекта.

Примеры создания четырех 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

Лист. 1. Два объекта-сателлита реализуют вспомогательные интерфейсы, необходимые для работы таких расширений оболочки Windows 95, как обработчики контекстного меню, списка параметров, для механизма drag-and-drop и пиктограмм.

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 практически не отличается от обработчика контекстного меню — в них используется даже один и тот же интерфейс 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, представленная в первой части этой статьи.

Большинство современных пакетов для разработки программ содержат встроенные средства отладки, обеспечивающие возможность выполнения в пошаговом режиме, трассировки кода, установки точек прерывания и просмотра значений переменных. Все они пригодны для отладки исполнимых 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 можно было бы вставить следующую строку:

Организовать передачу сообщения WM_LOGGIT — это еще полдела. Нужна программа, которая будет принимать и регистрировать сообщения о производимых операциях. Утилита Logger, предлагаемая службой PC Magazine Online, — один из возможных вариантов решения этой задачи.

Поскольку значение, имеющееся в сообщении WM_LOGGIT, становится известным только в процессе исполнения, нет возможности задать стандартный метод обработки сообщения. Поэтому в программе Logger переопределяется интерфейсный метод DefaultHandler. При прохождении сообщения WM_LOGGIT этот метод извлекает сообщение о состоянии из передаваемого элемента и добавляет его в имеющийся список окна просмотра. Помимо этой основной функции она обслуживает три рабочие кнопки — для вставки комментария пользователя, для очистки окна списка и для сохранения зарегистрированных сообщений в файле. На рис. А вы видите момент выполнения программы Logger.

В приведенном диалоговом окне представлены методы QueryInterface нескольких COM объектов, подготовленных в среде Delphi, инструментированные строкой, в которой регистрируется имя запрашиваемого интерфейса. Перед вами список запросов, отправленных, когда Explorer извлек пиктограмму для некоторого файла, затем пользователь щелкнул на ней правой клавишей мыши и просмотрел его параметры. Все работает правильно. Если же наша утилита вдруг выводит на экран неожиданные результаты, тогда в сомнительный фрагмент своей программы можно добавить новые обращения к функции Loggit и повторять эксперимент до тех пор, пока не удастся найти ошибку.

Объекты в Delphi. Создание объектов Обучающий материал

Возникновение объектно-ориентированного программирования стало очень важным шагом в развитии современных технологий. Это связано с тем, что до появления объектно-ориентированного программирования код был “плоским”, а теперь программисты могут оперировать не только функциями или процедурами, но и объектами.

Объекты в Delphi

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

Для примера можно рассмотреть кнопку, которая имеет:

1. Свойства, включающие в себя надписи на кнопке, цвет, размер шрифта и так далее.
2. События (например, нажатие).
3. Методы (прорисовку фокуса, способ вывода текста и так далее).

Если все перечисленные выше пункты объединить воедино, то в результате получится один полнофункциональный автономный объект Delphi. Причем, данный объект будет корректно отрабатывать в самых разных условиях.

Программисту нужно будет просто установить кнопку в определенной форме, и она готова к использованию. Таким образом, объекты в Delphi существенно облегчают жизнь специалистам.

Объекты в Delphi, как и классы, объявляются в разделе var. Например,

Стоит отметить тот факт, что объекты в Delphi имеют динамическую структуру. Переменная, обозначающая объект, включает в себя не данные, а только лишь ссылку на данные объекта. В связи с этим программисты должны заранее позаботиться о том, чтобы выделить определенный объем памяти для хранения информации.

Создание объектов Delphi

Для выделения необходимой памяти используется специальный метод класса-конструктора. Как правило, ему обычно дают имя Create (создать). С помощью слова constructor, которое применяется вместо привычного procedure при описании класса, подчеркивается особое значение и поведение конструктора.

Помимо того, что с помощью конструктора происходит выделение памяти, он еще позволяет решить задачу присвоения полям объекта первоначальных значений. Иначе говоря, с его помощью выполняется создание объектов.

Следует отметить тот факт, что если какой-то объект больше использоваться не будет, то стоит освободить память, отведенную для хранения полей. Данное действие выполняется с помощью метода-деструктора Free. Этот метод является очень важным, если говорить про создание объектов в Delphi.

Например, для освобождения памяти, которую занимают поля объекта professor, следует задать следующую команду:

Итак, создание объектов происходит до передачи управления телу конструктора. На начальном этапе под объект отводится определенное количество памяти, а все его значения обновляются. После этого, идет отработка кода конструктора, который пишется программистами.

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

Объект TQuery Delphi

Содержание

Delphi обеспечивает поддержку “pass through SQL”, это означает то, что Вы можете составлять предложения SQL и посылать их непосредственно серверам Oracle, Sybase, Inrterbase и другим. “Pass through SQL” — это мощный механизм по двум причинам:

  1. Большинство серверов могут обрабатывать SQL запросы очень быстро, а это означает, что используя SQL для удаленных данных, Вы получите ответ очень быстро.
  2. Есть возможность составлять SQL запросы, которые заставят сервер исполнить специализированные задачи, недоступные через родной язык Delphi.

Перед чтением этой статьи Вы должны иметь, по крайней мере, элементарное понятие о серверах и различиях между локальными и удаленными (remote) данными.

      1. Основные понятия о TQuery

Предыдущий Урок был, в основном, посвящен объекту TTable , который служит для доступа к данным. При использовании TTable , возможен доступ ко всему набору записей из одной таблицы. В отличие от TTable, TQuery позволяет произвольным образом (в рамках SQL ) выбрать набор данных для работы с ним. Во многом, методика работы с объектом TQuery похожа на методику работы с TTable , однако есть свои особенности.

Вы может создать SQL запрос используя компонент TQuery следующим способом:

  1. Назначите Псевдоним (Alias) DatabaseName.
  2. Используйте свойство SQL чтобы ввести SQL запрос типа
    “Select * from Country”.
  3. Установите свойство Active в True

Если обращение идет к локальным данным, то вместо псевдонима можно указать полный путь к каталогу, где находятся таблицы.

Две основных вещи, которые Вы должны понять прежде, чем перейти дальше:

  • Этот урок не является учебником для начинающих по SQL, а, скорее, описанием объекта TQuery и основных задач, которые Вы можете решить с его помощью. Если Вы не знаете ничто об SQL, Вы все же сможете воспользоваться этой статьей, и, в конце концов, приобретете некоторое понимание основ SQL. Однако, для полного изучения языка, Вы должны обратиться к любой из большого количества книг и документов, доступных по этому предмету.
  • Delphi использует pass through SQL, поэтому для разных SQL серверов синтаксис может быть несколько разным. Версия SQL для локальных таблиц ( Local SQL ) очень сильно урезан, по сравнению со стандартом. Чтобы узнать о его возможностях, Вы должны прочитать не только эту статью, но также файл LOCALSQL.HLP.

Вы увидите, что объект TQuery один из наиболее полезных и гибких компонентов, доступных в Delphi. С ним Вы сможете воспользоваться всей мощью, предоставляемой лидерами среди промышленных SQL серверов, вроде InrterBase, Oracle или Sybase.

Свойство SQL

Свойство SQL — вероятно, самая важная часть TQuery. Доступ к этому свойству происходит либо через Инспектор Объектов во время конструирования проекта (design time), или программно во время выполнения программы (run time).

Интересней, конечно, получить доступ к свойству SQL во время выполнения, чтобы динамически изменять запрос. Например, если требуется выполнить три SQL запроса, то не надо размещать три компонента TQuery на форме. Вместо этого можно разместить один и просто изменять свойство SQL три раза. Наиболее эффективный, простой и мощный способ — сделать это через параметризованные запросы, которые будут объяснены в следующей части. Однако, сначала исследуем основные особенности свойства SQL, а потом рассмотрим более сложные темы, типа запросов с параметрами.

Свойство SQL имеет тип TStrings, который означает что это ряд строк, сохраняемых в списке. Список действует также, как и массив, но, фактически, это специальный класс с собственными уникальными возможностями. В следующих нескольких абзацах будут рассмотрены наиболее часто используемые свойства.

При программном использовании TQuery, рекомендуется сначала закрыть текущий запрос и очистить список строк в свойстве SQL:

Обратите внимание, что всегда можно “безопасно” вызвать Close. Даже в том случае, если запрос уже закрыт, исключительная ситуация генерироваться не будет.

Следующий шаг — добавление новых строк в запрос:

Query1.SQL.Add(‘Select * from Country’);

Query1.SQL.Add(‘where Name = ’’Argentina’’’);

Метод Add используется для добавления одной или нескольких строк к запросу SQL. Общий объем ограничен только количеством памяти на вашей машине.

Чтобы Delphi отработал запрос и возвратил курсор, содержащий результат в виде таблицы, можно вызвать метод:

Демонстрационная программа THREESQL показывает этот процесс (см Рис.1)

Рис.1: Программа THREESQL показывает, как сделать несколько запросов с помощью единственного объекта TQuery.

Программа THREESQL использует особенность локального SQL, который позволяет использовать шаблоны поиска без учета регистра (case insensitive). Например, следующий SQL запрос:

Select * form Country where Name like ’C%’

возвращает DataSet, содержащий все записи, где поле Name начинается с буквы ‘C’. Следующий запрос позволит увидеть все страны, в названии которых встречается буква ‘C’:

Select * from Country where Name like ‘%C%’;

Вот запрос, которое находит все страны, название которых заканчивается на ‘ia’:

Select * from Country where Name like ‘%ia’;

Одна из полезных особенностей свойства SQL — это способность читать файлы, содержащие текст запроса непосредственно с диска. Эта особенность показана в программе THREESQL.

Вот как это работает. В директории с примерами к данному уроку есть файл с расширением SQL. Он содержат текст SQL запроса. Программа THREESQL имеет кнопку с названием Load, которая позволяет Вам выбрать один из этих файлов и выполнять SQL запрос, сохраненный в этом файле.

Кнопка Load имеет следующий метод для события OnClick:

procedure TForm1.LoadClick(Sender: TObject);

if OpenDialog1.Execute then

with Query1 do begin

Метод LoadClick сначала загружает компоненту OpenDialog и позволяет пользователю выбрать файл с расширением SQL. Если файл выбран, текущий запрос закрывается, выбраный файл загружается с диска в св-во SQL , запрос выполняется и результат показывается пользователю.

TQuery и Параметры

Delphi позволяет составить “гибкую” форму запроса, называемую параметризованным запросом. Такие запросы позволяют подставить значение переменной вместо отдельных слов в выражениях “where” или “insert”. Эта переменная может быть изменена практически в любое время. (Если используется локальный SQL, то можно сделать замену почти любого слова в утверждении SQL, но при этом та же самая возможность не поддерживается большинством серверов.)

Перед тем, как начать использовать параметризованные запросы, рассмотрим снова одно из простых вышеупомянутых предложений SQL:

Select * from Country where Name like ’C%’

Можно превратить это утверждение в параметризованный запрос заменив правую часть переменной NameStr:

select * from County where Name like :NameStr

В этом предложении SQL, NameStr не является предопределенной константой и может изменяться либо во время дизайна, либо во время выполнения. SQL parser (программа, которая разбирает текст запроса) понимает, что он имеет дело с параметром, а не константой потому, что параметру предшествует двоеточие «:NameStr». Это двоеточие сообщает Delphi о необходимости заменить переменную NameStr некоторой величиной, которая будет известна позже.

Обратите внимание, слово NameStr было выбрано абсолютно случайно. Использовать можно любое допустимое имя переменной, точно также, как выбирается идентификатор переменной в программе.

Есть два пути присвоить значение переменной в параметризованном запросе SQL. Один способ состоит в том, чтобы использовать свойство Params объекта TQuery . Второй — использовать свойство DataSource для получения информации из другого DataSet. Вот ключевые свойства для достижения этих целей:

property Params[Index: Word];

function ParamByName(const Value: string);

Если подставлять значение параметра в параметризованный запрос через свойство Params, то обычно нужно сделать четыре шага:

  1. Закрыть TQuery
  2. Подготовить объект TQuery, вызвав метод Prepare
  3. Присвоить необходимые значения свойству Params
  4. Открыть TQuery

Второй шаг выполняется в том случае, если данный текст запроса выполняется впервые, в дальнейшем его можно опустить.

Вот фрагмент кода, показывающий как это может быть выполнено практически:

Этот код может показаться немного таинственным. Чтобы понять его, требуется внимательный построчный анализ. Проще всего начать с третьей строки, так как свойство Params является “сердцем” этого процесса.

Params — это индексированное свойство, которое имеет синтаксис как у свойства Fields для TDataSet. Например, можно получить доступ к первой переменной в SQL запросе, адресуя нулевой элемент в массиве Params:

Если параметризованный SQL запрос выглядит так:

select * from Country where Name = :NameStr

то конечный результат (т.е. то, что выполнится на самом деле) — это следующее предложение SQL:

select * from Country where Name = “Argentina”

Все, что произошло, это переменной :Name Str было присвоено значение «Аргентина» через свойство Params. Таким образом, Вы закончили построение простого утверждения SQL.

Если в запросе содержится более одного параметра, то доступаться к ним можно изменяя индекс у свойства Params

либо используя доступ по имени параметра

Итак, параметризованные SQL запросы используют переменные, которые всегда начинаются с двоеточия, определяя места, куда будут переданы значения параметров.

Прежде, чем использовать переменную Params, сначала можно вызвать Prepare. Этот вызов заставляет Delphi разобрать ваш SQL запрос и подготовить свойство Params так, чтобы оно «было готово принять” соответствующее количество переменных. Можно присвоить значение переменной Params без предварительного вызова Prepare, но это будет работать несколько медленнее.

После того, как Вы вызывали Prepare, и после того, как присвоили необходимые значения переменной Params, Вы должны вызвать Open, чтобы закончить привязку переменных и получить желаемый D ataSet. В нашем случае, DataSet должен включать записи где в поле “Name” стоит “Argentina”.

Рассмотрим работу с параметрами на примере (программа PARAMS.DPR). Для создания программы, разместите на форме компоненты TQuery, TDataSource, T DB Grid и TTabSet. Соедините компоненты и установите в свойстве TQuery.DatabaseName псевдоним DBDEMOS . См. рис.2

Рис.2 : Программа PARAMS во время дизайна.

В обработчике события для формы OnCreate напишем код, заполняющий закладки для TTabSet , кроме того, здесь подготавливается запрос :

procedure TForm1.FormCreate(Sender: TObject);

for i:=0 to 25 do

Текст SQL запроса в компоненте Query1:

select * from employee where LastName like :LastNameStr

Запрос выбирает записи из таблицы EMPLOYEE , в которых поле LastName похоже ( like ) на значение параметра :LastNameStr . Параметр будет передаваться в момент переключения закладок :

procedure TForm1.TabSet1Change(Sender: TObject; NewTab: Integer; var AllowChange: Boolean); begin

with Query1 do begin

Рис.3: Программа PARAMS во время выполнения.

Передача параметров через TDataSource

В предыдущем Уроке Вы видели способ создания отношения однин-ко-многим между двумя таблицами. Теперь речь пойдет о выполнении того же самого действия с использованием объекта TQuery. Этот способ более гибок в том отношении, что он не требует индексации по полям связи.

Объект TQuery имеет свойство DataSource, которое может использоваться для того, чтобы создать связь с другим DataSet. Не имеет значения, является ли другой DataSet объектом TTable, TQuery, или некоторый другим потомком TDataSet. Все что нужно для установления соединения — это удостовериться, что у того D ataSet есть связанный с ним DataSource.

Предположим, что Вы хотите создать связь между таблицами ORDERS и CUSTOMERS так, что каждый раз, когда Вы просматриваете конкретную запись о заказчике, будут видны только заказы, связанные с ним.

Рассмотрите следующий параметризованный запрос:

select * from Orders where CustNo = :CustNo

В этом запросе :CustNo — связывающая переменная, которой должно быть присвоено значение из некоторого источника. Delphi позволяет использовать поле TQuery.DataSource чтобы указать другой DataSet, который предоставит эту информацию автоматически. Другими словами, вместо того, чтобы использовать свойство Params и “вручную” присваивать значения переменной, эти значения переменной могут быть просто взяты автоматически из другой таблицы. Кроме того, Delphi всегда сначала пытается выполнить параметризованный запрос используя свойство DataSource, и только потом (если не было найдено какое-то значение параметра) будет пытаться получить значение переменной из свойства Params. При получении данных из DataSource считается, что после двоеточия стоит имя поля из DataSource . При изменении текущей записи в главном DataSet запрос будет автоматически пересчитываться.

Давайте переделаем пример из прошлого урока ( LINKTBL — связывание двух таблиц). Создайте новый проект, положите на форму один набор TTable, TDataSource и TDBGr > Привяжите его к таблице CUSTOMER . Положите на форму второй набор — TQuery, TDataSource и TDBGr >и свяжите объекты между собой. (см рис.4).

В свойстве SQL наберите текст запроса :

select * from Orders where CustNo = :CustNo

В свойстве DatabaseName для Query1 укажите DBDEMOS.

В свойстве DataSource для Query1 укажите DataSource1.

Поставьте Active = True и запустите программу.

Рис.4: Программа LINKQRY — связанные курсоры с помощью SQL

Выполнение соединения нескольких таблиц

Вы видели что таблицы CUSTOMERS и ORDERS связаны в отношении один-ко-многим, основанному на поле CustNo. Таблицы ORDERS и ITEMS также связаны отношении один-ко-многим, только через поле OrderNo.

Более конкретно, каждый заказ который существует в таблице ORDERS будет иметь несколько записей в таблице ITEMS, связанных с этим заказом. Записи из таблицы ITEMS определяют тип и количество изделий, связанных с этим заказом.

Некто Иванов Ф.П. 1 мая 1995г. заказал следующее :

  1. Гайка 4х-угольная — 50 штук
  2. Вентиль — 1 штука

А некто Сидорчук Ю.Г. 8 декабря 1994г. заказал :

  1. М/схема КР580 ИК80 — 10 штук
  2. Транзистор КТ315 — 15 штук
  3. Моток провода — 1 штука

В ситуации подобной этой, иногда проще всего «соединить» данные из таблиц ORDERS и ITEMS так, чтобы результирующий DataSet содержал информацию из обеих таблиц:

Иванов Ф.П. 1 мая 1995г Гайка 4х-угольная 50 штук

Иванов Ф.П. 1 мая 1995г Вентиль 1 штука

Сидорчук Ю.Г. 8 декабря 1994г М/схема КР580 ИК80 10 штук

Сидорчук Ю.Г. 8 декабря 1994г Транзистор КТ315 15 штук

Сидорчук Ю.Г. 8 декабря 1994г Моток провода 1 штука

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

Взяв таблицы ORDERS и ITEMS из подкаталога DEMOS\DATA, их можно соединить их таким путем, что поля CustNo, OrderNo и SaleDate из таблицы ORDERS будут “слиты” с полями Part No и Qty из таблицы ITEMS и сформируют новый DataSet, содержащий все пять полей. Grid содержащий результирующий DataSet показан на рис.5

Рис.5: Соединение таблиц ORDERS и ITEMS может быть сделано так, что формируется новый D ataSet содержащий поля из каждой таблицы.

Имеется существенное различие между связанными курсорами и соединенными таблицами. Однако они имеют две общие черты:

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

Соединение таблиц ORDERS и ITEMS может быть выполнено единственным SQL запросом, который выглядит так:

O.CustNo, O.OrderNo, O.SaleDate, I.PartNo, I.Qty

from Orders O, Items I

where O.OrderNo = I.OrderNo

Этот запрос состоит из четырех различных частей:

  1. Выражение Select определяет, что Вы хотите получить — курсор, содержащий некоторую форму DataSet.
  2. Затем идет список полей которые Вы хотите включить в dataset. Этот список включает поля CustNo, OrderNo, SaleDate, PartNo и Qty. Первые три поля из таблицы ORDERS, а два других — из таблицы ITEMS.
  3. Выражение from объявляет, что Вы работаете с двумя таблицами, одна называется ORDERS, а другая ITEM S . Для краткости, в запросе используется особенность SQL, которая позволяет Вам ссылаться на таблицу ORDERS буквой O, а на таблицу ITEMS буквой I.
  4. Выражение where жизненно важно потому, что оно определяет поля связи для двух таблиц. Некоторые серверы могут вернуть DataSet, даже если Вы не включите выражение where в запрос, но почти всегда результирующий набор записей будет не тем, что Вы хотели видеть. Чтобы получить нужный результат, убедитесь что Вы включили выражение where.

Open или ExecSQL?

После того, как составлен SQL запрос, есть два различных способа выполнить его. Если Вы хотите получить курсор, то нужно вызывать Open. Если выражение SQL не подразумевает возвращение курсора, то нужно вызывать ExecSQL. Например, если происходит вставка, удаление или обновление данных (т.е. SQL запросы INSERT, DELETE, UPDATE ), то нужно вызывать ExecSQL. Тоже самое можно сказать по-другому: Open вызывается при запросе типа SELECT, а ExecSQL — во всех остальных случаях.

Вот типичный SQL запрос, который используется для удаления записи из таблицы:

delete from Country where Name = ‘Argentina’;

Этот запрос удалил бы любую запись из таблицы COUNTRY, которая имеет значение «Argentina» в поле Имя.

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

delete from Country where Name = :CountryName

В этом случае переменная : CountryName может быть изменена во время выполнения:

Код сначала вызывает Prepare, чтобы сообщить Delphi что он должен разобрать SQL запрос и подготовить свойство Params. Следующим шагом присваивается значение свойству Params и затем выполняется подготовленный SQL запрос. Обратите внимание, что он выполняется через ExecSQL, а не Open.

Программа INSQUERY из примеров Delphi демонстрирует эту технику (проект C:\DELPHI\DEMOS\DB\INSQUERY.DPR)

Специальные свойства TQuery

Есть несколько свойств, принадлежащих TQuery, которые еще не упоминались:

property UniDirectional: Boolean;

property Handle: HDBICur;

property StmtHandle: HDBIStmt;

property DBHandle: HDBIDB;

Свойство UniDirectional используется для того, чтобы оптимизировать доступ к таблице. Если Вы установите UniDirectional в True, то Вы можете перемещаться по таблице более быстро, но Вы сможете двигаться только вперед.

Свойство StmtHandle связано со свойством Handle TDataSet. То есть, оно включено исключительно для того, что Вы могли делать вызовы Borland Database Engine напрямую. При нормальных обстоятельствах, нет никакой необходимости использовать это свойство, так как компоненты Delphi могут удовлетворить потребностями большинства программистов. Однако, если Вы знакомы с Borland Database Engine, и если Вы знаете что существуют некоторые возможности не поддерживаемые в VCL, то Вы можете использовать TQuery.StmtHandle, или TQuery. Handle, чтобы сделать вызов напрямую в engine.

Следующий фрагмент кода показывает два запроса к BDE:

Object — Ключевое слово Delphi

Как преодолеть отсутствие множественного наследования в 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-интерфейс можно средствами практически любого языка: достаточно лишь объявить объект с требуемым списком виртуальных методов. Само собой разумеется, что задаваемые определения методов должны в точности соответствовать определениям функций в самих интерфейсах. Однако, кроме того, необходимо соблюдать правильный порядок их размещения в виртуальной таблице. Сказанное означает, что эти определения следуют в заданном порядке, а перед ними нет никаких других виртуальных методов.

Илон Маск рекомендует:  Директивы компилятора в Delphi

В файле 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-объекты с несколькими интерфейсами.

Объекты-сателлиты
Объектный тип 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_ROOT\CLSID системного реестра Windows, вы наверняка обращали внимание на десятки, а иногда и сотни непонятных строк с записанными в них цифрами. Все это — идентификаторы классов для всех COM-объектов, зарегистрированных на вашем компьютере. Не будем вдаваться в подробности; скажем лишь, что при программировании COM-объектов следует использовать имеющиеся параметры GUID, а также создавать новые, специфичные для вашей конкретной программы.

Создание COM-объектов средствами Delphi.

Как преодолеть отсутствие множественного наследования в 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-объекты с несколькими интерфейсами.

Объекты-сателлиты

Объектный тип 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_ROOT\CLSID системного реестра 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

Обработчик контекстного меню

Щелчок правой клавишей мыши на каком-то файле, в среде 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 можно было бы вставить следующую строку:

Организовать передачу сообщения WM_LOGGIT — это еще полдела. Нужна программа, которая будет принимать и регистрировать сообщения о производимых операциях. Утилита Logger, предлагаемая службой PC Magazine Online, — один из возможных вариантов решения этой задачи.

Поскольку значение, имеющееся в сообщении WM_LOGGIT, становится известным только в процессе исполнения, нет возможности задать стандартный метод обработки сообщения. Поэтому в программе Logger переопределяется интерфейсный метод DefaultHandler. При прохождении сообщения WM_LOGGIT этот метод извлекает сообщение о состоянии из передаваемого элемента и добавляет его в имеющийся список окна просмотра. Помимо этой основной функции она обслуживает три рабочие кнопки — для вставки комментария пользователя, для очистки окна списка и для сохранения зарегистрированных сообщений в файле. На рис. А вы видите момент выполнения программы Logger.

В приведенном диалоговом окне представлены методы QueryInterface нескольких COM объектов, подготовленных в среде Delphi, инструментированные строкой, в которой регистрируется имя запрашиваемого интерфейса. Перед вами список запросов, отправленных, когда Explorer извлек пиктограмму для некоторого файла, затем пользователь щелкнул на ней правой клавишей мыши и просмотрел его параметры. Все работает правильно. Если же наша утилита вдруг выводит на экран неожиданные результаты, тогда в сомнительный фрагмент своей программы можно добавить новые обращения к функции Loggit и повторять эксперимент до тех пор, пока не удастся найти ошибку.

Классы и объекты

22.01.2011, 09:10

Классы и объекты
Добрый день всем.Хотел узнать кое что,надеюсь вопрос будет соответствовать название темы:) На.

Классы и объекты
Создать класс Date для работы с датами в формате «год.месяц.день». Дата представляется структурой с.

Классы и объекты
Не могу разобраться с классами для курсовой. Нужно сделать мобов. Каждый должен иметь timer и.

Объекты и классы
Всем привет! Может кто-то нормальным русским языком объяснить, что такое классы и объекты, как.

Объекты и классы
Доброго всем! Возникла проблема с задачей. помогите пожалуйста разобраться. Нужно нарисовать.

22.01.2011, 09:16 2 18.04.2011, 23:58 3

Может вам это поможет.

Тема 2.2.41. Классы и объекты

1. Основные понятия
2. Составляющие класса
3. Объявление класса

1. Основные понятия

Классами в Object Pascal называются специальные типы, которые содержат поля, методы и свойства. Как и любой другой тип, класс служит лишь образцом для создания конкретных экземпляров реализации, которые называются объектами. Понятие класса разработчиками Object Pascal было заимствованно из языка Си++.
Всякий экземпляр класса называется объектом. По структуре и способу доступа класс в некоторой степени напоминает запись.
Важным отличием классов от других типов является то, что объекты класса всегда распределяются в куче, поэтому объект-переменная фактически представляет собой лишь указатель на динамическую область памяти. Однако в отличие от других указателей при ссылке на содержимое объекта запрещается использовать символ «^» за именем объекта:
Пример описания:

Таким образом, по умолчанию непосредственным родилем любого пользовательского класса считается класс TObject.
Принцип наследования приводит к созданию ветвящегося дерева классов, постепенно разрастающегося при перемещении от TObject к его потомкам. Каждый потомок дополняет возможности своего родителя новыми и передает их своим потомкам.
Полиморфизм – это свойство классов решать схожие по смыслу проблемы разными способами. В рамках Object Pascal поведенческие свойства класса определяются набором входящих в него методов. Изменяя алгоритм того или иного метода в потомках класса, программист может придавать этим потомкам отсутствующие у родителя специфические свойства. Для изменения метода необходимо перекрыть его в потомке, т. е. объявить в потомке одноименный метод и реализовать в нем нужные действия. В результате в объекте-родителе и объекте-потомке будут действовать два одноименных метода, имеющие разную алгоритмическую основу и, следовательно, придающие объектам разные свойства. Это и называется полиморфизмом объектов.
В Object Pascal полиморфизм достигается не только описанным выше механизмом наследования и перекрытия методов родителя, но и их виртуализацией, позволяющей родительским методам обращаться к методам своих потомков.

2. Составляющие класса

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

В контексте программы свойство ведет себя как обычное поле. Разница между полем и свойством заключается в том, что при обращении к свойству автоматически подключается соответствующий метод: при передачи свойству нового значения – метод, описанный после write; если же свойство передает свое значение – метод read. Если нет необходимости в специальных действиях при чтении или записи свойства, вместо имени соответствующего метода можно указывать имя поля.Если необходимо, чтобы поле было доступно только для чтения или только для записи, следует опустить соответственно часть write или read. Вообще говоря, свойство может и не связываться с полем. Фактически оно описывает один или два метода, кото рые осуществляют некоторые действия над данными того же типа, что и свойство.

3. Объявление класса

Любой вновь создаваемый класс может содержать секции (разделы), определяемые зарезервированными словами published (декларированные), private (личные), protected (защищенные), public (доступные) и automated (автоматизированные). Внутри каждой секции вначале определяются поля, а затем – методы и свойства.
Секции определяют области видимости элементов описания класса. Секция public не накладывает ограничений на область видимости перечисляемых в ней полей, методов и свойств – их можно вызывать в любом другом модуле программы. Секция published также не ограничивает область видимости, однако в ней перечисля ются свойства, которые должны быть доступны не только на этапе исполнения, но и на этапе конструирования программы (т. е. в окне Инспектора Объектов). Секция published используется только при разработке нестандартных компонентов. Среда Delphi помещает описания компонентов, вставленных в форму, в специальную секцию без названия, которая располагается сразу за заголовком класса и продолжается до первой объявленной секции. Эта секция – published. В нее не следует помещать собственные элементы описания класса или удалять из нее элементы, вставленные средой. Секция private сужает область видимости до минимума: личные элементы описания доступны только внутри методов данного класса и подпрограммах, находящихся в том же модуле, где описан класс. Элемент, объявленный в секции private, становится недоступным даже ближайшим потомкам класса, если они размещаются в других модулях. Секция protected доступна только методам самого класса, а также любым его потомкам независимо от того, находятся ли они в том же модуле или нет. Наконец, секция automated используется только для объявления свойств и методов, которые будут добавлены к так называемому интерфейсу OLE-объектов Автоматизации; область видимости членов этой секции не ограничена.
В Object Pascal разрешается сколько угодно раз объявлять любую секцию, причем порядок следования секций не имеет значения. Любая секция может быть пустой.
Класс может объявляться только в интерфейсной области модуля или в самом начале области реализации. Нельзя определять классы в разделе описаний подпрограмм.

Исключительные ситуации и интерфейсы в Delphi

  • Понятие исключительной ситуации, ее обработка средствами Delphi
  • Обработка RTL-исключений.Иерархия исключений
  • Создание собственных исключений
  • Интерфейсы и их совместное использование классами
  • Интерфейс IUnknown
    • Класс TInterfasedObject
    • Использование оператора as
    • Использование ключевого слова implements
  • Использование инрефейсов в распределительных приложениях

Большинство разработчиков на Delphi создают довольно сложные программы. Сложная программа подразумевает разносторонее взаимодействие с операционной системой и приложениями операционной системы. Любое из этих взаимодействий может завершиться неправильно. Примеров этого можно привести очень много, от банального деления на ноль, до открытия несуществующего файла. Обычно для обхода таких ситуаций разработчику приходится вводить многочисленные проверки. Но любой разработчик просто не в состоянии рассмотреть все ситуации, которые могут возникнуть у его программы при взаимодействии с операционной системой и другими приложениями. Именно для таких непредвиденных событий была придумана структурированная обработка исключительных ситуаций. Первоначально она была создана для разработчиков под Windows NT, впоследствии структурированная обработка получила поддержку и в операционных системах Windows 9x. Большинство современных программных сред для создания приложений под Windows поддерживают обработку исключительных ситуаций. Не осталась в стороне и среда Delphi. Обработка исключений была введена уже в Delphi 1.0, но, только начиная с Delphi 2.0, исключения стали частью Win32 API.
В этой главе мы узнаем, что такое исключительная ситуация и как средствами Delphi можно ее обработать. Рассмотрим, что такое RTL-исключения, как можно использовать в обработке исключительных ситуаций метод TAppllcationHandleException. В данной главе мы познакомимся с интерфейсами. Подробно рассмотрим интерфейс IUnknown. А также рассмотрим, как можно использовать интерфейсы в распределенных приложениях.
Понятие исключительной ситуации, ее обработка средствами Delphi
Под исключительной ситуацией мы будем понимать некое непредвиденное событие, способное повлиять на дальнейшее выполнение программы.
При обработке такой ситуации Delphi, как обычно, работает с объектами. С точки зрения компилятора Delphi исключительная ситуация — это объект. Для работы с этим специфичным объектом в Delphi (точнее, в Object Pascal) были введены следующие языковые конструкции: try .. except и try .. finally .
Рассмотрим эти языковые конструкции более подробно.
Итак, конструкция try .. except имеет следующий синтаксис (листинг 1.6):

Если при выполнении кода, размещенного в разделе try, генерируется исключение, то выполнение этого раздела прекращается и управление передается коду, размещенному в разделе except. Раздел except может использоваться двумя способами. Во-первых, в нем могут располагаться любые операторы, кроме обработчиков исключений, начинающихся с приставки on. Это и операторы сообщения об ошибке, и команды, позволяющие освобождать системные ресурсы, а также другие операторы и команды. Во-вторых, раздел except используется для обработки исключений. В этом случае в него могут включаться только операторы обработки исключений. Если среди обработчиков встретился обработчик, соответствующий сгенерированному исключению, то выполняется оператор этого обработчика, исключение разрушается и управление передается коду, расположенному после оператора on Exception do. Раздел, расположенный после ключевого слова else, служит для обработки любых исключений, не описанных в разделе except. Этот раздел не является обязательным. Если при обработке исключительной ситуации не будет найден подходящий обработчик, то произойдет обработка системным обработчиком исключений.
Рассмотрим простой пример обработки исключительной ситуации деления на ноль (листинг 1.7).

Листинг 1.7
try
а:=10;
b:=0;
c:=a/b;
except
on EZeroDivide do MessageBox(‘Делить на ноль нельзя!’);
end;

Итак, как можно видеть из приведенного выше примера, для обработки разных исключений служат разные операторы. Рассмотрим более подробно оператор обработки on .. do . Данный оператор находится внутри раздела except и может иметь две формы (листинг 1.8).

Листинг 1.8
on do ;
или
on :
do

Этот оператор обрабатывает только тот класс исключений, который в нем указан. При указании родительского (базового) класса, все классы исключений — потомки данного класса — также будут обработаны. Для обработки всех исключений можно обратиться к базовому классу всех исключений: Exception. После обработки исключения оно разрушается. Вторая форма оператора on .. do отличается от первой тем, что данному исключению можно временно присвоить имя и обращаться к свойствам исключения. Обращаться к свойствам исключения можно с помощью конструкции . . Посмотрим листинг 1.9.

Листинг 1.9
try
ScrollBarl.Max := ScrollBarl.Min — 1;
except
on E: EInvalidOperation do
MessageDlg( ‘Игнорируем исключение: ‘- + E.Message, mtlnformation, [mbOK], O)
end;

В приведенном примере мы присваиваем исключению EInvalidOperation временное имя Е. Затем в окне сообщения выводим текст ошибки E.Message, выдаваемый Delphi по умолчанию (если бы не было нашего обработчика ошибки).

Примечание
Никогда не уничтожайте временный объект исключения. Обработчик исключений автоматически уничтожает объекты исключения. Если вы уничтожите объект исключения самостоятельно, это может привести к ошибке доступа.

Иногда, бывает необходимо, чтобы после обработки исключительной ситуации своим кодом вызывался стандартный обработчик ошибки. Например, в случае возникновения некоторой ошибки вы хотите, чтобы приложение сообщало пользователю какую-либо информацию, а затем передавало управление стандартному обработчику ошибок. Как вы уже знаете, после обработки исключения вашим кодом, исключение уничтожается. Для того чтобы самостоятельно вызвать снова это исключение, можно воспользоваться регенерацией исключений. Для регенерации исключения служит команда raise. Рассмотрим листинг 1.10.

После выполнения операторов обработки исключения, написанных программистом, выполняется команда raise, которая снова принудительно вызывает это исключение, после чего управление передается стандартному обработчику исключений.
В случае, когда исключение успешно проходит через все блоки try в коде приложения, вызывается метод HandleException . Он показывает диалоговое окно ошибки. Вы можете вызвать этот метод так, как в листинге 1.11.

Листинг 1.11
try
< операторы >except
Application.HandieException(Self);
end;

Следующая конструкция try .. finally служит для защиты кода, записанного в разделе finally от исключительных ситуаций, которые в силу каких-либо причин могут происходить в разделе try. Синтаксис этой конструкции представлен на листинге 1.12.

Итак, операторы, которые размещены после ключевого слова finally , будут выполняться в любом случае, была сгенерирована исключительная ситуация или нет. Если в разделе try была сгенерирована исключительная ситуация, то управление немедленно передается разделу finally . Также, если исключительной ситуации в разделе, try не было, блок finally будет выполняться. Даже если в разделе finally произойдет ошибка, выполнение операторов этого раздела будет продолжено до конца. В конструкции try .. finally не происходит обработка исключений, она используется в основмом для освобождения ресурсов памяти, закрытия ненужных файлов и других операций освобождения ресурсов. Таким образом, в данной конструкции нуждаются операции с файлами, памятью, ресурсами Windows и объектами.
Код обработки исключения можно разбить на блоки try .. except .. end и try .. finally .. end. Эти блоки могут быть вложенными (рис. 1.24, а и б).
При разработке приложений на Delphi часто возникают ситуации, когда программисту не требуется обрабатывать исключения, а необходимо лишь прервать нежелательное действие, вызывающее ошибку. Для этого применяются так называемые молчаливые исключения (silent exceptions). Молчаливые исключения являются потомками стандартного исключения EAbort . По умолчанию обработчик ошибок VCL Delphi отображает на экране диалоговое окно ошибки для всех исключений, кроме наследников EAbort.

Примечание
При создании консольных приложений сведения об ошибке выводятся и для необработанных исключений EAbort.

Для того чтобы сгенерировать молчаливое исключение, можно вызвать процедуру Abort. Она автоматически сгенерирует исключение EAbort, которое прервет текущую операцию без вывода сведения об ошибке на экран. Рассмотрим пример. Пусть форма содержит пустой список (ListBoxi) и кнопку (Button1). Запишем в обработчик события кнопки onclick следующий код:

Листинг 1.13
procedure TForml.ButtonlClick(Sender: TObject);
var
I: Integer;
begin
for I := 1 to 10 do (цикл 10 раз>
begin
ListBoxl.Items.Add(IntToStr(I)); <добавляем номер в список>
if I = 7 then Abort; <прерываем добавление номеров в список после добавления седьмого номера>
end;
end;

В результате работы программы, после нажатия кнопки Button1, в список будет добавлено семь строк с номерами от 1 до 7.

Рис. 1.24. Вложенные блоки в обработчике исключений (а) и в конструкции защиты кода (б)
Обработка RTL-исключений. Иерархия исключений
RTL (real time library)-исключения определены в модуле Delphi sysutils и являются наследниками базового класса исключений Exception.
Имеется несколько типов исключений-наследников RTL (табл. 1.1).
Таблица 1.1 . Типы исключений из RTL

Ошибка доступа к файлу или устройству ввода/вывода

Большинство исключений ввода/вывода связано с кодом ошибки, возвращаемом Windows при обращении к файлу

Ошибка использования динамической памяти

Ошибки кучи возникают при недостатке памяти или когда в приложении присутствует указатель на область памяти вне кучи

Целочисленные математические операции

Неправильное действие с выражением целого типа

Ошибки включают в себя: деление на ноль, переполнение, выход за пределы диапазона и др.

Математические операции с плавающей точкой

Неправильное действие с выражением вещественного типа

Ошибки с вещественными числами могут исходить от математического сопроцессора или программного эмулятора. Ошибки включают в себя: неправильные инструкции, деление на ноль, переполнение и др.

Неправильная работа с классами при помощи операции as

Объекты могут работать только с совместимыми объектами

Неправильное преобразование типов

Функции преобразования типов (IntToStr, StrToInt И др.) генерируют эту ошибку в случае невозможности преобразования

Аппаратные ошибки указывают, что или процессор, или пользователь сгенерировал ошибку: доступа, переполнения стека или другую

Неправильное использование типа

Ошибка возникает в выражениях, где не может использоваться тип


Рассмотрим теперь иерархию классов исключений .более подробно:

— базовый класс исключений

— исключение для намеренного прерывания вычислений

— попытка вызова абстрактного метода

— ошибка доступа к памяти

— ошибка при работе с массивами

— ошибка при проверке истинности

— ошибка доступа к массиву булевых величин TBits

— ошибка построения кэша

— ошибка регистрации или переименования компонента

— нажатие пользователем клавиш + при выполнении консольного приложения

— ошибка преобразования строк (объектов)

— ошибка работы с базами данных

— ошибка в наборе данных клиента

— ошибка обновления данных компонента

— генерируется компонентом TQuery при попытке открыть запрос без select

— ошибка при обновлении В TProvider

— ошибка ввода даты или времени

— ошибка формата данных в кубе решений

— ошибочный индекс в задании размерности в кубе решений

— ошибка ввода/вывода в файл

— базовый класс исключений целочисленных математических операций

— ошибка деления на ноль

— значение или индекс вне допустимого диапазона

— ошибочное преобразование типов as к интерфейсу

— нераспознаваемый графический файл

— ошибка при операциях с графикой

— ошибка при работе с сеткой (Grid)

— ошибочная операция с компонентом

— ошибка при операциях с указателем

— ошибка при работе со списком

— ошибка выделения памяти для куба решений

— базовый класс исключений операций с плавающей запятой

— недопустимое значение параметра при обращении к математической функции

— потеря значащих разрядов

— ошибка деления на ноль

— ошибка доступа к устройствам через драйвер MCI (Media Control Interface)

— ошибка при работе с элементами меню

— ошибка при связывании приложения с элементом ActiveX

— низкоуровневая ошибка OLE

— ошибка интерфейса OLE IDispatch

— ошибка OLE, связанная со свойством или методом

— ошибка при работе с Tout line

— ошибка распределения памяти

— ошибка создания обработчика Windows

— исключение, генерируемое при загрузке или использовании пакета

— ошибка преобразования текста описания формы в двоичный формат

— ошибка выполнения инструкции процессора из-за нехватки привилегий

— ошибка записи с помощью OLE значения свойства, предназначенного только для чтения

— ошибка чтения с помощью OLE значения свойства, предназначенного только для записи

— ошибка при задании значения свойства

— ошибка при работе с реестром Windows

— ошибка задания типа сервера (компонент TReport не может соединиться с базой данных)

— ошибка загрузки файла ресурсов (*.DFM или *.RES) во время создания приложения

— переполнение стека — базовый класс исключений ошибок потоков

— ошибка создания файла

— ошибка открытия файла

— базовый класс исключений файловых потоков

— ошибка чтения заданного числа байт

— ошибка записи заданного числа байт

— ошибка связи компонента с приложением

— ошибка чтения файла ресурсов

— не найден метод

— ошибка доступа к окну списка

— ошибка многопоточного приложения

— ошибка индекса при работе с TTreeview

— ошибка при работе с типом данных variant

— внутренняя ошибка Windows


Итак, класс Exception является базовым классом всех исключений в Delphi. Все вышеописанные классы являются прямыми или косвенными наследниками класса Exception. При создании собственных новых классов исключений необходимо использовать класс Exception как родительский. Только в этом случае Delphi гарантированно распознает и обработает новый класс как исключение. В свою очередь, класс Exception является прямым наследником базового класса TObject, и наследует все его функции. В отличие от
других классов, классы исключений начинаются не с буквы т, а с буквы Е. При создании собственных классов исключений можно называть их по своему усмотрению, не обязательно начиная с буквы Е (но это считается плохим стилем программирования).
Создание собственных исключений
Для создания собственных типов исключений необходимо знать, как определить тип объекта исключения, а также как вызвать исключение.
Так как исключение является объектом Delphi, то определение нового типа исключения так же nporfro, как определение объекта нового типа. Теоретически возможно вызывать любой объект как объект исключения, но стандартные обработчики исключений работают только с теми объектами, предками которых являются Exception или потомки Exception.
В качестве примера создания собственного типа исключения рассмотрим следующее определение:
type
EMyException = class(Exception);
Теперь, если вы вызовете исключение EMyException, но не напишете обработчик для него, то произойдет вызов стандартного обработчика для Exception. Так как стандартный обработчик для Exception показывает имя вызванного исключения, вы можете увидеть имя вашего нового исключения.
Для вызова созданного исключения используйте команду raise. Для примера рассмотрим типичную задачу проверки введенного пользователем пароля:
type
EPasswordlnval >
После определения нового типа исключения EpasswordInwalid вы можете вызвать это исключение в любом месте программы:
if Password <> CorrectPassword then raise EPasswordlnvalidCreate(‘Введен неправильный пароль’);
Вызов созданного исключения производится по его имени.
Интерфейсы и их совместное использование классами
Ключевое слово Delphi interface позволяет создавать и использовать интерфейсы в ваших приложениях. Интерфейсы служат для расширения модели наследования в VCL, позволяя одному классу принадлежать нескольким
интерфейсам, а также нескольким классам — наследникам различных базовых классов использовать один интерфейс. Интерфейсы полезны в тех случаях, когда наборы операций, такие как потоки, используются большим количеством объектов.
Таким образом, интерфейсы — это средства для обеспечения взаимодействия между разными объектами.
Интерфейсы являются фундаментом для технологий компонентной объектной модели (СОМ) и CORBA.
Интерфейсы похожи на классы, которые содержат в себе только абстрактные методы и четкие определения их функциональности. Определение метода интерфейса включает в себя параметры и типы параметров, возвращаемый тип, а также ожидаемое поведение. Методы интерфейса семантически или логически связаны с отражением цели интерфейса. Существует соглашение об интерфейсах, гласящее, что каждый интерфейс должен быть назван в соответствии с задачей, которую он будет выполнять. Например, интерфейс iMalloc предназначен для распределения, освобождения и управления памятью. Аналогично, интерфейс IPersist может использоваться как базовый интерфейс для потомков, каждый из которых определяет специфичные прототипы методов для загрузки и сохранения состояния объектов в память, поток или в файл. Приведем простой пример объявления интерфейса (листинг 1.14):

Листинг 1.14
type
IEdit = interface
procedure Copy; stdcall;
procedure Cut; stdcall;
procedure Paste; stdcall;
function Undo: Boolean; stdcall;
end;

Примечание
Для тех читателей, которые имеют слабое представление о создании компонентов и новых классов, советуем прочитать вторую часть книги, посвященную созданию собственных и модификации существующих компонентов.

Нельзя создать экземпляр интерфейса при помощи интерфейса. Для получения экземпляра интерфейса вам нужно объявить его в классе, содержащем данный интерфейс. Таким образом, нужно определить класс, который содержит необходимый интерфейс в списке своих родителей (листинг 1.15).

Листинг 1.15
TEditor = class(TInterfacedObject, lEdit)
procedure Copy; stdcall;
procedure Cut; stdcall;
procedure Paste; stdcall;
function Undo: Boolean; stdcall;
end;

Как уже было отмечено выше, использование интерфейсов позволяет нескольким классам использовать один интерфейс, игнорируя требование наличия одного базового класса-предка. Следует запомнить, что интерфейс — это тип с управляемым временем жизни, т. е., он автоматически, при инициализации, принимает значение nil, обладает счетчиком ссылок и автоматически уничтожается, при выходе за пределы своей области видимости.
Интерфейс IUnknown
По аналогии с наследованием классов, предком которых является базовый класс TObject, все интерфейсы — это прямые или косвенные наследники интерфейса IUnknown. Этот базовый интерфейс описан в модуле System следующим образом (листинг 1.16):

Листинг 1.16
type
IUnknown = interface
[‘< 00000000-0000-0000-С000-000000000046>‘]
function Querylnterfасе(const IID: TGUID; out Obj): Integer; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;7
end;

Синтаксис описания интерфейса похож на описание класса. Главное отличие заключается в том, что интерфейс может быть связан с глобальным уникальным идентификатором (Global Unique Identifier, GUID).
GUID — это 128-разрядное целое число, которое используется для уникальной идентификации интерфейсов. Так как в 128-ми разрядах можно закодировать большое количество чисел, GUID практически гарантирует глобальную уникальность идентификатора интерфейса. То есть, практически невозможно, чтобы на двух компьютерах GUID совпал. Алгоритм генерации GUID основан на аппаратной части компьютера (частота процессора, номер сетевой карты и т. д.). В результате работы алгоритма, который может быть реализован с помощью функции API CocreateGUID (), получается запись типа TGUID. Эту запись можно определить в виде строки следующего формата:
‘<хххххххх-хххх-хххх-хххх-хххххххххххх>‘
В дальнейшем, при рассмотрении СОМ, мы увидим, что каждый интерфейс или класс СОМ имеет собственный GUID. Для интерфейсов — это идентификатор интерфейса (Interface ID, IID), а для класса — идентификатор класса (Class ID, CLSID).
Для создания нового GUID в среде Delphi достаточно нажать комбинацию клавиш + + в окне редактора кода.
Итак, интерфейс lunknown поддерживает три метода, которые наследуются всеми интерфейсами:
— QueryInterface() — используется для создания запроса, поддерживается ли данный интерфейс и если ответ положителен, то метод возвращает указатель на него. Для примера, предположим, что имеется некоторый объект object, который поддерживает несколько интерфейсов interface1, interface2 и др. Для получения указателя на интерфейс interface2, объекта Object, вам нужно вызвать метод Interface2.Query Interface О;
— _AddRef () — используется, когда получен указатель на данный интерфейс и вы хотите работать с этим указателем. Метод _AddRef() обязательно должен заканчиваться вызовом метода _Release ();
— _Release () — данный метод применяется для завершения работы с интерфейсом.
Интерфейсы являются фундаментальными элементами таких распределенных объектных моделей, как СОМ и CORBA.
Более подробно интерфейс lunknown мы рассмотрим в третьей части книги, посвященной использованию технологий СОМ и ActiveX.
Класс TlnterfacedObject
В VCL Delphi определен класс TlnterfacedObject, который служит базовым классом для объектов интерфейса. Данный класс определен в модуле Delphi system следующим образом (листинг 1.17):

Листинг 1.17.
type
TlnterfacedObject = class(TObject, IUnknown) private
FRefCount: Integer;
protected
function Querylnterface(const IID: TGUID; out Obj): Integer; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall; public
property RefCount: Integer read FRefCount;
end;

Как мы видим, данный класс в качестве родителей имеет класс TObject и интерфейс lunknown. Класс Tinterfacedobject позволяет достаточно легко создавать классы, поддерживающие интерфейсы. Например,
type
TMyObjInterfaced = class(TInterfacedObject, IPaint)
end;
На вышеприведенном примере мы определяем новый класс TMyObj interfaced, который является прямым потомком класса Tinterfacedobject и поддерживает некий интерфейс IPaint.
Использование оператора as
Объекты, поддерживающие интерфейсы, могут использовать оператор as для динамического присоединения интерфейса. Например,
procedurePaintObjecta(P: TInterfacedObject) var
X: IPaint; begin
X := P as IPaint;
<операторы>
end;
В этом примере переменная Р имеет тип Tinterfacedobject. Данная переменная может быть назначена переменной х, как ссылка на интерфейс IPaint.. Для такого назначения компилятор генерирует код для вызова метода Querylnterface, относяшегося к Интерфейсу IUnknown переменной Р. Подобное назначение возможно, даже если Р не поддерживает данный интерфейс. То есть, компилятор не выдаст ошибку при таком назначении.
Во время выполнения вышеприведенного примера либо успешно происходит присваивание
Х:= Р as IPaint;
либо генерируется исключительная ситуация.
При использовании оператора as вы должны выполнять следующие требования:
— при объявлении интерфейса, явно объявляйте в качестве предка интерфейс lunknown. Так как только в этом случае вы сможете воспользоваться оператором аs;
— если вы используете оператор as для интерфейса, данный интерфейс должен иметь свой IID. Напомним, что для создания нового IID достаточно, находясь в редакторе кода, использовать комбинацию клавиш + + .
Использование ключевого слова implements
Многие классы VCL Delphi имеют в качестве некоторых своих свойств объекты. Кроме того, вы можете использовать в качестве свойств класса интерфейсы. В том случае, когда свойство имеет тип интерфейса, то вы можете использовать ключевое слово implements для определения методов, которые данный интерфейс передает объекту. По умолчанию, ключевое слово implements передает все методы интерфейса. Тем не менее, вы можете самостоятельно определить список тех методов интерфейса, которые передаются объекту.
На приведенном ниже листинге 1.18 представлен пример использования ключевого слова implements при создании объекта адаптера цвета, предназначенного для преобразования восьмибитного значения цвета RGB.

Листинг 1.18
unit cadapt;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;
type
IRGBSbit = interface
[‘‘]
function Red: Byte;
function Green: Byte;
function Blue: Byte;
end;
IColorRef = interface
[41d76360b-f4f5-lldl-87d4-00c04fbl7199>’] function Color: Integer; end;
TRGBSColorRefAdapter = class(TInterfacedObject, IRGBSbit, IColorRef) private
FRGBSbit: IRGBSbit;
FPalRelative: Boolean; public
constructor Create(rgb: IRGBSbit);
property RGBSIntf: IRGBSbit read FRGBSbit implements IRGBSbit;
property PalRelative: Boolean read FPalRelative write-FPalRelative;
function Color: Integer; end;
implementation
constructor TRGBSColorRefAdapter.Create(rgb: IRGBSbit);
begin
FRGBSbit := rgb; end;
function TRGBSColorRefAdapter.Color: Integer;
begin
if FPalRelative then
Result := PaletteRGB(RGBSIntf.Red, RGBSIntf.Green, RGBSIntf.Blue) else
Result := RGB(RGBSIntf.Red, RGBSIntf.Green, RGBSIntf.Blue);
end;
end.

Использование интерфейсов в распределенных приложениях
Интерфейсы являются фундаментальным элементом распределенных объектных моделей СОМ и CORBA (более подробно о моделях читайте в третьей части книги). Delphi обеспечивает базовые классы для этих технологий, которые расширяют возможности объекта TInterfacedObject.
Классы СОМ добавляют возможности использования фабрик классов и идентификаторов классов (CLSID). Фабрики классов отвечают за создание экземпляров классов посредством CLSID. В свою очередь, CLSID используются для регистрации и манипуляции классами СОМ. Классы СОМ, которые обладают и фабрикой класса, и идентификатором класса, называются CoClasses. CoClasses имеют преимущество перед Querylnterface по части поддержки новых версий интерфейсов. Новые версии старых интерфейсов автоматически становятся доступными для программ-клиентов. В приложе ниях СОМ разработчик может вносить правку в код интерфейса для улучшения работы приложения, не изменяя клиентской части кода.
Другая распределенная технология называется CORBA (Архитектура Брокера Общих Объектных Запросов, Common Object Request Broker Architecture). Описание данной технологии не входит в эту книгу, заметим только, что она позволяет создавать приложения для взаимодействия с различными аппаратными или программными платформами. Так, клиентское приложение CORBA, работающее в операционной системе Windows 98, также легко будет работать с сервером приложений операционной системы UNIX.

НОВОСТИ ФОРУМА
Рыцари теории эфира
01.10.2020 — 05:20: ВОСПИТАНИЕ, ПРОСВЕЩЕНИЕ, ОБРАЗОВАНИЕ — Upbringing, Inlightening, Education ->
[center][Youtube]69vJGqDENq4[/Youtube][/center]
[center]14:36[/center]
Osievskii Global News
29 сент. Отправлено 05:20, 01.10.2020 г.’ target=_top>Просвещение от Вячеслава Осиевского — Карим_Хайдаров.
30.09.2020 — 12:51: ВОСПИТАНИЕ, ПРОСВЕЩЕНИЕ, ОБРАЗОВАНИЕ — Upbringing, Inlightening, Education ->
[center][Ok]376309070[/Ok][/center]
[center]11:03[/center] Отправлено 12:51, 30.09.2020 г.’ target=_top>Просвещение от Дэйвида Дюка — Карим_Хайдаров.
30.09.2020 — 11:53: ВОСПИТАНИЕ, ПРОСВЕЩЕНИЕ, ОБРАЗОВАНИЕ — Upbringing, Inlightening, Education ->
[center][Youtube]VVQv1EzDTtY[/Youtube][/center]
[center]10:43[/center]

интервью Раввина Борода https://cursorinfo.co.il/all-news/rav.
мой телеграмм https://t.me/peshekhonovandrei
мой твиттер https://twitter.com/Andrey54708595
мой инстаграм https://www.instagram.com/andreipeshekhonow/

[b]Мой комментарий:
Андрей спрашивает: Краснодарская синагога — это что, военный объект?
— Да, военный, потому что имеет разрешение от Росатома на манипуляции с радиоактивными веществами, а также иными веществами, опасными в отношении массового поражения. Именно это было выявлено группой краснодарцев во главе с Мариной Мелиховой.

[center][Youtube]CLegyQkMkyw[/Youtube][/center]
[center]10:22 [/center]

Доминико Риккарди: Россию ждёт страшное будущее (хотелки ЦРУ):
https://tainy.net/22686-predskazaniya-dominika-rikardi-o-budushhem-rossii-sdelannye-v-2000-godu.html

Завещание Алена Даллеса / Разработка ЦРУ (запрещено к ознакомлению Роскомнадзором = Жид-над-рус-надзором)
http://av-inf.blogspot.com/2013/12/dalles.html

[center][b]Сон разума народа России [/center]

[center][Youtube]CLegyQkMkyw[/Youtube][/center]
[center]10:22 [/center]

Доминико Риккарди: Россию ждёт страшное будущее (хотелки ЦРУ):
https://tainy.net/22686-predskazaniya-dominika-rikardi-o-budushhem-rossii-sdelannye-v-2000-godu.html

Завещание Алена Даллеса / Разработка ЦРУ (запрещено к ознакомлению Роскомнадзором = Жид-над-рус-надзором)
http://av-inf.blogspot.com/2013/12/dalles.html

[center][b]Сон разума народа России [/center]

Программирование на языке Delphi

Глава 3. Объектно-ориентированное программирование (ООП)


Авторы: А.Н. Вальвачев
К.А. Сурков
Д.А. Сурков
Ю.М. Четырько

Опубликовано: 19.11.2005
Исправлено: 10.12.2020
Версия текста: 1.0

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

Сейчас преимущества использования объектов очевидны для всех. Однако так было не всегда. Сначала старая гвардия не поняла и не приняла объекты, поэтому они почти 20 лет потихоньку развивались в различных языках, первым из которых была Simula 67. Постепенно объектно-ориентированный подход нашел себе место и в более мощных языках, таких как C++, Delphi и множестве других языков. Блестящим примером реализации объектов была библиотека Turbo Vision, предназначенная для построения пользовательского интерфейса программ в операционной системе MS-DOS.

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

3.1. Краеугольные камни ООП


3.1.1. Формула объекта

Авторы надеются, что читатель помнит кое-что из главы 2 и такие понятия как тип данных, процедура, функция, запись для него не в новинку. Это прекрасно. Так вот, в конце 60-х годов кому-то пришло в голову объединить эти понятия, и то, что получилось, назвать объектом. Рассмотрение данных в неразрывной связи с методами их обработки позволило вывести формулу объекта :

Объект = Данные + Операции

На основании этой формулы была разработана методология объектно-ориентированного программирования (ООП).

3.1.2. Природа объекта

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

В общем случае каждый объект «помнит» необходимую информацию, «умеет» выполнять некоторый набор действий и характеризуется набором свойств. То, что объект «помнит», хранится в его полях . То, что объект «умеет делать», реализуется в виде его внутренних процедур и функций, называемых методами . Свойства объектов аналогичны свойствам, которые мы наблюдаем у обычных предметов. Значения свойств можно устанавливать и читать. Программно свойства реализуются через поля и методы.

Например, объект «кнопка» имеет свойство «цвет». Значение цвета кнопка запоминает в одном из своих полей. При изменении значения свойства «цвет» вызывается метод, который перерисовывает кнопку.

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

3.1.3. Объекты и компоненты

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

Компоненты в среде Delphi — это особые объекты, которые являются строительными кирпичиками визуальной среды разработки и приспособлены к визуальной установке свойств. Чтобы превратить объект в компонент, первый разрабатывается по определенным правилам, а затем помещается в палитру компонентов. Конструируя приложение, вы берете компоненты из Палитры Компонентов, располагаете на форме и устанавливаете их свойства в окне Инспектора Объектов. Внешне все выглядит просто, но чтобы достичь такой простоты, потребовалось создать механизмы, обеспечивающие функционирование объектов-компонентов уже на этапе проектирования приложения! Все это было придумано и блестяще реализовано в среде Delphi. Таким образом, компонентный подход значительно упростил создание приложений с графическим пользовательским интерфейсом и дал толчок развитию новой индустрии компонентов.

В данной главе мы рассмотрим лишь вопросы создания и использования объектов. Чуть позже мы научим вас превращать объекты в компоненты (см. главу 13).

3.1.4. Классы объектов

Каждый объект всегда принадлежит некоторому классу объектов. Класс объектов — это обобщенное (абстрактное) описание множества однотипных объектов. Объекты являются конкретными представителями своего класса, их принято называть экземплярами класса . Например, класс СОБАКИ — понятие абстрактное, а экземпляр этого класса МОЙ ПЕС БОБИК — понятие конкретное.

3.1.5. Три кита ООП

Весь мир ООП держится на трех китах: инкапсуляции, наследовании и полиморфизме. Для начала о них надо иметь только самое общее представление.

Объединение данных и операций в одну сущность — объект — тесно связано с понятием инкапсуляции, которое означает сокрытие внутреннего устройства. Инкапсуляция делает объекты похожими на маленькие программные модули, в которых скрыты внутренние данные и у которых имеется интерфейс использования в виде подпрограмм. Переход от понятий «структура данных» и «алгоритм» к понятию «объект» значительно повысил ясность и надежность программ.

Второй кит ООП — наследование . Этот простой принцип означает, что если вы хотите создать новый класс объектов, который расширяет возможности уже существующего класса, то нет необходимости в переписывании заново всех полей, методов и свойств. Вы объявляете, что новый класс является потомком (или дочерним классом ) имеющегося класса объектов, называемого предком (или родительским классом ), и добавляете к нему новые поля, методы и свойства. Процесс порождения новых классов на основе других классов называется наследованием . Новые классы объектов имеют как унаследованные признаки, так и, возможно, новые. Например, класс СОБАКИ унаследовал многие свойства своих предков — ВОЛКОВ.

Третий кит — это полиморфизм . Он означает, что в производных классах вы можете изменять работу уже существующих в базовом классе методов. При этом весь программный код, управляющий объектами родительского класса, пригоден для управления объектами дочернего класса без всякой модификации. Например, вы можете породить новый класс кнопок с рельефной надписью, переопределив метод рисования кнопки. Новую кнопку можно «подсунуть» вместо стандартной в какую-нибудь подпрограмму, вызывающую рисование кнопки. При этом подпрограмма «думает», что работает со стандартной кнопкой, но на самом деле кнопка принадлежит производному классу кнопок и отображается в новом стиле. Пока достаточно самого поверхностного понимания всех приведенных выше понятий, ниже мы рассмотрим их подробнее и покажем, как они реализованы в среде Delphi.

3.2. Классы

Для поддержки ООП в язык Delphi введены объектные типы данных , с помощью которых одновременно описываются данные и операции над ними. Объектные типы данных называют классами , а их экземпляры — объектами .

Классы объектов определяются в секции type глобального блока. Описание класса начинается с ключевого слова class и заканчивается ключевым словом end . По форме объявления классы похожи на обычные записи, но помимо полей данных могут содержать объявления пользовательских процедур и функций. Такие процедуры и функции обобщенно называют методами , они предназначены для выполнения над объектами различных операций. Приведем пример объявления класса, который предназначен для чтения текстового файла в формате «delimited text» (файл в таком формате представляет собой последовательность строк; каждая строка состоит из значений, которые отделены друг от друга символом-разделителем):

Класс содержит поля (FileVar, Items, Delimiter) и методы (PutItem, SetActive, ParseLine, NextLine, GetEndOfFile). Заголовки методов, (всегда) следующие за списком полей, играют роль упреждающих (forward) описаний. Программный код методов пишется отдельно от определения класса и будет приведен позже.

Класс обычно описывает сущность, моделируемую в программе. Например, класс TDelimitedReader представляет собой «читатель» текстового файла с разбором считываемых строк на элементы (подстроки), которые отделены друг от друга некоторым символом, называемым разделителем.

Класс содержит несколько полей:

  • FileVar — файловая переменная, необходимая для доступа к файлу;
  • Delimiter — символ, который служит разделителем элементов;
  • Items — массив элементов, полученных разбором последней считанной строки;

Класс также содержит ряд методов (процедур и функций):

  • PutItem — помещает элемент в массив Items по индексу Index; если индекс превышает верхнюю границу массива, то размер массива автоматически увеличивается;
  • SetActive — открывает или закрывает файл, из которого производится чтение строк;
  • ParseLine — осуществляет разбор строки: выделяет элементы из строки и помещает их в массив Items; возвращает количество выделенных элементов;
  • NextLine — считывает очередную строку из файла и с помощью метода ParseLine осуществляет ее разбор; в случае успешного чтения очередной строки функция возвращает значение True, а иначе — значение False (достигнут конец файла);
  • GetEndOfFile — возвращает булевское значение, показывающее, достигнут ли конец файла.

Обратите внимание, что приведенное выше описание является ничем иным, как декларацией интерфейса для работы с объектами класса TDelimitedReader. Реализация методов PutItem, SetActive, ParseLine, NextLine и GetEndOfFile на данный момент отсутствует, однако для создания и использования экземпляров класса она пока и не нужна.

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

3.3. Объекты

Чтобы от описания класса перейти к объекту, следует выполнить соответствующее объявление в секции var :

При работе с обычными типами данных этого объявления было бы достаточно для получения экземпляра типа. Однако объекты в среде Delphi являются динамическими данными, т.е. распределяются в динамической памяти. Поэтому переменная Reader — это просто ссылка на экземпляр ( объект в памяти), которого физически еще не существует. Чтобы сконструировать объект (выделить память для экземпляра) класса TDelimitedReader и связать с ним переменную Reader, нужно в тексте программы поместить следующий оператор:

Create — это так называемый конструктор объекта; он всегда присутствует в классе и служит для создания и инициализации экземпляров. При создании объекта в памяти выделяется место только для его полей. Методы, как и обычные процедуры и функции, помещаются в область кода программы; они умеют работать с любыми экземплярами своего класса и не дублируются в памяти.

После создания объект можно использовать в программе: получать и устанавливать значения его полей, вызывать его методы. Доступ к полям и методам объекта происходит с помощью уточненных имен, например:

Кроме того, как и при работе с записями, допустимо использование оператора with , например:

Если объект становится ненужным, он должен быть удален вызовом специального метода Destroy, например:

Destroy — это так называемый деструктор объекта; он присутствует в классе наряду с конструктором и служит для удаления объекта из динамической памяти. После вызова деструктора переменная Reader становится несвязанной и не должна использоваться для доступа к полям и методам уже несуществующего объекта. Чтобы отличать в программе связанные объектные переменные от несвязанных, последние следует инициализировать значением nil . Например, в следующем фрагменте обращение к деструктору Destroy выполняется только в том случае, если объект реально существует:

Вызов деструктора для несуществующих объектов недопустим и при выполнении программы приведет к ошибке. Чтобы избавить программистов от лишних ошибок, в объекты ввели предопределенный метод Free, который следует вызывать вместо деструктора. Метод Free сам вызывает деструктор Destroy, но только в том случае, если значение объектной переменной не равно nil . Поэтому последнюю строчку в приведенном выше примере можно переписать следующим образом.

После уничтожения объекта переменная Reader сохраняет свое значение, продолжая ссылаться на место в памяти, где объекта уже нет. Если эту переменную предполагается еще использовать, то желательно присвоить ей значение nil , чтобы программа могла проверить, существует объект или нет. Таким образом, наиболее правильная последовательность действий при уничтожении объекта должна быть следующая:

С помощью стандартной процедуры FreeAndNil это можно сделать проще и элегантнее:

Значение одной объектной переменной можно присвоить другой. При этом объект не копируется в памяти, а вторая переменная просто связывается с тем же объектом, что и первая:

Объекты могут выступать в программе не только в качестве переменных, но также элементов массивов, полей записей, параметров процедур и функций. Кроме того, они могут служить полями других объектов. Во всех этих случаях программист фактически оперирует указателями на экземпляры объектов в динамической памяти. Следовательно, объекты изначально приспособлены для создания сложных динамических структур данных, таких как списки и деревья. Указатели на объекты для этого не нужны.

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

Первое объявление класса TDelimitedReader называется упреждающим (от англ. forward). Оно необходимо для того, чтобы компилятор нормально воспринял объявление поля Owner в классе TDelimitedReader.

Итак, вы уже имеете некоторое представление об объектах, перейдем теперь к вопросу реализации их методов.

3.4. Конструкторы и деструкторы

Особой разновидностью методов являются конструкторы и деструкторы . Напомним, что конструкторы создают, а деструкторы разрушают объекты. Создание объекта включает выделение памяти под экземпляр и инициализацию его полей, а разрушение — очистку полей и освобождение памяти. Действия по инициализации и очистке полей специфичны для каждого конкретного класса объектов. По этой причине язык Delphi позволяет переопределить стандартный конструктор Create и стандартный деструктор Destroy для выполнения любых полезных действий. Можно даже определить несколько конструкторов и деструкторов (имена им назначает сам программист), чтобы обеспечить различные процедуры создания и разрушения объектов.

Объявление конструкторов и деструкторов похоже на объявление обычных методов с той лишь разницей, что вместо зарезервированных слов function и procedure используются слова constructor и destructor . Для нашего класса TDelimitedReader потребуется конструктор, которому в качестве параметра будет передаваться имя обрабатываемого файла и разделитель элементов:

Приведем их возможную реализацию:

Если объект содержит встроенные объекты или другие динамические данные, то конструктор — это как раз то место, где их нужно создавать.

Конструктор применяется к классу или к объекту. Если он применяется к классу,

то выполняется следующая последовательность действий:

  • в динамической памяти выделяется место для нового объекта;
  • выделенная память заполняется нулями. В результате все числовые поля и поля порядкового типа приобретают нулевые значения, строковые поля становятся пустыми, а поля, содержащие указатели и объекты получают значение nil ;
  • затем выполняются заданные программистом действия конструктора;
  • ссылка на созданный объект возвращается в качестве значения конструктора. Тип возвращаемого значения совпадает с типом класса, использованного при вызове (в нашем примере это тип TDelimitedReader).

Если конструктор применяется к объекту,

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

Деструктор уничтожает объект, к которому применяется:

  • выполняется заданный программистом код завершения;
  • освобождается занимаемая объектом динамическая память.

В теле деструктора обычно должны уничтожаться встроенные объекты и динамические данные, как правило, созданные конструктором.

Как и обычные методы, деструктор может иметь параметры, но эта возможность используется редко.

3.5. Методы

Процедуры и функции, предназначенные для выполнения над объектами действий, называются методами . Предварительное объявление методов выполняется при описании класса в секции interface модуля, а их программный код записывается в секции implementation . Однако в отличие от обычных процедур и функций заголовки методов должны иметь уточненные имена, т.е. содержать наименование класса. Приведем возможную реализацию одного из методов в классе TDelimitedReader:

Обратите внимание, что внутри методов обращения к полям и другим методам выполняются как к обычным переменным и подпрограммам без уточнения экземпляра объекта. Такое упрощение достигается путем использования в пределах метода псевдопеременной Self (стандартный идентификатор). Физически Self представляет собой дополнительный неявный параметр, передаваемый в метод при вызове. Этот параметр и указывает экземпляр объекта, к которому данный метод применяется. Чтобы пояснить сказанное, перепишем метод SetActive, представив его в виде обычной процедуры:

Согласитесь, что метод SetActive выглядит лаконичнее процедуры TDelimitedReader_SetActive.

Практика показывает, что псевдопеременная Self редко используется в явном виде. Ее необходимо применять только тогда, когда при написании метода может возникнуть какая-либо двусмысленность для компилятора, например при использовании одинаковых имен и для локальных переменных, и для полей объекта.

Если выполнить метод SetActive,

то обрабатываемый файл будет открыт. При этом неявный параметр Self будет содержать значение переменной Reader. Такой вызов реализуется обычными средствами процедурного программирования приблизительно так:

3.6. Свойства


3.6.1. Понятие свойства

Помимо полей и методов в объектах существуют свойства . При работе с объектом свойства выглядят как поля: они принимают значения и участвуют в выражениях. Но в отличие от полей свойства не занимают места в памяти, а операции их чтения и записи ассоциируются с обычными полями или методами. Это позволяет создавать необходимые сопутствующие эффекты при обращении к свойствам. Например, в объекте Reader присваивание свойству Active значения True вызовет открытие файла, а присваивание значения False — закрытие файла. Создание сопутствующего эффекта (открытие или закрытие файла) достигается тем, что за присваиванием свойству значения стоит вызов метода.

Объявление свойства выполняется с помощью зарезервированного слова property , например:

Ключевые слова read и write называются спецификаторами доступа. После слова read указывается поле или метод, к которому происходит обращение при чтении (получении) значения свойства, а после слова write — поле или метод, к которому происходит обращение при записи (установке) значения свойства. Например, чтение свойства Active означает чтение поля FActive, а установка свойства — вызов метода SetActive. Чтобы имена свойств не совпадали с именами полей, последние принято писать с буквы F (от англ. field). Мы в дальнейшем также будем пользоваться этим соглашением. Начнем с того, что переименуем поля класса TDelimitedReader: поле FileVar переименуем в FFile, Items — в FItems, а поле Delimiter — в FDelimiter.

Обращение к свойствам выглядит в программе как обращение к полям:

Если один из спецификаторов доступа опущен, то значение свойства можно либо только читать (задан спецификатор read ), либо только записывать (задан спецификатор write ). В следующем примере объявлено свойство, значение которого можно только читать.

Здесь свойство ItemCount показывает количество элементов в массиве FItems. Поскольку оно определяется в результате чтения и разбора очередной строки файла, пользователю объекта разрешено лишь узнавать количество элементов.

В отличие от полей свойства не имеют адреса в памяти, поэтому к ним запрещено применять операцию @. Как следствие, их нельзя передавать в var — и out -параметрах процедур и функций.

Технология объектно-ориентированного программирования в среде Delphi предписывает избегать прямого обращения к полям, создавая вместо этого соответствующие свойства. Это упорядочивает работу с объектами, изолируя их данные от непосредственной модификации. В будущем внутренняя структура класса, которая иногда является достаточно сложной, может быть изменена с целью повышения эффективности работы программы. При этом потребуется переработать только методы чтения и записи значений свойств; внешний интерфейс класса не изменится.

3.6.2. Методы получения и установки значений свойств

Методы получения (чтения) и установки (записи) значений свойств подчиняются определенным правилам. Метод чтения свойства — это всегда функция, возвращающая значение того же типа, что и тип свойства. Метод записи свойства — это обязательно процедура, принимающая параметр того же типа, что и тип свойства. В остальных отношениях это обычные методы объекта. Примерами методов чтения и записи свойств являются методы GetItemCount и SetActive в классе TDelimitedReader:

Использование методов для получения и установки свойств позволяет проверить корректность значения свойства, сделать дополнительные вычисления, установить значения зависимых полей и т.д. Например, в методе SetActive вполне целесообразно осуществить проверку состояния файла (открыт или закрыт), чтобы избежать его повторного открытия или закрытия:

Наличие свойства Active позволяет нам отказаться от использования методов Open и Close, традиционных при работе с файлами. Согласитесь, что открывать и закрывать файл с помощью свойства Active гораздо удобнее и естественнее. Одновременно с этим свойство Active можно использовать и для проверки состояния файла (открыт или нет). Таким образом, для осуществления трех действий требуется всего лишь одно свойство! Это делает использование Ваших классов другими программистами более простым, поскольку им легче запомнить одно понятие Active, чем, например, три метода: Open, Close и IsOpen.

Значение свойства может не храниться, а вычисляться при каждом обращении к свойству. Примером является свойство ItemCount, значение которого вычисляется как Length(FItems).

3.6.3. Свойства-массивы

Кроме обычных свойств в объектах существуют свойства-массивы (array properties). Свойство-массив — это индексированное множество значений. Например, в классе TDelimitedReader множество элементов, выделенных из считанной строки, удобно представить в виде свойства-массива:

Элементы массива Items можно только читать, поскольку класс TDelimitedReader предназначен только для чтения данных из файла.

В описании свойства-массива разрешено использовать только методы, но не поля. В этом состоит отличие свойства-массива от обычного свойства.

Основная выгода от применения свойства-массива — возможность выполнения итераций с помощью цикла for , например:

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

Свойства-массивы имеют два важных отличия от обычных массивов:

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

операции над свойством-массивом в целом запрещены; разрешены операции только с его элементами.

3.6.4. Свойство-массив как основное свойство объекта

Свойство-массив можно сделать основным свойством объектов данного класса. Для этого в описание свойства добавляется слово default :

Такое объявление свойства Items позволяет рассматривать сам объект класса TDelimitedReader как массив и опускать имя свойства-массива при обращении к нему из программы, например:

Следует помнить, что только свойства-массивы могут быть основными свойствами объектов; для обычных свойств это недопустимо.

3.6.5. Методы, обслуживающие несколько свойств

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

В следующем примере уже известный Вам метод GetItem обслуживает три свойства: FirstName, LastName и Phone:

Обращения к свойствам FirstName, LastName и Phone заменяются компилятором на вызовы одного и того же метода GetItem, но с разными значениями параметра Index:

Обратите внимание, что метод GetItem обслуживает как свойство-массив Items, так и свойства FirstName, LastName и Phone. Удобно, не правда ли!

Перед тем, как перейти к более сложным понятиям ООП, приведем полную реализацию класса TDelimitedReader. Настоятельно рекомендуем Вам внимательно ознакомиться с этой реализацией, поскольку в ней сведено воедино все то, о чем говорилось в предыдущих разделах.

3.7. Наследование


3.7.1. Понятие наследования

Классы инкапсулируют (т.е. включают в себя) поля, методы и свойства; это их первая черта. Следующая не менее важная черта классов — способность наследовать поля, методы и свойства других классов. Чтобы пояснить сущность наследования обратимся к примеру с читателем текстовых файлов в формате «delimited text».

Класс TDelimitedReader описывает объекты для чтения из текстового файла элементов, разделенных некоторым символом. Он не пригоден для чтения элементов, хранящихся в другом формате, например в формате с фиксированным количеством символов для каждого элемента. Для этого необходим другой класс:

Поля, свойства и методы класса TFixedReader практически полностью аналогичны тем, что определены в классе TDelimitedReader. Отличие состоит в отсутствии свойства Delimiter, наличии поля FItemWidths (для хранения размеров элементов), другой реализации метода ParseLine и немного отличающемся конструкторе. Если в будущем появится класс для чтения элементов из файла еще одного формата (например, зашифрованного текста), то придется снова определять общие для всех классов поля, методы и свойства. Чтобы избавиться от дублирования общих атрибутов (полей, свойств и методов) при определении новых классов, воспользуемся механизмом наследования. Прежде всего, выделим в отдельный класс TTextReader общие атрибуты всех классов, предназначенных для чтения элементов из текстовых файлов. Реализация методов TTextReader, кроме метода ParseLine, полностью идентична реализации TDelimitedReader, приведенной в предыдущем разделе.

При реализации класса TTextReader ничего не известно о том, как хранятся элементы в считываемых строках, поэтому метод ParseLine ничего не делает. Очевидно, что создавать объекты класса TTextReader не имеет смысла. Для чего тогда нужен класс TTextReader? Ответ: чтобы на его основе определить ( породить ) два других класса — TDelimitedReader и TFixedReader, предназначенных для чтения данных в конкретных форматах:

Классы TDelimitedReader и TFixedReader определены как наследники TTextReader (об этом говорит имя в скобках после слова class ). Они автоматически включают в себя все описания, сделанные в классе TTextReader и добавляют к ним некоторые новые. В результате формируется дерево классов , показанное на рисунке 3.1 (оно всегда рисуется перевернутым).

Рисунок 3.1. Дерево классов

Класс, который наследует атрибуты другого класса, называется порожденным классом или потомком . Соответственно класс, от которого происходит наследование, выступает в роли базового, или предка . В нашем примере класс TDelimitedReader является прямым потомком класса TTextReader. Если от TDelimitedReader породить новый класс, то он тоже будет потомком класса TTextReader, но уже не прямым.

Очень важно, что в отношениях наследования любой класс может иметь только одного непосредственного предка и сколь угодно много потомков. Поэтому все связанные отношением наследования классы образуют иерархию . Примером иерархии классов является библиотека VCL; с ее помощью в среде Delphi обеспечивается разработка GUI-приложений.

3.7.2. Прародитель всех классов

В языке Delphi существует предопределенный класс TObject, который служит неявным предком тех классов, для которых предок не указан. Это означает, что объявление

Класс TObject выступает корнем любой иерархии классов. Он содержит ряд методов, которые по наследству передаются всем остальным классам. Среди них конструктор Create, деструктор Destroy, метод Free и некоторые другие методы.

Таким образом, полное дерево классов для чтения элементов из текстового файла в различных форматах выглядит так, как показано на рисунке 3.2.

Рисунок 3.2. Полное дерево классов

Поскольку класс TObject является предком для всех других классов (в том числе и для ваших собственных), то не лишним будет кратко ознакомиться с его методами:

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

Краткое описание методов в классе TObject:

  • Create — стандартный конструктор.
  • Free — уничтожает объект: вызывает стандартный деструктор Destroy, если значение псевдопеременной Self не равно nil .
  • InitInstance (Instance: Pointer): TObject — при создании объекта инициализирует нулями выделенную память. На практике нет необходимости вызывать этот метод явно.
  • CleanupInstance — освобождает память, занимаемую полями с типом string, Variant, динамический массив и интерфейс. На практике нет необходимости вызывать этот метод явно.
  • ClassType : TClass — возвращает описатель класса (метакласс).
  • ClassName : ShortString — возвращает имя класса.
  • ClassNameIs (const Name: string): Boolean — проверяет, является ли заданная строка именем класса.
  • ClassParent : TClass — возвращает описатель базового класса.
  • ClassInfo : Pointer — возвращает указатель на соответствующую классу таблицу RTTI (от англ. Runtime Type Information). Таблица RTTI используется для проверки типов данных на этапе выполнения программы.
  • InstanceSize : Longint — возвращает количество байт, необходимых для хранения в памяти одного объекта соответствующего класса. Заметим, что значение, возвращаемое этим методом и значение, возвращаемое функцией SizeOf при передаче ей в качестве аргумента объектной переменной — это разные значения. Функция SizeOf всегда возвращает значение 4 (SizeOf(Pointer)), поскольку объектная переменная — это ни что иное, как ссылка на данные объекта в памяти. Значение InstanceSize — это размер этих данных, а не размер объектной переменной.
  • InheritsFrom (AClass: TClass): Boolean — проверяет, является ли класс AClass базовым классом.
  • MethodAddress (const Name: ShortString): Pointer — возвращает адрес published-метода, имя которого задается параметром Name.
  • MethodName (Address: Pointer): ShortString — возвращает имя published-метода по заданному адресу.
  • FieldAddress (const Name: ShortString): Pointer — возвращает адрес published-поля, имя которого задается параметром Name.
  • GetInterface (const IID: TGUID; out Obj): Boolean — возвращает ссылку на интерфейс через параметр Obj; идентификатор интерфейса задается параметром IID. (Интерфейсы рассмотрены в главе 6)
  • GetInterfaceEntry (const IID: TGUID): PInterfaceEntry — возвращает информацию об интерфейсе, который реализуется классом. Идентификатор интерфейса задается параметром IID.
  • GetInterfaceTable : PInterfaceTable — возвращает указатель на таблицу с информацией обо всех интерфейсах, реализуемых классом.
  • AfterConstruction — автоматически вызывается после создания объекта. Метод не предназначен для явного вызова из программы. Используется для того, чтобы выполнить определенные действия уже после создания объекта (для этого его необходимо переопределить в производных классах).
  • BeforeDestruction — автоматически вызывается перед уничтожением объекта. Метод не предназначен для явного вызова из программы. Используется для того, чтобы выполнить определенные действия непосредственно перед уничтожением объекта (для этого его необходимо переопределить в производных классах).
  • Dispatch (var Message) — служит для вызова методов, объявленных с ключевым словом message .
  • DefaultHandler (var Message) — вызывается методом Dispatch в том случае, если метод, соответствующий сообщению Message, не был найден.
  • NewInstance : TObject — вызывается при создании объекта для выделения динамической памяти, чтобы разместить в ней данные объекта. Метод вызывается автоматически, поэтому нет необходимости вызывать его явно.
  • FreeInstance — вызывается при уничтожении объекта для освобождения занятой объектом динамической памяти. Метод вызывается автоматически, поэтому нет необходимости вызывать его явно.
  • Destroy — стандартный деструктор.

3.7.3. Перекрытие атрибутов в наследниках

В механизме наследования можно условно выделить три основных момента:

  • наследование полей;
  • наследование свойств;
  • наследование методов.

Любой порожденный класс наследует от родительского все поля данных, поэтому классы TDelimitedReader и TFixedReader автоматически содержат поля FFile, FActive и FItems, объявленные в классе TTextReader. Доступ к полям предка осуществляется по имени, как если бы они были определены в потомке. В потомках можно определять новые поля, но их имена должны отличаться от имен полей предка.

Наследование свойств и методов имеет свои особенности.

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

Метод базового класса тоже можно перекрыть в производном классе, например чтобы изменить логику его работы. Обратимся к классам TDelimitedReader и TFixedReader. В них методы PutItem, GetItem, SetActive и GetEndOfFile унаследованы от TTextReader, поскольку логика их работы не зависит от того, в каком формате хранятся данные в файле. А вот метод ParseLine перекрыт, так как способ разбора строк зависит от формата данных:

В классах TDelimitedReader и TFixedReader перекрыт еще и конструктор Create. Это необходимо для инициализации специфических полей этих классов (поля FDelimiter в классе TDelimitedReader и поля FItemWidths в классе TFixedReader):

Как видно из примера, в наследнике можно вызвать перекрытый метод предка, указав перед именем метода зарезервированное слово inherited . Когда метод предка полностью совпадает с методом потомка по формату заголовка, то можно использовать более короткую запись. Воспользуемся ей и перепишем деструктор в классе TTextReader правильно:

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

3.7.4. Совместимость объектов различных классов

Для классов, связанных отношением наследования, вводится новое правило совместимости типов. Вместо объекта базового класса можно подставить объект любого производного класса. Обратное неверно. Например, переменной типа TTextReader можно присвоить значение переменной типа TDelimitedReader:

Объектная переменная Reader формально имеет тип TTextReader, а фактически связана с экземпляром класса TDelimitedReader.

Правило совместимости классов чаще всего применяется при передаче объектов в параметрах процедур и функций. Например, если процедура работает с объектом класса TTextReader, то вместо него можно передать объект класса TDelimitedReader или TFixedReader.

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

3.7.5. Контроль и преобразование типов

Поскольку реальный экземпляр объекта может оказаться наследником класса, указанного при описании объектной переменной или параметра, бывает необходимо проверить, к какому классу принадлежит объект на самом деле. Чтобы программист мог выполнять такого рода проверки, каждый объект хранит информацию о своем классе. В языке Delphi существуют операторы is и as , с помощью которых выполняется соответственно проверка на тип (type checking) и преобразование к типу (type casting).

Например, чтобы выяснить, принадлежит ли некоторый объект Obj к классу TTextReader или его наследнику, следует использовать оператор is :

Для преобразования объекта к нужному типу используется оператор as , например

Стоит отметить, что для объектов применим и обычный способ приведения типа:

Вариант с оператором as лучше, поскольку безопасен. Он генерирует ошибку (точнее исключительную ситуацию; об исключительных ситуациях мы расскажем в главе 4) при выполнении программы (run-time error), если реальный экземпляр объекта Obj не совместим с классом TTextReader. Забегая вперед, скажем, что ошибку приведения типа можно обработать и таким образом избежать досрочного завершения программы.

3.8. Виртуальные методы


3.8.1. Понятие виртуального метода

Все методы, которые до сих пор рассматривались, имеют одну общую черту — все они статические . При обращении к статическому методу компилятор точно знает класс, которому данный метод принадлежит. Поэтому, например, обращение к статическому методу ParseLine в методе NextLine (принадлежащем классу TTextReader) компилируется в вызов TTextReader.ParseLine:

В результате метод NextLine работает неправильно в наследниках класса TTextReader, так как внутри него вызов перекрытого метода ParseLine не происходит. Конечно, в классах TDelimitedReader и TFixedReader можно продублировать все методы и свойства, которые прямо или косвенно вызывают ParseLine, но при этом теряются преимущества наследования, и мы возвращаемся к тому, что необходимо описать два класса, в которых большая часть кода идентична. ООП предлагает изящное решение этой проблемы — метод ParseLine всего-навсего объявляется виртуальным :

Объявление виртуального метода в базовом классе выполняется с помощью ключевого слова virtual , а его перекрытие в производных классах — с помощью ключевого слова override . Перекрытый метод должен иметь точно такой же формат (список параметров, а для функций еще и тип возвращаемого значения), что и перекрываемый:

Суть виртуальных методов в том, что они вызываются по фактическому типу экземпляра, а не по формальному типу, записанному в программе. Поэтому после сделанных изменений метод NextLine будет работать так, как ожидает программист:

Работа виртуальных методов основана на механизме позднего связывания (late binding). В отличие от раннего связывания (early binding), характерного для статических методов, позднее связывание основано на вычислении адреса вызываемого метода при выполнении программы. Адрес метода вычисляется по хранящемуся в каждом объекте описателю класса.

Благодаря механизму наследования и виртуальных методов в среде Delphi реализуется такая концепция ООП как полиморфизм. Полиморфизм существенно облегчает труд программиста, поскольку обеспечивает повторное использование кода уже написанных и отлаженных методов.

3.8.2. Механизм вызова виртуальных методов

Работа виртуальных методов основана на косвенном вызове подпрограмм. При косвенном вызове команда вызова подпрограммы оперирует не адресом подпрограммы, а адресом места в памяти, где хранится адрес подпрограммы. Вы уже сталкивались с косвенным вызовом при использовании процедурных переменных. Процедурная переменная и была тем местом в памяти, где хранился адрес вызываемой подпрограммы. Для каждого виртуального метода тоже создается процедурная переменная, но ее наличие и использование скрыто от программиста.

Все процедурные переменные с адресами виртуальных методов пронумерованы и хранятся в таблице, называемой таблицей виртуальных методов (VMT — от англ. Virtual Method Table). Такая таблица создается одна для каждого класса объектов, и все объекты этого класса хранят на нее ссылку.

Структуру объекта в оперативной памяти поясняет рисунок 3.3:

Рисунок 3.3. Структура объекта TTextReader в оперативной памяти

Вызов виртуального метода осуществляется следующим образом:

  1. Через объектную переменную выполняется обращение к занятому объектом блоку памяти;
  2. Далее из этого блока извлекается адрес таблицы виртуальных методов (он записан в четырех первых байтах);
  3. На основании порядкового номера виртуального метода извлекается адрес соответствующей подпрограммы;
  4. Вызывается код, находящийся по этому адресу.

Покажем, как можно реализовать косвенный вызов виртуального метода ParseLine (он имеет нулевой номер в таблице виртуальных методов) обычными средствами процедурного программирования:

Поддержка механизма вызова виртуальных методов на уровне языка Delphi избавляет программиста от всей этой сложности.

3.8.3. Абстрактные виртуальные методы

При построении иерархии классов часто возникает ситуация, когда работа виртуального метода в базовом классе не известна и наполняется содержанием только в наследниках. Так случилось, например, с методом ParseLine, тело которого в классе TTextReader объявлено пустым. Конечно, тело метода всегда можно сделать пустым или почти пустым (так мы и поступили), но лучше воспользоваться директивой abstract :

Директива abstract записывается после слова virtual и исключает необходимость написания кода виртуального метода для данного класса. Такой метод называется абстрактным , т.е. подразумевает логическое действие, а не конкретный способ его реализации. Абстрактные виртуальные методы часто используются при создании классов-полуфабрикатов. Свою реализацию такие методы получают в законченных наследниках.

3.8.4. Динамические методы

Разновидностью виртуальных методов являются так называемые динамические методы . При их объявлении вместо ключевого слова virtual записывается ключевое слово dynamic , например:

В наследниках динамические методы перекрываются так же, как и виртуальные — с помощью зарезервированного слова override .

По смыслу динамические и виртуальные методы идентичны. Различие состоит только в механизме их вызова. Методы, объявленные с директивой virtual , вызываются максимально быстро, но платой за это является большой размер системных таблиц, с помощью которых определяются их адреса. Размер этих таблиц начинает сказываться с увеличением числа классов в иерархии. Методы, объявленные с директивой dynamic вызываются несколько дольше, но при этом таблицы с адресами методов имеют более компактный вид, что способствует экономии памяти. Таким образом, программисту предоставляются два способа оптимизации объектов: по скорости работы ( virtual ) или по объему памяти ( dynamic ).

3.8.5. Методы обработки сообщений

Специализированной формой динамических методов являются методы обработки сообщений . Они объявляются с помощью ключевого слова message , за которым следует целочисленная константа — номер сообщения . Следующий пример взят из исходных текстов библиотеки VCL:

Метод обработки сообщений имеет формат процедуры и содержит единственный var -параметр. При перекрытии такого метода название метода и имя параметра могут быть любыми, важно лишь, чтобы неизменным остался номер сообщения, используемый для вызова метода. Вызов метода выполняется не по имени, как обычно, а с помощью обращения к специальному методу Dispatch, который имеется в каждом классе (метод Dispatch определен в классе TObject).

Методы обработки сообщений применяются внутри библиотеки VCL для обработки команд пользовательского интерфейса и редко нужны при написании прикладных программ.

3.9. Классы в программных модулях

Классы очень удобно собирать в модули. При этом их описание помещается в секцию interface , а код методов — в секцию implementation . Создавая модули классов, нужно придерживаться следующих правил:

  • все классы, предназначенные для использования за пределами модуля, следует определять в секции interface ;
  • описание классов, предназначенных для употребления внутри модуля, следует располагать в секции implementation ;
  • если модуль B использует модуль A, то в модуле B можно определять классы, порожденные от классов модуля A.

Соберем рассмотренные ранее классы TTextReader, TDelimitedReader и TFixedReader в отдельный модуль ReadersUnit:

Как можно заметить, в описании классов присутствуют новые ключевые слова private , protected и public . С их помощью регулируется видимость частей класса для других модулей и основной программы. Назначение каждого ключевого слова поясняется ниже.

3.10. Разграничение доступа к атрибутам объектов

Программист может разграничить доступ к атрибутам своих объектов для других программистов (и себя самого) с помощью специальных ключевых слов: private , protected , public , published (последнее не используется в модуле ReadersUnit).

  • Private . Все, что объявлено в секции private недоступно за пределами модуля. Секция private позволяет скрыть те поля и методы, которые относятся к так называемым особеностям реализации. Например, в этой секции класса TTextReader объявлены поля FFile, FActive и FItems, а также методы PutItem, SetActive, GetItemCount и GetEndOfFile.
  • Public . Поля, методы и свойства, объявленные в секции public не имеют никаких ограничений на использование, т.е. всегда видны за пределами модуля. Все, что помещается в секцию public , служит для манипуляций с объектами и составляет программный интерфейс класса. Например, в классе TTextReader в эту секцию помещены конструктор Create, метод NextLine, свойства Active, Items, ItemCount.
  • Protected . Поля, методы и свойства, объявленные в секции protected , видны за пределами модуля только потомкам данного класса; остальным частям программы они не видны. Так же как и private , директива protected позволяет скрыть особенности реализации класса, но в отличие от нее разрешает другим программистам порождать новые классы и обращаться к полям, методам и свойствам, которые составляют так называемый интерфейс разработчика. В эту секцию обычно помещаются виртуальные методы. Примером такого метода является ParseLine.
  • Published . Устанавливает правила видимости те же, что и директива public . Особенность состоит в том, что для элементов, помещенных в секцию published , компилятор генерирует информацию о типах этих элементов. Эта информация доступна во время выполнения программы, что позволяет превращать объекты в компоненты визуальной среды разработки. Секцию published разрешено использовать только тогда, когда для самого класса или его предка включена директива компилятора $TYPEINFO .

Перечисленные секции могут чередоваться в объявлении класса в произвольном порядке, однако в пределах секции сначала следует описание полей, а потом методов и свойств. Если в определении класса нет ключевых слов private , protected , public и published , то для обычных классов всем полям, методам и свойствам приписывается атрибут видимости public , а для тех классов, которые порождены от классов библиотеки VCL, — атрибут видимости published .

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

3.11. Указатели на методы объектов

В языке Delphi существуют процедурные типы данных для методов объектов. Внешне объявление процедурного типа для метода отличается от обычного словосочетанием of object , записанным после прототипа процедуры или функции:

Переменная такого типа называется указателем на метод (method pointer). Она занимает в памяти 8 байт и хранит одновременно ссылку на объект и адрес его метода.

Методы объектов, объявленные по приведенному выше шаблону, становятся совместимы по типу со свойством OnReadLine.

Если установить значение свойства OnReadLine:

и переписать метод NextLine,

то объект Form1 через метод HandleLine получит уведомление об очередной считанной строке. Обратите внимание, что вызов метода через указатель происходит лишь в том случае, если указатель не равен nil . Эта проверка выполняется с помощью стандартной функции Assigned, которая возвращает True, если ее аргумент является связанным указателем.

Описанный выше механизм называется делегированием , поскольку он позволяет передать часть работы другому объекту, например, сосредоточить в одном объекте обработку событий, возникающих в других объектах. Это избавляет программиста от необходимости порождать многочисленные классы-наследники и перекрывать в них виртуальные методы. Делегирование широко применяется в среде Delphi. Например, все компоненты делегируют обработку своих событий той форме, в которую они помещены.

3.12. Метаклассы


3.12.1. Ссылки на классы

Язык Delphi позволяет рассматривать классы объектов как своего рода объекты, которыми можно манипулировать в программе. Такая возможность рождает новое понятие — класс класса ; его принято обозначать термином метакласс .

Для поддержки метаклассов введен специальный тип данных — ссылка на класс (class reference). Он описывается с помощью словосочетания class of , например:

Переменная типа TTextReaderClass объявляется в программе обычным образом:

Значениями переменной ClassRef могут быть класс TTextReader и все порожденные от него классы. Допустимы следующие операторы:

По аналогии с тем, как для всех классов существует общий предок TObject, у ссылок на классы существует базовый тип TClass, определенный, как:

Переменная типа TClass может ссылаться на любой класс.

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

Физический смысл и взаимосвязь таких понятий, как переменная-объект, экземпляр объекта в памяти, переменная-класс и экземпляр класса в памяти поясняет рисунок 3.4.

Рисунок 3.4. Переменная-объект, экземпляр объекта в памяти, переменная-класс и экземпляр класса в памяти

3.12.2. Методы классов

Метаклассы привели к возникновению нового типа методов — методов класса. Метод класса оперирует не экземпляром объекта, а непосредственно классом. Он объявляется как обычный метод, но перед словом procedure или function записывается зарезервированное слово class , например:

Передаваемый в метод класса неявный параметр Self содержит не ссылку на объект, а ссылку на класс, поэтому в теле метода нельзя обращаться к полям, методам и свойствам объекта. Зато можно вызывать другие методы класса, например:

Метод ClassName объявлен в классе TObject и возвращает имя класса, к которому применяется. Очевидно, что надуманный метод GetClassName просто дублирует эту функциональность для класса TTextReader и всех его наследников.

Методы класса применимы и к классам, и к объектам. В обоих случаях в параметре Self передается ссылка на класс объекта. Пример:

Методы классов могут быть виртуальными. Например, в классе TObject определен виртуальный метод класса NewInstance. Он служит для распределения памяти под объект и автоматически вызывается конструктором. Его можно перекрыть в своем классе, чтобы обеспечить нестандартный способ выделения памяти для экземпляров. Метод NewInstance должен перекрываться вместе с другим методом FreeInstance, который автоматически вызывается из деструктора и служит для освобождения памяти. Добавим, что размер памяти, требуемый для экземпляра, можно узнать вызовом предопределенного метода класса InstanceSize.

3.12.3. Виртуальные конструкторы

Особая прелесть ссылок на классы проявляется в сочетании с виртуальными конструкторами. Виртуальный конструктор объявляется с ключевым словом virtual . Вызов виртуального конструктора происходит по фактическому значению ссылки на класс, а не по ее формальному типу. Это позволяет создавать объекты, классы которых неизвестны на этапе компиляции. Механизм виртуальных конструкторов применяется в среде Delphi при восстановлении компонентов формы из файла. Восстановление компонента происходит следующим образом. Из файла считывается имя класса. По этому имени отыскивается ссылка на класс (метакласс). У метакласса вызывается виртуальный конструктор, который создает объект нужного класса.

На этом закончим изучение теории объектно-ориентированного программирования и в качестве практики рассмотрим несколько широко используемых инструментальных классов среды Delphi. Разберитесь с их назначением и работой. Это поможет глубже понять ООП и пригодится на будущее.

3.13. Классы общего назначения

Как показывает практика, в большинстве задач приходится использовать однотипные структуры данных: списки, массивы, множества и т.д. От задачи к задаче изменяются только их элементы, а методы работы сохраняются. Например, для любого списка нужны процедуры вставки и удаления элементов. В связи с этим возникает естественное желание решить задачу «в общем виде», т.е. создать универсальные средства для управления основными структурами данных. Эта идея не нова. Она давно пришла в голову разработчикам инструментальных пакетов, которые быстро наплодили множество вспомогательных библиотек. Эти библиотеки содержали классы объектов для работы со списками, коллекциями (динамические массивы с переменным количеством элементов), словарями (коллекции, индексированные строками) и другими «абстрактными» структурами. Для среды Delphi тоже разработаны аналогичные классы объектов. Их большая часть сосредоточена в модуле Classes. Наиболее нужными для вас являются списки строк (TStrings, TStringList) и потоки (TSream, THandleSream, TFileStream, TMemoryStream и TBlobStream). Рассмотрим кратко их назначение и применение.

3.13.1. Классы для представления списка строк

Для работы со списками строк служат классы TStrings и TStringList. Они используются в библиотеке VCL повсеместно и имеют гораздо большую универсальность, чем та, что можно почерпнуть из их названия. Классы TStrings и TStringList служат для представления не просто списка строк, а списка элементов, каждый из которых представляет собой пару строка-объект. Если со строками не ассоциированы объекты, получается обычный список строк.

Класс TStrings используется визуальными компонентами и является абстрактным. Он не имеет собственных средств хранения строк и определяет лишь интерфейс для работы с элементами. Класс TStringList является наследником TStrings и служит для организации списков строк, которые используются отдельно от управляющих элементов. Объекты TStringList хранят строки и объекты в динамической памяти.

Свойства класса TStrings описаны ниже.

  • Count : Integer — число элементов в списке.
  • Strings [Index: Integer]: string — обеспечивает доступ к массиву строк по индексу. Первая строка имеет индекс, равный 0. Свойство Strings является основным свойством объекта.
  • Objects [Index: Integer]: TObject — обеспечивает доступ к массиву объектов. Свойства Strings и Objects позволяют использовать объект TStrings как хранилище строк и ассоциированных с ними объектов произвольных классов.
  • Text : string — позволяет интерпретировать список строк, как одну большую строку, в которой элементы разделены символами #13#10 (возврат каретки и перевод строки).

Наследники класса TStrings иногда используются для хранения строк вида Имя=Значение, в частности, строк INI-файлов (см. гл. 6). Для удобной работы с такими строками в классе TStrings дополнительно имеются следующие свойства.

  • Names [Index: Integer]: string — обеспечивает доступ к той части строки, в которой содержится имя.
  • Values [const Name: string]: string — обеспечивает доступ к той части строки, в которой содержится значение. Указывая вместо Name ту часть строки, которая находится слева от знака равенства, вы получаете ту часть, что находится справа.

Управление элементами списка осуществляется с помощью следующих методов:

  • Add (const S: string): Integer — добавляет новую строку S в список и возвращает ее позицию. Новая строка добавляется в конец списка.
  • AddObject (const S: string; AObject: TObject): Integer — добавляет в список строку S и ассоциированный с ней объект AObject. Возвращает индекс пары строка-объект.
  • AddStrings (Strings: TStrings) — добавляет группу строк в существующий список.
  • Append (const S: string) — делает то же, что и Add, но не возвращает значения.
  • Clear — удаляет из списка все элементы.
  • Delete (Index: Integer) — удаляет строку и ассоциированный с ней объект. Метод Delete, также как метод Clear не разрушают объектов, т.е. не вызывают у них деструктор. Об этом вы должны позаботиться сами.
  • Equals (Strings: TStrings): Boolean — Возвращает True, если список строк в точности равен тому, что передан в параметре Strings.
  • Exchange (Index1, Index2: Integer) — меняет два элемента местами.
  • GetText : PChar — возвращает все строки списка в виде одной большой нуль-терминированной строки.
  • IndexOf (const S: string): Integer — возвращает позицию строки S в списке. Если заданная строка в списке отсутствует, функция возвращает значение -1.
  • IndexOfName (const Name: string): Integer — возвращает позицию строки, которая имеет вид Имя=Значение и содержит в себе Имя, равное Name.
  • IndexOfObject (AObject: TObject): Integer — возвращает позицию объекта AObject в массиве Objects. Если заданный объект в списке отсутствует, функция возвращает значение -1.
  • Insert (Index: Integer; const S: string) — вставляет в список строку S в позицию Index.
  • InsertObject (Index: Integer; const S: string; AObject: TObject) — вставляет в список строку S и ассоциированный с ней объект AObject в позицию Index.
  • LoadFromFile (const FileName: string) — загружает строки списка из текстового файла.
  • LoadFromStream (Stream: TStream) — загружает строки списка из потока данных (см. ниже).
  • Move (CurIndex, NewIndex: Integer) — изменяет позицию элемента (пары строка-объект) в списке.
  • SaveToFile (const FileName: string) — сохраняет строки списка в текстовом файле.
  • SaveToStream (Stream: TStream) — сохраняет строки списка в потоке данных.
  • SetText (Text: PChar) — загружает строки списка из одной большой нуль-терминированной строки.

Класс TStringList добавляет к TStrings несколько дополнительных свойств и методов, а также два свойства-события для уведомления об изменениях в списке. Они описаны ниже.

Свойства:


  • Duplicates : TDuplicates — определяет, разрешено ли использовать дублированные строки в списке. Свойство может принимать следующие значения: dupIgnore (дубликаты игнорируются), dupAccept (дубликаты разрешены), dupError (дубликаты запрещены, попытка добавить в список дубликат вызывает ошибку).
  • Sorted : Boolean — если имеет значение True, то строки автоматически сортируются в алфавитном порядке.

Методы:


  • Find (const S: string; var Index: Integer): Boolean — выполняет поиск строки S в списке строк. Если строка найдена, Find помещает ее позицию в переменную, переданную в параметре Index, и возвращает True.
  • Sort — сортирует строки в алфавитном порядке.

События:


  • OnChange : TNotifyEvent — указывает на обработчик события, который выполнится при изменении содержимого списка. Событие OnChange генерируется после того, как были сделаны изменения.
  • OnChanging : TNotifyEvent — указывает на обработчик события, который выполнится при изменении содержимого списка. Событие OnChanging генерируется перед тем, как будут сделаны изменения.

Ниже приводится фрагмент программы, демонстрирующий создание списка строк и манипулирование его элементами:

3.13.2. Классы для представления потока данных

В среде Delphi существует иерархия классов для хранения и последовательного ввода-вывода данных. Классы этой иерархии называются потоками . Потоки лучше всего представлять как файлы. Классы потоков обеспечивают различное физическое представление данных: файл на диске, раздел оперативной памяти, поле в таблице базы данных (таблица 3.1).

Класс Описание
TStream Абстрактный поток, от которого наследуются все остальные. Свойства и методы класса TStream образуют базовый интерфейс потоковых объектов.
THandleStream Поток, который хранит свои данные в файле. Для чтения-записи файла используется дескриптор (handle), поэтому поток называется дескрипторным. Дескриптор — это номер открытого файла в операционной системе. Его возвращают низкоуровневые функции создания и открытия файла.
TFileStream Поток, который хранит свои данные в файле. Отличается от ThandleStream тем, что сам открывает (создает) файл по имени, переданному в конструктор.
TMemoryStream Поток, который хранит свои данные в оперативной памяти. Моделирует работу с файлом. Используется для хранения промежуточных результатов, когда файловый поток не подходит из-за низкой скорости передачи данных.
TResourceStream Поток, обеспечивающий доступ к ресурсам в Windows-приложении.
TBlobStream Обеспечивает последовательный доступ к большим полям таблиц в базах данных.
Таблица 3.1. Классы потоков

Потоки широко применяются в библиотеке VCL и наверняка вам понадобятся. Поэтому ниже кратко перечислены их основные общие свойства и методы.

Общие свойства:


  • Position : Longint — текущая позиция чтения-записи.
  • Size : Longint — текущий размер потока в байтах.

Общие методы:


  • CopyFrom (Source: TStream; Count: Longint): Longint — копирует Count байт из потока Source в свой поток.
  • Read (var Buffer; Count: Longint): Longint — читает Count байт из потока в буфер Buffer, продвигает текущую позицию на Count байт вперед и возвращает число прочитанных байт. Если значение функции меньше значения Count, то в результате чтения был достигнут конец потока.
  • ReadBuffer (var Buffer; Count: Longint) — читает из потока Count байт в буфер Buffer и продвигает текущую позицию на Count байт вперед. Если выполняется попытка чтения за концом потока, то генерируется ошибка.
  • Seek (Offset: Longint; Origin: Word): Longint — продвигает текущую позицию в потоке на Offset байт относительно позиции, заданной параметром Origin. Параметр Origin может иметь одно из следующих значений: 0 — смещение задается относительно начала потока; 1 — смещение задается относительно текущей позиции в потоке; 2 — смещение задается относительно конца потока.
  • Write (const Buffer; Count: Longint): Longint — записывает в поток Count байт из буфера Buffer, продвигает текущую позицию на Count байт вперед и возвращает реально записанное количество байт. Если значение функции отличается от значения Count, то при записи была ошибка.
  • WriteBuffer (const Buffer; Count: Longint) — записывает в поток Count байт из буфера Buffer и продвигает текущую позицию на Count байт вперед. Если по какой-либо причине невозможно записать все байты буфера, то генерируется ошибка.

Ниже приводится фрагмент программы, демонстрирующий создание файлового потока и запись в него строки:

3.14. Итоги

Теперь для вас нет секретов в мире ООП. Вы на достаточно серьезном уровне познакомились с объектами и их свойствами; узнали, как объекты создаются, используются и уничтожаются. Если не все удалось запомнить сразу — не беда. Возвращайтесь к материалам главы по мере решения стоящих перед вами задач, и работа с объектами станет простой, естественной и даже приятной. Когда вы достигните понимания того, как работает один объект, то автоматически поймете, как работают все остальные. Теперь мы рассмотрим то, с чем вы встретитесь очень скоро — ошибки программирования.

Объект TQuery Delphi

Содержание

Delphi обеспечивает поддержку “pass through SQL”, это означает то, что Вы можете составлять предложения SQL и посылать их непосредственно серверам Oracle, Sybase, Inrterbase и другим. “Pass through SQL” — это мощный механизм по двум причинам:

  1. Большинство серверов могут обрабатывать SQL запросы очень быстро, а это означает, что используя SQL для удаленных данных, Вы получите ответ очень быстро.
  2. Есть возможность составлять SQL запросы, которые заставят сервер исполнить специализированные задачи, недоступные через родной язык Delphi.

Перед чтением этой статьи Вы должны иметь, по крайней мере, элементарное понятие о серверах и различиях между локальными и удаленными (remote) данными.

      1. Основные понятия о TQuery

Предыдущий Урок был, в основном, посвящен объекту TTable , который служит для доступа к данным. При использовании TTable , возможен доступ ко всему набору записей из одной таблицы. В отличие от TTable, TQuery позволяет произвольным образом (в рамках SQL ) выбрать набор данных для работы с ним. Во многом, методика работы с объектом TQuery похожа на методику работы с TTable , однако есть свои особенности.

Вы может создать SQL запрос используя компонент TQuery следующим способом:

  1. Назначите Псевдоним (Alias) DatabaseName.
  2. Используйте свойство SQL чтобы ввести SQL запрос типа
    “Select * from Country”.
  3. Установите свойство Active в True

Если обращение идет к локальным данным, то вместо псевдонима можно указать полный путь к каталогу, где находятся таблицы.

Две основных вещи, которые Вы должны понять прежде, чем перейти дальше:

  • Этот урок не является учебником для начинающих по SQL, а, скорее, описанием объекта TQuery и основных задач, которые Вы можете решить с его помощью. Если Вы не знаете ничто об SQL, Вы все же сможете воспользоваться этой статьей, и, в конце концов, приобретете некоторое понимание основ SQL. Однако, для полного изучения языка, Вы должны обратиться к любой из большого количества книг и документов, доступных по этому предмету.
  • Delphi использует pass through SQL, поэтому для разных SQL серверов синтаксис может быть несколько разным. Версия SQL для локальных таблиц ( Local SQL ) очень сильно урезан, по сравнению со стандартом. Чтобы узнать о его возможностях, Вы должны прочитать не только эту статью, но также файл LOCALSQL.HLP.

Вы увидите, что объект TQuery один из наиболее полезных и гибких компонентов, доступных в Delphi. С ним Вы сможете воспользоваться всей мощью, предоставляемой лидерами среди промышленных SQL серверов, вроде InrterBase, Oracle или Sybase.

Свойство SQL

Свойство SQL — вероятно, самая важная часть TQuery. Доступ к этому свойству происходит либо через Инспектор Объектов во время конструирования проекта (design time), или программно во время выполнения программы (run time).

Интересней, конечно, получить доступ к свойству SQL во время выполнения, чтобы динамически изменять запрос. Например, если требуется выполнить три SQL запроса, то не надо размещать три компонента TQuery на форме. Вместо этого можно разместить один и просто изменять свойство SQL три раза. Наиболее эффективный, простой и мощный способ — сделать это через параметризованные запросы, которые будут объяснены в следующей части. Однако, сначала исследуем основные особенности свойства SQL, а потом рассмотрим более сложные темы, типа запросов с параметрами.

Свойство SQL имеет тип TStrings, который означает что это ряд строк, сохраняемых в списке. Список действует также, как и массив, но, фактически, это специальный класс с собственными уникальными возможностями. В следующих нескольких абзацах будут рассмотрены наиболее часто используемые свойства.

При программном использовании TQuery, рекомендуется сначала закрыть текущий запрос и очистить список строк в свойстве SQL:

Обратите внимание, что всегда можно “безопасно” вызвать Close. Даже в том случае, если запрос уже закрыт, исключительная ситуация генерироваться не будет.

Следующий шаг — добавление новых строк в запрос:

Query1.SQL.Add(‘Select * from Country’);

Query1.SQL.Add(‘where Name = ’’Argentina’’’);

Метод Add используется для добавления одной или нескольких строк к запросу SQL. Общий объем ограничен только количеством памяти на вашей машине.

Чтобы Delphi отработал запрос и возвратил курсор, содержащий результат в виде таблицы, можно вызвать метод:

Демонстрационная программа THREESQL показывает этот процесс (см Рис.1)

Рис.1: Программа THREESQL показывает, как сделать несколько запросов с помощью единственного объекта TQuery.

Программа THREESQL использует особенность локального SQL, который позволяет использовать шаблоны поиска без учета регистра (case insensitive). Например, следующий SQL запрос:

Select * form Country where Name like ’C%’

возвращает DataSet, содержащий все записи, где поле Name начинается с буквы ‘C’. Следующий запрос позволит увидеть все страны, в названии которых встречается буква ‘C’:

Select * from Country where Name like ‘%C%’;

Вот запрос, которое находит все страны, название которых заканчивается на ‘ia’:

Select * from Country where Name like ‘%ia’;

Одна из полезных особенностей свойства SQL — это способность читать файлы, содержащие текст запроса непосредственно с диска. Эта особенность показана в программе THREESQL.

Вот как это работает. В директории с примерами к данному уроку есть файл с расширением SQL. Он содержат текст SQL запроса. Программа THREESQL имеет кнопку с названием Load, которая позволяет Вам выбрать один из этих файлов и выполнять SQL запрос, сохраненный в этом файле.

Кнопка Load имеет следующий метод для события OnClick:

procedure TForm1.LoadClick(Sender: TObject);

if OpenDialog1.Execute then

with Query1 do begin

Метод LoadClick сначала загружает компоненту OpenDialog и позволяет пользователю выбрать файл с расширением SQL. Если файл выбран, текущий запрос закрывается, выбраный файл загружается с диска в св-во SQL , запрос выполняется и результат показывается пользователю.

TQuery и Параметры

Delphi позволяет составить “гибкую” форму запроса, называемую параметризованным запросом. Такие запросы позволяют подставить значение переменной вместо отдельных слов в выражениях “where” или “insert”. Эта переменная может быть изменена практически в любое время. (Если используется локальный SQL, то можно сделать замену почти любого слова в утверждении SQL, но при этом та же самая возможность не поддерживается большинством серверов.)

Перед тем, как начать использовать параметризованные запросы, рассмотрим снова одно из простых вышеупомянутых предложений SQL:

Select * from Country where Name like ’C%’

Можно превратить это утверждение в параметризованный запрос заменив правую часть переменной NameStr:

select * from County where Name like :NameStr

В этом предложении SQL, NameStr не является предопределенной константой и может изменяться либо во время дизайна, либо во время выполнения. SQL parser (программа, которая разбирает текст запроса) понимает, что он имеет дело с параметром, а не константой потому, что параметру предшествует двоеточие «:NameStr». Это двоеточие сообщает Delphi о необходимости заменить переменную NameStr некоторой величиной, которая будет известна позже.

Обратите внимание, слово NameStr было выбрано абсолютно случайно. Использовать можно любое допустимое имя переменной, точно также, как выбирается идентификатор переменной в программе.

Есть два пути присвоить значение переменной в параметризованном запросе SQL. Один способ состоит в том, чтобы использовать свойство Params объекта TQuery . Второй — использовать свойство DataSource для получения информации из другого DataSet. Вот ключевые свойства для достижения этих целей:

property Params[Index: Word];

function ParamByName(const Value: string);

Если подставлять значение параметра в параметризованный запрос через свойство Params, то обычно нужно сделать четыре шага:

  1. Закрыть TQuery
  2. Подготовить объект TQuery, вызвав метод Prepare
  3. Присвоить необходимые значения свойству Params
  4. Открыть TQuery

Второй шаг выполняется в том случае, если данный текст запроса выполняется впервые, в дальнейшем его можно опустить.

Вот фрагмент кода, показывающий как это может быть выполнено практически:

Этот код может показаться немного таинственным. Чтобы понять его, требуется внимательный построчный анализ. Проще всего начать с третьей строки, так как свойство Params является “сердцем” этого процесса.

Params — это индексированное свойство, которое имеет синтаксис как у свойства Fields для TDataSet. Например, можно получить доступ к первой переменной в SQL запросе, адресуя нулевой элемент в массиве Params:

Если параметризованный SQL запрос выглядит так:

select * from Country where Name = :NameStr

то конечный результат (т.е. то, что выполнится на самом деле) — это следующее предложение SQL:

select * from Country where Name = “Argentina”

Все, что произошло, это переменной :Name Str было присвоено значение «Аргентина» через свойство Params. Таким образом, Вы закончили построение простого утверждения SQL.

Если в запросе содержится более одного параметра, то доступаться к ним можно изменяя индекс у свойства Params

либо используя доступ по имени параметра

Итак, параметризованные SQL запросы используют переменные, которые всегда начинаются с двоеточия, определяя места, куда будут переданы значения параметров.

Прежде, чем использовать переменную Params, сначала можно вызвать Prepare. Этот вызов заставляет Delphi разобрать ваш SQL запрос и подготовить свойство Params так, чтобы оно «было готово принять” соответствующее количество переменных. Можно присвоить значение переменной Params без предварительного вызова Prepare, но это будет работать несколько медленнее.

После того, как Вы вызывали Prepare, и после того, как присвоили необходимые значения переменной Params, Вы должны вызвать Open, чтобы закончить привязку переменных и получить желаемый D ataSet. В нашем случае, DataSet должен включать записи где в поле “Name” стоит “Argentina”.

Рассмотрим работу с параметрами на примере (программа PARAMS.DPR). Для создания программы, разместите на форме компоненты TQuery, TDataSource, T DB Grid и TTabSet. Соедините компоненты и установите в свойстве TQuery.DatabaseName псевдоним DBDEMOS . См. рис.2

Рис.2 : Программа PARAMS во время дизайна.

В обработчике события для формы OnCreate напишем код, заполняющий закладки для TTabSet , кроме того, здесь подготавливается запрос :

procedure TForm1.FormCreate(Sender: TObject);

for i:=0 to 25 do

Текст SQL запроса в компоненте Query1:

select * from employee where LastName like :LastNameStr

Запрос выбирает записи из таблицы EMPLOYEE , в которых поле LastName похоже ( like ) на значение параметра :LastNameStr . Параметр будет передаваться в момент переключения закладок :

procedure TForm1.TabSet1Change(Sender: TObject; NewTab: Integer; var AllowChange: Boolean); begin

with Query1 do begin

Рис.3: Программа PARAMS во время выполнения.

Передача параметров через TDataSource

В предыдущем Уроке Вы видели способ создания отношения однин-ко-многим между двумя таблицами. Теперь речь пойдет о выполнении того же самого действия с использованием объекта TQuery. Этот способ более гибок в том отношении, что он не требует индексации по полям связи.

Объект TQuery имеет свойство DataSource, которое может использоваться для того, чтобы создать связь с другим DataSet. Не имеет значения, является ли другой DataSet объектом TTable, TQuery, или некоторый другим потомком TDataSet. Все что нужно для установления соединения — это удостовериться, что у того D ataSet есть связанный с ним DataSource.

Предположим, что Вы хотите создать связь между таблицами ORDERS и CUSTOMERS так, что каждый раз, когда Вы просматриваете конкретную запись о заказчике, будут видны только заказы, связанные с ним.

Рассмотрите следующий параметризованный запрос:

select * from Orders where CustNo = :CustNo

В этом запросе :CustNo — связывающая переменная, которой должно быть присвоено значение из некоторого источника. Delphi позволяет использовать поле TQuery.DataSource чтобы указать другой DataSet, который предоставит эту информацию автоматически. Другими словами, вместо того, чтобы использовать свойство Params и “вручную” присваивать значения переменной, эти значения переменной могут быть просто взяты автоматически из другой таблицы. Кроме того, Delphi всегда сначала пытается выполнить параметризованный запрос используя свойство DataSource, и только потом (если не было найдено какое-то значение параметра) будет пытаться получить значение переменной из свойства Params. При получении данных из DataSource считается, что после двоеточия стоит имя поля из DataSource . При изменении текущей записи в главном DataSet запрос будет автоматически пересчитываться.

Давайте переделаем пример из прошлого урока ( LINKTBL — связывание двух таблиц). Создайте новый проект, положите на форму один набор TTable, TDataSource и TDBGr > Привяжите его к таблице CUSTOMER . Положите на форму второй набор — TQuery, TDataSource и TDBGr >и свяжите объекты между собой. (см рис.4).

В свойстве SQL наберите текст запроса :

select * from Orders where CustNo = :CustNo

В свойстве DatabaseName для Query1 укажите DBDEMOS.

В свойстве DataSource для Query1 укажите DataSource1.

Поставьте Active = True и запустите программу.

Рис.4: Программа LINKQRY — связанные курсоры с помощью SQL

Выполнение соединения нескольких таблиц

Вы видели что таблицы CUSTOMERS и ORDERS связаны в отношении один-ко-многим, основанному на поле CustNo. Таблицы ORDERS и ITEMS также связаны отношении один-ко-многим, только через поле OrderNo.

Более конкретно, каждый заказ который существует в таблице ORDERS будет иметь несколько записей в таблице ITEMS, связанных с этим заказом. Записи из таблицы ITEMS определяют тип и количество изделий, связанных с этим заказом.

Некто Иванов Ф.П. 1 мая 1995г. заказал следующее :

  1. Гайка 4х-угольная — 50 штук
  2. Вентиль — 1 штука

А некто Сидорчук Ю.Г. 8 декабря 1994г. заказал :

  1. М/схема КР580 ИК80 — 10 штук
  2. Транзистор КТ315 — 15 штук
  3. Моток провода — 1 штука

В ситуации подобной этой, иногда проще всего «соединить» данные из таблиц ORDERS и ITEMS так, чтобы результирующий DataSet содержал информацию из обеих таблиц:

Иванов Ф.П. 1 мая 1995г Гайка 4х-угольная 50 штук

Иванов Ф.П. 1 мая 1995г Вентиль 1 штука

Сидорчук Ю.Г. 8 декабря 1994г М/схема КР580 ИК80 10 штук

Сидорчук Ю.Г. 8 декабря 1994г Транзистор КТ315 15 штук

Сидорчук Ю.Г. 8 декабря 1994г Моток провода 1 штука

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

Взяв таблицы ORDERS и ITEMS из подкаталога DEMOS\DATA, их можно соединить их таким путем, что поля CustNo, OrderNo и SaleDate из таблицы ORDERS будут “слиты” с полями Part No и Qty из таблицы ITEMS и сформируют новый DataSet, содержащий все пять полей. Grid содержащий результирующий DataSet показан на рис.5

Рис.5: Соединение таблиц ORDERS и ITEMS может быть сделано так, что формируется новый D ataSet содержащий поля из каждой таблицы.

Имеется существенное различие между связанными курсорами и соединенными таблицами. Однако они имеют две общие черты:

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

Соединение таблиц ORDERS и ITEMS может быть выполнено единственным SQL запросом, который выглядит так:

O.CustNo, O.OrderNo, O.SaleDate, I.PartNo, I.Qty

from Orders O, Items I

where O.OrderNo = I.OrderNo

Этот запрос состоит из четырех различных частей:

  1. Выражение Select определяет, что Вы хотите получить — курсор, содержащий некоторую форму DataSet.
  2. Затем идет список полей которые Вы хотите включить в dataset. Этот список включает поля CustNo, OrderNo, SaleDate, PartNo и Qty. Первые три поля из таблицы ORDERS, а два других — из таблицы ITEMS.
  3. Выражение from объявляет, что Вы работаете с двумя таблицами, одна называется ORDERS, а другая ITEM S . Для краткости, в запросе используется особенность SQL, которая позволяет Вам ссылаться на таблицу ORDERS буквой O, а на таблицу ITEMS буквой I.
  4. Выражение where жизненно важно потому, что оно определяет поля связи для двух таблиц. Некоторые серверы могут вернуть DataSet, даже если Вы не включите выражение where в запрос, но почти всегда результирующий набор записей будет не тем, что Вы хотели видеть. Чтобы получить нужный результат, убедитесь что Вы включили выражение where.

Open или ExecSQL?

После того, как составлен SQL запрос, есть два различных способа выполнить его. Если Вы хотите получить курсор, то нужно вызывать Open. Если выражение SQL не подразумевает возвращение курсора, то нужно вызывать ExecSQL. Например, если происходит вставка, удаление или обновление данных (т.е. SQL запросы INSERT, DELETE, UPDATE ), то нужно вызывать ExecSQL. Тоже самое можно сказать по-другому: Open вызывается при запросе типа SELECT, а ExecSQL — во всех остальных случаях.

Вот типичный SQL запрос, который используется для удаления записи из таблицы:

delete from Country where Name = ‘Argentina’;

Этот запрос удалил бы любую запись из таблицы COUNTRY, которая имеет значение «Argentina» в поле Имя.

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

delete from Country where Name = :CountryName

В этом случае переменная : CountryName может быть изменена во время выполнения:

Код сначала вызывает Prepare, чтобы сообщить Delphi что он должен разобрать SQL запрос и подготовить свойство Params. Следующим шагом присваивается значение свойству Params и затем выполняется подготовленный SQL запрос. Обратите внимание, что он выполняется через ExecSQL, а не Open.

Программа INSQUERY из примеров Delphi демонстрирует эту технику (проект C:\DELPHI\DEMOS\DB\INSQUERY.DPR)

Специальные свойства TQuery

Есть несколько свойств, принадлежащих TQuery, которые еще не упоминались:

property UniDirectional: Boolean;

property Handle: HDBICur;

property StmtHandle: HDBIStmt;

property DBHandle: HDBIDB;

Свойство UniDirectional используется для того, чтобы оптимизировать доступ к таблице. Если Вы установите UniDirectional в True, то Вы можете перемещаться по таблице более быстро, но Вы сможете двигаться только вперед.

Свойство StmtHandle связано со свойством Handle TDataSet. То есть, оно включено исключительно для того, что Вы могли делать вызовы Borland Database Engine напрямую. При нормальных обстоятельствах, нет никакой необходимости использовать это свойство, так как компоненты Delphi могут удовлетворить потребностями большинства программистов. Однако, если Вы знакомы с Borland Database Engine, и если Вы знаете что существуют некоторые возможности не поддерживаемые в VCL, то Вы можете использовать TQuery.StmtHandle, или TQuery. Handle, чтобы сделать вызов напрямую в engine.

Следующий фрагмент кода показывает два запроса к BDE:

Name: array[0..100] of Char;

НОВОСТИ ФОРУМА
Рыцари теории эфира
01.10.2020 — 05:20: ВОСПИТАНИЕ, ПРОСВЕЩЕНИЕ, ОБРАЗОВАНИЕ — Upbringing, Inlightening, Education ->
[center][Youtube]69vJGqDENq4[/Youtube][/center]
[center]14:36[/center]
Osievskii Global News
29 сент. Отправлено 05:20, 01.10.2020 г.’ target=_top>Просвещение от Вячеслава Осиевского — Карим_Хайдаров.
30.09.2020 — 12:51: ВОСПИТАНИЕ, ПРОСВЕЩЕНИЕ, ОБРАЗОВАНИЕ — Upbringing, Inlightening, Education ->
[center][Ok]376309070[/Ok][/center]
[center]11:03[/center] Отправлено 12:51, 30.09.2020 г.’ target=_top>Просвещение от Дэйвида Дюка — Карим_Хайдаров.
30.09.2020 — 11:53: ВОСПИТАНИЕ, ПРОСВЕЩЕНИЕ, ОБРАЗОВАНИЕ — Upbringing, Inlightening, Education ->
[center][Youtube]VVQv1EzDTtY[/Youtube][/center]
[center]10:43[/center]

интервью Раввина Борода https://cursorinfo.co.il/all-news/rav.
мой телеграмм https://t.me/peshekhonovandrei
мой твиттер https://twitter.com/Andrey54708595
мой инстаграм https://www.instagram.com/andreipeshekhonow/

[b]Мой комментарий:
Андрей спрашивает: Краснодарская синагога — это что, военный объект?
— Да, военный, потому что имеет разрешение от Росатома на манипуляции с радиоактивными веществами, а также иными веществами, опасными в отношении массового поражения. Именно это было выявлено группой краснодарцев во главе с Мариной Мелиховой.

[center][Youtube]CLegyQkMkyw[/Youtube][/center]
[center]10:22 [/center]

Доминико Риккарди: Россию ждёт страшное будущее (хотелки ЦРУ):
https://tainy.net/22686-predskazaniya-dominika-rikardi-o-budushhem-rossii-sdelannye-v-2000-godu.html

Завещание Алена Даллеса / Разработка ЦРУ (запрещено к ознакомлению Роскомнадзором = Жид-над-рус-надзором)
http://av-inf.blogspot.com/2013/12/dalles.html

[center][b]Сон разума народа России [/center]

[center][Youtube]CLegyQkMkyw[/Youtube][/center]
[center]10:22 [/center]

Доминико Риккарди: Россию ждёт страшное будущее (хотелки ЦРУ):
https://tainy.net/22686-predskazaniya-dominika-rikardi-o-budushhem-rossii-sdelannye-v-2000-godu.html

Завещание Алена Даллеса / Разработка ЦРУ (запрещено к ознакомлению Роскомнадзором = Жид-над-рус-надзором)
http://av-inf.blogspot.com/2013/12/dalles.html

[center][b]Сон разума народа России [/center]

Object — Ключевое слово Delphi

Нил Дж. Рубенкинг

Как преодолеть отсутствие множественного наследования в 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-объекты должны иметь интерфейс 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-объекты с несколькими интерфейсами.

// «Обобщенные» объекты. Предназначены для создания 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 ;

function QueryInterface(const WantIID: TIID; var ReturnedObject): HResult; override ; function AddRef: LongInt; override ; function Release: LongInt; override ; end ; IMy >с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

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;

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;

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.

При создании и работе 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_ROOT\CLSID системного реестра 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 отличаются между собой лишь цифрой в отдельной позиции, легче разбираться, какой идентификатор к чему относится. Теперь вы можете вырезать эти значения из текстового файла и вставить в нужные места своего проекта.

Примеры создания четырех 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

Лист. 1. Два объекта-сателлита реализуют вспомогательные интерфейсы, необходимые для работы таких расширений оболочки Windows 95, как обработчики контекстного меню, списка параметров, для механизма drag-and-drop и пиктограмм.

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 практически не отличается от обработчика контекстного меню — в них используется даже один и тот же интерфейс 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, представленная в первой части этой статьи.

Большинство современных пакетов для разработки программ содержат встроенные средства отладки, обеспечивающие возможность выполнения в пошаговом режиме, трассировки кода, установки точек прерывания и просмотра значений переменных. Все они пригодны для отладки исполнимых 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 можно было бы вставить следующую строку:

Организовать передачу сообщения WM_LOGGIT — это еще полдела. Нужна программа, которая будет принимать и регистрировать сообщения о производимых операциях. Утилита Logger, предлагаемая службой PC Magazine Online, — один из возможных вариантов решения этой задачи.

Поскольку значение, имеющееся в сообщении WM_LOGGIT, становится известным только в процессе исполнения, нет возможности задать стандартный метод обработки сообщения. Поэтому в программе Logger переопределяется интерфейсный метод DefaultHandler. При прохождении сообщения WM_LOGGIT этот метод извлекает сообщение о состоянии из передаваемого элемента и добавляет его в имеющийся список окна просмотра. Помимо этой основной функции она обслуживает три рабочие кнопки — для вставки комментария пользователя, для очистки окна списка и для сохранения зарегистрированных сообщений в файле. На рис. А вы видите момент выполнения программы Logger.

В приведенном диалоговом окне представлены методы QueryInterface нескольких COM объектов, подготовленных в среде Delphi, инструментированные строкой, в которой регистрируется имя запрашиваемого интерфейса. Перед вами список запросов, отправленных, когда Explorer извлек пиктограмму для некоторого файла, затем пользователь щелкнул на ней правой клавишей мыши и просмотрел его параметры. Все работает правильно. Если же наша утилита вдруг выводит на экран неожиданные результаты, тогда в сомнительный фрагмент своей программы можно добавить новые обращения к функции Loggit и повторять эксперимент до тех пор, пока не удастся найти ошибку.

Илон Маск рекомендует:  Dos fn 17h переименовать файл через fcb
Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL