1.1.3. Дескрипторы вместо классов
Программируя в Delphi, мы быстро привыкаем к тому, что каждый объект реализуется экземпляром соответствующего класса. Например, кнопка реализуется экземпляром класса TButton
, контекст устройства — классом TCanvas
. Но когда создавались первые версии Windows, объектно-ориентированный метод программирования еще не был общепризнанным, поэтому он не был реализован. Современные версии Windows частично унаследовали этот недостаток, поэтому в большинстве случаев приходится работать "по старинке", тем более что DLL могут экспортировать только функции, но не классы. Когда мы будем говорить об объектах, создаваемых через Windows API, будем подразумевать не объекты в терминах ООП, а некоторую сущность, внутренняя структура которой скрыта от нас, поэтому с этой сущностью мы можем оперировать только как с единым и неделимым (атомарным) объектом.
Каждому объекту, созданному с помощью Windows API, присваивается уникальный номер
Таким образом, главное различие между методами класса и функциями Windows API заключается в том. что первые связаны с тем экземпляром класса, через который они вызываются, и поэтому не требуют явного указания на объект. Вторым необходимо указание объекта через его дескриптор, т. к. они сами по себе никак не связаны ни с одним объектом. Компоненты VCL нередко являются оболочками над объектами Delphi. В этом случае они имеют свойство (которое обычно называется Handle
), содержащее дескриптор соответствующего объекта. Иногда класс Delphi инкапсулирует несколько объектов Windows. Например, класс TBitmap
включает в себя HBITMAP
и HPALETTE
— картинку и палитру к ней. Соответственно, он хранит два дескриптора: в свойствах Handle
и Palettе
.
Следует учитывать, что внутренние механизмы VCL не могут включиться, если изменение объекта происходит через Windows API. Например, если спрятать окно не с помощью метода Hide
, а путем вызова функции Windows API ShowWindow(Handle, SW_HIDE)
, не возникнет событие OnHide
, потому что оно запускается теми самыми внутренними механизмами VCL. Но такие недоразумения случаются обычно только тогда, когда функциями Windows API дублируется то, что можно сделать и с помощью VCL.
Все экземпляры классов, созданные в Delphi, должны удаляться. В некоторых случаях это происходит автоматически, а иногда программист должен сам позаботиться о "выносе мусора". Аналогичная ситуация и с объектами, создаваемыми в Windows API. Если посмотреть справку по функции, создающей какой-то объект, то там обязательно будет информация о том. какой функцией можно удалить объект и нужно ли это делать вручную, или система сделает это автоматически. Во многих случаях совершенно разные объекты могут удаляться одной и той же функцией. Так, функция DeleteObject
удаляет косметические перья, геометрические перья, кисти, шрифты, регионы, растровые изображения и палитры. Обращайте внимание на возможные исключения. Например, регионы не удаляются системой автоматически, однако если вызвать для региона функцию SetWindowRgn
, то он переходит в собственность операционной системы. Никакие дальнейшие операции с ним, в том числе и удаление, совершать нельзя.
Если системный объект используется только одним приложением, то он будет удален при завершении работы приложения. Тем не менее хороший стиль программирования требует, чтобы программа удаляла объекты явно, а не полагалась на систему.
1.1.4. Формы VCL и окна Windows
Под словом "окно" обычно подразумевается некоторая форма наподобие тех, что можно создать с помощью класса TForm
. Однако это понятие существенно шире. В общем случае окном называется любой объект, который имеет экранные координаты и может реагировать на мышь и клавиатуру. Например, кнопка, которую можно создать с помощью класса TButton
, — это тоже окно. VCL вносит некоторую путаницу в это понятие. Некоторые визуальные компоненты VCL не являются окнами, а только имитируют их, как, например, TImage
. Это позволяет экономить ресурсы системы и повысить быстродействие программы. Механизм этой имитации мы рассмотрим позже, а пока следует запомнить, что окнами являются только те визуальные компоненты которые имеют в числе предков класс TWinControl
. Разработчики VCL постарались, чтобы разница междуTLabel
и оконный TStaticText
кажутся практически близнецами. Разница становится заметной тогда, когда используется Windows API. С неоконными компонентами можно работать только средствами VCL, они даже не имеют свойства Handle
, в то время как оконными компонентами можно управлять с помощью Windows API.
Отметим также еще одно различие между оконными и неоконными компонентами: неоконные компоненты рисуются непосредственно на поверхности TLabel
, размещенный на форме, не может закрывать собой часть кнопки TButton
, потому что TLabel
рисуется на поверхности формы, а кнопка — это независимый объект, лежащий на форме и имеющий свою поверхность. A TStaticText
может оказаться над кнопкой, потому что он тоже находится над формой.
Чтобы разместить неоконный визуальный компонент над оконным, если в этом есть необходимость, можно поступить следующим образом. Положить на форму панель (TPanel
) — она является оконным компонентом и может быть размещена поверх других оконных элементов. На панель теперь можно положить любой неоконный визуальный компонент, и он будет рисоваться не на поверхности формы, а на поверхности панели. Если теперь убрать у панели рамку и уменьшить ее до размеров содержащегося в ней неоконного компонента, панель станет незаметной, и все вместе это будет выглядеть так, будто неоконный компонент находится над оконным.
Каждое окно принадлежит к какому-тоRegisterClassEx
). В качестве параметра эта функция принимает запись типа TWndClassEx
, поля которой содержат параметры класса.
С каждым окном должна быть связана специальная функция, называющаяся
Здесь следует отметить некоторую путаницу в терминах. В англоязычной справке есть слово module, служащее для обозначения файла, отображенного в адресное пространство процесса, т. е., в первую очередь, exe-файла, породившего процесс, и загруженных им DLL. И есть слово unit, которое обозначает модуль в Delphi и которое также переводится как модуль. Ранее мы говорили о модулях как об отображаемых в адресное пространство файлах — это они имеют дескрипторы. Модули Delphi не являются системными объектами и дескрипторов не имеют.
Дескриптор модуля, загруженного в память, можно получить с помощью функции GetModuleHandle
. Функция LoadLibrary
в случае успешного завершения также возвращает дескриптор загруженной DLL. Кроме того, Delphi предоставляет две переменные: MainInstance
из модуля System
и HInstance
из модуля SysInit
(оба этих модуля подключаются к программе автоматически, без явного указания в списке uses
). MainInstance
содержит дескриптор exe-файла, породившего процесс, HInstance
— текущего модуля. В исполняемом файле MainInstance
и HInstance
равны между собой, в DLL HInstance
содержит дескриптор самой библиотеки, а MainIstance
— загрузившего ее главного модуля.
Каждое окно в Windows привязывается к какому-либо модулю (в Windows 9х/МЕ необходимо явно указать дескриптор этого модуля. NT 2000 ХР определяет модуль, из которого вызвана функция создания окна, автоматически). Соответственно, оконные классы делятся наTWndClassEx
при регистрации класса.
Оконный класс, к которому принадлежит окно, указывается при его создании
. Это может быть зарегистрированный ранее класс или один из'BUTTON'
, 'COMBOBOX'
, 'EDIT'
, 'LISTBOX'
, 'MDICLIENT'
, 'SCROLLBAR'
и 'STATIC'
. Назначение этих классов понятно из их названий (класс 'STATIC'
реализует статические текстовые или графические элементы, т. е. не реагирующие на мышь и клавиатуру, но имеющие дескриптор). Кроме этих классов существуют также классы из библиотеки ComCtl32.dll, они тоже доступны всем приложениям без предварительной регистрации (подробнее об этих классах можно узнать в MSDN в разделе Common Controls Reference).
Для окон в обычном понимании этого слова готовых классов не существует, их приходится регистрировать самостоятельно. В частности, VCL для форм регистрирует оконные классы, имена которых совпадают с именами соответствующих классов VCL.
Кроме имени, класс включает в себя другие параметры, такие как стиль, кисть и т. д. Они подробно перечислены в справке.
Для создания окна служат функции CreateWindow
и CreateWindowEx
. При создании окна в числе других параметров задается модуль, к которому оно привязано, имя оконного класса,
Еще один важный параметр этих функций — дескриптор MDIChild
, то родительским для него будет MDIForm
(если быть до конца точным, то не сама форма MDIForm
, а специальное окно класса MDICLIENT
, которое является дочерним по отношению к MDIForm
; дескриптор этого окна хранится в свойстве ClientHandle
главной формы). Другими словами, отношения "родительское — дочернее окно" отражают принадлежность одного окна другому, визуальную связь между ними. Окна, родитель которых не задан (т. е. в качестве дескриптора родителя передан ноль), располагаются непосредственно на рабочем столе. Если при создании окна задан стиль WS_CHILD
, то его координаты отсчитываются от левого верхнего угла клиентской области родительского окна, и при перемещении родительского окна все дочерние окна будут перемещаться вместе с ним. Окно, имеющее стиль WS_CHILD
, не может располагаться ни рабочем столе, попытка создать такое окно окончится неудачей. Визуальные компоненты VCL имеют два свойства, которые иногда путают: Owner и Parent. Свойство Parent указывает на объект, реализующий окно, являющееся родительским для данного визуального компонента (компоненты, не являющиеся наследником TWinControl, также имеют это свойство — VCL для них имитирует эту взаимосвязь, однако сами они не могут быть родителями других визуальных компонентов). Свойство Owner указывает на Owner
есть у любого наследника TComponent
, в том числе и у невизуальных компонентов, и владельцем других компонентов также может быть невизуальный компонент (например, TDataModule
). При уничтожении компонента он автоматически уничтожает все компоненты, владельцем которых он является (здесь, впрочем, есть некоторое дублирование функций, т. к. оконный компонент также при уничтожении уничтожает все визуальные компоненты, родителем которых он является). Еще владелец отвечает за загрузку всех установленных во время разработки свойств принадлежащих ему компонентов.
Свойство Owner
доступно только для чтения. Владелец компонента задается один раз при вызове конструктора и остается неизменным на протяжении всего жизненного цикла компонента (за исключением достаточно редких случаев явного вызова методов InsertComponent
и RemoveComponent
). Свойство Parent
задается отдельно и может быть впоследствии изменено (визуально это будет выглядеть как "перепрыгивание" компонента из одного окна в другое).
Визуальный компонент может не иметь владельца. Это означает, что ответственность за его удаление лежит на программисте, создавшем его. Но большинство визуальных компонентов не может функционировать, если свойство Parent
не задано. Например, невозможно отобразить на экране компонент TButton
, у которого не установлено свойство Parent
. Это связано с тем, что большинство оконных компонентов имеет стиль WS_CHILD
, который, напомним. не позволяет разместить окно на рабочем столе. Окнами без родителя могут быть только наследники TCustomForm
.
Впрочем, сделать кнопку, не имеющую родителя, можно средствами Windows API. Например, такой командой (листинг 1.2).
CreateWindow('BUTTON', 'Test', WS_VISIBLE or BS_PUSHBUTTON or WS_POPUP, 10, 10, 100, 50, 0, 0, HInstance, nil);
Рекомендуем в этом примере убрать стиль WS_POPUP
и посмотреть, что получится — эффект достаточно забавный. Отметим, что создавать такие висящие сами по себе кнопки смысла нет, поскольку сообщения о событиях, происходящих со стандартными элементами управления, получает родительское окно, и при его отсутствии программа не может отреагировать, например, на нажатие кнопки.
Кроме обычного конструктора Create
, у класса TWinControl
есть конструктор CreateParented
, позволяющий создавать оконные компоненты, родителями которых являются окна, созданные без использования VCL. В качестве параметра этому конструктору передается дескриптор родительского окна. У компонентов, созданных таким образом, не нужно устанавливать свойство Parent
.
Путаницу между понятием родителя и владельца усиливает то, что в MSDN по отношению к окнам тоже используются термины owner и owned (принадлежащий), однако это не имеет никакого отношения к владельцу в понимании VCL. Если окно имеет стиль WS_CHILD
, то оно обязано иметь родителя, но не может иметь владельца. Если такого стиля у окна нет, оно не может иметь родителя, но может (хотя и не обязано) иметь владельца. Владельцем в этом случае становится то окно, чей дескриптор передан в качестве родительского, т. е. родитель и владелец в терминах системы — это один и тот же параметр, который по-разному интерпретируется в зависимости от стиля самого окна. Окно, имеющее владельца, уничтожается при уничтожении владельца, прячется при его минимизации и всегда находится над владельцем. Окно, имеющее стиль WS_CHILD
, может быть родителем, но не может быть владельцем другого окна; если передать дескриптор такого окна в качестве владельца, то реальным владельцем станет родитель дочернего окна. Чтобы не путать владельца в терминах VCL и в терминах системы, мы в дальнейшем всегда будем оговаривать, в каком смысле будет упомянуто слово "владелец".
Создание окон через Windows API требует кропотливой работы. VCL справляется с этой задачей замечательно, поэтому создавать окна самостоятельно приходится только тогда, когда использование VCL нежелательно, например, если необходимо написать как можно более компактное приложение. Во всех остальных случаях приходится только слегка подправлять работу VCL. Например, с помощью Windows API можно изменить форму окна или убрать из нею заголовок, оставив рамку. Подобные действия не требуют от программиста создания нового окна, можно воспользоваться тем, что уже создано VCL.
Другой случай, когда могут понадобиться функции Windows API для окон, — если приложение должно что-то делать с чужими окнами. Например, хотя бы просто перечислить все окна, открытые в данный момент, как это делает входящая в состав Delphi утилита WinSight32. Но в этом случае также не приходится самому создавать окна, работа идет с уже имеющимися.
1.1.5. Функции обратного вызова
Прежде чем двигаться дальше, необходимо разобраться с тем, что такое
Ничто не мешает вызывать напрямую, например, метод FormCreate
, но делать это приходится крайне редко. С другой стороны, даже если этот метод не вызывается явно, он все равно выполняется, потому что VCL автоматически вызывает его без прямого указания программиста. Еще одно общее свойство — конкретное имя метода при косвенном вызове не важно. Можно изменить его, но если этот метод по-прежнему будет связан с событием OnCreate
, он так же будет успешно вызываться. Разница заключается только в том, что такие методы вызываются внутренними механизмами VCL, а функции обратного вызова — самой системой Windows. Соответственно, на эти функции налагаются следующие требования: во-первых, они должны быть именно функциями, а не методами класса; во-вторых, они должны быть написаны в соответствии с моделью вызова stdcall
(MSDN предлагает использовать модель callback
, которая в имеющихся версиях Windows является синонимом stdcall
). Что же касается того, как программист сообщает системе о том, что он написал функцию обратного вызова, то это в каждом случае будет по-своему.
В качестве примера рассмотрим перечисление окон с помощью функции EnumWindows
. В справке она описана так:
BOOL EnumWindows(WNDENUMPROC lpEnumFunc, LPARAM lParam);
Соответственно, в Windows.pas она имеет вид
function EnumWindows(lpEnumFunc: TFNWndEnumProc; lParam: LPARAM): BOOL; stdcall;
Параметр lpEnumFunc
должен содержать указатель на функцию обратного вызова. Прототип этой функции описан так:
BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam);
Функции с таким именем в Windows API не существует. Это так называемый TFNWndEnumProc
, описанный в модуле Windows
— это не процедурный тип, а просто нетипизированный указатель, поэтому компилятор Delphi не будет контролировать соответствие передаваемой функции обратного вызова ее прототипу). Что касается типа функции и типа первого параметра, то они имеют определенный смысл, и изменение их типа вряд ли может быть полезным. Но второй параметр предназначен специально для передачи значения, которое программист волен использовать но своему усмотрению, система просто передает через него в функцию обратного вызова то значение, которое имел параметр lParam
при вызове функции EnumWindows
. А программисту может показаться удобнее работать не с типом lParam
(т. е. LongInt
), а, например, с указателем или же с массивом из четырех байтов. Лишь бы были именно четыре байта, а не восемь, шестнадцать или еще какое-то число. Можно даже превратить этот параметр в параметр-переменную, т. к. при этом функции будут передаваться все те же четыре байта — адрес переменной. Впрочем, тем, кто не очень хорошо разбирается с тем, как используется стек для передачи параметров при различных моделях вызова, лучше не экспериментировать с изменением типа параметра, а строго следовать заявленному прототипу, при необходимости выполняя требуемые преобразования внутри функции обратного вызова.
Функция EnumWindows
работает так: после вызова она начинает по очереди перебирать все имеющиеся в данный момент окна верхнего уровня, т. е. те, у которых нет родителя. Для каждого такого окна вызывается заданная функция обратного вызова, в качестве первого параметра ей передается дескриптор данного окна (каждый раз, естественно, новый), в качестве второго — то, что было передано самой функции EnumWindows
в качестве второго параметра (каждый раз одно и то же). Получая по очереди дескрипторы всех окон верхнего уровня, функция обратного вызова может выполнить с каждым из них определенное действие (закрыть, минимизировать и т. п.). Или можно проверять все эти окна на соответствие какому-то условию, пытаясь найти нужное. А значение, возвращаемое функцией обратного вызова, влияет на работу EnumWindows
. Если она возвращает False
, значит, все, что нужно, уже сделано, можно не перебирать остальные окна.
Окончательный код для того случая, когда второй параметр имеет тип Pointer
, иллюстрирует листинг 1.3.
EnumWindows
с функцией обратного вызоваfunction MyCallbackFunction(Wnd: HWND; Р: Pointer): BOOL; stdcall;
begin
{ что-то делаем}
end;
……………
var
MyPointer: Pointer;
……………
EnumWindows(@MyCallbackFunction, LongInt(MyPointer));
Что бы мы ни делали с типом второго параметра функции обратного вызова, тип соответствующего параметра EnumWindows
не меняется. Поэтому необходимо явное приведение передаваемого параметра к типу LongInt
. Обратное преобразование типов при вызове MyCallbackFunction
, осуществляется автоматически.
Использование EnumWindows
и функций обратного вызова демонстрируется примером EnumWnd
.
Отметим, что функции обратного вызова будут вызываться до того, как завершит работу функция EnumWindows
. Однако это не является распараллеливанием работы. Чтобы проиллюстрировать это, рассмотрим ситуацию, когда программа вызывает некоторую функцию А, которая, в свою очередь, вызывает функцию В. Функция В, очевидно, начнет свою работу до того, как завершит работу функция А. То же самое произойдет и с функцией обратного вызова, переданной в EnumWindows
: она будет вызываться из кода EnumWindows
так же, как и функция В из кода функции А. Поэтому код функции обратного вызова получит управление (и не один раз, т. к. EnumWindows
будет вызывать эту функцию в цикле) до завершения работы EnumWindows
.
Однако это правило действует не во всех ситуациях. В некоторых случаях система запоминает адрес переданной ей функции обратного вызова, чтобы использовать ее потом. Примером такой функции является оконная процедура: ее адрес передается системе один раз при регистрации класса, и затем система многократно вызывает эту функцию при необходимости.
В 16-разрядных версиях Windows вызов функций обратного вызова осложнялся тем, что для них необходим был специальный код. называемыйMakeProcInstance
, удалялся после завершения с помощью FreeProcInstance
. Таким образом, вызов EnumWindows
должен был бы выглядеть так. как показано в листинге 1.4.
EnumWindows
в 16-разрядных версиях Windowsvar
MyProcInstanсe: TFarProc;
……………
MyProcInstance:= MakeProcInstance(@MyCallBackFunction, HInstance);
EnumWindows(MyProcInstance, LongInt(MyPointer));
FreeProcInstance(MyProcInstance);
В Delphi этот код будет работоспособным, т. к. для совместимости MakeProcInstance
и FreeProcInstance
оставлены. Но они ничего не делают (в чем легко убедиться, просмотрев исходный файл Windows.pas), поэтому можно обойтись и без них. Тем не менее эти функции иногда до сих пор используются, видимо, просто в силу привычки. Другой способ, с помощью которого и 16-разрядных версиях можно сделать пролог — описать функцию с директивой export
. Эта директива сохранена для совместимости и в Delphi, но в 32-разрядных версиях она также ничего не делает (несмотря на то, что справка, например, по Delphi 3 утверждает обратное; в справке по Delphi 4 этой ошибки уже нет).
1.1.6. Сообщения Windows
Человеку, знакомому с Delphi, должна быть ясна схема событийного управления. Программист пишет только методы реакции на различные события, а затем этот код получает управление тогда, когда соответствующее событие произойдет. Простые программы в Delphi состоят исключительно из методов реакции на события (например, OnCreate
, OnClick
, OnCloseQuery
). Причем событием называется не только событие в обычном смысле этого слова, т. е. когда происходит что-то внешнее, но и ситуация, когда событие используется просто для передачи управления коду, написанному разработчиком программы, в тех случаях, когда VCL не может сама справиться с какой-то задачей. Пример такого события — TListBox.OnDrawItem
. Устанавливая стиль списка в lbOwnerDrawFixed
или lbOwnerDrawVariable
, программист указывает, что ему требуется нестандартный вид элементов списка, поэтому их рисование он берет на себя. И каждый раз, когда возникает необходимость в рисовании элемента, VCL передает управление специально написанному коду. На самом деле разница между двумя типами событий весьма условна. Можно сказать, что когда пользователь нажимает клавишу, VCL не "знает", что делать, и поэтому передает управление обработчику OnKeyPress
.
Событийное управление не есть изобретение авторов Delphi. Такой подход заложен в самой системе Windows. Только здесь события называются
В Delphi для реакции на каждое событие обычно создается свой метод. В Windows одна процедура, называемая оконной, обрабатывает все сообщения, адресованные конкретному окну. (В C/C++ нет понятия "процедура", там термин "оконная процедура" не вызывает путаницы, а вот в Delphi четко определено, что такое процедура. И здесь можно запутаться: то, что в системе называется оконной процедурой, с точки зрения Delphi будет не процедурой, а функцией. Тем не менее мы будем употреблять общепринятый термин "оконная процедура".) Каждое сообщение имеет свой уникальный номер, а оконная процедура обычно целиком состоит из оператора case, и каждому сообщению соответствует своя альтернатива этого оператора. Номера сообщений знать не обязательно, потому что можно использовать константы, описанные в модуле Messages
. Эти константы начинаются с префикса, указывающего на принадлежность сообщения к какой-то группе. Например, сообщения общего назначения начинаются с WM_
: WM_PAINT
, WM_GETTEXTLENTH
. Сообщения, специфичные, например, для кнопок, начинаются с префикса BM_
. Остальные группы сообщений также связаны либо с теми или иными элементами управления, либо со специальными действиями, например, с динамическим обменом данными (Dynamic Data Exchange, DDE). Обычной программе приходится обрабатывать довольно много сообщений, поэтому оконная процедура бывает, как правило, очень длинной и громоздкой. Оконная процедура описывается программистом как функция обратного вызова и указывается при создании оконного класса. Таким образом, все окна данного класса имеют одну и ту же оконную процедуру. Впрочем, существует возможность породить так называемый подкласс, т. е. новый класс, наследующий все свойства существующего, за исключением оконной процедуры. Несколько подробнее об этом будет сказано далее.
Кроме номера, каждое сообщение содержит два параметра: wParam и lParam. Префиксы lParam
) содержит указатель на дополнительные данные. После обработки сообщения оконная процедура должна вернуть какое-то значение. Обычно это значение просто сигнализирует, что сообщение не нуждается в дополнительной обработке, но в некоторых случаях оно более осмысленно, например, WM_SETICON
должно вернуть дескриптор иконки, которая была установлена ранее. Прототип оконной процедуры выглядит следующим образом:
LRESULT CALLBACK WindowProc(
HWND hwnd, // дескриптор окна
UINT uMsg, // номер сообщения
WPARAM wParam, // первый параметр соообщения
LPARAM lParam // второй параметр сообщения
);
В Delphi оконная процедура объявляется следующим образом:
function WindowProc(hWnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
Все, что "умеет" окно, определяется тем. как его оконная процедура реагирует на сообщения. Чтобы окно можно было, например, перетаскивать мышью, его оконная процедура должна обрабатывать целый ряд сообщений, связанных с мышью. Чтобы не заставлять программиста каждый раз реализовывать стандартную для всех окон обработку событий, в системе предусмотрена функция DefWindowProc
. Разработчик приложения в своей оконной процедуре должен предусмотреть только специфическую для данного окна обработку сообщений, а обработку всех остальных сообщений передать этой функции. Существуют также аналоги функции DefWindowProc
для специализированных окон: DefDlgProc
для диалоговых окон, DefFrameProc
для родительских MDI окон, DefChildMDIProc
для дочерних MDI-окон.
Сообщение окну можно либоPostMessage
). Соответственно, кто-то должен извлекать эти сообщения из очереди и передавать их окнам-адресатам. Это делается с помощью специального цикла, который называетсяGetMessage
(реже — PeekMessage
) и передаются в функцию DispatchMessage
. Эта функция определяет, какому окну предназначено сообщение, и вызывает его оконную процедуру. Таким образом, простейший цикл обработки сообщений выглядит так, как показано в листинге 1.5.
var
Msg: TMsg;
…
while GetMessage(Msg, 0, 0, 0) do
begin
TranslateMessage(Msg);
DispatchMessage(Msg);
end;
Блок-схема петли сообщений показана на рис. 1.4.
Рис 1.4. Блок-схема петли сообщений
Функция GetMessage
возвращает True
до тех пор, пока не будет получено сообщение WM_QUIT
, указывающее на необходимость завершения программы. Обычная программа для Windows, выполнив предварительные действия (регистрация класса и создание окна), входит в петлю сообщений, которую выполняет до конца своей работы. Все остальные действия выполняются в оконной процедуре при реакции на соответствующие сообщения.
Если нить не имеет петли сообщений, сообщения, которые посылаются нам, не будут обработаны. Это следует учитывать при создании таких компонентов, как, например, TTimer
и TClientSocket
. Эти компоненты создают невидимые окна для получения сообщений, которые необходимы им для работы. Если нить, создавшая эти объекты, не будет иметь петли сообщений, они будут неработоспособными
Сообщение, извлеченное из очереди, GetMessage
помещает в первый параметр-переменную типа TMsg
. Последние три параметра служат для фильтрации сообщений, позволяя извлекать из очереди только те сообщения, которые соответствуют определенным критериям. Если эти параметры равны нулю, как это обычно бывает, фильтрация при извлечении сообщений не производится.
Функция TranslateMessage
, которая обычно вызывается в петле сообщений, служит для трансляции клавиатурных сообщении (если петля сообщений реализуется только для обработки сообщении невидимым окнам, которые использует, например, COM/DCOM, или по каким-то другим причинам ввод с клавиатуры не обрабатывается или обрабатывается нестандартным образом, вызов TranslateMessage
можно опустить). Когда пользователь нажимает какую-либо клавишу на клавиатуре, система посылает окну, находящему в фокусе, сообщение WM_KEYDOWN
. Через параметры этого сообщения передаётся виртуальный код нажатой клавиши — двухбайтное число, которое определяется только положением нажатой клавиши на клавиатуре и не зависит от текущей раскладки, состояния клавиш <CapsLock> и т. п. Функция TranslateMessage
. обнаружив такое сообщение, добавляет в очередь (причем не в конец, а в начало) сообщение WM_CHAR
, в параметрах которого передается код символа, соответствующего нажатой клавише, с учетом раскладки, состояния клавиш <CapsLock>, <Shift> и т. п. Именно функция TranslateMessage
по виртуальному коду клавиши определяет код символа. При этом нажатие любой клавиши приводит к генерации WM_KEYDOWN
, а вот WM_CHAR
генерируется не для всех клавиш, а только для тех, которые соответствуют какому-то символу (например, не генерирует WM_CHAR
нажатие таких клавиш, как <Shift> <Ctrl>, <Insert>, функциональных клавиш).
У многих компонентов VCL есть события OnKeyDown
и OnKeyPress
. Первое возникает при получении компонентом сообщения WM_KEYDOWN
, второе — сообщения WM_CHAR
.
Если очередь сообщений пуста, функция GetMessage
ожидает, пока там не появится хотя бы одно сообщение, и только после этого завершает работу. Во время этого ожидания нить не загружает процессор.