Как применять FTP в документах Microsoft Office

Область видимости переменных и методов

В любом классе определяется три секции, в которых можно определить переменные и методы: private, protected и public. В секции published, которая важна только для компонентов, можно определять только методы и свойства – переменные определять нельзя (правда, это и не требуется). Разбиение на секции необходимо для того, чтобы скрыть переменные и методы, – то есть чтобы запретить изменения значений переменных или вызов методов в классах-потомках или в экземплярах классов. Согласно правилам любой класс должен быть максимально скрытым и экспонировать только те методы, которые необходимы для изменения его состояния, но не вспомогательные. Что касается переменных, то их экспонирование вообще запрещено – вместо них используют свойства.

Рассмотрим следующий модельный класс:

TNameSave=class(TComponent)
private
    FData1:string;
    procedure CheckData1;
protected
    FData2:string;
    procedure CheckData2;
public
    FData3:string;
    procedure CheckData3;
end;

Описанное ниже поведение различных секций кода класса TNameSave относится к тому случаю, когда данный класс определен в отдельном модуле. Предположим, был создан экземпляр класса TNameSave. Используя ссылку на рабочую копию класса, можно изменить или прочитать значение переменной FData3 и вызвать метод CheckData3. При попытке же обратиться к переменным FData1, FData2 или вызвать методы CheckData1, CheckData2 компиляция завершается с диагностическим сообщением об отсутствии данной переменной или метода:

procedure TForm1.Button1Click(Sender: TObject);
var
    FC:TNameSave;
begin
    FC:=TNameSave.Create(Self);
    FC.FData3:='Test'; {Legal}
    FC.CheckData3; {Legal}
    FC.FData2:='12'; {Compile error}
    FC.CheckData1; {Compile error}
    FC.Free;
end;

Таким образом, переменные и методы, определенные в секции Private и Protected, скрыты для использования из экземпляра класса. Чтобы понять различие между ними, создадим новый класс – потомок класса TNameSave:

TSecondName=class(TNameSave)
private
    procedure DoSomething;
end;

В этом классе определим новый метод DoSomething, а в реализации этого метода попытаемся обратиться к переменным и методам класса TNameSave:

procedure TSecondName.DoSomething;
begin
    FData3:='AA'; {Legal}
    FData2:='BB'; {Legal}
    FData1:='CC'; {Compile error}
    CheckData3; {Legal}
    CheckData2; {Legal}
    CheckData1; {Compile error}
end;

В этом случае уже можно обратиться к методам и переменным, определенным в секции protected (так же, как и в public), но по-прежнему невозможно обратиться к методам, определенным в секции private, которые являются полностью закрытой. Поэтому в этой секции не имеет смысла определять виртуальные или динамические методы. В этом случае для выполнения приложения потребуется больше ресурсов (или код будет выполняться дольше), а переписать методы директивой override невозможно, поскольку они не видны в классах-потомках.

В C++ имеется такое понятие, как дружественный класс (friend class). Можно один класс объявить дружественным другому и после этого использовать переменные в секции private или вызывать методы из этой секции. В Delphi также существует понятие дружественных классов, однако синтаксис объявления классов дружественными отсутствует. Классы считаются дружественными, если они объявлены в одном и том же модуле (unit). Таким образом, если для примера выше класс TSecondName объявить в том же самом модуле, что и класс TNameSave, то можно вызывать все его методы и обращаться к переменным, даже определенным в секции private. Более того, если в этом же модуле будет создан экземпляр класса TNameSave, то, используя ссылку на него, можно также обращаться ко всем переменным и методам:

procedure TForm1.Button1Click(Sender: TObject);
var
    FC:TNameSave;
begin
    FC:=TNameSave.Create(Self);
    FC.FData3:='Test'; {Legal}
    FC.CheckData3; {Legal}
    FC.FData2:='12'; {Now Legal!}
    FC.CheckData1; {Now Legal!}
    FC.Free;
end;

