А. Григорьев - О чём не пишут в книгах по Delphi
Глобальное сообщение обязано иметь имя (именно поэтому такие сообщения называются также строковыми), под которым оно регистрируется в системе с помощью функции в RegisterWindowMessage. Эта функция возвращает уникальный номер регистрируемого сообщения. Если сообщение с таким именем регистрируется впервые, номер выбирается из числа ещё не занятых. Если же сообщение с таким именем уже было зарегистрировано, то возвращается тот же самый номер, который был присвоен ему при первой регистрации. Таким образом, разные программы, регистрирующие сообщения с одинаковыми именами, получат одинаковые номера и смогут понимать друг друга. Для прочих же окон это сообщение не будет иметь никакого смысла. Создание и использование оконных сообщений демонстрируется примером NumBroadcast, содержащимся на прилагаемом компакт-диске. Разумеется, существует вероятность, что два разных приложения выберут для своих глобальных сообщений одинаковые имена, и это приведет к проблемам при широковещательной рассылке этих сообщений. Но, если давать своим сообщениям осмысленные имена, а не что-то вроде WM_MYMESSAGE1, вероятность такого совпадения будет очень мала. В особо критических ситуациях можно в качестве имени сообщения использовать GUID, уникальность которого гарантируется.
Номера глобальных сообщений становятся известными только на этапе выполнения программы. Это означает, что для их обработки нельзя использовать методы с директивой message, вместо этого следует перекрывать методы WndProc или DefaultHandler.
1.1.10. Особые сообщения
Отправка и обработка некоторых сообщений производится не по общим правилам, а с различными исключениями. Приведенный далее список таких сообщений не претендует на полноту, но все-таки может дать представление о таких исключениях.
Сообщение WM_COPYDATA служит для передачи блока данных от одного процесса к другому. В 32-разрядных версиях Windows память, выделенная некоторому процессу, недоступна для всех остальных процессов. Поэтому просто передать указатель другому процессу нельзя, поскольку он не сможет получить доступ к этой области памяти. При передаче сообщения WM_COPYDATA система копирует указанный блок из адресного пространства отправителя в адресное пространство получателя, передает получателю указатель на этот блок, и при завершении обработки сообщения освобождает блок. Все это требует определенной синхронности действий, которой невозможно достичь при посылке сообщения, поэтому с помощью WM_COPYDATA можно только отправлять, но не посылать (т.е. можно использовать SendMessage, но не PostMessage).
Сообщение WM_PAINT предназначено для перерисовки клиентской облаcти окна. Если изображение сложное, этот процесс занимает много времени, поэтому в Windows предусмотрены механизмы, минимизирующие количество перерисовок. Перерисовывать свое содержимое окно должно при получении сообщения WM_PAINT. С каждым таким сообщением связан регион, нуждающийся в обновлении. Этот регион может совпадать с клиентской областью окна или быть ее частью. В последнем случае программа может ускорить перерисовку, рисуя не все окно, а только нуждающуюся в этом часть (VCL игнорирует возможность перерисовки только части окна, поэтому при работе с этой библиотекой окно всегда перерисовывается полностью). Послать сообщение WM_PAINT с помощью PostMessage окну нельзя, т.к. оно не ставится в очередь. Вместо этого можно пометить регион как нуждающийся в обновлении с помощью функций InvalidateRect и InvalidateRgn. Если на момент вызова этих функций регион, который необходимо обновить, не был пуст, новый регион объединяется со старым. Функции GetMessage и PeekMessage, если очередь сообщений пуста, а регион, требующий обновления, не пуст, возвращают сообщение WM_PAINT. Таким образом, перерисовка окна откладывается до того момента, когда все остальные сообщения будут обработаны. Отправить WM_PAINT с помощью SendMessage тоже нельзя. Если требуется немедленная перерисовка окна, следует вызвать функции UpdateWindow или RedrawWindow, которые не только отправляют сообщение окну, но и выполняют сопутствующие действия, связанные с регионом обновления. Обработка сообщения WM_PAINT также имеет некоторые особенности. Обработчик должен получить контекст устройства окна (см. разд. 1.1.11 данной главы) с помощью функции BeginPaint и по окончании работы освободить его с помощью EndPaint. Эти функции должны вызываться только один раз при обработке сообщения. Соответственно, если сообщение обрабатывается поэтапно несколькими обработчиками, как это бывает при перехвате сообщений, получать и освобождать контекст устройства должен только первый из них, а остальные должны пользоваться тем контекстом, который он получил. Система не накладывает обязательных требований, которые могли бы решить проблему, но предлагает решение, которое используют все предопределенные системные классы. Когда сообщение WM_PAINT извлекается из очереди, его параметр wParam равен нулю. Если же обработчик получает сообщение с wParam <> 0, то он рассматривает значение этого параметра как дескриптор контекста устройства и использует его, вместо того чтобы получать дескриптор через BeginPaint. Первый в цепочке обработчиков должен передать вниз по цепочке сообщение с измененным параметром wParam. Компоненты VCL также пользуются этим решением. При перехвате сообщения WM_PAINT это нужно учитывать.
Примеры PanelMsg и Line, имеющиеся на прилагаемом компакт-диске, демонстрируют, как правильно перехватывать сообщение WM_PAINT.
Простые таймеры, создаваемые системой с помощью функции SetTimer, сообщают об истечении интервала посредством сообщения WM_TIMER. Проверка того, истек ли интервал, осуществляется внутри функций GetMessage, PeekMessage. Таким образом, если эти функции долго не вызываются, сообщение WM_TIMER не ставится в очередь, даже если положенный срок истек. Если за время обработки других сообщений срок истек несколько раз, в очередь ставится только одно сообщение WM_TIMER. Если в очереди уже есть сообщение WM_TIMER, новое в очередь не добавляется, даже если срок истек. Таким образом, часть сообщений WM_TIMER теряется, т.е., например, если интервал таймера установить равным одной секунде, то за час будет получено не 3600 сообщений WM_TIMER, а меньшее число, и разница будет тем больше, чем интенсивнее программа загружает процессор.
ПримечаниеКласс TTimer инкапсулирует таймер, работающий через WM_TIMER. Сообщения получает невидимое окно, создающееся специально для этого. Поэтому событие OnTimer за час при секундном интервале также возникнет меньше, чем 3600 раз.
Некоторую специфику имеют и сообщения от клавиатуры. При обработке таких сообщений можно использовать функцию GetKeуState, которая возвращает состояние любой клавиши (нажата-отпущена) в момент возникновения данного события. Именно в момент возникновения, а не в момент вызова функции. Если функцию GetKeyState использовать при обработке не клавиатурного сообщения, оно вернет состояние клавиши на момент последнего извлеченного из очереди клавиатурного сообщения.
1.1.11. Графика в Windows API
Та часть Windows API, которая служит для работы с графикой, обычно называется GDI (Graphic Device Interface). Ключевое понятие в GDI — контекст устройства (Device Context, DC). Контекст устройства — это специфический объект, хранящий информацию о возможностях устройства, о способе работы с ним и о разрешенной для изменения области. В Delphi контекст устройства представлен классом TCanvas, свойство Handle которого содержит дескриптор контекста устройства. TCanvas универсален в том смысле, что с его помощью рисование в окне, на принтере или в метафайле выглядит одинаково. То же самое справедливо и для контекста устройства. Разница заключается только в том, как получить в разных случаях дескриптор контекста.
Большинство методов класса TCanvas являются "калькой" с соответствующих (в большинстве случаев одноименных) функций GDI. Но в некоторых случаях (прежде всего в методах вывода текста и рисования многоугольников) параметры методов TCanvas имеют более удобный тип, чем функции GDI. Например, метод TCanvas.Polygon требует в качестве параметра открытый массив элементов типа TPoint, а соответствующая функция GDI — указатель на область памяти, содержащую координаты точек, и число точек. Это означает, что до вызова функции следует выделить память, а потом — освободить ее. Еще нужен код, который заполнит эту область памяти требуемыми значениями. И ни в коем случае нельзя ошибаться в количестве элементов массива. Если зарезервировать память для одного числа точек, а при вызове функции указать другое, программа будет работать неправильно. Но для простых функций работа через GDI ничуть не сложнее, чем через TCanvas. Для получения дескриптора контекста устройства существует много функций. Только для того, чтобы получить дескриптор контекста обычного окна, существуют четыре функции: BeginPaint, GetDC, GetWindowDC и GetDCEx. Первая из них возвращает контекст клиентской области окна при обработке сообщения WM_PAINT. Вторая дает контекст клиентской области окна, который можно использовать в любой момент времени, а не только при обработке WM_PAINT. Третья позволяет получить контекст всего окна, вместе с неклиентской частью. Последняя же дает возможность получить контекст определенной области клиентской части окна.