СТАТЬЯ | 24.08.01 |
Профессиональная разработка приложений с помощью Delphi5
Часть 1. Основы объектно-ориентированного программирования
Сергей Трепалин,
УКЦ Interface Ltd.
КомпьютерПресс #1 2001
Рассмотрим одну из ветвей иерархического дерева, которая приводит к компонентам, а также некоторые базовые классы, используемые для написания компонентов. Это очень важный момент: при восхождении вверх по дереву объектов происходит накопление полей и методов, и все, что здесь будет изложено, относится и к компонентам.
В корне иерархического дерева объектов находится 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, предлагают более мощные инструменты для манипулирования с классами, а именно работу с компонентами. Созданию компонентов будет посвящена следующая статья данного цикла.
Дополнительную информацию Вы можете получить в компании Interface Ltd.
Отправить ссылку на страницу по e-mail
Обсудить на форуме Inprise/Borland
Interface Ltd. Отправить E-Mail http://www.interface.ru |
|
Ваши
замечания и предложения отправляйте
автору По техническим вопросам обращайтесь к вебмастеру Документ опубликован: 24.08.01 |