Наличие дружественных классов, а также возможность доступа в классах-потомках к методам и переменным секции protected можно использовать в целях получения доступа к секции protected из экземпляра класса в произвольном модуле. В качестве примера рассмотрим часто встречающуюся задачу: получены (например, посредством автоматизации) какие-то данные, на которые в адресном пространстве приложения имеется указатель и размер которых известен. Необходимо считать эти данные в приложение через поток. Тривиальное решение таково: создается экземпляр класса TMemoryStream, вызывается его метод Write, данные помещаются в поток. Далее указатель текущей позиции устанавливается на начало потока, и происходит считывание данных из него. У такого решения имеются два недостатка: тратятся системные ресурсы на создание второй копии данных и время на их копирование. С другой стороны, TMemoryStream имеет метод SetPointer, который позволяет просто объявить данный указатель началом потока без создания его копии в памяти. Но этот метод вызвать нельзя – он содержится в секции protected! Поэтому мы поступаем следующим образом:

type
    TMyStream=class(TMemoryStream)
    end;
    procedure ReadPointer(PData:pointer; Size:integer);
var
    Stream:TMyStream;
begin
    Stream:=TMyStream.Create;
    Stream.SetPointer(PData,Size);
    {... reading from stream ...}
    Stream.SetPointer(nil,0);
    Stream.Free;
end;

Иными словами, мы просто объявляем новый класс – и все! А поскольку класс для данного модуля считается дружественным, в экземпляре класса можно вызвать protected-метод SetPointer. Единственный нюанс: перед вызовом деструктора необходимо установить указатель равным nil, иначе деструктор TMemoryStream попытается освободить память и, если память была выделена не в данном модуле, произойдет исключение.

В начало

В начало

Свойства

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

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

TPropClass=class(TObject)
private
    FData:integer;
public
    property Data:integer read FData write FData;
end;

В данном классе объявлено свойство Data типа integer. При чтении свойства возвращается содержимое переменной FData, а при записи этой переменной присваивается новое значение. Переменная FData должна быть того же типа, что и свойство. Чтение и запись свойств в Delphi осуществляется посредством оператора присваивания (:=) с использованием ссылки на экземпляр класса:

procedure TForm1.Button1Click(Sender: TObject);
var
    PC:TPropClass;
    N:integer;
begin
    PC:=TPropClass.Create;
    PC.Data:=5; {Property writing}
    N:=PC.Data; {Property reading}
    PC.Free;
end;

Свойство присваивается как обычная переменная. Однако в отличие от обычной переменной свойство не может быть послано в качестве фактического параметра метода, если его формальный параметр имеет модификатор var.

Имя свойства может быть любым, но по соглашению оно совпадает с именем переменной без буквы F в начале слова. Типы свойств могут быть любые – как стандартные (integer, string и др.), в том числе и классовые типы, так и определенные ранее программистом (TMyListBox…). В классе TPropClass свойство Data имеет доступ как для чтения (служебное слово read после указания типа), так и для записи (служебное слово write). Также свойство может быть только для чтения (property Data:integer read FData) или только для записи (property Data:integer write FData). При попытке присвоить какое-либо значение свойству только для чтения или при попытке считать значение свойства только для записи возникает ошибка при компиляции проекта. Не бывает свойства, которое не будет доступно ни для чтения, ни для записи: конструкцию типа property Dummy, string не пропустит компилятор. Однако немного похожая конструкция property Dummy (без указания типа) вполне легальна (ее использование будет обсуждено ниже).

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

TPropClass=class(TObject)
private
    FData:integer;
    function GetData:integer;
    procedure SetData(Value:integer);
public
    property Data:integer read GetData write SetData;
end;

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

procedure TPropClass.SetData(Value:integer);
begin
    if (Value>0) and (Value<100) then FData:=Value 
    else begin
        beep;
        ShowMessage(‘Illegal value’);
    end;
end;

Название функции для считывания значения свойства GetData:integer может быть произвольным, но по соглашению оно должно совпадать с именем свойства с приставкой Get перед ним. Возвращаемый результат обязан совпадать с типом свойства (в данном случае – integer). В процедуре SetData (Value:integer) название также не играет роли, но по соглашению должно совпадать с названием свойства с добавлением приставки Set перед ним. Название параметра (Value) может быть любым, но обычно используют Value. А вот тип параметра обязан совпадать с типом свойства. Методы для чтения/записи свойства могут быть статическими и виртуальными, но не динамическими. И наконец, свойство может быть смешанным. Это означает, что при чтении свойства вызывается метод, а при записи обращаются к переменной и наоборот:

