Этот курс посвящен среде разработки Delphi фирмы Borland
(известной также как Inprise), которая на протяжении многих лет успешно
выдерживает (и выигрывает!) жесткую конкуренцию с другими средами
программирования.
Концепция Delphi1 была реализована в конце 1994 года, когда
вышла первая версия среды разработки. В основу этого программного продукта
легли концепции объектно-ориентированного программирования (ООП) на базе
языка Object Pascal и визуального подхода к построению приложений.
После выхода Delphi 1 все компьютерные издания писали об этой
среде, как об “убийце Visual Basic”. Появление Delphi 2 (32-разрядной)
ознаменовало новую эпоху, - появился доступ к возможностям программных
интерфейсов Windows NT и Windows 95, протоколам OLE. Delphi 2 стала средством
разработки полноценных приложений клиент/сервер. Вскоре Delphi 3 предоставила
разработчикам средства создания распределенных многоуровневых приложений и
полноценный инструментарий проектирования приложений для Internet и intranet.
Появилась полноценная поддержка COM - модели объектов, ставшей краеугольным
камнем современного программирования. Четвертая версия Delphi позволяет
полностью интегрировать ваши разработки с объектами COM. Поддержка
архитектуры CORBA (Common Object Request Broker Architecture) открывает перед
приложениями, созданными в Delphi для платформы Wintel (Windows + Intel), мир
других операционных систем (UNIX, OS/2, WMS). Общаться с крупными
корпоративными СУБД стало также просто, как и со старым добрым Paradox. Вы
можете использовать в своей работе любые уровни межзадачного взаимодействия:
от простейшего на уровне сокетов, до связи с такими перспективными
инструментами, как Microsoft Transaction Server.
Delphi представляет следующие новые свойства и
усовершенствования:
- Новые
расширения языка. В Delphi в язык Object Pascal включены динамические
массивы, методы обработки переполнения, установка значения параметров по
умолчанию, и многое другое.
- Менеджер
Проекта Новый менеджер проекта позволяет Вам объединять проекты которые
работают вместе в одину проектную группу. Это позволяет Вам организовать
как работу взаимозависимых проектов, таких как однозадачные и
многозадачные приложения или DLL, так и совместную работу исполняемых
программ.
- Новый
проводник Новый проводник содержит выполняемые классы, навигацию по
модулям, и браузер кода. Проводник кода делает создание классов проще,
автоматизирую многие из шагов. Введите прототип метода в разделе
интерфейса и свойство выполняемого класса сгенерирует скелетный код в
разделе реализации. Также проводник позволяет быстро перемещаться через
файлы модуля, а так же между интерфейсом и реализацией. Использование
символа Tooltip, позволяет просматривать информацию об объявлении любого
идентификатора, затем используя борузер код, можно перейти к его объявлению.
- Закрепляемые
окна инструментов. IDE (Интегрированная Среда азработки) содержит более
перенастраеваемую конфигурацию окон инструментов, которые можно
закреплять с редактором кода. Просто перетащите и отпустите окно
инструмента к тому месту, к которому хотите. Проводник кода и менеджер
проекта можно как закреплять, так и незакреплять.
- Улучшенная
отладка. Интегрированный отладчик имеет много новых свойств, включая
удаленную и многопроцессорную отладку, просмотр кода центрального
процессора, инспекторов, усовершенствованные точки прерывания, отладчик
специфических подменю и закрепленных окон.
- Поддержка
MTS. Явная поддержка для использования MTS интегрирована в поддержку
многоуровневых баз данных. Кроме того, новый мастер облегчит Вам
создание объектов сервера MTS.
- Усовершенствования
ActiveX.
- Delphi
обеспечивает расширенную поддержку ActiveX.
- Усовершенствования
VCL. Иерархия объектов Delphi быда расширена, чтобы включить новый
компонент для NT Service приложений. Кроме того, новый компонент
выполняемого списка (на Стандартной странице палитры), позволяет Вам
централизовать управление меню и команд от кнопок. Управление VCL
расширено, чтобы поддерживають drag-and-drop перетаскивания,
обеспечивать дополнительный контроль над размещением окна, и многое другое.
- Поддержка RTL
для 2000-го года.
- Глобальная
переменная TwoDigitYearCenturWwindow используется функциями StrtToDate и
StrToTateTime, чтобы управлять интерпретацией лет с двумя цифрами при
преобразовании дат.
- Поддержка
CORBA. Версии Клинт/Сервер и предприятие включают поддержку для CORBA
клиент и сервер приложений. Мастера помогут Вам легко создать сервер
CORBA и Динамический Интерфейс Вызова (DII), позволяя Вам записывать
клиентов для существующих серверов CORBA. CORBA имеет возможность
поддержки в много-уровневых баз данных. Вы можете даже создать сервер,
который обрабатывает COM клиентов и CORBA клиентов одновременно.
Delphi - это комбинация нескольких важнейших технологий:
- Высокопроизводительный
компилятор в машинный код
- Объектно-ориентированная
модель компонент
- Визуальное
(а, следовательно, и скоростное) построение приложений из программных
прототипов
- Масштабируемые
средства для построения баз данных
Компилятор, встроенный в Delphi, обеспечивает высокую
производительность, необходимую для построения приложений в архитектуре
“клиент-сервер”. Он предлагает легкость разработки и быстрое время проверки
готового программного блока, характерного для языков четвертого поколения
(4GL) и в то же время обеспечивает качество кода, характерного для компилятора
3GL. Кроме того, Delphi обеспечивает быструю разработку без необходимости
писать вставки на Си или ручного написания кода (хотя это возможно).
В процессе построения приложения разработчик выбирает из палитры
компонент готовые компоненты как художник, делающий крупные мазки кистью. Еще
до компиляции он видит результаты своей работы - после подключения к
источнику данных их можно видеть отображенными на форме, можно перемещаться
по данным, представлять их в том или ином виде. В этом смысле проектирование
в Delphi мало чем отличается от проектирования в интерпретирующей среде,
однако после выполнения компиляции мы получаем код, который исполняется в
10-20 раз быстрее, чем то же самое, сделанное при помощи интерпретатора.
Кроме того, компилятор компилятору рознь, в Delphi компиляция производится
непосредственно в родной машинный код, в то время как существуют компиляторы,
превращающие программу в так называемый p-код, который затем интерпретируется
виртуальной p-машиной. Это не может не сказаться на фактическом
быстродействии готового приложения.
Объектно-ориентированная модель программных компонент. Основной
упор этой модели в Delphi делается на максимальном реиспользовании кода. Это
позволяет разработчикам строить приложения весьма быстро из заранее подготовленных
объектов, а также дает им возможность создавать свои собственные объекты для
среды Delphi. Никаких ограничений по типам объектов, которые могут создавать
разработчики, не существует. Действительно, все в Delphi написано на нем же,
поэтому разработчики имеют доступ к тем же объектам и инструментам, которые
использовались для создания среды разработки. В результате нет никакой
разницы между объектами, поставляемыми Borland или третьими фирмами, и
объектами, которые вы можете создать.
В стандартную поставку Delphi входят основные объекты, которые
образуют удачно подобранную иерархию базовых классов. Но если возникнет
необходимость в решении какой-то специфической проблемы на Delphi, советуем,
прежде чем попытаться начинать решать проблему “с нуля”, просмотреть список
свободно распространяемых или коммерческих компонент, разработанных третьими
фирмами, количество этих компонент в настоящее время составляет несколько
тысяч. Событийная модель в Windows всегда была сложна для понимания и
отладки. Но именно разработка интерфейса в Delphi является самой простой
задачей для программиста.
Объекты БД в Delphi основаны на SQL и включают в себя полную
мощь Borland Database Engine. В состав Delphi также включен Borland SQL Link,
поэтому доступ к СУБД Oracle, Sybase, Informix и InterBase происходит с
высокой эффективностью. Кроме того, Delphi включает в себя локальный сервер
Interbase для того, чтобы можно было разработать расширяемые на любые внешние
SQL-сервера приложения в офлайновом режиме. азработчик в среде Delphi,
проектирующий информационную систему для локальной машины (к примеру,
небольшую систему учета медицинских карточек для одного компьютера), может
использовать для хранения информации файлы формата .dbf (как в dBase или
Clipper) или .db (Paradox). Если же он будет использовать локальный InterBase
for Windows (это локальный SQL-сервер, входящий в поставку), то его
приложение безо всяких изменений будет работать и в составе большой системы с
архитектурой клиент-сервер. Вот она - масштабируемость на практике - одно и
то же приложение можно использовать как для локального, так и для более
серьезного клиент-серверного вариантов.
1. Основы объектно-ориентированного программирования
Понятие класса.
Классом в Delphi называется особый тип, который может иметь в
своем составе поля, методы и свойства. Такой тип также называют объектным
типом.
type
tMyClass=class(tObject)
fMyFiled: integer;
function MyMethod: integer;
end;
Поля класса аналогичны полям записи. Это - данные, уникальные в
программе для каждого созданного в программе экземпляра класса. Описанный
выше класс tMyClass имеет одно поле - fMyFiled. В отличие от полей, методы у
двух экземпляров одного класса общие. Методы - это процедуры и функции,
описанные внутри класса, и предназначенные для операций над его полями. В
состав класса входит указатель на специальную таблицу - таблицу виртуальных
методов (VMT), в которой содержится вся информация, необходимая для вызова
методов. От обычных процедур и функций методы отличаются тем, что при вызове
в них передается указатель на экземпляр класса, который их вызвал. Поэтому
обрабатываться будут поля именно того объекта, который вызвал метод. Внутри
методов указатель на вызвавший их объект доступен через зарезервированное
слово Self. Свойством класса называется совокупность поля и методов
чтения/записи, обеспечивающих доступ к этому полю. При этом само поле
декларируется как private (доступно только внутри данного класса), и доступ к
нему возможен только посредством соответствующих методов. Подробнее свойства
будут обсуждаться ниже.
Понятие объекта
Чтобы использовать новый тип в программе, нужно, как минимум,
определить переменную этого типа. Переменная объектного типа называется
экземпляром типа или объектом:
var aMyObject: tMyClass;
До введения понятия “класс” в языке Pascal существовала
двусмысленность определения “объект”, который мог обозначать и тип, и
переменную этого типа. Теперь существует четкая граница: класс - это
описание, объект - то, что создано в соответствии с этим описанием.
Создание и уничтожение объектов
В отличие от С++ и Turbo Pascal в Delphi объекты могут быть
только динамическими!!!. Это означает, что в приведенном выше примере
переменная aMyObject на самом деле является указателем, содержащем адрес
объекта.
Объект создается конструктором и уничтожается деструктором.
aMyObject := tMyClass.Create;
//
// действия с созданным объектом
//
aMyObject.Destroy;
Следует обратить внимание на то, что для создания объекта
aMyObject вызывается метод класса tMyClass.Create. Конструктор класса (и ряд
других методов) успешно работает и до создания объекта. Однако большинство
обычных методов (в частности все виртуальные и динамические методы). Вызывать
до инициализации объекта не следует.
В Delphi конструкторов у класса может быть несколько.
Общепринято называть конструктор Create, в отличие от Turbo Pascal, где
конструкторы назывались Init, и С++, в котором имя конструктора совпадает с
именем класса. Типичное название деструктора - Destroy.
type
tMyClass=class(tObject)
fMyFiled: integer;
Constructor Create;
Destructor Destroy;
function MyMethod: integer;
end;
Для уничтожения объекта в Delphi рекомендуется использовать не
деструктор, а метод Free, который первоначально проверяет указатель, и только
затем вызывает деструктор Destroy:
procedure tObject.Free;
До передачи управления телу конструктора происходит собственно
создание объекта: под него отводится память, значения всех полей обнуляются.
Далее выполняется код конструктора, написанный программистом для
инициализации объектов данного класса. Таким образом, несмотря на то, что
синтаксис конструктора схож с вызовом процедуры (отсутствует возвращаемое
значение), на самом деле конструктор - это функция, возвращающая созданный и
проинициализированный объект.
Примечание. Конструктор создает новый объект только в том
случае, если перед его именем указано имя класса. Если указать имя уже
существующего объекта, он поведет себя по-другому: не создаст новый объект, а
только выполнит код, содержащийся в теле конструктора.
Чтобы правильно проинициализировать в создаваемом объекте поля,
относящиеся к классу - предку, нужно сразу же при входе в конструктор вызвать
конструктор предка при помощи зарезервированного слова inherited:
constructor tMyClass.Create;
Begin
inherited Create;
// Код инициализации tMyClass
End;
Как правило, в коде программ, написанных на Delphi, практически
н встречается вызовов конструкторов и деструкторов. Дело в том, что любой
компонент, попавший при визуальном проектировании в приложение из палитры
компонентов, включается в определенную иерархию. Эта иерархия замыкается на
форме (класс tForm): для всех ее составных частей конструкторы и деструкторы
вызываются автоматически, незримо для программиста. Кто создает и уничтожает
формы? Это делает приложение (объект с именем Application). В файле проекта
(с расширением DPR) вы можете увидеть вызовы метода Application.CreateForm,
предназначенного для этой цели.
Что касается объектов, создаваемых динамически (во время
выполнения программы), то здесь нужен явный вызов конструктора и метода Free.
Свойства
Как известно, существует три основных принципа, составляющих
суть объектно-ориентированного программирования: инкапсуляция, наследование и
полиморфизм. Классическое правило объектно-ориентированного программирования
утверждает, что для обеспечения надежности нежелателен прямой доступ к полям
объекта: чтение и изменение их содержимого должно осуществляться посредством
вызова соответствующих методов. Это правило называется инкапсуляцией
(сокрытие данных). В старых реализациях ООП (например в Turbo Pascal) эта
мысль внедрялась только посредством призывов и примеров в документации; в
Delphi есть соответствующая конструкция. Пользователь объекта в Delphi может
быть полностью отгорожен от полей объекта при помощи свойств.
Обычно свойство определяется тремя элементами: полем и двумя
методами осуществляющими его чтение/запись:
type
tMyClass=class(tObject)
function GetaProperty: tSomeType;
procedure SetaProperty(Value: tSomeType);
property aProperty: tSomeType read GetaProperty
write SetaProperty;
end;
В данном примере доступ к значению свойства aProperty
осуществляется через вызовы методов GetaProperty и SetaProperty, однако в
обращении к этим методам в явном виде нет необходимости: достаточно написать
aMyObject.aProperty:=aValue;
aVarable:= aMyObject.aProperty;
и Delphi откомпилирует эти операторы в вызовы соответствующих
методов. То есть внешне свойство выглядит в точности как обычное поле, но за
всяким обращением к нему могут стоять вызовы необходимых программисту методов.
Например, если есть объект, представляющий собой квадрат на экране, и его
свойству “цвет” присваивается значение “белый”, то произойдет немедленная
прорисовка, приводящая реальный цвет на экране в соответствие значению
свойства.
В методах, устанавливающих значения свойства, может
производиться проверка значения на попадание в заданный диапазон значений и
вызов других процедур зависящих от вносимых изменений. Если же потребности в
специальных процедурах чтения/записи нет, можно вместо имен методов применять
имена полей.
tPropClass=class
fValue: tSomeType;
procedure SetValue(aValue: tSomeType);
property Value:tSomeType read fValue write SetValue;
End;
В этом примере поле fValue модифицируется при помощи метода
SetValue, а читается напрямую.
Если свойство должно только читаться или только записываться, в
его описании может присутствовать только соответствующий метод:
tReadOnlyClass=class
property NotChanged:tSomeType read GetNotChanged;
End;
В этом примере свойство доступно только для чтения. Попытка
присвоить значение свойству NotChanged вызовет ошибку компиляции.
Свойствам можно присваивать значения по умолчанию. Для этого
служит ключевое слово default:
Property Visible:boolean read fVisible write SetVisible default TRUE;
Это означает, что при запуске программы свойство будет
установлено компилятором в TRUE.
Свойство может быть и векторным. В этом случае оно выглядит как
массив:
Property Points[index:integer]:tPoint read GetPoint write SetPoint;
Для векторного свойства необходимо описать не только тип
элементов массива, но также и тип индекса. После ключевых слов read и write
должны идти имена соответствующих методов. Использование здесь полей массивов
недопустимо. Метод, читающий значение векторного свойства, должен быть описан
как функция, возвращающая значение того же типа, что и элементы свойства, и
имеющая единственный параметр того же типа и с тем же именем, что и индекс
свойства:
function GetPoint(index:integer):tPoint;
Аналогично, метод, помещающий значения в такое свойство, должен
первым параметром иметь индекс, а вторым - переменную нужного типа.
procedure SetPoint(index:integer; Value:tPoint);
У векторных свойств есть еще одна важная особенность: некоторые
классы в Delphi (списки tList, наборы строк tStrings и т.д.) “построены”
вокруг одного основного векторного свойства. Основной метод такого класса
дает доступ к элементам некоторого массива, а все основные методы являются
как бы вспомогательными. Для упрощения работы с объектами подобного класса
можно описать подобное свойство с ключевым словом default:
type tMyList=class
property list[Index:integer]:string read Getlist write Setlist; default;
end;
Если у объекта есть такое свойство, его можно не упоминать, а
ставить индекс в квадратных скобках сразу после имени объекта:
var MyList:tMyList
Begin
MyList.list[1]:=’First’; {Первый способ}
MyList.[2]:=’Second’; {Первый способ}
End;
Употребляя ключевое слово default необходимо соблюдать
осторожность, т.к. для обычных и векторных свойств оно употребляется в разных
значениях.
О роли свойств в Delphi красноречиво говорит тот факт, что у
всех имеющихся в распоряжении программиста стандартных классов 100% полей
недоступны и заменены базирующимися на них свойствами. Того же правила
следует придерживаться и при разработке собственных классов.
Наследование
Вторым “столпом” ООП является наследование. Этот простой принцип
означает, что если необходимо создать новый класс, лишь немного отличающийся
от уже имеющегося, нет необходимости в переписывании заново уже существующего
кода. Вы объявляете, что новый класс
tNewClass=class(tOldClass);
является потомком или дочерним классом класса tOldClass,
называемого предком или родительским классом, и добавляете к нему новые поля
методы и свойства.
В Delphi все классы являются потомками класса tObject. Поэтому,
если вы строите дочерний класс прямо от tObject, то в определении его можно
не упоминать. Следующие два описания одинаково верны:
tMyClass=class(tObject);
tMyClass=class;
Более подробно класс tObject будет рассмотрен ниже.
Унаследованные от класса-предка поля и методы доступны в
дочернем классе; если имеет место совпадение имен методов, говорят, что они
перекрываются.
Рассмотрим поведение методов при наследовании. По тому, какие
действия происходят при вызове, методы делятся на три группы. В первую группу
отнесем статические методы, во вторую - виртуальные (virtual) и динамические
(dynamic) и, наконец, в третью - появившиеся только в Delphi 4 перегружаемые
(overload) методы.
Статические методы, а также любые поля в классах-потомках ведут
себя одинаково: можно без ограничений перекрывать старые имена и при этом
менять тип методов. Код нового статического метода полностью перекрывает
(заменяет собой) код старого метода:
type
tFirstClass=class
fData:Extended;
procedure SetData(aValue:Extended);
end;
tSecondClass=class(tFirstClass)
fData:Integer;
procedure SetData(aValue:Integer);
end;
procedure tFirstClass.SetData(aValue:Extended);
Begin
fData:=1.0;
End;
procedure tFirstClass.SetData(aValue:Extended);
Begin
fData:=1;
inherited SetData(0.99);
End;
В этом примере разные методы с именем SetData присваивают
значение разным полям с именем fData. Перекрытое (одноименное) поле предка
недоступно в потомке. Поэтому два одноименных поля с именем fData приведены
только для примера.
В отличие от поля, внутри других методов перекрытый метод
доступен при указании ключевого слова inherited. По умолчанию методы объектов
классов статические - их адрес определяется еще на этапе компиляции проекта,
поэтому они вызываются быстрее всего.
Принципиально отличаются от статических виртуальные и
динамические методы. Они должны быть объявлены путем добавления
соответствующей директивы dynamic или virtual. С точки зрения наследования
методы этих двух категорий одинаковы: они могут быть перекрыты в дочернем
классе только одноименными методоми, имеющими тот же тип.
Полиморфизм. Виртуальные и динамические методы
Рассмотрим следующий пример. Пусть имеется некое обобщенное поле
для хранения данных - класс tFiled и три его потомка - для хранения строк,
целых и вещественных чисел:
type
tFiled = class
function GetData:string; virtual; abctract;
end;
tStringFiled = class(tFiled)
fData:string;
function GetData: string; override;
end;
tIntegerFiled = class(tFiled)
fData:Integer;
function GetData: string; override;
end;
tExtendedFiled = class(tFiled)
fData:Extended;
function GetData: string; override;
end;
function tStringFiled.GetData: string;
Begin
Result:=fData;
End;
function tIntegerFiled.GetData: string;
Begin
Result:=IntToStr(fData);
End;
function tExtendedFiled.GetData: string;
Begin
Result:=FloatToStr(fData,ffFixed, 7, 2);
End;
function ShowData(aFiled:tFiled): string;
Begin
Form1.Label1.Caption:=aFiled.GetData;
End;
В этом примере классы содержат разнотипные поля данных fData, а
также имеют унаследованный от tFiled виртуальный метод GetData, возвращающий
данные в виде строки. Внешняя по отношению к ним процедура ShowData получает
объект в виде параметра и показывает эту строку.
Согласно правилам контроля соответствия типов (typecasting)
ObjectPascal, объекту, как указателю на экземпляр класса, может быть присвоен
адрес экземпляра любого из дочерних типов. Это означает, что в предыдущем
примере в процедуру ShowData можно передавать объекты классов tStringFiled,
tIntegerFiled, tExtendedFiled и любого другого потомка tFiled.
Но какой (точнее, чей) метод GetData будет при этом вызван? Тот,
который соответствует классу фактически переданного объекта. Этот принцип
называется полиморфизмом.
Возвращаясь к рассмотренному выше примеру, отметим, что у
компилятора нет возможности определить класс объекта, фактически переданного
в процедуру ShowData на этапе компиляции. Механизм, позволяющий определить
этот класс прямо во время выполнения называется поздним связыванием.
Естественно, такой механизм должен быть связан с передаваемым объектом. Для
этого служит таблица виртуальных методов (Virtual Method Table, VMT) и
таблица динамических методов (Dynamic Method Table, DMT).
Различие между виртуальными и динамическими методами заключается
в особенности поиска адреса. Когда компилятор встречает обращение к
виртуальному методу, он подставляет вместо прямого вызова по конкретному
адресу код, который обращается к VMT и извлекает оттуда нужный адрес. Такая
таблица есть для каждого класса. В ней хранятся адреса всех виртуальных
методов класса, независимо от того, унаследованы ли они от предка или
перекрыты в данном классе. Отсюда и достоинства и недостатки виртуальных
методов: они вызываются сравнительно быстро, однако для хранения указателей
на них в таблице VMT требуется большое количество памяти.
Динамические методы вызываются медленнее, но позволяют более
экономно расходовать память. Каждому динамическому методу системой
присваивается уникальный индекс. В таблице динамических методов класса
хранятся индексы только тех методов только тех динамических методов, которые
описаны в данном классе. При вызове динамического метода происходит поиск в
этой таблице. В случае неудачи просматриваются DMT всех классов-предков в
порядке их иерархии и, наконец, tObject, где имеется стандартный обработчик
вызова динамических методов. Экономия памяти очевидна.
Для перекрытия и виртуальных и динамических методов служит
директива override, с помощью которой (и только с ней!) можно переопределять
оба этих типа методов.
type
tParentClass=class
fFirstFiled:Integer;
fSecondFiled:longInt;
procedure StaticMethod;
procedure VirtualMethod1; virtual;
procedure VirtualMethod2; virtual;
procedure DynamicMethod1; dynamic;
procedure DynamicMethod2; dynamic;
end;
tChildClass=class(tParentClass)
procedure StaticMethod;
procedure VirtualMethod1; override;
procedure DynamicMethod1; override;
end;
Первый метод класса tChildClass создается заново, два остальных
перекрываются. Создадим объекты этих классов:
var Obj1: tParentClass;
Obj2: tChildClass;
Внутренняя структура этих объектов показана ниже.
Первое поле каждого экземпляра каждого объекта содержит
указатель на его класс. Класс, как структура состоит из двух частей. Начиная
с адреса, на который ссылается указатель на класс, располагается таблица
виртуальных методов. Она содержит адреса всех виртуальных методов класса,
включая и унаследованные от предков. Перед таблицей виртуальных методов
расположена специальная структура, содержащая дополнительную информацию. В
ней содержатся данные, полностью характеризующие класс: имя, размер
экземпляров, указатели на класс-предок и т.д. Одно из полей структуры
содержит адрес таблицы динамических методов класса (DMT). Таблица имеет
следующий формат: в начале - слово, содержащее количество элементов таблицы.
Затем - слова, соответствующие индексам методов. Нумерация индексов начинается
с –1 и идет по убывающей. После индексов идут собственно адреса динамических
методов. Следует обратить внимание на то, что DMT объекта Obj1 состоит из
двух элементов, Obj2 - из одного, соответствующего перекрытому методу
DynamicMethod1. В случае вызова Obj2.DynamicMethod2 индекс не будет найден в
DMT Obj2, и произойдет обращение к DMT Obj1. Именно так экономится память при
использовании динамических методов.
Как указывалось выше, указатель на класс указывает на первый
виртуальный метод. Служебные данные размещаются перед таблицей виртуальных
методов, то есть с отрицательным смещением. Эти смещения описаны в модуле
SYSTEM.PAS:
vmtSelfPtr = -76
vmtIntfTable = -72
vmtAutoTable = -68
vmtInitTable = -64
vmtTypeInfo = -60
vmtFiledTable = -56
vmtMethodTable = -52
vmtDynamicTable = -48
vmtClassName = -44
vmtInstanceSize = -40
vmtParent = -36
vmtSafeCallException = -32
vmtAfterConstruction = -28
vmtBeforeDestruction = -24
vmtDispatch = -20
vmtDefaultHandler = -16
vmtNewInstance = -12
vmtFreeInstance = -8
vmtDestroy = -4
Поля vmtDynamicTable, vmtDispatch и vmtDefaultHandler отвечают
за вызов динамических методов. Поля vmtNewInstance, vmtFreeInstance и
vmtDestroy содержат адреса методов создания и уничтожения экземпляров класса.
Поля vmtIntfTable, vmtAutoTable, vmtSafeCallException введены для обеспечения
совместимости с моделью объекта COM. Остальные поля доступны через методы
объекта tObject. В Object Pascal эта информация играет важную роль и может
использоваться программистом неявно. В языке определены два оператора - is и
as, неявно обращающиеся к ней. Оператор is предназначен для проверки
совместимости по присвоению экземпляра объекта с заданным классом. Выражение
вида:
AnObject is tObjectType
Принимает значение True только если объект AnObject совместим по
присвоению с классом tObjectType, то есть является объектом этого класса или
его потомком.
Оператор as введен в язык специально для приведения объектных
типов. С его помощью можно рассматривать экземпляр объекта как принадлежащий
к другому совместимому типу:
with AObjectOfSomeType as tAnotherType do . . .
От стандартного способа приведения типов использование оператора
as отличается наличием проверки на совместимость типов во время выполнения:
попытка приведения к несовместимому типу приводит к возникновению исключительной
ситуации eInvalidCast. После выполнения оператора as сам объект остается
неизменным, но выполняются те его методы, которые соответствуют
присваиваемому классу.
Вся информация, описывающая класс, создается и размещается в
памяти на этапе компиляции. Доступ к информации вне методов этого класса
можно получить, описав соответствующий указатель, называющийся указателем на
класс или указателем на объектный тип (class reference). Он описывается при
помощи зарезервированных слов class of. Например, указатель на класс tObject
описан в модуле SYSTEM.PAS и называется tClass. Аналогичные указатели
определены и для других важнейших классов: tComponentClass, tControlClass и
т.д.
С указателем на класс тесно связано понятие методов класса.
Такие методы можно вызывать и без создания экземпляра объекта - с указанием
имени класса в котором они описаны. Перед описанием метода класса нужно
поставить ключевое слово class.
Разумеется, методы класса не могут использовать значения,
содержащиеся в полях класса: ведь экземпляра класса не существует!!! Методы
класса служат для извлечения внутренней информации класса. Ниже перечислены
методы класса tObject:
Метод и Описание
сlass function ClassName:ShortString
Возвращает имя класса
сlass function ClassNameIs(const Name:ShortString):Boolean
Принимает значение True, если имя класса равно заданному
сlass function ClassParent:tClass
Возвращает указатель на родительский класс
сlass function ClassInfo:pointer
Возвращает указатель на структуру с дополнительными данными об опубликованных
методах и свойствах.
сlass function InstanceSize:Longint
Возвращает размер экземпляра класса
сlass function InheritsFrom (aClass: tClass):Boolean
Возвращает True, если данный класс наследует от заданного
сlass function MethodAddress(const Name:ShortString):Pointer
Возвращает адрес метода по его имени (только для опубликованных методов)
сlass function MethodName (Addres: pointer):ShortString
Возвращает имя метода по его адресу (только для опубликованных методов)
В Delphi 4 в класс tObject добавлены еще два виртуальных метода
- AfterConstruction и BeforeDestruction. Как следует из названия, они
вызываются сразу после создания экземпляра объекта и непосредственно перед
уничтожением.
Перегрузка методов
В Delphi 4 появилась новая разновидность методов -
перегружаемые. Перегрузка нужна для того, чтобы произвести одинаковые или
похожие действия над разнотипными данными. Перегружаемые методы описываются с
ключевым словом overload.
Type
tFirstClass=class
E:extended;
procedure SetData(aValue: Extended); overload;
end;
tSecondClass=class(tFirstClass)
I:integer;
procedure SetData(aValue: Integer); overload;
end;
Объявив метод SetData перегружаемым, в программе можно
использовать обе его реализации одновременно. Это возможно потому, что
компилятор определяет тип передаваемого параметра (целый или вещественный) и
в зависимости от этого подставит вызов соответствующего метода.
Для перегрузки виртуального метода используется
зарезервированное слово reintroduce:
procedure SetData(aValue:string); reintrouce;overload;
На перегрузку методов накладывается ограничение: нельзя
перегружать методы, находящиеся в области видимости published.
Абстрактные методы
Абстрактными называются методы, которые определены в классе, но
не содержат никаких действий, никогда не вызываются и обязательно должны быть
переопределены в классах-потомках. Абстрактными могут быть только виртуальные
и динамические методы. Для описания абстрактного метода используется
директива abstract:
Procedure NeverCallMe; virtual; abstract;
Никакого кода абстрактный метод не содержит. Его вызов приведет
к созданию исключительной ситуации eAbstractError.
События
Операционная система Windows® основана на сообщениях. Сообщения
эти возникают в результате действий пользователя, аппаратуры компьютера или
других программ. Таких сообщений в Windows сотни, и по большому счету,
написать программу для Windows - значит определить реакцию на некоторые из
них.
Работать с таким количеством сообщений, даже имея под рукой
справочник, нелегко. Поэтому одним из главных преимуществ Delphi является то,
что программист полностью избавлен от необходимости работать с сообщениями
Windows (хотя такая возможность у него имеется). Типовых событий в Delphi -
не более двух десятков, и все они имеют простую интерпретацию, не требующую
глубоких знаний среды.
С точки зрения языка, событие - это поле процедурного типа,
предназначенное для создания пользовательской реакции на те или иные входные
воздействия:
Property OnMyEvent: tMyEvent read FMyEvent write FMyEvent;
Здесь OnMyEvent - поле объекта, содержащее адрес некоторого
метода. Присвоить такому свойству значение - значит указать программе адрес
метода, который будет вызываться в момент наступления события. Такие методы
называются обработчиками событий.
Внутри библиотеки времени выполнения Delphi вызовы обработчиков
событий находятся внутри методов, обрабатывающих сообщения Windows. Выполнив
необходимые действия, этот метод проверяет, известен ли адрес обработчика, и,
если это так, вызывает его.
События имеют разное количество и тип параметров, в зависимости
от происхождения и назначения. Общим для всех события является параметр
Sender - он указывает на объект, вызвавший событие. Самое простое событие -
tNotifyEvent - не имеет других параметров.
tNotifyEvent = procedure(Sender:tObject) of object;
Тип метода, предназначенный для извещения о нажатии клавиши,
предусматривает передачу в процедуру кода этой клавиши, о передвижении мыши -
ее текущих координат и т.д.
Имя события в Delphi начинается с префикса On: OnClick,
OnCreate, OnMouseDown и т.д. Имена методов - обработчиков событий состоят из
имени объекта, генерирующего событие, и корня имени события: OkButtonClick.
Дважды щелкнув мышью в инспекторе объектов на странице Events в поле напротив
любого события, вы получите скелетный код (заготовку) этого события.
Поскольку события - это свойства объекта, их значения можно
менять в любой момент во время выполнения программы. Эта возможность
называется делегированием. Можно в любой момент взять способы реакции на
события у одного объекта и присвоить (делегировать) их другому.
Принцип делегирования позволяет избежать трудоемкого процесса
порождения новых классов для каждого специфического случая, заменяя нго
простой подстановкой процедур.
Один и тот же обработчик может обрабатывать события от разных
объектов. Типичный пример - в современных программах одно и то же действие
можно вызвать несколькими способами - через главное меню, контекстное меню и
т. д. Этого легко добиться, назначив один и тот же обработчик всем
необходимым объектам в инспекторе объектов. азумеется, события должны быть
одного и того же типа.
Более сложный случай - когда внутри обработчика необходимо
определить, какой именно объект вызвал событие. Если объекты, которые
потенциально могут это сделать, имеют разный тип, то именно их тип можно
применить в качестве критерия:
If Sender is tMenuItem then ...
Если же все объекты, разделяющее между собой один обработчик
события, относятся к одному классу, можно использовать свойство tObject.Tag.
Ниже приведены названия некоторых, наиболее употребительных
событий Delphi:
Событие - Тип обработчика - Когда возникает
OnClick - TNotifyEvent - При нажатии левой клавиши мыши
OnActivate - TnotifyEvent - После передачи объекту фокуса
OnCreate - TnotifyEvent - После создание объекта
OnDestroy - TnotifyEvent - Перед уничтожением объекта
OnDeactivate - TnotifyEvent - Перед уходом фокуса из объекта
OnKeyPress - TkeyPressEvent - При нажатии клавиши
OnMouseDown - TmouseEvent - При нажатии клавиши мыши
OnMouseMove - TmouseMoveEvent - При движении мыши над объектом
OnMouseUp - TmouseEvent - При отпускании клавиши мыши
а также их типы:
Тип
Описание
TnotifyEvent
type TnotifyEvent = procedure (Sender: TObject) of object;
TkeyPressEvent
type TkeyPressEvent = procedure (Sender: TObject; var Key: Char) of object;
TmouseEvent
TmouseEvent = procedure (Sender: TObject; Button: TmouseButton; Shift:
TShiftState; X, Y: Integer) of object;
TmouseMoveEvent
TmouseMoveEvent = procedure(Sender: TObject; Shift: TShiftState; X, Y:
Integer) of object;
Стандартные события описаны в модуле Classes.
Динамические массивы
В Delphi существуют динамические массивы, то есть массивы, длину
которых можно изменять во время выполнения программы. аньше проблема динамических
массивов стояла довольно остро. Стандартных средств для работы с ними не
было, и программистам приходилось вручную создавать подобные структуры (как
правило, на основе динамических переменных). Теперь компилятор Delphi сам
выполняет всю “грязную” работу. Параллельно с динамическими, в Delphi
сохранились и обычные, статические массивы.
Длина динамических массивов определяется уже во время выполнения
программы. Описав переменную как
var A1:array of real,
мы будем иметь указатель, соответствующий массиву вещественных
чисел. Память под него выделяется процедурой SetLength:
procedure SetLength(var A; NewLength: Integer);
Здесь A - указатель на динамический массив, NewLength - длина
массива.
В динамических массивах нумерация индексов начинается только с
нуля.
То обстоятельство, что динамические массивы фактически являются
указателями, приводит к определенным нюансам в их использовании. Первый из
них касается присвоения массивов. Запись
Var A1,A2:array[0..4] of real;
A1:=A2;
В случае со статическими массивами, означает, что всем элементам
массива A1 присваиваются значения соответствующих элементов массива A2, но
при этом, переменные A1и A2 - разные переменные, хранящиеся в разных областях
памяти.
В случае с динамическими массивами присвоение их друг другу - не
более чем присвоение указателей. ассмотрим пример:
Var A1,A2:array of real;
. . .
SetLength(A1,5);
A1[0]:=0.0;
A2:=A1;
A2[0]:=2.0;
Значение A2[0] также изменится и станет равно 2. Это произошло
потому, что после присвоения указатели ссылаются на один и тот же адрес в
памяти и изменения в одном массиве приведут к синхронным изменениям в другом.
Для того, чтобы действительно скопировать элементы в A2,
необходимо использовать функцию Copy:
function Copy(A; Index, Count: Integer): string;
Здесь А - указатель на копируемый массив,
Index - индекс первого копируемого элемента,
Count - число копируемых элементов.
A2:=Copy(A1,0,Length(A1));
Освободить выделенную под массив память можно при помощи
процедуры finalize:
procedure Finalize( var A [; Count: Integer] );
здесь V - указатель на динамический массив, Count - количество
массивов (в случае, когда несколько динамически создаваемых структур
размещаются подряд в одном непрерывном блоке памяти, их можно освободить
одним вызовом Finalize); или просто присвоив указателю на массив значение
nil. В этом случае память не будет потеряна (как могло бы показаться на
первый взгляд), а будет вызвана соответствующая процедура, корректно
освобождающая память. Причем, память освободиться только тогда, когда на нее
не уже не ссылается ни одна динамически созданная структура.
Значения граничных индексов (как статических так и динамических)
можно получить при помощи функций Low(A) - нижний граничный индекс и Hight(A)
- верхний граничный индекс. Число элементов массива - функцией Length:
function Length(A): Integer;
Дальнейшим развитием идеи динамических массивов являются
многомерные динамические массивы.
Var AA: array of arrray of real;
SetLength(AA,10,5);
AA[9,4]:=1;
AA[9][4]:=1;
Можно пойти еще дальше, и создать динамические массивы с
переменной длиной по разным индексам. Для создания такого массива сначала
нужно задать его размерность по первому индексу:
SetLength(AA,10);
Это означает, что массив будет состоять из десяти строк. Теперь
длина каждой строки задается отдельно:
SetLength(AA[0],5);
SetLength(AA[1],6);
SetLength(AA[2],3);
SetLength(AA[3],8);
. . .
В использовании многомерных динамических массивов есть одно
ограничение: передавать их в процедуры как параметры нельзя.
Перегружаемые функции
Перегрузка функций ничем не отличается от перегрузки методов.
Основная идея перегрузки функций и методов заключается в том, что в программе
можно иметь две или болеефункции с одинаковыми именами и разными наборами
параметров. Перегружаемые функции, также как и методы описываются с ключевым
словом overload. Например:
function OverloadDemo(val : Byte) : string; overload;
function OverloadDemo(val : Char) : string; overload;
Такой подход делает ваш код более простым, его легче
сопровождать и расширять. Перегрузка нужна для того, чтобы произвести
одинаковые или похожие действия над разнотипными данными.
При вызове перегруженной функции компилятор определяет тип
фактически передаваемого параметра (в приведенном выше примере - Byte или
Char) и в зависимости от этого подставит вызов соответствующего варианта
функции.
Параметры функций по умолчанию
Параметры функций по умолчанию пришли в Delphi из С++. Часто при
вызове процедур и функций в них надо передать параметры, обычно имеющие одно
и то же значение. Это имеет место в частности в научных расчетах. Теперь эта
проблема упрощается путем введения параметров по умолчанию. Это означает, что
прямо в объявлении функции можно указать, что те или иные параметры должны
принимать определенные значения, если они не заданы явно. Нарпример, можно
описать функцию
function Test(x:extended; Rz:extended = 6371):extended;
При ее вызове второй параметр можно указать явно:
Test(100,6372);
А можно и не указывать:
Test(100);
В первом случае параметр Rz будет иметь значение 6372, а во
втором - по умолчанию - 6371.
Параметры по умолчанию должны быть сосредоточены в конце списка
параметров. Допускается умалчивать последний, последний и предпоследний, и
т.д. параметры В этом состоит отличие от С++ - там можно опускать любые из
параметров по умолчанию.
При применении параметров по умолчанию следует соблюдать
осторожность. ассмотрим следующий пример:
Procedure Confused(I:integer); overload;
Procedure Confused(I:integer; J:integer = 0); overload;
. . .
Confused(x);
Компилятор не сможет определить, какую именно реализацию
Confused следует вызвать, и выдаст сообщение об ошибке.
2. Приемы работы в среде разработки
Репозиторий
Репозиторий (Repository) представляет собой хранилище
разнородных объектов, объединенных общим предназначением. Основная цель
создания репозитория - предоставление разработчику возможности повторного
использования уже готовых и отлаженных частей программы. Здесь можно найти
готовое стандартное приложение, динамическую библиотеку, форму, модуль,
мастера диалогов и приложений, а также различные вспомогательные программы.
Репозиторий в Delphi представляет собой текстовый файл, в
котором данны обо всех объектах храняться в специальном формате.
Открыть репозиторий можно при помощи команды New меню File
главного окна Delphi.
Рассмотрим некоторые объекты, входящие в репозиторий. Основу
любой разработки в Delphi составляет проект, объединяющий самые разные
структурные элементы в единое приложение. В него чаще всего входят модули и
формы различных видов. Типы приложений представлены исполняемыми программами,
динамическими библиотеками и приложениями для запуска служб. Особое место
занимают приложения для работы с базами данных (БД), поэтому значительная
часть объектов репозитория предназначена для конструирования таких программ.
Для обеспечения удаленного доступа к данным предназначены стандартные
заготовки для приложений, использующие различные механизмы взаимодействия, в
том числе технологии CORBA и Microsoft Transaction Server (MTS).
Состав объектов репозитория можно обновлять, также можно
создавать, переименовывать страницы и изменять состав объектов для них.
едактирование репозитория осуществляется в диалоговом окне Object Repository
которое появляется при выборе команды Repository меню Tools главного окна
Delphi, или при выборе команды Properties всплывающего меню репозитория.
Добавить новую форму в репозиторий можно командой Add to repository
всплывающего меню формы или перетаскиванием формы в панель репозитория.
Проект и группа проектов
Любая работа в Delphi начинается с создания нового проекта. Как
правило, из одного проекта получается одно приложение. Таким образом, проект
представляет собой фундамент или каркас, на который “крепятся” все нужные
детали. Самый маленький проект представляет собой только один файл с
расширением DPR, он называется главным файлом проекта. Серьезный большой
проект содержит десятки разнообразный форм, файлов и модулей.
В главном файле содержится описание всех модулей и форм,
входящих в проект. Кроме того, в секции begin … end может располагаться любой
код, который должен выполняться перед началом работы приложения или в
процессе его закрытия.
Модулем называется текстовый файл с расширением PAS, в котором
содержится некоторый исходный код на языке Object Pascal. С модулем может
быть связана форма, представляющая собой отдельно взятое окно программы с
расположенными на нем интерфейсными элементами. Служебная информация о формах
хранится в файлах с расширением DFM. В общем случае, в состав проекта могут
входить модули, не имеющие связанных с ними форм.
В репозитории содержится несколько заготовок для форм (страница
Forms), а также некоторые стандартные диалоги, которые с точки зрения
пользовательского интерфейса являются модальными формами (страница Dialogs).
В проекте Delphi помимо модулей и форм могут присутствовать различные
служебные файлы. Среди них важное место занимают ресурсы (расширение RES),
которые хранят значки, курсоры, строки сообщений информацию о версии
программы и т.д.
Репозиторий предлагает на выбор несколько типов проектов:
например проекты с многодокументным (MDI) и однодокументным (SDI)
интерфейсом. При необходимости создания динамической библиотеки также можно
воспользоваться соответствующей заготовкой из репозитория.
Помимо проекта в Delphi пристствует и более важная структурная
единица - группа проектов, которая , как видно из названия, объединяет для
совместной разработки несколько проектов. Файл группы проектов имет
расширение BPG и представляет собой текстовый файл с системной информацией и
описанием проектов, входящих в группу.
Работа с группой проектов практически ничем не отличается от
работы с отдельным проектом, необходимо только следить за принадлежностью
отдельных форм и модулей и овремя переключаться на нужный проект в Диспетчере
проектов. При компиляции в группе также необходимо выбрать нужный проект из
списка на панели инструментов Delphi или Диспетчера проектов.
Диспетчер проекта предназначен для управления составными частями
проекта или группы проектов. В группе можно добавить или удалить отдельный
проект, в проекте - добавить или удалить отдельный модуль.
Управление проектом осуществляется кнопками на панели
инструментов диспетчера проекта или командами всплывающего меню.
Вызывается диспетчер командой Project Manager меню View главного
окна Delphi. Создание новой группы проектов осуществляется также при помощи
диспетчера проекта. Возможность объединения групп или включения групп в
другие группы отсутствует.
В целом, параметры проекта доступны в диалоговом окне, которое
открывается при выборе команды Project Options меню Project главного окна
Delphi.
Палитра компонентов
Основная палитра компонентов Delphi имеет двенадцать страниц.
Standard. Большинство компонентов на этой странице являются
аналогами экранных элементов самой Windows. Меню, кнопки, полосы прокрутки —
здесь есть все. Но компоненты Delphi обладают также некоторыми удобными
дополнительными встроенными возможностям.
Additional. Эта страница содержит более развитые компоненты.
Например, компонент Outline удобен для отображения информации с иерархической
структуройДанная страница также содержит компоненты, главное назначение
которых — отображение графической информации. Компонент Image загружает и
отображает растровые изображения, а компонент Shape позволяет рисовать
графические примитивы - окружности, квадраты и т.д.
System. Страница System содержит неотображаемые системные
компоненты: Timer, MediaPlayer, компоненты, реализующие механизм DDE.
Страница System также содержит компоненты, обрабатывающие обмен высокого
уровня между программами посредством OLE (Object Linking and Embedding).
Win32. Эта страница содержит компоненты, позволяющие созданным с
помощью Delphi программам использовать такие нововведения в пользовательском
интерфейсе 32-разрядной Windows, как просмотр древовидных структур, просмотр
списков, панель состояния, расширенный текстовый редактор и др.
Dialogs. Windows 3.1 ввела в употребление стандартные диалоговые
окна для операций над файлами, выбора шрифтов, цветов и т.д. Однако для
использования их в обычной программе Windows может потребоваться написать
немало вспомогательного кода. Страница Dialogs предоставляет программам
Delphi простой доступ к этим стандартным диалоговым окнам.
Data Access и Data Controls. Delphi использует механизм баз
данных компании Borland (Borland Database Engine, BDE) для организации
доступа к файлам баз данных различных форматов. Компоненты этих двух страниц
облегчают программам Delphi использование сервиса баз данных,
предоставляемого BDE, например многопользовательского считывания, записи,
индексации и выдачи запросов для таблиц dBASE и Paradox.
С использованием этих компонентов создание программы просмотра и
редактирования базы данных почти не требует программирования.
Win 3.1. На этой странице находятся компоненты Delphi 1.0,
возможности которых перекрываются аналогичными компонентами Windows 95.
Internet. Эта страница предоставляет компоненты для разработки
приложений, позволяющих создавать HTML-файлы непосредственно из файлов баз
данных и других типов, взаимодействующих с другими приложениями для Internet.
Delphi 4 дает вам возможность создавать приложения для Web-сервера в виде
DLL-файлов : (Dynamic Link Library — Динамически компонуемая библиотека),
способных содержать невизуальные компоненты. С помощью компонентов страницы
Internet довольно просто создавать обработчики событий для обращения к
определенному URL (Uniform Resource Locator — Унифицированный локатор
ресурса), представлению документов в HTML-формате и пересылки их
клиент-программе.
Samples. Эта страница содержит компоненты, которые не встроены в
Delphi, но демонстрируют мощь системы компонентов. Для этих компонентов нет
встроенной интерактивной справки. Все же они не менее полезны, чем компоненты
с других страниц.
ActiveX. Эта страница содержит компоненты ActiveX, разработанные
независимыми производителями программного обеспечения: сетка, диаграмма,
средство проверки правописания.
QReport. Эта страница предоставляет компоненты баз данных. Здесь
содержатся особые версии надписей, полей, примечаний и других элементов
управления.
Midas и Decision Cube. Здесь собраны компоненты для доступа к
удаленным серверам и осуществления SQL – запросов.
Размещение компонентов
Размещать компоненты на форме очень просто. Требуется только
щелкнуть на нужной вкладке палитры компонентов, затем на кнопке с
пиктограммой соответствующего компонента и после этого щелкнуть в окне формы.
Если щелкнуть на компоненте, а затем нарисовать прямоугольник с помощью мыши
на форме — компонент появится внутри этого прямоугольника. Если размеры
компонента поддаются изменению, при появлении на форме он заполнит собой
прямоугольник.
Если вы забыли, на какой странице расположен конкретный
компонент, выберите пункт Component List из меню View , и на экране появится
список компонентов в алфавитном порядке. Если щелкнуть на компоненте в
палитре компонентов, его кнопка окажется нажатой. Если щелкнуть на другом
компоненте, первая кнопка вернется в исходное состояние: только один
компонент может быть выбран в каждый момент времени. Для того чтобы все
кнопки оказались в исходном состоянии, и было восстановлено нормальное
использование мыши, следует щелкнуть на кнопке со стрелкой выбора, которая
появляется с левой стороны каждой страницы палитры. Для размещения нескольких
копий компонента при выборе компонента необходимо нажать и удерживать клавишу
Shift. Если дважды щелкнуть на пиктограмме компонента, то компонент
автоматически появится в центре активной формы проекта.
При перемещении и изменении размера компоненты выравниваются по
точкам координатной сетки формы. Как правило, это хорошо — такая возможность
помогает поддерживать порядок в формах. Для отмены этой возможности или
изменения плотности точек координатной сетки необходимо выберать пункт
Options меню Tools. Первая страница параметров предназначена для настройки
пользователем параметров среды. На этой странице имеется группа Form
designer, флажки опций Display grid и Snap to grid которой определяют, видна
ли координатная сетка и активна ли она. Можно также изменить значения
параметров Grid Size X (Шаг по оси X) и Grid Size Y (Шаг по оси Y), что
приведет к изменению шага координатной сетки по горизонтали и вертикали,
соответственно.
Не каждый компонент виден на форме во время запуска программы.
Например, размещение на форме компонента MainMenu приводит к появлению в
разрабатываемом приложении меню, но соответствующая пиктограмма во время
запуска программы не отображается. Компоненты, представляющие диалоговые окна
общего назначения, вообще никак не визуализируются во время работы программы.
азмеры невидимого компонента в процессе разработки не изменяются: он всегда
отображается в виде пиктограммы.
Инспектор объектов
Инспектор объектов предназначен для установки доступных на этапе
проектирования свойств компонентов (то есть свойств, объявленных как
published). Окно инспектора состоит из трех основных частей:
- Селектора
компонентов
- Страницы
свойств (Properties)
- Страницы
событий (Events)
Селектор компонентов представляет собой выпадающий
комбинированный список и предназначен для выбора компонент для просмотра и
редактирования. Обычно, в использовании селектора нет необходимости, так как
выделенный компонент появляется в инспекторе объектов автоматически. Селектор
компонентов отображает имя компонента и класс, от которого данный компонент
происходит. Например, кнопка с именем OkButton будет представлена как
«OkButton: TButton».
Страница Properties инспектора объектов отображает все свойства
выбранного компонента, доступные на стадии проектирования. Страница разделена
на два столбца. Столбец Property слева показывает имя свойства, столбец Value
справа - значение этого свойства. Если свойство имеет тип класса, записи, или
множества, то слева от его имени отображается знак +. Дважды щелкнув на имени
такого свойства, можно получить доступ к отдельным его полям. Того же эффекта
можно достичь, выбрав пункт Expand в контекстном меню инспектора объектов.
Для закрытия развернувшегося списка полей необходимо повторно дважды щелкнуть
на имени свойства, либо использовать пункт Collapse в контекстном меню инспектора
объектов. Во многих случаях редактор отображает список возможных значений
свойства. Для свойств, представляющих собой объекты (экземпляры классов VCL)
существуют две возможности для редактирования. Во-первых, можно, щелкнув на
кнопке с многоточием (…) в столбце Value свойства вызвать редактор
соответствующего свойства; либо, щелкнув дважды на имени свойства, можно
развернуть список его полей, как описано выше. Если на форме выделено
несколько компонентов, инспектор объектов покажет их общие свойства. Это дает
возможности менять свойства нескольких компонентов одновременно.
Страница Events редактора объектов содержит список событий,
которые может обрабатывать данный компонент. Для создания обработчика события
необходимо дважды щелкнуть на столбце Value рядом с именем этого события. При
этом Delphi создаст метод, со всеми параметрами, необходимыми для обработки
события. Активизируется окно редактора кода с курсором, расположенным внутри
обработчика события. Остается только ввести код обработчика! Обычно имя
обработчика события генерируется автоматически, однако, можно вручную задать
требуемое имя в столбце Value напротив имени события. После нажатия клавиши
Enter произойдет генерация скелетного кода обработчика события с заданным
именем. Можно связать событие с уже имеющимся обработчиком. Для этого
используется выпадающий список, появляющийся при нажатии соответствующей
кнопки в столбце Value напротив имени события. В список включаются только
обработчики событий, совпадающие по типу с данным событием.
Редактор кода
Основные средства упрощения процесса создания исходного кода
приложе- ний собраны в едакторе кода (Code Editor), который из окна для
отображе- ния и редактирования кода модулей приложения превратился в удобное
сред- ство с многочисленными справочными и вспомогательными возможностями.
Шаблоны компонентов
Шаблоны компонентов предоставляют удобный способ использования
наи- более часто применяемых групп компонентов (например, часто требуется
объединить пояснительную надпись, поле редактирования и кнопку). При этом
сделанные при создании шаблона настройки свойств составляющих его компонентов
сохраняются и переносятся на новую форму вместе с этими компонентами.
Создание шаблона включает следующие шаги:
1. На форме необходимо разместить требуемые компоненты,
выровнять их и настроить нужные свойства.
2. Выделить на форме компоненты, которые предполагается включить
в шаблон, в группу.
3. Выбрать в меню Components Главного окна среды разработки
команду Create Component Template.
4. В появившемся диалоге требуется задать имя нового шаблона и
указать страницу Палитры компонентов, на которой шаблон будет доступен.
Нажать кнопку ОК.
Теперь шаблон можно использовать обычным способом: после выбора
его на странице Палитры компонентов и переноса, на форме появляется заданный
набор компонентов с определенными ранее свойствами.
Шаблоны кода
Шаблоны кода предназначены для быстрой вставки в исходные код
моду-. лей проекта стандартных (или часто используемых) комбинаций операторов
языка Object Pascal. Для вызова шаблона кода используется комбинация клавиш
иш
+, после нажатия которой появляется список доступных в настоящее
время шаблонов.
Выбор наименования шаблона из раскрывшегося списка приводит к
появ- лению на указанном мышью месте набора операторов, определяемых шаб-
лоном. Стандартная поставка Delphi включает несколько типовых шаблонов с
наиболее распространенными наборами операторов.
Опция завершения кода
Опция завершения кода применяется при необходимости использовать
в исходном коде проекта свойства и методы стандартных компонентов Delphi.
Включение механизма завершения кода происходит после ввода символа точки за
названием класса или объекта. едактор кода автоматически разворачивает список
всех свойств и методов компонента. После выбора нужного элемента списка и
нажатия клавиши свойство или метод дописывается в имени класса или объекта.
Так как многие классы обладают. внуши- тельным списком свойств, доступных
программисту, то при выборе нужного элемента из списка можно воспользоваться
наращиваемым поиском — при вводе после точки первых букв свойства или метода
осуществляется автоматическое позиционирование списка на первый похожий
элемент.
Опция включается на странице Code Insight в диалоговом окне
команды Environment Options меню Tools Главного меню среды разработки при
помощи флажка Сне Completion (рис. 2.1).
Опция параметров
Опция параметров осуществляет вывод справочной информации о
количестве и типах параметров, входящих в состав любой процедуры или функции,
название которой есть в исходном коде проекта в окне едактора кода.
Справочные данные появляются при позиционировании курсора на названии
процедуры или функции.
Опция включается на странице Code Insight в диалоговом окне
команды Environment Options меню Tools Главного меню среды разработки при
помощи флажка Соде Parameters (рис. 2.1).
Опция заполнения класса
Эта функция автоматически генерирует исходный код для любых
составных частей нового класса (свойств и методов), которые уже объявлены в
секции interface, а также создает объявление для тех элементов, которые уже
описаны в секции implementation.
Чтобы создать описание объявленных свойств и методов, необходимо
установить курсор в любое место объявления класса в секции interface и нажать
комбинацию клавиш ++.
После этого Delphi, во-первых, создаст для объявленных свойств
класса не- обходимые свойства и методы для чтения и записи в секции private.
Во- вторых, в секции implementation будет сгенерировано описание необходимых
методов.
Например, имеется простейший класс:
type
TSimpleObject = class (TObject)
property Empty: Integer;
function ClearEmpty: Boolean;
end;
Тогда, после нажатия комбинации клавиш ++, исходный код в секции
interface будет приведен к следующему виду:
type
TSimpleObject = class (TObject)
property Empty: Integer read FEmpty write SetEmpty;
function ClearEmpty: Boolean;
private
FEmpty: Integer;
procedure SetEmpty(const Value: Integer);
end;
А в секции implementation появятся следующие строки:
function TSimpleObject.ClearEmpty: Boolean;
begin
end;
procedure TSimpleObject.SetEmpty(const Value: Integer);
begin
FEmpty := Value;
end;
3. Межпрограммное взаимодействие
Часто возникает необходимость организовать взаимодействие
программ, написанных разными людьми или даже разными фирмами. Задача решается
в рамках идеологии «клиент-сервер», когда одно приложение должно реагировать
на запросы других приложений. Наконец, с развитием сетей совершенно обычной
стала задача взаимодействия приложения на разных машинах.
Для организации взаимодействия между задачами существует очень
много инструментов. Некоторые из них позволяют работать в рамках только одной
машины, некоторые - в рамках локальной или глобальной сети. Связь между двумя
программами можно установить таким образом, что изменения в одном приложении
будут отражаться во втором. Например, если Вы меняете число в электронной
таблице, то во втором приложении данные обновятся автоматически и отобразят
изменения. Кроме того, можно из своего приложения управлять другими
приложениями такими, как Word for Windows, Excel и др.
Dynamic Data Exchange (DDE)
DDE дает возможность перейти через рамки приложения и
взаимодействовать с другими приложениями и системами Windows. Dynamic Data
Exchange получило свое имя потому, что позволяет двум приложениям
обмениваться данными (текстовыми, через глобальную память) динамически во
время выполнения. DDE - давний и прижившийся протокол обмена данными,
появившийся еще на заре эры Windows. Ниша, занимаемая DDE - это оперативная
передача и синхронизация данных в приложениях.
Приложения, использующие DDE, подразделяются на две категории:
клиенты и серверы. Оба участника процесса совершают контакты (conversations)
по определенным темам (topics), при этом в рамках темы производится обмен
элементами данных (items). Устанавливает контакт DDE-клиент, который посылает
запрос, содержащий имена контакта и темы. После установления контакта всякое
изменение элемента данных на DDE-сервере передается элементу данных клиента.
Одно и то-же приложение может быть одновременно и сервером, и клиентом (например,
MicroSoft Word).
DDE-серверы
Для построения DDE-сервера в Delphi имеются два объекта,
расположенные на странице System Палитры Компонент - TDdeServerConv и
TDdeServerItem. Обычно в проекте используется один объект TDdeServerConv и
один или более TDdeServerItem. Для получения доступа к сервису DDE-сервера,
клиенту потребуется знать несколько параметров: имя сервиса (Service Name) -
это имя приложения (обычно - имя выполняемого файла без расширения EXE,
возможно с полным путем); Topic Name - в название темы; Item Name – название
элемента данных.
Назначение объекта TDdeServerConv - общее управление DDE и
обработка запросов от клиентов на выполнение макроса. Последнее выполняется в
обработчике события OnExecuteMacro, например, как это сделано ниже:
procedure TDdeSrvrForm.DoMacro(Sender: TObject;Msg: TStrings);
var
Text: string;
Begin
Text := '';
if Msg.Count > 0 then Text := Msg.Strings[0];
MessageDlg ('Executing Macro - ' + Text, mtInformation, [mbOK], 0);
End;
Важнейшим свойством компонента TDdeServerConv является название
темы, содержащееся в свойстве Name. Клиент должен знать это имя при
установлении контакта, за исключением случая, когда он подключается к данным
контакта, скопированным в буфер обмена.
В моменты открытия и закрытия контакта возникают события:
Property OnOpen: tNotifyEvent;
Property OnClose: tNotifyEvent;
Объект TDdeServerItem связывается с TDdeServerConv посредством
свойства
Property ServerConv:tDDEServerConv,
и определяет, что, собственно, будет пересылаться по DDE. В
принципе, потокол DDE подразумевает обмен любыми данными, формат которых
зарегистрирован в системе для передачи через буфер обмена. Однако для
рассматриваемых компонентов эти возможности ограничиваются только текстовыми данными.
Для этого у компонента TDdeServerItem есть свойства Text и Lines (Text имеет
то же значение, что и Lines[0]).
Property Text:string;
Property Lines:tStrings;
При изменении значения этих свойств автоматически происходит
пересылка обновленных данных во все приложения-клиенты, установившие связь с
сервером.
Данный компонент позволяет также осуществлять установление связи
через буфер обмена (Clipboard). Для этого служит метод CopyToClipboard.
Необходимая информации помещается в через буфер обмена и может быть вызвана
из приложения-клиента при установлении связи. Обычно, в DDE-серверах для
этого есть специальный пункт меню Paste Special или Paste Link.
DDE-клиенты
Для построения DDE-клиента в Delphi используются два компонента
TDDEClientConv и TDDEClientItem. Аналогично серверу, в программе обычно
используется один объект TDDEClientConv и один и более связанных с ним
TDDEClientItem.
TDDEClientConv служит для установления связи с сервером и общим
управлением DDE-связью. Установить связь с DDE-сервером можно как во время
разработки, так и во время выполнения программы, причем двумя способами.
Первый способ - заполнить вручную необходимые свойства компонента. Это
DdeService, DdeTopic и ServiceApplication. Если во время разработки дважды
щелкнуть на одном из первых двух свойств в Инспекторе Объектов - появится
диалог для определения DDE-связи. Для установления связи через Clipboard в
диалоге есть специальная кнопка Past Link. Ей можно воспользоваться, если Вы
запустили DDE-сервер, сохранили каким-то образом информацию о связи и вошли в
этот диалог. Например, если DDE-сервером является DataBase Desktop, то нужно
загрузить в него какую-нибудь таблицу Paradox, выбрать любое поле и выбрать
пункт меню Edit|Copy. Свойство ServiceApplication заполняется в том случае,
если в поле DDEService содержится имя, отличное от имени программы, либо если
эта программа не находится в текущей директории. В этом поле указывается
полный путь и имя программы без расширения (.EXE). Данная информация нужна
для автоматического запуска сервера при установлении связи по DDE, если тот
еще не был запущен.
Имена сервера DDE и темы содержатся в свойствах:
Property DDEService:string;
Property DDETopic:string;
Способ вхождения в контакт определяется свойством
Property ConnectMode:tDataMode;
Type tDataMode = (DDEAutomatic,DDEManual);
Метод
Function SetLinc(const Service: string; const Topic: string):Boolean;
Присваивает свойствам DDEService и DDETopic соответствующие значения, а в
случае, если задан режим DDEAutomatic, и устанавливает контакт. В режиме
DDEManual для установления контакта необходимо дополнительно вызвать метод
Function OpenLink: Boolean;
Этот метод сначала закрывает предыдущий контакт, затем пытается связаться с
сервером DDEService на тему DDETopic. Если это не удается сразу,
предпринимается попытка загрузить приложение с именем, определенным в
свойстве:
Property ServiceApplication: string;
Можно связаться с сервером, поместившим данные в буфер обмена с помощью
метода
Function PasteLink: boolean;
Ниже приведен пример процедуры, осуществляющей связь с сервером.
procedure TMainForm.doNewLink(Sender: TObject);
begin
DdeClient.SetLink(AppName.Text, TopicNameEdit.Text);
DdeClientItem.DdeConv := DdeClient;
DdeClientItem.DdeItem := ItemName.Text;
end;
procedure TMainForm.doPasteLink(Sender: TObject);
var
Service, Topic, Item : String;
begin
if GetPasteLinkInfo (Service, Topic, Item) then begin
AppName.Text := Service;
TopicName.Text := Topic;
ItemName.Text := Item;
DdeClient.SetLink (Service, Topic);
DdeClientItem.DdeConv := DdeClient;
DdeClientItem.DdeItem := ItemName.Text;
end;
end;
После того, как установлена связь, нужно позаботиться о
поступающих по DDE данных, это делается в обработчике события OnChange
объекта TDdeClietItem:
procedure TFormD.DdeClientItemChange(Sender: TObject);
begin
DdeDat.Lines := DdeClientItem.Lines;
end;
Это единственная задача объекта TDdeClientItem.
Свойство
Property DDEConv: TddeClientConv
Этого компонента предназначено для связи с соответствующим
объектом DdeClientConv. А свойство
Property DDEItem:string;
Должно содержать имя элемента данных.
Свойства
Property Text: string;
Property Lines: tStrings;
Аналогичны соответствующим свойствам tDDEServerItem и содержат
данные.
На объект TDdeClientConv возлагаются еще две задачи: пересылка
данных на сервер и выполнение макросов. Для этого у данного объекта есть
соответствующие методы.
Function ExecuteMacroLines(Cmd:tStrings, WaitFlg:Boolean):Boolean;
Function PokeDataLines(const Item:string,Data:tStrings):Boolean;
Обмен сообщениями
Как уже упоминалось ранее, операционная система Windows®
основана на сообщениях, возникающих в результате действий пользователя,
аппаратуры компьютера или других программ. Поведение каждого окна полностью
определяется тем, какие оно принимает сообщения и как их обрабатывает. В
большинстве случаев, обработка сообщений в Delphi выполняется через события.
Однако, бывают ситуации, когда может потребоваться послать и/или обработать
сообщение самостоятельно. Существуют два типа сообщений, которые могут
потребовать обработки в обход обычной системы сообщений Delphi:
Сообщения Windows®, не обрабатываемые VCL
Сообщения, определяемые пользователем
В принципе, сообщения делятся на две категории:
Командные сообщения
Уведомляющие сообщения
Командные сообщения используются как программистами, тек и
Windows®. Они управляют элементам операционной системы и прикладным
программам. Уведомляющие сообщения содержат информацию об изменении состояния
окон Windows®, их отдельных элементов и устройств системы. Они посылаются
только самой средой окон Windows®.
Каждое сообщение имеет два параметра: WPARAM и LPARAM. В 32-х
битной среде оба эти параметра имеют размер 32 бита (longword). В 16-битной
Windows WPARAM - это 16 битное число (word), а LPARAM - 32-битное (longint).
Для отправки сообщений API Windows® содержит две функции:
function PostMessage( HWND hWnd, // handle of destination window
UINT Msg, // message to post
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
):BOOLEAN;
function SendMessage( HWND hWnd, // handle of destination window
UINT Msg, // message to send
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
): LRESULT;
Первая из них отправляет сообщение в очередь сообщений Windows®
и немедленно возвращает управление. PostMessage возвращает TRUE, если вызов
функции прошел успешно и FALSE в противном случае.
Функция SendMessage отличается от PostMessage тем, что, послав
сообщение, она не возвратит управление до тех пор, пока сообщение не будет
доведено до получателя.
Обе функции имеют одинаковые параметры: HWND - дескриптор окна,
которому предназначается сообщение, UINT - сообщение, которое должно быть
послано, WPARAM и LPARAM - параметры сообщения.
В следующем примере главной форме проекта посылается сообщение о
закрытии приложения:
PostMessage(Handle,WM_QUIT,0,0);
В дополнение к функциям API Windows® VCL содержит метод Perform,
который можно использовать для посылки сообщений любому окну VCL.
function Perform(Msg: Cardinal; WParam, LParam: Longint): Longint;
Perform обходит систему передачи сообщений Windows® и направляет
сообщение непосредственно механизму обработки данного окна. С использованием
предыдущий пример будет выглядеть следующим образом:
Perform(WM_QUIT,0,0);
VCL имеет события примерно лишь для 20% наиболее часто
используемых сообщений Windows®. У программиста может возникнуть
необходимость обработать сообщения, для которых в VCL события не
предусмотрены. Для обработки сообщений ОС, не имеющих соответствующих событий
в Delphi есть ключевое слово message. С его помощью конкретный метод в коде
программы может быть связан с любым сообщением Windows®. Когда окно
приложения получает это сообщение вызывается соответствующий метод. Для
реализации обработки сообщения на этом уровне необходимо:
1. Включить объявление метода для обработки сообщения в
объявление класса, указав ключевое слово message и название сообщения, для
обработки которого данный метод редназначен.
2. Включить определение метода в раздел implementation.
Приведем пример определения метода, обрабатывающего сообщение
WM_ERASEBKGND:
Procedure WmEraseBkgnd(var Msg:tWMEraseBkgnd); message WM_ERASEBKGND;
Ключевое слово message указывает на то, что данный метод
используется для обработки сообщения ОС, имя которого указано после этого
ключевого слова: WM_ERASEBKGND. Следует также отметить, что параметр метода
является записью типа tWMEraseBkgnd.
type TWMEraseBkgnd=record
Msg:Cardinal;
DC:HDC;
Unused:Longint;
Result:Longint;
end;
В VCL имеются записи для большинства сообщений Windows® (они
определены в модуле Messages.pas). Именем записи является имя сообщения с
префиксом t и без подчеркивания. Сам метод можно назвать как угодно, но
приведенная выше форма является общепринятой. В этом методе может возникнуть
необходимость вызвать обработчик сообщения, установленный по умолчанию. Для
этого необходимо вызвать виртуальный метод класса tObject DefaultHandler:
procedure tObject.DefaultHandler(var Message); virtual;
Помимо нормальных сообщений Windows®, можно создать свое
собственное сообщение. еализация и перехват определяемого пользователем
сообщения идентичны обработке сообщений Windows®. Единственное отличие
состоит в том, что это сообщение необходимо сначала определить:
Const My_Message = WM_USER + 1;
Эта строка объявляет пользовательское сообщение с именем
My_Message.
Для обработки сообщений, определенных пользователем в качестве
типа параметра обработчика сообщения следует использовать запись общего вида
tMessage:
type tMessage = record
Msg:Cardinal;
WParam:Longint;
LParam:Longint;
Result:Longint;
end;
Здесь, как и во всех других определенных в VCL для сообщений
записях, поле Msg определяет передаваемое сообщение, поле Result - результат
действия обработчика. Уникальные для данной записи поля WParam и LParam
содержат соответствующие параметры сообщения.
Для посылки определяемых пользователем сообщений можно
использовать функции SendMessage, PostMessage, однако, предпочтительнее в
данном случае использовать Perform.
Сокеты
Наиболее современным и даже "модным" является общение
процессов на уровне сокетов. Популярность их обусловлена взрывным ростом
интереса как пользователей, так и специалистов к Internet и всему, что
связано с этой сетью.
Общепринятой и стандартизованной на международном уровне
является семиуровневая модель структуры протоколов связи под названием
интерфейс открытых систем (Open Systems Interface, OSI). На каждом из уровней
— от первого, физического, до высших уровней представления и приложений — решается
свой объем задач и используется свой инструментарий.
Сокеты находятся как раз на промежуточном, так называемом
транспортном уровне семиуровневой структуры. "Под ним", на сетевом
уровне, находится протокол IP (основа TCP/IP — Transmission Control Protocol/Internet
Protocol). Над ним находятся протоколы сеансового уровня (сервисы),
ориентированные на конкретные задачи — например, FTP (для передачи файлов),
SMTP (почтовый), всем известный гипертекстовый протокол HTTP и другие.
Использование сокетов, с одной стороны, позволяет абстрагироваться от
частностей работы на нижних уровнях, с другой — решать широкий круг задач,
недоступный специализированным протоколам.
С точки зрения своей сущности сокет — это модель одного конца
сетевого соединения, со своими свойствами и возможностью читать и записывать
данные. С точки зрения содержания — это прикладной программный интерфейс,
входящий в состав разных операционных систем, в том числе Windows — начиная с
версии 3.11. Последняя его реализация носит название WinSock 2.0. Прототипы
функций содержатся в файле WINSOCK.PAS, поставляемом с Delphi.
API сокетов впервые появился в среде Unix и стал популярен
вместе с (и благодаря) протоколом TCP/IP. Именно они являются строительным
материалом, из которого построена сеть Internet. Но сокеты не обязательно
базируются на TCP/IP, они могут базироваться на IPX/SPX и других протоколах.
Механизм соединения при помощи сокетов таков. На одной стороне
создается клиентский сокет. Для инициализации связи ему нужно задать путь к
серверному сокету, с которым предстоит установить соединение.
Путь в сети задается двумя параметрами: адресом или равноценным
ему именем хоста, или просто хостом и номером порта. Хост — это система, в
которой запущено приложение, содержащее сокет. Неверно приравнивать понятие
"хост" или "адрес" к понятию "компьютер" — у
компьютера может быть несколько сетевых устройств и несколько адресов. Адрес
в сетях TCP/IP задается четверкой чисел в диапазоне 0..255, например, так:
192.168.99.1. Естественно, каждый адрес даже в рамках Internet уникален — за
этим следит специальная организация. Имя хоста, как правило, — символьная
строка, поставленная в соответствие адресу и записанная по правилам UNC,
например http://www.microsoft.com. Взаимное соответствие между именами и адресами
может осуществляться по-разному, в зависимости от масштаба сети и применяемой
ОС. В Internet существует система имен доменов (DNS) — специальные серверы,
хранящие и поддерживающие таблицы соответствия между символьным именем и
адресом. Но в любом случае соединение по адресу быстрее, так как не нужно
обращаться за дополнительной информацией.
В случае, если ваши клиент/серверные приложения отлаживаются на
одной и той же машине, можно связать сокеты четырьмя способами:
- Указанием
сетевого имени вашего компьютера (узнать его можно через апплет
"Сеть" Панели управления)
- Указанием IP
— адреса вашего компьютера (узнать его можно в свойствах протокола
ТСР/IP: на машине должен стоять этот протокол и иметься постоянный
IP-адрес)
- Указанием
имени localhost, указывающего, что сервер находится на том же компьютере
- Указанием
IP-адреса 127.0.0.1, указывающего на тоже самое
Номер порта — простое средство для поддержания одновременно
нескольких связей между двумя хостами. Это число, обычно зарезервированное
для протоколов более высокого уровня. Так, для протокола FTP выделен порт 21,
SMTP — 25, популярная игра Quake II использует порт 27910 и т. п. Программист
должен ознакомиться со списком уже закрепленных портов, прежде чем установит
и использует свое значение.
С одной из двух вступающих в связь сторон запускается серверный
сокет. Первоначально он находится в состоянии просушивания (listening), то
есть ожидания связи. После получения запроса от другой стороны — клиента —
устанавливается связь. Но в то же время создается новый сокет для продолжения
прослушивания.
Естественно, в составе Delphi имеется полноценная поддержка
сокетов. Еще в версии 2 появился заголовочный файл WINSOCK.PAS. Есть он и
сейчас — для желающих использовать API WinSock напрямую. Мы же рассмотрим
здесь компоненты TServerSocket и TClientSocket, имеющиеся в Delphi 4 на
странице Internet Палитры компонентов.
Очень важным моментом в использовании сокетов является задание
их типа — блокирующего (синхронного) и неблокирующего (асинхронного). По
существу, работа с сокетами — не что иное, как операции ввода/вывода,
которые, как мы знаем, также могут быть синхронными и асинхронными
(отложенными). В первом случае при вызове функции ввода/вывода приложение
блокируется до его окончания. Во втором — инициируется ввод/вывод и
выполнение приложения сразу же продолжается; окончание ввода/вывода будет
"ознаменовано" в системе возникновением некоторого события. В
библиотеке WinSock 2.0 поддерживаются оба типа операций с сокетами;
соответственно, в компонентах Delphi также можно установить нужный тип.
Отвечают за него свойства serverType и clientType, о которых рассказано ниже.
Специфика компонентов TServerSocket и TClientSocket в том. что
они являются "двухэтажными" надстройками над API сокетов. И у того,
и у другого имеется свойство:
property Socket: TClientWinSocket;
у компонента TClientSocket и
property Socket: TServerWinSocket;
у компонента TServerSocket
Это свойство представляет собой объект — собственно оболочку
сокета, со всеми функциями поддержки установления соединения, чтения и
записи. азделение труда между ними такое—на уровне TServerSocket
(TClientSocket) сосредоточены основные опубликованные свойства и события,
обработку которых можно запрограммировать; на уровне TServerWinSocket
(TClientWinSocket) следует искать функции, в том числе чтения и записи в
сокет.
Объект TServerWinSocket
На уровне этого объекта ведется список соединений с клиентскими
сокетами, содержащийся в свойстве:
property Connections [Index: Integer]: TCustomWinSocket;
Общее число соединений (и число элементов в свойстве
connections) равно значению свойства:
property ActiveConnections: Integer;
Этим списком и счетчиком удобно пользоваться для рассылки всем
клиентам какой-нибудь широковещательной информации, например:
for i:=0 to ServerSocket.Socket.ActiveConnections-1 do
ServerSocket.Socket.Connections[i].SendText('Hi! ');
Тип сервера (блокирующий/неблокирующий) задается свойством
type TServerType = (stNonBiocking, stThreadBiocking);
property ServerType: TServerType;
Поскольку сервер, который блокируется каждым чтением/записью,
представить себе трудно, разработчики фирмы Inprise пошли таким путем.
Блокирующий режим заменен режимом stThreadBlocking. В этом случае при
установлении каждого нового соединения запускается отдельный программный
поток3 (объект класса TServerclientThread). Он отвечает за связь с отдельным
клиентом, и его блокировка не влияет на работу остальных соединений.
Если вы не хотите порождать TServerclientThread, а хотите
описать свой класс потока и использовать его для работы с сокетом, вам нужно
создать обработчик события:
property OnGetThread: TGetThreadEvent;
type TGetThreadEvent = procedure (Sender: TObject;
ClientSocket: TServerClientWinSocket; var SocketThread: TServerCiientThread)
of object;
В отличие от stThreadBlocking, тип stNonBlocking своим
поведением ничем не отличается от описанного выше — операции происходят
асинхронно, и программист должен лишь описать реакцию на события, возникающие
в момент их окончания.
Как известно, создание и уничтожение нового программного потока
влечет за собой определенные системные накладные расходы. Чтобы избежать
этого, в рассматриваемом объекте ведется кэш потоков. По завершении
соединения потоки не уничтожаются, а переводятся в состояние ожидания нового
соединения.
Свойство: property ThreadCacheSize: Integer;
задает количество свободных потоков, которые могут находиться в
готовности для установления соединения с клиентом. Это количество должно
рассчитываться в зависимости от интенсивности и продолжительности контакта с
клиентами. Лишние потоки поглощают системные ресурсы, в первую очередь память
и процессорное время. Чтобы оптимизировать использование кэша свободных
потоков, полезно поинтересоваться значением двух свойств:
property ActiveThreads: Integer;
property IdieThreads: Integer;
показывающих число активных (занятых обслуживанием клиентов) и
простаивающих (ожидающих) потоков соответственно.
Старт и завершение потока, работающего с сокетом, обрамлены
событиями:
property OnThreadStart: TThreadNotifyEvent;
property OnThreadEnd: TThreadNotifyEvent;
type TTnreadNotifyEvent=procedure(Sender: TObject;
Thread: TServerClientThread) of object;
Чтобы избежать ситуаций тупиков или гонок при работе с сокетами,
имеются два метода:
procedure Lock;
procedure Unlock;
Если вами предусмотрен код, который может вызвать проблемы в
многозадачной среде, заключите его между вызовами методов Lock и unlock — на
это время остальные потоки, работающие с сокетами, будут блокированы.
Методы чтения и записи для блокирующего и неблокирующего режима
существенно отличаются. ассмотрим сначала те, что предназначены для
неблокирующего (асинхронного) режима.
Средства для организации чтения представлены группой из трех
методов:
function ReceiveLength: Integer;
возвращает число байт, которые могут быть приняты в ответ на
оповещение клиента о передаче
function ReceiveText: string;
возвращает прочитанную из сокета текстовую строку
function ReceiveBuf(var Buf; Count:Integer): Integer;
возвращает данные, прочитанные из сокета в буфер Buf, в
количестве count байт
Аналогично, методы:
function SendBuf(var Buf; Count: Integer): Integer;
procedure SendText(const S: string);
function SendStream (AStream: TStream) : Boolean;
посылают клиенту буфер, текстовую строку и поток данных. В
дополнение к этому метод:
function SendStrearoThenDrop (AStream: TStream): Boolean;
посылает клиенту поток данных и завершает соединение.
Передаваемый в качестве параметра последних двух функций поток
данных AStream переходит "под надзор" объекта TServerWinsocket и
удаляется им по мере пересылки. Программист не должен предпринимать попыток
удалить AStream после вызова методов SendSrteam или SendSrteamThenDrop.
При помощи метода:
function GetClientThread(ClientSocket: TServerClientWinSocket): TServerClientThread;
можно получить указатель на поток, занимающийся обслуживанием
конкретного сокета.
События:
property OnClientConnect;
property OnClientDisconnect;
property OnClientRead;
property OnClientWrite;
имеют одинаковый тип:
TSocketNotifyEvent=procedure(Sender: TObject;
Socket: TCustomWinSocket) of object;
Они происходят при соединении/отключении от клиента, а также при
чтении и записи. Если произошла ошибка, возникает событие:
property OnClientError; TSocketErrorEvent;
type
TErrorEvent = (eeGeneral, eeSend, eeReceive, eeConnect, eeDisconnect,
eeAccept);
TSocketErrorEvent = procedure (Sender: TObject;
Socket: TCustomWinSocket;
ErrorEvent: TErrorEvent;
var ErrorCode: Integer) of object;
Параметры его имеют следующее назначение. ErrorEvent указывает
на тип операции, во время которой произошла ошибка. При этом ErrorCode
содержит код ошибки Windows. Если вы сами обработали ошибку и не хотите
дальнейших действий по ее обработке со стороны системы, нужно установить
параметр ErrorCode в 0.
Компонент TServerSocket
Самое главное свойство этого компонента — уже упоминавшаяся
ссылка на объект:
property Socket: TServerWinSocket;
Именно через него доступны все функциональные возможности
сокета. Компонент же создан только для того, чтобы опубликовать необходимые
свойства и события. В нем имеются свои события OnclientConnect,
OnClientDisconnect, OnClientRead, OnClientWrite, OnClientError, но они не
самостоятельны, а только отсылают к соответствующим событиям объекта
TServerWinSocket. Также обстоит дело и со свойствами serverType и
ThreadCacheSize.
Дополнительно в компоненте предусмотрены события:
property OnListen: TSocketNotifyEvent;
происходит после того, как заданы адрес и порт сокета и перед
тем, как он включается в режим прослушивания (готовности к соединению)
property OnAccept: TSocketNotifyEvent;
происходит непосредственно после установки соединения
Свойство property Active: Boolean;
отвечает за состояние сокета. Для клиентского сокета изменение его значения
соответствует подключению/отключению от сервера. Для серверного —
включение/выключение состояния прослушивания. Использование этого свойства
равносильно применению следующих методов:
procedure Open;
procedure Close;
Свойство property Service: string;
служит для идентификации предназначения сокета. Здесь должно храниться
символьное имя сервиса, для которого используется сокет (ftp, http, telnet и
др.)
Объект TClientWinSocket
Многие из событий и методов этого объекта уже описаны выше (см.
объект TServerwinSocket), так как они имеют общего предка. Но есть и
различия, требующие комментария. Как и серверный, клиентский сокет может быть
двух типов:
type TCiientType = (ctNonBlocking, ctBlocking);
property ClientType: TCiientType;
В отличие от сервера, в блокировке клиента большой беды нет.
Если установлен режим ctBlocking, клиентское приложение блокируется до
завершения операции. В режиме ctNonBiocking операции выполняются асинхронно.
Компонент TClientSocket
Основное внимание при рассмотрении этого компонента обратим на
логику событий, происходящих при подключении клиентского сокета к серверу.
Она такова:
1. Вызывается метод open (или свойство Active устанавливается в
True).
2. Перед началом инициализации происходит событие
property onLookup: TSocketNotifyEvent;.
В этот момент еще можно поменять свойства объекта
TClieniwinSocket: адрес, номер порта и т. п.
3. Сокет полностью инициализируется и начинает поиск. Когда
серверный сокет обнаружен, происходит событие
property onConneciing: TSocketNotifyEvent;.
4. Когда клиентский запрос удовлетворен сервером и установлено
соединение, происходит событие
property OnConnect: TSocketNotifyEvent;
Проиллюстрируем сказанное на примере пересылки информации о дате
и времени. Процесс подключения к серверу выглядит таким образом:
procedure TClientForm.FileConnectltemClick(Sender: TObject);
Begin
if ClientSocket.Active then ClientSocket.Active := False;
if InputQuery('Сервер', 'Адрес (имя)', Server) then
if Length(Server)>0 then
with ClientSocket do begin
Host := Server;
Active := True;
end;
End;
После установления соединения клиент реагирует на событие
onClientRead:
procedure TCiientFom.ClientSocketRead(Sender: TObject; Socket:
TCustomWinSocket);
var s: string;
Begin
s:= Socket.ReceiveText;
if ((s[l]='T') and (TimeSpeedButton.Down)) then
TimeSpeedButton.Caption:=Copy(s, 2, Length(s))
else if ((s[l]='M') and (MemSpeedButton. Down)) then
KemSpeedButton.Caption:=Copy(s, 2, Length (s));
End;
В серверном приложении сокет устанавливается в активное
состояние (прослушивание) при запуске программы. Все подключившиеся клиенты
автоматически заносятся как элемент списка (свойство connections). Саму
информацию о дате и времени сервер рассылает по таймеру в виде
отформатированных текстовых строк:
procedure TServerForm.TimerITimerlSender: TObject);
var i: Integer;
s: string;
ms : TMemoryStatus;
Begin
with ServerSocket.Socket do
for i:=0 to ActiveCcnnections-I do
Connections[i].SendText('T'+TimeToStr(Now));
GlobaiMemoryStatus(ms);
s:=Format('%1OdK',[(ms.dwAvaiiPageFile + ms.dwAvaiiPhys) div 1024]); with
ServerSocket.Socket do
for i:=0 to ActiveConnections-I do
Connections [ i ] . SendText ( ' M' +s ) ;
End;
Сервер может отреагировать на сообщение от клиента. Ответ
следует отправлять через параметр socket произошедшего события onClientRead:
procedure TServerForm.ServerSocketClientRead (Sender: TObject;
Socket: TCustomWinSocket);
Begin
Memo1.Lines.Add(Socket. ReceiveText );
Socket.SendText( ' I am understand' );
End;
К сокетам проявляют интерес многие разработчики, что можно
объяснить их универсальностью и широким распространением. Если вы не нашли
чего-то для вас необходимого в компонентах TClientSocket и TServerSocket, или
наоборот — сочли их слишком сложными в применении, вы можете использовать
компонент TPowersock, разработанный компанией NetMasters. Он находится также
на странице Internet Панели инструментов.
Совместное использование общей памяти
Традиционным является метод межзадачного взаимодействия при
помощи совместно используемой памяти. В DOS и 16-разрядной Windows он был
простым и не требующим пояснений — у всех задач, в том числе у операционной
системы, было общее адресное пространство. Но именно из этого проистекали все
беды и проблемы данных ОС. В 32x разрядных версиях Windows у каждого приложения
свое адресное пространство, недоступное другим приложениям. Тем не менее,
способ обмена данными через память существует.
Разделение данных осуществляется посредством отображения
некоторого объема совместно используемой памяти в адресное пространство
каждого приложения, участвующего в обмене. Память может быть разделяемой
потому, что она фактически располагается вне адресного пространства
приложения. Вообще говоря, в 32-разрядной Windows под "памятью"
подразумевается не только оперативная память (ОЗУ), но также и память,
резервируемая операционной системой на жестком диске. Этот вид памяти
называется виртуальной памятью. Код и данные отображаются на жесткий диск
посредством страничной системы (paging system) подкачки. Страничная система
использует для отображения страничный файл (win386.swp в Windows 95 и
pagefile.sys в Windows NT). Необходимый фрагмент виртуальной памяти
переносится из страничного файла в ОЗУ и, таким образом, становится
доступным.
Для выделения фрагмента совместно используемой памяти должен
быть создан специальный системный объект Win 32, называемый отображаемым
файлом. Этот объект "знает", как соотнести файл, находящийся на
жестком диске, с памятью, адресуемой процессами. В нашем случае файл, который
мы посредством объекта файлового отображения хотим сделать доступным нашему
приложению — это не файл с расширением ТХТ, DLL. или ЕХЕ. Это страничный файл
подкачки, используемый системой. Отобразив фрагмент страничного файла в
память, адресуемую нашим приложением, мы, по сути дела, получили фрагмент
памяти, на который в дальнейшем будем ссылаться, как на данные объекта
файлового отображения.
Примечание. Объекты файлового отображения могут использоваться
для отображения в память любого файла, а не только страничного файла
операционной системы.
Одно или более приложений могут открыть отображаемый файл и
получить тем самым доступ к данным этого объекта. Таким образом, данные,
помещенные в страничный файл приложением, использующим отображаемый файл,
будут доступны другим приложениям, если они открыли и используют тот же самый
отображаемый файл.
Разделение данных между приложениями осуществляется посредством
использования функций API, предоставляемых Win 32 для создания и
использования объектов файлового отображения. Вот наиболее важные из этих
функций:
CreateFileMapping
MapViewOfFile
UnMapViewOfFiie
Создание объекта файлового отображения
Отображаемый файл создается операционной системой при вызове
функции CreateFileMapping. Этот объект поддерживает соответствие между
содержимым файла и адресным пространством процесса, использующего этот файл.
В нашем случае целью будет совместное использование данных. Страничный файл
операционной системы используется как расширение памяти. Таким образом,
создав отображаемый файл, связанный со страничным файлом операционной
системы, мы имеем в качестве результата выделение глобально доступной памяти.
Так как эта память является общедоступной, то другой экземпляр объекта
файлового отображения, открытый программой, запущенной в качестве другого
процесса, может иметь к ней доступ.
Функция CreateFileMapping имеет шесть параметров:
function CreateFileMapping (hFile: THandle;
ipFileMappingAttributes: PSecurityAttributes;
flProtect,
dwMaximumSizeHign,
dwMaximurnSizeLow: DWORD;
lpName: PChar): THandle; stdcall;
Первый параметр имеет тип THandle. Для наших целей значение
этого параметра всегда должно быть $FFFFFFFF (значок "$" указывает
на то, что значение шестнадцатеричное). В принципе на этом месте должен
стоять дескриптор уже открытого при помощи функции CreateFile файла; но,
поскольку мы имеем дело с необычным файлом, указание значения $FFFFFFFF
приводит к связыванию объекта файлового отображения именно со страничным
файлом операционной системы.
Второй параметр — указатель на запись типа TSecurityAttributes.
В нашем случае значение этого параметра всегда равно nil. Третий параметр
имеет тип dword. Он определяет атрибут защиты. Чтобы при помощи отображаемого
файла организовать совместное использование данных, третьему параметру
следует присвоить значение PAGE_READWRITE.
Четвертый и пятый параметры также имеют тип dword. В
32-разрядной операционной системе значение dword имеет дайну 32 бита. Когда
выполняется функция CreateFileMapping, значение типа dword четвертого
параметра сдвигается влево на четыре байта и затем объединяется со значением
пятого параметра посредством операции and. Проще говоря, значения
объединяются в одно 64-разрядное число, равное объему памяти, выделяемой
объекту файлового отображения из страничного файла операционной системы.
Поскольку вы вряд ли попытаетесь осуществить выделение более чем
четырех гигабайт данных, то значение четвертого параметра всегда должно быть
равно нулю. Используемый затем пятый параметр должен показывать, сколько
памяти в байтах необходимо зарезервировать в качестве совместной.
Шестой параметр имеет тип pСhar. Это значение представляет собой
имя объекта файлового отображения, которое должно быть уникальным.
Функция СreateFileMapping возвращает значение типа THandle. В
случае успешного завершения возвращаемое функцией значение представляет собой
дескриптор созданного объекта файлового отображения. В случае возникновения
какой-либо ошибки возвращаемое значение будет равно 0.
Следующий фрагмент кода демонстрирует использование функции
СreateFileMapping:
var
hMappedFiie: THandie;
Begin
hMappedFile:=CreateFileMapping($FFFFFFFF, nil, PAGE_READWRITE, 0,
25, 'SharedBiock');
if (hMappedFiie = 0) then ShowMessage('Mapping error ! ');
End;
В этом примере функция выделяет 25 байт. езультирующий
отображаемый файл называется SharedBlock. В случае успеха функция вернет
описатель текущего объекта файлового отображения в переменную hFileMapObj.
Переменная hFileMapObj имеет тип THandie. Если переменная hFileMapObj равна
нулю, то это значит, что функция завершилась с ошибкой, о чем будет выведено
соответствующее сообщение.
Подключение объекта файлового отображения к адресному
пространству
При помощи функции CreateFileMapping мы только создали объект
типа "отображаемый файл"; следующая задача — спроецировать данные
файла в адресное пространство нашего процесса. Этой цели служит функция
MapViewOfFile. Функция MapViewOfFile имеет пять параметров:
function MapViewOfFile(hFileMappingObject: THandie;
dwDesiredAccess: DWORD;
dwFileOffsetHigh,
dwFiieOffsetLow,
dwIMumberOfBytesToMap: DWORD): Pointer; stdcall;
Первый параметр имеет тип THandle. Его значением должен быть
дескриптор созданного объекта файлового отображения — тот, который возвращает
функция CreateFileMapping. Второй параметр имеет тип dword. Его значение
должно быть установлено в FILE_MAP_WRITE (или. что то же самое, в
FILE_MAP_ALL_ACCESS); это означает, что данные объекта файлового отображения
будут доступны как по считыванию, так и по записи.
Третий и четвертый параметры также имеют тип dword. Это —
смещение отображаемого участка относительно начала файла в байтах. В нашем
случае эти параметры должны быть установлены в нуль, поскольку значение,
которое мы даем пятому (последнему) параметру функции MapViewOfFile, также
равно нулю.
Пятый и последний параметр функции MapViewOfFile, как и
предыдущие параметры, имеет тип dword. Он используется для определения (в
байтах) количества данных объекта файлового отображения, которые надо
отобразить в процесс (сделать доступными для вас). Для достижения наших целей
это значение должно быть установлено в нуль, что означает автоматическое
отображение в процесс всех данных, выделенных перед этим функцией
CreateFileMapping.
Значение, возвращаемое функцией MapViewOfFile, имеет тип
"указатель". Если функция отработала успешно, то она вернет
начальный адрес данных объекта файлового отображения.
Следующий фрагмент кода демонстрирует вызов функции
MapViewOfFile:
var
hMappedFile: THandie;
pSharedBuf: PChar;
Begin
hMappedFile:=CreateFileMapping ($FFFFFFFF, nil, PAGE_READWRITE, 0,
25, 'SharedBlock');
if (hMappedFile = 0) then ShowMessage ('Mapping error !') else begin
pSharedBuf:=MapViewOfFile(hMappedFile,FILE_MAP_ALL_ACCESS,0,0,0);
if (pSharedBuf = nil) then ShowMessage ('MapView error');
end;
End;
В этом примере выделяется 25 байт разделяемой памяти при помощи
функции CreateFileMapping. Имя результирующего объекта файлового отображения
будет SharedBlock. Если функция завершится успешно, то ссылка на новый
отображаемый файл будет возвращена в переменную hMappedFile. Переменная
hMappedFile имеет тип Thandle, если значение переменной hMappedFile равно
нулю, то это означает, что функция отработала с ошибкой, о чем будет выведено
соответствующее сообщение. Если значение не нулевое, то будет вызвана функция
MapViewOfFile. Дескриптор hMappedFile является первым параметром этой функции
и связывает созданный отображаемый файл с текущим процессом. Если функция
завершится успешно, то в переменную pSharedBuf будет помещен начальный адрес
памяти, по которому располагаются данные объекта файлового отображения с
именем SharedBlock. Если функция отработает с ошибкой, то значение,
возвращаемое функцией MapViewOfFile будет равно nil, в этом случае будет
показано сообщение, констатирующее этот факт.
Совместное использование отображаемых данных
Чтобы разделить между процессами память, выделенную из
страничного файла операционной системы, каждый процесс, участвующий в
разделении, должен отобразить один и тот же фрагмент одного и того же файла.
Такой результат может быть достигнут передачей во всех указанных процессах
одинаковых параметров для функций CreateFileMapping и MapViewOfFile.
Указатель, возвращаемый последней функцией, вы можете впоследствии считывать
или записывать согласно своим потребностям. Если данные, связанные с этим
указателем, доступны одному из процессов, то они также доступны и другим
процессам.
Займемся очередным "клонированием" нашего примера с
передачей данных о времени и свободной памяти. Общим для клиента и сервера
теперь является описанный выше код по открытию и отображению файла. Он описан
как реакция на событие формы onCreate. Когда запускается программа и
создается форма, соответственно создается и подключается отображаемый файл.
Код выглядит так:
procedure TSrvForm.FormCreate (Sender: TObjecr);
begin
hFileMapObj:=CreateFiieMapping(MAXDWORD, nil, PAGE_READWRITE, 0, 25,
'SharedMem');
if (hFiieMapObj:=0) then ShowMessage('Error: cannot map this file')
else begin
pSharedBuf:=MapViewOfFile(hFiieMapObj,FILE_MAP_WRITE,0,0,0);
if (pSharedBuf = nil) then
ShowMessage ('Error: pSharedBuf is nil');
end;
end;
Приведенный выше код работает точно так, как описано выше в
разделе "Подключение объекта файлового отображения к адресному
пространству". Единственным отличием является лишь то, что объявления
переменных hFileMapObj и pSharedBuf перенесены в раздел var секции interface
так, как это показано ниже:
interface
var Form1: TForm1;
hFileMapObj: THandle;
pSharedBuf; Pointer;
implementation
Компоненты типа TTimer есть и у клиента и у сервера. При каждом
очередном событии от этого компонента сервер помещает нужные данные в
совместно используемую область памяти:
type
pDataRecord = ^TDataRecord;
TDataRecord = record
DateTime : TDateTime;
Mem : Cardinal ;
end;
procedure TServerForm.Timer1Timer(Sender: TObject);
var ms : TMemoryStatus;
begin
GlobalMemoryStatus (ms);
pDataRecord(pSharedBuf)^.DateTime:= Now;
pDataRecord(pSharedBuf)^.Mem := ms.dwAvailPageFile + ms.dwAvaiiPhys;
end;
а клиент — читает эти данные, если нажата соответствующая
кнопка:
procedure TClientForm.TimerITimerfSender: TObject);
begin
if MemSpeedButton.Down then MemSpeedButton.Caption:=
Format('%8dK',[pDataRecord(pSharedBuf)^.Mem div 1024]);
if TimeSpeedButton.Down then TimeSpeedButton.Caption:=
TimeToStr(pDataRecord(pSharedBuf)^.DateTime);
end;
Последним штрихом станет восстановление надписи на кнопке после
того, как она снова нажата пользователем:
procedure TClientForm.TimeSpeedButtonClick (Sender: TObject);
begin
if not TimeSpeedButton.Down then TimeSpeedButton.Caption:='Time';
end;
procedure TClientForm.Mem.SpeedButtonClick(Sender: TObject);
begin
if not MemSpeedButton. Down then MemSpeedButton.Caption:='Memory';
end;
В рассматриваемой программе есть несколько операторов,
необходимых для прекращения отображения данных и освобождения объекта
файлового отображения. Эти операторы обсуждаются в следующем разделе.
Прекращение отображения данных и закрытие объекта файлового
отображения
Последние две функции, имеющие отношение к объекту файлового
отображения, называются UnMapViewOfFile И CloseHandle. ФУНКЦИЯ
UnMapViewOfFile делает то, что подразумевает ее название. Она прекращает
отображение в адресное пространство процесса того файла, который перед этим
был отображен при помощи функции MapViewOfFile. Функция CloseHandle также делает
то, что подразумевает ее название; она закрывает дескриптор, в нашем случае
это дескриптор объекта файлового отображения, возвращаемый функцией
CreateFileMapping.
Функция UnMapViewOfFile должна вызываться перед функцией
CloseHandle Функции UnMapViewOfFile передается единственный параметр типа
указатель:
procedure TCIientForm.FormDestroy(Sender: TObject);
begin
UnMapViewOfFiie (pSharedBuf) ;
CloseHandle (hFileMapObj);
end;
Помимо возможности совместного доступа отображаемые файлы
позволяют заметно ускорить доступ к файлам, особенно большого размера,
являясь очень мощным инструментом доступа к данным.
Каналы
Протокол DDE пережил полосу своего расцвета, и хотя впоследствии
появилась его сетевая версия (Network DDE, или NDDE), стандартом де-факто она
не стала. Одним из кандидатов на замену DDE стал механизм каналов (pipes). О
его роли говорит хотя бы то, что это один из основных протоколов работы
такого продукта, как Microsoft SQL Server.
Сразу оговоримся, что каналы бывают двух видов — именованные
(named pipes) и безымянные (anonymous pipes). Вторые не предназначены для
связи между самостоятельными приложениями и представляют собой, так сказать,
рудимент Win32 API. В этой книге рассматриваются только именованные каналы.
И еще одна очень важная оговорка. Каналы рассматривались
Microsoft как протокол для организации клиент-серверных приложений. Поэтому
их серверная часть реализована только в среде Windows NT и не поддерживается
в Windows 95/98; клиенты могут быть созданы во всех этих ОС.
Канал можно представить себе как среду, через которую могут
обмениваться данными два приложения. Обмен данными может быть как
односторонним, так и двухсторонним. Тем не менее, одно из приложений играет
роль сервера (оно создает канал), другое (или другие) лишь подключается к
нему. Противоречия с вышесказанной фразой о двух приложениях здесь нет: на
сервере канал виден как ресурс с единственным уникальным именем. Когда к
серверу подключается очередной клиент, для него создается уникальный
экземпляр канала со своими дескриптором, буфером и т, п. Но имя канала для
всех клиентов одинаково.
Имя канала записывается в соответствии с так называемым
соглашением UNC (Universal Naming Convention). Выглядеть оно должно так:
servernamepipepipename
де servernanie — сетевое имя компьютера — сервера,
ipename — имя канала.
В случае, если клиенту нужно подключиться к серверу на том же
компьютере, его сетевое имя заменяется точкой:
.pipepipename
Максимальная длина имени канала ограничена 256 символами;
строчные и прописные буквы не отличаются.
Для программиста алгоритмы создания серверного и клиентского
"концов" канала отличаются. Для создания сервера применяются
специальные функции Win 32 API; клиентский же конец открывается как обычный
файл и работа с ним также напоминает работу с файлом. Неудивительно, что в
дальнейшем мы "упрячем" эти функции в класс Delphi — потомок
HandleStream, ближайшего родственника файлового потока TFileStream.
Пока же рассмотрим искомые функции API. Главная из них для нас
следующая:
function CreateNainedPipe(lpName: PChar;
dwOperiMode,
dwPipeMode,
nMaxInstances,
nOutBufferSize,
nInBufferSize,
nDefauitTimeOut: DWORD;
lpSecurityAttributes:PSecurityAttributes):THandle;
Она создает серверный конец канала с именем lpName. Остальные
параметры перечислены в табл. 8.1.
Параметры функции createNamedPipe
Параметр Назначение
dwOpenMode
Режим открытия. Флаги:
PIPE_ACCESS_DUPLEX — двунаправленный обмен данными
PIPE_ACCESS_INBOUND — только от клиента к серверу
PIPE_ACCESS_OUTBOUND — только от сервера к клиенту
FILE_FLAG_WRITE_THROUGH — запись данных, минуя кэш
FILE_FLAG_OVERLAPPED — режим отложенной операции ввода/ вывода
dwPipeMode
Режим работы канала. Флаги:
PIPE_TYPE_BYTE — запись в режиме потока байт
PIPE TYPE_MESSAGE — запись в режиме потока сообщений
PIPE_READMODE_BYTE — чтение в режиме потока байт
PIPE_READMODE_MESSAGE — чтение в режиме потока сообщений
PIPE_WAIT — функции ввода/вывода не будут возвращать управление до завершения
операции
PIPE_NOWAIT — функции ввода/вывода возвращают управление немедленно
nMaxinstances
Максимальное количество открываемых экземпляров канала, от 1 до
PIPE_UNLIMITED_INSTANCES
nOutBufferSize
Размер буфера для записи
nInBufferSize
Размер буфера для чтения
nDefauitTimeOut
Задает время ожидания конца операции ввода/вывода в канале (в мс)
IpSecurityAttributes
Указатель на структуру Windows NT, содержащую информацию о правах доступа к
каналу Pежимы потока байт и потока сообщений не слишком отличаются друг от
друга — в первом случае система смешивает данные от различных операций
чтения/записи в единый поток, во втором — разделяет их на отдельные порции.
Канал в режиме PIPE_TYPE_BYTE может работать на чтение только в режиме чтения
PIPE_READMODE_BYTE, канал в режиме PIPE_TYPE_MESSAGE — в обоих режимах
чтения.
Функция
function ConnectNainedPipe(hNamedPipe: THandle;
lpOverlapped: POverlapped): BOOL;
позволяет подключиться к уже созданному каналу hNamedPipe, а
функция
function DisconnectNaitledPipe (hNamedPipe: THandie): BOOL;
позволяет отключить клиента — она разрывает связь клиентского и
серверного концов канала hNamedPipe.
Чтобы собрать информацию о состоянии канала hNamedPipe, нужно
вызвать функцию
function GetNamedPipeln.fo(hMamedPipe: THandle;
var ipFlags: DWORD;
lpOutBufferSize,
lpInBufferSize,
lpMaxInstances: Pointer): BOOL;
Указатели lpOutBufferSize, lpInBufferSize, ipMaxInstances должны
указывать на переменные, куда будут записаны размеры буферов и число открытых
экземпляров канала. Параметр lpFlags указывает на переменную, в которую будут
записаны флаги состояния канала. Среди них уже знакомый флаг
PIPE_TYPE_MESSAGE, а также флаг PIPE_SERVER_END. Он установлен, если
hNamedPipe — серверный конец канала.
function PeekNamedPipe (hNamedPipe: THandle;
lpBuffer: Pointer;
nBufferSize: DWORD;
lpBytesRead,
lpTotalBytesAvail,
lpBytesLeftThisMessage: Pointer): BOOL;
Эта функция позволяет прочитать данные из буфера канала, оставив
их там (доступными для последующего чтения). Кроме того, она возвращает
дополнительную полезную информацию о состоянии канала, в частности по адресу,
указанному в параметре lpTotalBytesAvail, будет записано число еще не
прочитанных байт в канале. Функция
Function WaitNamedPipe(lpNamedPipeName: PChar; nTimeOut: DWORD): BOOL;
позволяет организовать ожидание окончания операции в канале.
Параметр nTimeOut задает время ожидания в миллисекундах; возможны еще два
особых значения — NMPWAIT_USE_DEFAULT_WAIT (ожидать в течение времени,
указаного при создании канала) и NMPWAIT_WAIT_FOREVER (ждать бесконечно).
Осталось добавить, что чтение и запись в канал осуществляется
так же, как и в обычный файл — функциями ReadFile и WriteFile.
Каналы хорошо приспособлены для обмена данными в сети. Но все же
их применение сдерживается наличием необходимых протоколов и поддержкой UNC.
Поэтому в глобальных сетях применяется другой вариант механизма межзадачного
взаимодействия — сокеты.
4. Многопоточные приложения
Потоки - это объекты, получающие время процессора. Потоки
позволяют в рамках одной программы решать несколько задач одновременно.
Операционная система предоставляет приложению некоторый интервал времени
центрального процессора (называемый квантом) и в момент, когда приложение
переходит к ожиданию сообщений или освобождает процессор, ОС передает
управление другой задаче. Планируя время центрального процессора, Windows
распределяет его межу потоками, а не между приложениями. Время процессора
выделяется квантами (около 19 мс). Чтобы использовать все возможности
операционной системы, программист должен знать, как создавать потоки.
Существует два типа потоков: асимметричные и симметричные.
Асимметричные потоки (asymmetric threads) решают различные
задачи, и, как правило, не разделяют совместные ресурсы.
Симметричные потоки (symmetric threads) выполняют одну и ту же
работу, разделяет одни и те же ресурсы и исполняют один код. Пример
приложения с симметричными потоками - электронные доски объявлений (Bulletin
Board Systems, BBS). Для обслуживания каждого дозвонившегося туда
пользователя BBS запускает новый поток.
Программа в терминах операционной системы представляет собой
процесс. Процесс - это совокупность виртуальной памяти, исполняемого кода,
потоков и данных. Процесс должен содержать по крайней мере один поток.
Если задачи приложения можно органично разделить на различные подмножества:
обработка событий, ввод-вывод, связь и т.д., то потоки могут быть органично
встроены в программное решение. Сделав приложение многопоточным, программист
получает дополнительные возможности управления им. Например, через управление
приоритетами потоков. Другое важное преимущество использования потоков - при
возрастании "нагрузки" на приложение можно увеличить их количество
и можно увеличить их количество, и, тем самым, снять проблему.
Две типичные проблемы, с которыми может столкнуться программист
при работе с потоками - это ситуации тупиков и гонок.
Гонки
Ситуация гонок возникает, когда два или более потоков пытаются
получить доступ к общему ресурсу и изменить его состояние. ассмотрим
следующий пример. Пусть поток 1 получил доступ к ресурсу и изменил его в
своих интересах; затем активизировался поток 2 и модифицировал этот же ресурс
до завершения потока 1. Поток 1 полагает, что ресурс остался в том же
состоянии, в котором он был до переключения (строго говоря, потоку 1 вообще
ничего не известно о переключении). В зависимости от того, когда был изменен
ресурс, результаты будут варьироваться - иногда код будет выполняться
правильно, иногда - нет. Программист не должен строить никаких иллюзий по
поводу выполнения потоков, т.к. планировщик ОС может запускать их и
останавливать в любое время.
Тупики
Тупики имеют место, когда поток ожидает ресурс, который в данный
момент принадлежит другому потоку. ассмотрим пример: поток 1 захватывает
объект А и, для того, чтобы продолжать работу, ждет возможности захватить
объект Б. В то же время, поток 2 захватывает объект Б и ждет возможности
захватить объект А. В результате оба потока оказываются заблокированными - ни
один из них не будет выполняться. Для исключения ситуаций тупиков и гонок
потоки следует синхронизировать.
Приоритеты потоков
Операционная система планирует время в соответствии с
приоритетами потоков. Когда поток создается, ему назначается приоритет,
соответствующий приоритету породившего его процесса. Процессы, в свою
очередь, имеют следующие классы приоритетов:
1. Pеального времени (Real Time). Этот класс определяет
приоритет даже больший, чем у многих процессов операционной системы. Такой
процесс необходим для процессов, обрабатывающих высокоскоростные потоки
данных.
2. Высокий (High). Этот класс определяет приоритет выше
нормального. Он используется процессами, которые должны завершиться за
короткое время.
3. Нормальный (Normal). Большинство процессов запускается в
рамках этого класса.
4. Фоновый (Idle). Процессы с таким приоритетом запускаются
только в случае, если в очереди диспетчера задач нет других процессов.
Программисты могут использовать этот класс приоритетов для организации
фоновых процедур и реорганизации данных. Примером может служить проверка
орфографии и сохранение файла в MS Word.
Приоритеты имеют значения от 0 до 31. Приоритет потока может
отличаться от приоритета породившего его процесса на плюс-минус две единицы.
Соответствующие величины показаны ниже.
Низший
Пониженный
Нормальный
Повышенный
Высший
Фоновый
2
3
4
5
6
Нормальный заднего плана
5
6
7
8
9
Нормальный переднего плана
7
8
9
10
11
Высокий
11
12
13
14
15
Реального времени
22
23
24
25
26
Класс tThread
Нужно отдавать себе отчет, что с точки зрения операционной
системы, поток - это ее объект. При создании он получает дескриптор и
отслеживается ОС. Объект класса tThread - это конструкция Delphi,
соответствующая потоку ОС.
Для создания потока служит конструктор
Constructor Create(CreateSuspended:Boolean);
Если параметр CreateSuspended равен TRUE, вновь созданный поток
не начинает выполняться до тех пор, пока не будет вызван метод Resume. В
противном случае сразу после создания поток начинает исполнение.
Метод Procedure Resume;
класса tThread вызывается, когда поток возобновляется после
остановки, или если он был создан конструктором с параметром CreateSuspended,
равным TRUE.
Вызов метода
Procedure Suspend;
Приостанавливает поток с возможностью повторного запуска в
последствии. Выполнение продолжается с точки останова.
Свойство Property Suspended:boolean;
Позволяет программисту определить, не приостановлен ли поток. С
помощью этого свойства можно также запускать и останавливать поток.
Для окончательного завершения потока (без последующего запуска)
существует метод
procedure Terminate;
он останавливает поток и возвращает управление вызвавшему его
процессу только после того, как это произошло. Свойство
property Terminated:boolean;
позволяет узнать, вызывался ли уже метод Terminate.
Метод Function WaitFor: integer;
Предназначен для синхронизации и позволяет одному потоку
дождаться момента, когда завершится другой. Если внутри потока FirstThread
написать код:
Code:=SecondThread.WaitFor;
то это означает, что поток FirstThread останавливается до
завершения потока SecondThread . Метод WaitFor возвращает значение свойства
ReturnValue.
Свойство Property Priority: tThreadPriority;
Определяет приоритет потока. Допустимыми значениями приоритета
являются: tpIdle, tpLowest, tpLower, tpNormal, tpHigher, tpHighest,
tpTimeCritrcal.
Метод Procedure Synchronyze(Method: tTHreadMethod);
предназначен для безопасного вызова методов VCL внутри потоков.
Этот метод гарантирует, что к каждому объекту VCL одновременно имеет доступ
только один поток, что позволяет избежать ситуации гонок. Аргумент,
передаваемый в метод Synchronyze - это имя метода, который производит
обращение к VCL. Вызов Synchronyze с таким аргументом, это то же самое, что и
вызов самого метода. Synchronyze находится в области видимости protected, и,
следовательно, доступен только в методах потомков класса tThread.
Главным методом класса tThread является абстрактный метод
Procedure Execute; virtual; abstract;
Этот метод должен быть переопределен в классе-потомке tThread. В
его теле должен содержаться код, который и представляет собой собственно
поток. Если поток был создан с аргументом CreateSuspended, равным FALSE, то
метод Execute вызывается сразу после создания потока; в противном случае -
после вызова метода Resume.
Если поток рассчитан на однократное выполнение каких либо
действий, то никакого специального кода завершения для него писать не нужно.
После выполнения метода Execute будет вызван деструктор, который сделает все
необходимое.
Если же в потоке будет выполняться бесконечный цикл, и поток
должен завершиться вместе с приложением, то условия окончания цикла должны
быть примерно такими:
rocedure tMyThread.Execute;
Begin
Repeat
// Код потока
Until CancelCondition or Terminated;
End;
Здесь CancelCondition - ваше личное условие завершения потока, а
свойство Terminated говорит о завершении потока извне (скорее всего,
завершился породивший его процесс).
С завершением потока нужно быть очень внимательным: если он
зациклился, зависнет все приложение.
Свойство Property ReturnValue: Integer;
Позволяет узнать код завершения потока. Эта величина полностью
определяется пользователем.
Для создания своего потока следует найти в репозитории значок
Thread Object (он находится на странице New). Двойной щелчок на этом значке
вызовет диалоговое окно New Thread Object, в которое следует ввести имя
будущего класса-потомка tThread. Нажатие кнопки ОК вызовет генерацию шаблона
нового потока:
unit ThreadUnit;
interface
uses Classes;
type
tMyThread = class(TThread)
private
{ Private declarations }
protected
procedure Execute; override;
end;
implementation
{ Important: Methods and properties of objects in VCL can only be used in a
method called using Synchronize, for example,
Synchronize(UpdateCaption);
and UpdateCaption could look like,
procedure tMyThread.UpdateCaption;
begin
Form1.Caption := 'Updated in a thread';
end; }
{ tMyThread }
procedure tMyThread.Execute;
begin
{ Place thread code here }
end;
end.
Средства синхронизации потоков
Главные понятия механизма синхронизации - функции ожидания и объекты
ожидания. В API предусмотрен ряд функций, позволяющих приостановить
выполнение вызвавшего эту функцию потока вплоть до того момента, как будет
изменено состояние какого-нибудь объекта, называемого объектом ожидания. (Под
этим термином понимается не объект Delphi, а объект операционной системы.) К
возможным вариантам относятся четыре объекта, разработанных специально для
синхронизации:
1. Событие (event);
2. Взаимное исключение (mutex);
3. Семафор (semaphore);
4. Таймер (timer).
Кроме них, ждать можно и других объектов, дескриптор которых
используется в основном для других целей, но может применяться и для
ожидания. К ним относятся: процесс, поток, оповещение об изменении в файловой
системе (change notification) и консольный ввод. Соответствующие классы
находятся в модуле SYNCOBJS.PAS.
Событие
(класс tEvent) имеет два метода: SetEvent и ResetEvent,
переводящие объект в активное и пассивное состояние соответственно.
Конструктор класса имеет вид:
Constructor Create(EventAttributes: pSecurityAttributes;
ManualReset, InitialState:Boolean; const Name:string);
Здесь параметр EventAttributes определяет права доступа к
событию и, в большинстве случаев должен быть равен nil. Параметр InitialState
определяет начальное состояние события. Параметр ManualReset - способ его
сброса. Если этот параметр равен TRUE, то событие должно быть сброшено
вручную. В противном случае событие сбрасывается автоматически после того,
как стартует хотя бы один дожидавшийся его поток.
Метод
type tWaitResult = (wrSignaled, wrTimeOut, wrAbandoned, wrError);
Function WaitFor(TimeOut:DWORD): tWaitResult;
Дает возможности ожидать активизации события в течение TimeOut
миллисекунд. Метод возвращает wrSignaled в случае, если произошла активизация
события и wrTimeOut, если за время тайм-аута ничего не произошло. Если нужно
ждать бесконечно долго, следует установить параметр TimeOut в значение
INFINITE.
Для синхронизации потоков следует вставить вызов WaitFor в
необходимом месте кода потока (метода Execute), и тогда выполнение потока будет
приостановлено до момента активизации события (то есть когда какой-либо
другой поток вызовет его метод SetEvent) или окончания тайм-аута.
Взаимные исключения
Объект типа взаимное исключение (класс tMutex, файл IPCTHRD.PAS)
позволяет только одному потоку в данный момент владеть им. Его можно сравнить
с эстафетной палочкой. Первоначально, данный объект не принадлежит никому.
Метод
function Get(TimeOut:Integer):Boolean;
Производит попытку в течение TimeOut миллисекунд завладеть
объектом (в этом случае метод возвращает TRUE). Если объект больше не нужен,
следует вызвать метод
function Release: boolean;
Критическая секция
Критические секции (класс tCriticalSection) подобны взаимным
исключениям по сути, но между ними существуют два главных отличия:
1. Взаимные исключения могут быть совместно использованы
потоками в различных процессах.
2. Если критическая секция принадлежит другому потоку, ожидающий
поток блокируется вплоть до освобождения критической секции. В отличие от
этого, взаимное исключение разрешает продолжение по истечении тайм-аута.
Критические секции эффективнее взаимных исключений, так как они
используют меньше системных ресурсов.
Работа с критическими секциями похожа на работу с взаимными
исключениями. В многопотоковом приложении создается и инициализируется общая
для всех потоков критическая секция. Когда один из потоков достигает
критически важного участка кода, он пытается захватить секцию вызовом метода
Enter.
. . .
CriticalSection.Enter;
try
// критический участок кода
finally
CriticalSection.Leave;
end;
. . .
Когда другие потоки доходят до оператора захвата секции и
обнаруживают, что она уже захвачена, они приостанавливаются вплоть до
освобождения секции первым потоком путем вызова метода Leave. Критические
секции являются системными объектами и подлежат обязательному освобождению
(впрочем, как и все другие рассматриваемые здесь объекты).
Процесс. Порождение дочернего процесса
Объект типа процесс может быть использован для того, чтобы
приостановить выполнение потока в случае, если для своего продолжения он
нуждается в завершении процесса. С практической точки зрения такая ситуация
возникает, когда в рамках вашего приложения возникает необходимость исполнить
другое приложение.
Вместо устаревшей и поддерживаемой только для совместимости
функции WinExec, гораздо правильнее пользоваться более мощной функцией:
function CreateProcess(lpApplicationName: pChar; lpCommandLine: pChar;
lpProcessAttributes, lpThreadAttributes: pSecurityAttribytes;
bInheritHandles:BOOL;
dwCreationFlags: DWORD;
lpEnviroment: Pointer;
lpCurrentDirectory:pChar;
const lpStartupInfo: tStartupInfo;
var lpProcessInformation: tProcessInformation):BOOL;
Первые два параметра - это имя запускаемого приложения и
передаваемые ему в командной строке параметры. Параметры dwCreationFlags
содержит флаги, определяющие способ создания нового процесса и его приоритет.
Структура lpStartupInfo содержит сведения о размере, цвете, положении окна
создаваемого приложения. На выходе функции заполняется структура lpProcessInformation.
В ней программисту возвращаются дескрипторы и идентификаторы созданного
процесса и его первичного потока.
Локальные данные потока
Интересная проблема возникает, если в приложении будет несколько
одинаковых потоков. Как избежать совместного использования одних и тех же
переменных различными потоками? В первую очередь следует использовать поля
объекта – потомка tThread. Каждый поток соответствует отдельному экземпляру
объекта, и их данные пересекаться не будут. Однако, может возникнуть необходимость
в использовании функций API, которые знать не знают об объектах Delphi и их
полях и свойствах. Для поддержки разделения данных на нижнем уровне в Object
Pascal введена специальная директива - threadvar, которая отличается от
обычной var тем, что применяется только к локальным данным потока. Следующее
описание:
var data1:integer;
threadvar data2:integer;
Означает, что переменная data1 будет использоваться всеми
потоками данного приложения, а переменная data2 будет у каждого потока своя.
Ермаков P.В.
|