Как ни странно, но для того только, чтоб вывести пустое окно нам потребуется совершить довольно много действий. А именно:
Такая конструкция возникает от того, что программа для Win32 - это по сути набор окон, которые общаются друг с другом и с операционной системой посредством сообщений. Оконная функция - это и есть функция обработки сообщений.
Минимальная оконная функция будет иметь следующий вид:
function WndProc ( Wnd : HWnd; Msg : cardinal; wParam, lParam : longint ) : longint; stdcall; begin result := 0; case Msg of WM_DESTROY : PostQuitMessage (0) else result := DefWindowProc (Wnd, Msg, wParam, lParam) end end;
Рассмотрим аргументы:
Wnd : HWnd
- дескриптор (handle) окна. Уникальный идентификатор нашего окна в системе. Заметим, что одна оконная функция может обрабатывать и несколько окон, поскольку является атрибутом класса, а не конкретного экземпляра окна. Тип HWnd
определен в модуле Windows
.Msg : cardinal
- целое без знака, являющееся идентификатором сообщения, посылаемого окну и, соответственно, обрабатываемого данной функцией. Идентификаторы сообщений, посылаемых системой, равно как и сообщений, предназначенных для работы со стандартными оконными элементами управления, определены также в модуле Windows
.wParam, lParam : longint
- дополнительная информация, передаваемая вместе с сообщением. Конкретное содержание зависит от сообщения.Возвращаемое значение зависит от переданного сообщения. В большинстве случаев можно возвращать 0, как указание на то, что никакие дополнительные действия не потребуются.
Следует обратить внимание на тип вызова функции - stdcall
. На данный момент Free Pascal использует такое соглашение о вызове по умолчанию, однако для надежности и во избежание возможных проблем в дальнейшем, я бы рекомендовал указывать его явно - как в данном примере. Напомню, что соглашение о вызове stdcall
предполагает, что параметры передаются в стек справа налево, а очищает стек вызываемая подпрограмма.
Итак, что мы делаем в теле функции? Во-первых, указываем умолчательный возврат нуля. В нашем простом случае, конечно, можно его прописать и в обработке конкретного сообщения, пока оно одно, но когда нам придется обрабатывать десятки (а то и больше) сообщений, для большинства из которых нормальное выполнение должно возвращать 0, привычка устанавливать его в самом начале лишней не будет.
Затем, оператор case
разбирает сообщения. В данном примере, поскольку мы хотим получить простейшее окно без каких-либо полезных действий, почти все сообщения предоставим обработке по умолчанию. Такую обработку и выполняет функция DefWindowProc()
, аргументы которой (и возвращаемое значение тоже) соответствуют аргументам оконной функции. DefWindowProc()
объявлена в модуле Windows
.
Единственное сообщение, которое нам требуется обрабатывать - WM_DESTROY
, которое система посылает при закрытии окна. Дело в том, что нам требуется закончить работу программы, получив это сообщение, тогда как обработка по умолчанию не предполагает этого. Очевидно потому, что окно в общем случае может быть и не одно. Мы же вызываем функцию PostQuitMessage()
, которая устанавливает в очередь сообщений приложения сообщение WM_QUIT
, по которому в дальнейшем мы должны прекратить цикл обработки сообщений и выйти из программы. Аргументом PostQuitMessage()
является код возврата приложения. По традиции нулевое значение кода возврата означает нормальное завершение.
Замечательно - минимальную оконную функцию, которая нам когда-нибудь послужит каркасом для более сложных, мы написали. Что теперь с ней делать?
Займемся определением класса окна. По сути - это набор некоторых атрибутов, которые будут одинаковы для всех окон данного типа. Например, вышеупомянутая оконная функция определяет одинаковую реакцию на сообщения.
Следующий код реализует создание нового класса окна:
. . . . . const CN_MAIN = 'our_main_window'; . . . . . var WndClass : PWndClassEx; initialization WndClass := new(PWndClassEx); WndClass^.cbSize := sizeof(TWndClassEx); WndClass^.style := CS_DBLCLKS or CS_HREDRAW or CS_OWNDC or CS_VREDRAW; WndClass^.lpfnWndProc := @WndProc; WndClass^.cbClsExtra := 0; WndClass^.cbWndExtra := 0; WndClass^.hInstance := HInstance; WndClass^.hIcon := LoadIcon (0, IDI_ASTERISK); WndClass^.hCursor := LoadCursor (0, IDC_ARROW); WndClass^.hbrBackground := COLOR_BTNFACE + 1; WndClass^.lpszMenuName := nil; WndClass^.lpszClassName := CN_MAIN; WndClass^.hIconSm := 0; if RegisterClassEx (WndClass^) = 0 then begin MessageBox (0, 'Класс не регистрируется!', 'Ошибка!', MB_ICONSTOP); Halt (1) end; dispose(WndClass) . . . . .
Итак, по порядку:
Константа CN_MAIN
- это имя класса. Поскольку в дальнейшем, когда класс зарегистрирован, мы будем обращаться к оконному классу сугубо по этому имени, вынос его в константу позволит нам избежать ошибок, связанных с мелкими опечатками, точнее - обнаруживать их на этапе компиляции, а не выполнения.
Переменная WndClass
используется для того, чтобы создать в динамической памяти структуру, описывающую класс. Мы используем структуру типа TWndClassEx
, хотя можно было бы и удовлетвориться TWndClass
. Однако, варианты без Ex
- это наследие Win16, и лучше отучаться (или не приучаться) их использовать. Хотя в данном случае - разница между TWndClassEx
и TWndClass
всего в одном поле - hIconSm
, которое к тому же в настоящем примере мы просто зануляем.
Вначале мы выделяем память под структуру (new
), а в конце ее освобождаем (dispose
), поскольку после того, как класс зарегистрирован, нужда в ней отпадает.
Рассмотрим поля структуры:
cbSize
- целое число, определяющее размер структуры.style
- стиль класса, образуется побитовым сложением различных флагов. В настоящем примере мы указали флаги CS_DBLCLKS
, CS_HREDRAW
, CS_OWNDC
и CS_VREDRAW
. Впрочем, на этапе минимальной и примитивнейшей программы, которую мы создаем, можно было и 0 поставить. Эти же флаги применимы в большинстве случаев, и мы их использовали скорее в шаблонных, чем в конкретных целях. Тем не менее - стоит пояснить их значения:
CS_DBLCLKS
- определяет, будут ли окна данного класса получать сообщения о двойном щелчке мыши. Если данный флаг не указан, то для определения двойного щелчка придется анализировать интервал и расстояние между одиночными, что, согласитесь, менее удобно.CS_HREDRAW
и CS_VREDRAW
- определяют, нуждается ли окно в перерисовке при изменении его горизонтальных и вертикальных, соответственно, размеров.CS_OWNDC
- означает, что каждое окно использует свой собственный графический контекст для рисования, а не контест родительского окна или общий для всех окон класса.lpfnWndProc
- как явствует из имени данного поля, оно содержит указатель на оконную функцию. Которую мы собственно и определили в предыдущем разделе.cbClsExtra
и cbWndExtra
- количество дополнительных байт, отводимых системой для класса и для каждого окна соответсвенно. В дальнейшем программа может использовать эти данные посредством функций GetClassLong()
/SetClassLong()
и GetWindowLong()
/SetWindowLong()
соответственно (есть также варианты с Word
вместо Long
). Мы сейчас не нуждаемся в выделении дополнительной памяти, хотя в реальных задачах часто требуется придать окну собственный блок памяти. Замечу однако, что объем памяти, который может быть выделен таким образом - весьма невелик, и обычно используется для хранения указателя на область памяти, выделенную другими средствами.hInstance
- дескриптор (handle) программного модуля. При инициализации модуля System
необходимое значение помещается в глобальную переменную HInstance
, к которой мы здесь и обратились.hIcon
- дескриптор значка, который будет присвоен окну. Поскольку мы пока не используем собственных ресурсов и собственных значков, загружаем стандартный вызовом LoadIcon()
с нулевым первым параметром. Второй параметр - это имя, или идентификатор, значка в ресурсе - мы использовали предопределенное значение - константу IDI_ASTERISK
.hCursor
- дескриптор курсора, который будет использоваться, когда указатель мыши будет находиться в нашем окне. Опять же, за неимением своего - используем стандартный. Впрочем, и в дальнейшем, когда сможем использовать свои курсоры, лучше ими не злоупотреблять.hbrBackground
- дескриптор (handle) кисти, которая будет использоваться в качестве фона клиентской части окна. Вообще, кисть (brush) - это такой графический объект, определяющий способ закраски. Может быть трех типов: цветом, штриховкой и точечным шаблоном. Однако, здесь мы использовали самый простой и самый неочевидный способ ее задания - через указание системного цвета с добавлением единицы. Константа COLOR_BTNFACE
дает нам закраску стандартных кнопок, определенную текущими настройками Windows.lpszMenuName
- имя ресурса меню, которое будет присвоено по умолчанию всем окнам класса. Мы пока не пользуемся меню, кроме того, существует более гибкий способ его задания - при непосредственном создании окна.lpszClassName
- это поле должно указывать на строку содержащую имя класса. В нашем случае - это константа CN_MAIN
.hIconSm
- дескриптор малого значка, то есть того, который выводится слева от заголовка окна (да и почти во всех других случаях). Когда это поле равно нулю, используется значок, указанный hIcon
.Заполнив все поля мы передаем структуру функции RegisterClassEx()
. Если бы мы использовали старую версию структуры - TWndClass
, то и функцию бы взяли RegisterClass()
. RegisterClassEx()
возвращает специальное значение - атом, соответствующий нашему классу, если регистрация прошла успешно. В противном случае возвращаемое значение - 0, что мы и проверяем.
Мы намеренно поместили регистрацию класса в секцию initialization
, таким образом она происходит гарантированно один раз при начале работы программы. В случае, если регистрация неудачна, дальнейшее выполнение программы не имеет никакого смысла, посему вызываем процедуру Halt()
. Замечу, что память, выделенная средствами Free Pascal, в таком случае освобождается автоматически операционной системой, поскольку находится в локальной куче приложения.
Итак, мы зарегистрировали класс и теперь можно создать наше окно.
. . . . . wndMain := CreateWindowEx ( WS_EX_APPWINDOW or WS_EX_CONTROLPARENT or WS_EX_WINDOWEDGE, CN_MAIN, 'Hello, World!', WS_CAPTION or WS_CLIPCHILDREN or WS_MAXIMIZEBOX or WS_MINIMIZEBOX or WS_OVERLAPPED or WS_SIZEBOX or WS_SYSMENU or WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, HInstance, nil ); . . . . .
Что мы делаем? Для того, чтобы создать окно, мы вызываем функцию WinAPI CreateWindowEx()
. Как и во многих других случаях, мы могли бы использовать вариант без Ex
- функцию CreateWindow()
и были бы лишены первого параметра - расширенных стилей окна.
Разберем аргументы функции:
dwExStyle
- расширенные стили. Представляет из себя комбинацию флагов стилей. В нашем случае были использованы следующие флаги:
WS_EX_APPWINDOW
- употребляется для главного окна приложения - явное указание того, что окно должно иметь кнопку на панели задач.WS_EX_CONTROLPARENT
- флаг используется для того, чтобы между дочерними окнами данного можно было переключаться по клавише Tab. В нашем минимальном примере, конечно, эта возможность не используется, флаг я указал в "шаблонных" целях.WS_EX_WINDOWEDGE
- флаг "объемной" кромки окна. Именно такой вид границы окна, как правило, используется программами Win32.lpClassName
- имя класса окна. Тут-то нам предусмотрительно введенная константа CN_MAIN
и пригодилась.lpWindowName
- заголовок (имя) окна. Строка, которая будет выведена в заголовке окна. Она же в дальнейшем может быть использована для поиска окна, если неизвестен его дескриптор.dwStyle
- стили окна. Набор флагов, определяющих различные свойства окна. Вообще, таких стилей огромное количество. Поясню использованные:
WS_CAPTION
- указание выводить заголовок окна.WS_CLIPCHILDREN
- связан с механизмом прорисовки содержимого окна - указывает на то, что из области перерисовки исключаются области, занятые дочерними окнами, что логично - все равно те перерисовывают себя сами. В нашем случае - добавлен на вырост.WS_MAXIMIZEBOX
- окно имеет кнопку максимизации.WS_MINIMIZEBOX
- окно имеет кнопку минимизации (сворачивания).WS_OVERLAPPED
- нормальное перекрывающееся окно. В отличие от окон со стилями WS_CHILD
и WS_POPUP
окна с данным стилем перекрывают друг друга в зависимости только от того, какое активно (точнее от позиции Z-Order), не обращая внимание на отношения "родительское-дочернее".WS_SIZEBOX
, несмотря на BOX
, кнопок не добавляет. Обозначает то, что размеры окна могут быть изменены пользователем.WS_SYSMENU
- окно имеет системное меню - то, которое открывается по щелчку на иконке слева от заголовка. Без стиля WS_CAPTION
смысла не имеет.WS_VISIBLE
- флаг видимости - окно отображается сразу после создания.x, y
- позиция окна. Точнее - позиция его левого верхнего угла. Мы использовали константу CW_USEDEFAULT
, чтобы система сама определила, где ей удобнее его выводить.width, height
- ширина и высота окна. И снова отдаем право решать системе.hWndParent
- дескриптор родительского окна. Для главного окна программы естественно указать ноль.hMenu
- дескриптор (handle) меню окна. Мы пока с меню не заморачиваемся и указываем ноль.hInstance
- дескриптор программного модуля. Как и в случае регистрации класса, мы используем глобальную переменную HInstance
.lpParam
- указатель на некие дополнительные данные, которые должны быть переданы окну после его создания. В нашем случае - не используется.Функция CreateWindowEx()
возвращает дескриптор свежесозданного окна, при помощи которого мы сможем в дальнейшем к окну обращаться.
Окно создано. Теперь требуется организовать передачу ему сообщений. Сообщения система ставит в очередь сообщений приложения, откуда нам следует их выбирать и передавать окну для обработки.
. . . . . var Msg : TMsg; . . . . . while GetMessage (Msg, 0, 0, 0) do begin TranslateMessage (Msg); DispatchMessage (Msg) end; . . . . .
Вот и наш цикл. Вообще говоря, в цикле могут производиться и дополнительные действия, например - для того, чтобы преобразовывать нажатия клавиш в команды в соответствии с таблицей акселераторов.
Тип TMsg
описывает структуру, содержащую данные сообщения. Пока мы ее поля разбирать не будем. Заметим лишь, что они соответствуют аргументам оконной функции, с добавлением времени и позиции курсора на момент посылки сообщения.
Функция GetMessage()
выбирает сообщение из очереди. Если это не сообщение WM_QUIT
, то функция возвращает TRUE
, и выполняется очередная итерация цикла. В случае же WM_QUIT
, возращается FALSE
и, соответственно, происходит выход из цикла и завершение программы. Первый параметр - структура, куда записываются данные сообщения, второй параметр - дескриптор окна, если мы хотим получать сообщения только для него (0 - для всех окон). Третий и четвертый параметр определют диапазон выбираемых сообщений - нули соответствуют отсутствию фильтрации.
Функция TranslateMessage()
преобразует сообщения о нажатии/отпускании клавиш в сообщения о клавиатурном вводе в соответствии с текущей раскладкой и состоянием Shift, Caps Lock и т.д. В нашем случае потребности в ней нет, однако в большинстве реальных приложений использование этой функции необходимо.
Функция DispatchMessage()
передает сообщения соответствующим окнам на обработку.
Собственно, все необходимое для создания приложения "Hello, World!" мы разобрали по косточкам. Осталось свести воедино.
В модуле HelloWindow
(hellowindow.pp
) находится оконная функция и регистрация класса. Создание окна и цикл обработки сообщений - в главном модуле программы Hello
(hello.pp
). См. архив.
Советую обратить внимание на директиву компилятора {$APPTYPE GUI}
в начале файла hello.pp
- в Win32 существует два типа приложений: графические (как наше) и консольные.
В результате должна получиться программа, выглядящая примерно так:
FPC | 3.2.2 | release |
Lazarus | 3.2 | release |
MSE | 5.10.0 | release |
fpGUI | 1.4.1 | release |