property Data:integer read GetData write FData;
property Data:integer read FData write SetData;

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

TArrayClass=class(TObject)
    private
        procedure SetFirstName(Index:integer; 
    Value:string);
        function GetFirstName(Index:integer):string;
    public
        property FirstName[NIndex:integer]:string 
    read GetFirstName write SetFirstName;
    end;

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

Нетрудно объявить и двухмерный массив:

TArrayClass=class(TObject)
    private
        procedure SetFirstName(Index1,Index2:integer; 
    Value:string);
        function GetFirstName(Index1,Index2:integer):string;
    public
        property FirstName[I1,I2:integer]:string 
    read GetFirstName write SetFirstName;
    end;

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

К свойствам-массивам обращаются как к обычному массиву после создания экземпляра класса:

procedure TForm1.Button1Click(Sender: TObject);
var
    AC:TArrayClass;
    S:string;
begin
    AC:=TArrayClass.Create;
    AC.FirstName[1,2]:='Test';
    S:=AC.FirstName[5,1];
    AC.Free;
end;

Теперь о типе индексов массивов. В Object Pascal в качестве индексов используются переменные ординального (ordinal) типа (то есть переменные, содержащие данные, которые можно упорядочить и пронумеровать). Разрешается объявлять массивы:

var
    N:array [1..5] of integer;
    K:array ['A'..'Z'] of string;
    M:array [mrOK..mrNo] of boolean; {Modal result enumeration}

Но запрещается объявлять следующие массивы:

var
    N:array [1.4..3.14] of integer; {Floating-point index}
    K:array ['AZ'..'ZA'] of string; {String index}

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

TDistanceClass=class(TObject)
    private
        function GetDistance(Index:double):string;
    public
        property Distance[Index:double]:string 
    read GetDistance;
end;
function TDistanceClass.GetDistance(Index:double):string;
begin
    if Index<0 then Result:='Illegal' else
    if Index<1 then Result:='Short' else
    if Index<3.24 then Result:='Medium' else Result:='Long';
end;

и при создании экземпляра класса использовать индексы – действительные числа:

procedure TForm1.Button2Click(Sender: TObject);
var
    DC:TDistanceClass;
begin
    DC:=TDistanceClass.Create;
    Caption:=DC.Distance[-1.345]+' '+DC.Distance[Pi];
    DC.Free;
end;

Формально это выглядит как обращение к массиву, индексированному действительными числами!

Со свойствами-массивами тесно связано одно из двух значений служебного слова default. Если свойство-массив определить со служебным словом default (для примера выше):

property Distance[Index:double]:string read GetDistance; default;

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

Caption:=DC[-1.345]+' '+DC[Pi];

Обратите внимание, что идентификатор distance отсутствует после идентификатора экземпляра класса (DC). Если имеется несколько свойств-массивов, то служебное слово default можно использовать только с одним из них, в противном случае во время компиляции произойдет ошибка.

Второе значение служебного слова default – информирование компилятора Delphi о необходимости запоминания текущего (указанного в инспекторе объектов) свойства в поток ресурсов. Эта директива работает только с ординарными типами данных и используется следующим образом:

property Age:integer read FAge write FAge default 30;

Если значение свойства, установленное в инспекторе объектов, отличается от 30 и отсутствует директива stored (см ниже) или ее значение равно True, то это значение будет сохранено в потоке ресурсов. Если для свойства не указывается директива default, то берется соответствующее значение для класса-предка. Если директива вообще отсутствует, то любое значение свойства будет запоминаться в потоке ресурсов. Начинающие программисты часто пытаются при помощи этой директивы установить значения свойств по умолчанию, то есть немедленно после выполнения конструктора класса. Из вышесказанного ясно, что желаемый результат в этом случае достигнут не будет. Для изменения свойств по умолчанию необходимо в явном виде присваивать им значения в конструкторе. Как видно, данное значение директивы default настолько отличается от ее использования с массивами, что было бы разумно сделать два различающихся служебных слова, чтобы не путать их действия.

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

И наконец, последняя директива, используемая со свойствами, – stored. Она используется следующим образом:

property Age:integer read FAge write FAge stored False;

После директивы stored указывается либо True, либо False, либо идентификатор логической переменной в данном классе. Если значение этой переменной равно True, то будет ли запоминаться свойство в ресурсах или нет регулируется директивой default. Если же значение этой переменной равно False, то значение свойства не будет запоминаться в ресурсах, каким бы мы его не назначили в инспекторе объектов.

Свойства можно определять в любой из секций (private, protected, public и published). Компилятор Delphi разрешает определять свойства в секции private, но это не имеет смысла. Свойство в секции private невозможно ни использовать, ни переопределить. Далее, если свойство объявлено в секции published, то для компонента оно показывается в инспекторе объектов (если оно имеет доступ read и write). Секция public – стандартное место объявления свойств для классов, не являющихся компонентами. Для компонентов в секции public объявляются так называемые run-time properties – свойства, к которым можно обращаться только во время выполнения приложения. И наконец, в секции protected свойства объявляются для переопределения в классах-потомках.

Переопределение (редекларация) свойств означает повторное объявление свойства в классе-потомке. Редекларация используется в целях:

  1. Изменения способа чтения или записи свойства. Если в классе-предке при чтении свойство ссылалось на переменную, то в классе-потомке можно определить метод.
  2. Изменения значение default (для параметра) или в случае необходимости отмены ранее определенной директивы default либо изменения директивы stored. Все эти изменения влияют на способ запоминания свойств в ресурсах.
  3. Экспонирования свойства компонента в инспекторе объектов. Если свойство ранее не демонстрировалось в инспекторе объектов, то его можно экспонировать в классе-потомке простым переопределением в секции published.

Сказанное выше можно проиллюстрировать следующим примером:

TInitialClass=class(TComponent)
    private
    FData1,FData2,FData3,FData4,FData5:integer;
    protected
        property Data1:integer read 
    FData1 write FData1;
        property Data2:integer read 
    FData2 write FData2 default 20;
        property Data3:integer read 
    FData3 write FData3 default 20;
        property Data4:integer read 
    FData4 write FData4 stored True;
        property Data5:integer read 
    FData5 write FData5;
    end;
    TRedeclaredClass=class(TInitialClass)
    private
        procedure SetData1(Value:integer);
    protected
        property Data1 write SetData1; 
    {Using method instead of variable}
        property Data2 default 30; 
    {Changing default value}
        property Data3 nodefault; {Reject 
    early defined Default}
        property Data4 stored False; 
    {Not store in stream now}
    published
        property Data5; {The property 
    will be exposured in the Object Inspector}
    end;

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

В начало

В начало

Некоторые классы Delphi

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

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

Потомок TObject – класс TPersistent. В этом классе реализованы механизмы запоминания значений всех классовых переменных в поток и считывания их из потока через объект TFiler. Кроме того, определен метод Assign, который позволяет скопировать значения классовых переменных из одного объекта в другой. Эти методы являются виртуальными и «пустыми» — в секции реализации определены только заголовки и небольшое количество кода, информирующего об ошибках, но они будут переписаны в потомках этого класса.

И наконец, класс TComponent является потомком TPersistent. В этом классе реализована иерархия «владелец-вассалы». Класс TComponent имеет поле Owner, в котором хранится ссылка на его владельца. Он также имеет список Components, куда помещаются ссылки на его вассалов. Эта иерархия используется в деструкторе компонента, при вызове деструктора владельца автоматически вызываются все деструкторы вассалов. Такая иерархия характерна именно для библиотеки Delphi Visual Component Library, и ее не следует путать с иерархией «родитель-дети», которая реализована в Windows API и вводится в потомках TComponent. В классе определен виртуальный конструктор. Компоненты Delphi могут быть только потомками класса TComponent. Можно создать потомка любого другого класса, но установить его на палитру компонентов не удастся.

Следующий важный потомок TComponent – TControl. На этом этапе появляются свойства Left, Top, Width, Height. Очевидно, что потомков TControl можно увидеть на экране во время работы приложения. Становится понятной разница между визуальными и невизуальными компонентами: визуальные компоненты имеют в качестве предка TControl, а невизуальные не содержат его в списке предков. В этом же классе появляется свойство Parent – начало иерархии «отец-дети», которую мы обсудим ниже. Анализируя методы класса TControl, следует отметить появление ряда методов, название которых начинается с букв WM… и CM… Это обработчики сообщений. Многие начинающие программисты заблуждаются, думая что TControl способен получать сообщения. Принимать сообщения может только класс TWinControl, и он уведомляет элементы управления о приеме сообщения посредством вызова данных методов.

Следующим потомком является класс TWinControl. Этот класс инкапсулирует окно Windows API. На этом этапе появляется иерархия «родитель-дети». Помимо ссылки на родителя (Parent), которая была определена еще на уровне TControl, появляется список Controls, содержащий ссылки на «детей» главного окна. Иерархия «родители-дети» определяет, каким образом окна будут расположены на экране: дети всегда лежат поверх родителей. Рассматриваемый выше класс TControl всегда выступает формально как чей-нибудь ребенок, хотя для рисования содержимого он использует полотно (Canvas) родителя. Координаты левого верхнего угла ребенка всегда отсчитываются относительно клиентской области родителя. Если TWinControl и TControl содержат пересекающуюся область, то TWinControl всегда будет нарисован сверху TControl. Не все потомки класса TWinControl могут выступать в качестве родителей: этим свойством обладают классы (и их потомки) TForm, TPanel, TGroupBox, TScrollBox.

Кроме того, класс TWinControl способен принимать сообщения. Подробно про сообщения и их обработку мы расскажем ниже. Пока же достаточно знать, что для каждого экземпляра класса TWinControl создается довольно объемная процедура, которая обрабатывает сообщения по умолчанию (термин «по умолчанию» используется здесь потому, что программист может изменить обработчики событий). Поэтому объекты TWinControl требуют значительного количества системных ресурсов: по моим расчетам, примерно на порядок больше, чем аналогичный объект — потомок TControl (например, TButton и TSpeedButton). У TWinControl появляется свойство Handle, которое имеет тип HWND(Windows API). Он ссылается на некоторую область в памяти, которую занимает процедура обработки событий по умолчанию.

Для TWinControl существует такое понятие, как фокус ввода. Это понятие связано с тем, какому окну в данный момент посылаются сообщения о нажатии клавиш на клавиатуре. Клавиатура у компьютера одна, а окон в приложении обычно бывает много. К тому же одновременно может быть загружено несколько приложений. Про то окно, которое в данный момент принимает сигналы с клавиатуры, говорят, что оно имеет фокус ввода. В каждый данный момент только одно окно может иметь фокус ввода. Оно отмечается визуально: пунктирная черта на кнопках и списках, мигающий курсор на редакторах. Окно, имеющее фокус ввода, определяется на уровне операционной системы. Если вызвать деструктор такого окна, не проинформировав операционную систему (которая перенесет фокус ввода), то, как только пользователь нажмет клавишу или система захочет изменить фокус ввода, произойдет исключение – возможно, нескоро. Для TWinControl при вызове деструктора перемещение фокуса ввода произойдет автоматически, если только окно находится на форме. Для окон, имеющих стиль WS_POPUP (Hint и его аналоги), необходимо самим переставлять фокус ввода или информировать систему об этом посредством посылки разрушаемому окну сообщения WM_DESTROY. Обработчик события по умолчанию информирует систему при получении этого сообщения.

В начало

В начало

Сообщения в Windows и их обработка

Принцип работы операционной системы Windows полностью базируется на получении и анализе сообщений. Вся работа любого приложения, показываемого на экране и имеющего меню, заключена в команде Application.Run. Это бесконечный цикл, в задачу которого входит: достать очередное сообщение из очереди, определить, какому окну оно адресовано, и отправить это сообщение для дальнейшей обработки. Как только этот цикл разрывается, приложение прекращает свою работу и закрывается. Именно способностью обрабатывать сообщения среда Windows принципиально отличается от DOS. Если в среде DOS все, как правило, жестко детерминировано – пользователь, например, должен сначала ввести фамилию, затем имя и после отчество, то в среде Windows он, как правило, такой ввод может осуществлять в произвольном порядке. Именно после победного шествия Windows в конце 80-х умерло структурное программирование, которое всех учило, что программа должна иметь одну точку входа и выхода, может ветвиться, но не возвращаться назад. С сообщениями так не получается: программа может в любой момент выполнить произвольные участки кода.

Сообщения в Windows посылаются как реакция на какое-либо событие. Пользователь нажал кнопку мыши – поступило сообщение, COM-порт получил какие-либо данные – поступило другое сообщение, необходимо перерисовать какую-либо область экрана – поступило еще одно сообщение. В Windows имеется около 130 зарегистрированных сообщений, которые генерируются при возникновении какого-либо события в системе. Кроме того, большинство стандартных элементов управления Microsoft также имеют свои сообщения – как командные, так и нотификационные. Например, список (TListBox) имеет командные сообщения, начинающиеся с приставки LB: LB_ADDSTRING – добавляет строку в список, LB_SETSEL – меняет селекцию выбранной строки. Нотификационные сообщения начинаются с префикса LBN: LBN_DBLCLK – двойной щелчок мыши и др. Всего для списка предусмотрено 46 специальных сообщений. Аналогичные сообщения (в разном количестве) имеются и для других элементов управления. Кроме того, программист может определять свои собственные сообщения в достаточно большом количестве. Они начинаются с константы WM_USER.

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

Сообщения посылаются автоматически, но программист имеет возможность послать их из кода приложения. Имеются два метода Windows API — SendMessage и PostMessage, — которые посылают сообщения конкретному окну приложения.

PostMessage ставит сообщение в конец очереди, и приложение продолжает выполнять код, следующий после оператора PostMessage. Формально PostMessage просто информирует операционную систему о том, что при необходимости придется произвести определенные действия. PostMessage часто используют для асинхронной развязки: когда один метод вызывает другой, а последний вызывает первый (рекурсия), то реализацию первого метода делают в обработчике сообщения (о нем речь ниже), а когда второму методу необходимо вызвать первый, он выполняет оператор PostMessage. Сообщение ставится в конец очереди, приложение завершает второй метод и очищает стек. Когда очередь дойдет до сообщения, то вновь будет вызван первый метод и т.д.

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

Чтобы лучше понять разницу между этими методами, можно рассмотреть методы Invalidate и Refresh класса TControl. Оба этих метода объявляют, что прямоугольная область, занимаемая элементом управления, нуждается в перерисовке. Но метод Invalidate посылает родителю сообщение WM_PAINT через метод PostMessage, а Refresh — методом SendMessage. Поэтому при вызове метода Invalidate перерисовка совершается только после окончания работы кода, а метод Refresh заставляет элемент управления перерисоваться немедленно.

Теперь рассмотрим структуру записи, используемую для передачи сообщения. Сообщение передается в записи TMessage, структура ее следующая:

type
    TMessage = record
        Msg: Cardinal;
        case Integer of
            0: 
    (
                
    WParam: Longint;
                
    LParam: Longint;
                
    Result: Longint);
            1: 
    (
                
    WParamLo: Word;
                
    WParamHi: Word;
                
    LParamLo: Word;
                
    LParamHi: Word;
                
    ResultLo: Word;
                
    ResultHi: Word);
        end;

Поле Msg содержит тип сообщения (WM_PAINT, WM_DESTROY, LB_ADDSTRING…). Содержимое параметров WParam и LParam зависит от типа сообщения (поле Msg). Перед тем как делать обработчик или посылать какое-либо сообщение, следует внимательно изучить содержимое этих полей. Это крайне важно. Наконец, сообщение может вернуть результат в поле Result. Он также зависит от значения поля Msg. Как правило, это 0, но для некоторых сообщений это может быть, например, указатель на область памяти, где хранится графическое изображение для окна, и др.

В Delphi определен ряд записей – TWMMouseMove, TWMKeyUp… Эти записи также используются для передачи сообщений. Размер их полностью совпадает с размером TMessage. Они имеют поля Msg и Result, а вместо переменных WParam и LParam определены другие переменные с суммарным размером 8 байт. Форма записи используется исключительно для удобства в реализации обработчика сообщений. Вместо обработчика сообщений на нажатие левой кнопки мыши, которое использует запись TWMLButtonDown:

Procedure WMLButtonDown(var Message:TWMLButtonDown);

можно записать обработчик с использованием TMessage:

Procedure WMLButtonDown(var Message:TMessage);

Теперь мы вплотную приблизились к обработчикам событий. События обрабатывает специальный метод (WndProc), который создается для каждого экземпляра класса TWinControl и его потомков. Если программиста не устраивает обработчик события по умолчанию, он может его переопределить:

TMyButton=class(TButton)
private
    procedure WMLButtonUp(var Message:TMessage); message 
    WM_LBUTTONUP;
end;
implementation
procedure TMyButton.WMLButtonUp(var Message:TMessage);
begin
    inherited;
    Beep;
end;

В данном примере переопределен обработчик события, который вызывается, когда пользователь отпускает нажатую левую кнопку мыши. Обработчики событий следует реализовывать в секции private. Имя метода (WMLButtonUp) может быть произвольным, но по соглашению он называется так же, как и константа, идентифицирующая событие, но без нижнего подчеркивания ( _ ). Метод должен зависеть от переменного параметра типа TMessage или сопоставимого с ним типа (TWMLButtonDown, TWMGetMinMaxInfo… ). Имя параметра может быть любым. То, что это обработчик является сообщением, определяет служебное слово message, а тип перехватываемого сообщения определяет константа после слова message.

Следует обратить внимание на реализацию обработчика сообщений. Используется служебное слово inherited без названия метода и списка параметров. Delphi транслирует это как необходимость вызвать обработчик события по умолчанию. В обработчиках событий обязательно надо вызывать метод по умолчанию или, по крайней мере, четко себе представлять, к чему это может привести. В данном примере при нажатии левой кнопки мыши над объектом TMyButton происходит захват сообщений мыши. Это значит, что если передвинуть мышь в сторону и отпустить кнопку, то сообщение все равно будет послано экземпляру TMyButton. Захват мыши прекращается в обработчике событий по умолчанию WM_LBUTTONUP. Если убрать оператор inherited из кода, то мышь не будет освобождена, – где бы мы ни щелкали кнопкой, все сообщения будут направляться объекту TMyButton. Так что приложение даже нельзя будет закрыть при помощи мыши.

Ситуацию, когда не надо вызывать обработчик события по умолчанию, хорошо иллюстрирует обработчик WM_CLOSE главной формы, когда ее нежелательно закрывать. Это событие возникает, когда пользователь нажимает кнопку Close в правой части заголовка формы. Вообще, форма имеет обработчик события OnClose, где можно изменить переменную Action и отказаться от закрытия формы. Но значение переменной Action абсолютно никакой роли для главной формы приложения не играет. Поэтому следует переписать обработчик события WM_CLOSE без оператора inherited. Если обработчик события не вызывается по умолчанию, обязательно следует присвоить подходящее значение полю Result переменной TMessage! Значение его зависит от типа сообщения, но в большинстве случаев оно равно нулю (сообщение обработано). При вызове обработчика событий по умолчанию поле Result трогать не надо, обработчик сам присвоит ему подходящее значение.

В начало

В начало

Заключение

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

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

С автором можно связаться по адресу: trep@ism.ac.ru.

КомпьютерПресс 1'2001


Наш канал на Youtube

1999 1 2 3 4 5 6 7 8 9 10 11 12
2000 1 2 3 4 5 6 7 8 9 10 11 12
2001 1 2 3 4 5 6 7 8 9 10 11 12
2002 1 2 3 4 5 6 7 8 9 10 11 12
2003 1 2 3 4 5 6 7 8 9 10 11 12
2004 1 2 3 4 5 6 7 8 9 10 11 12
2005 1 2 3 4 5 6 7 8 9 10 11 12
2006 1 2 3 4 5 6 7 8 9 10 11 12
2007 1 2 3 4 5 6 7 8 9 10 11 12
2008 1 2 3 4 5 6 7 8 9 10 11 12
2009 1 2 3 4 5 6 7 8 9 10 11 12
2010 1 2 3 4 5 6 7 8 9 10 11 12
2011 1 2 3 4 5 6 7 8 9 10 11 12
2012 1 2 3 4 5 6 7 8 9 10 11 12
2013 1 2 3 4 5 6 7 8 9 10 11 12
Популярные статьи
КомпьютерПресс использует