Книги

2. Управление программой - примитив


На этом занятии мы рассмотрим простейшие способы управления программой - меню и кнопки. Объединяет их то, что во всех случаях внутри программы мы будем иметь дело со специально для такого взаимодействия с пользователем придуманным сообщением WM_COMMAND.

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

function MessageBox (
         hWnd : HWnd;
         lpText : pchar;
         lpCaption : pchar;
         uType : cardinal
         ) : longint; external 'user32' name 'MessageBoxA';

Как видим, функция принадлежит системной библиотеке user32.dll, которая ее экспортирует под именем 'MessageBoxA'. Здесь постфикс 'A' обозначает ANSI-версию данной функции. Дело в том, что Win32-системы в той или иной степени предполагают поддержку UNICODE, в результате большинство функций API, имеющих дело с текстовыми строками, существуют в двух вариантах: ANSI и Wide. Второй вариант, соответственно, содержит в имени постфикс 'W', вместо 'A'. О поддержке UNICODE в своих программах нам пока задумываться рано, посему мы будем использовать ANSI-версии. Замечу, что именно их по умолчанию, то есть - без постфикса, предлагает Free Pascal.

Параметры функции:

  • hWnd - дескриптор окна, создающего сообщение. Зачастую не имеет значения, как в нашем прошлом случае, и можно указать 0.
  • lpText - текст сообщения.
  • lpCaption - заголовок окна сообщения.
  • uType - комбинация флагов, определяющая тип сообщения. Представляет собой сочетание флагов кнопок и флага иконки, отображаемой рядом с сообщением. В случае, если мы не предоставляем пользователю никакого права выбора, флаг кнопок - MB_OK, который, впрочем, равен нулю и потому может быть опущен, что мы и сделали. Флаг иконки MB_ICONSTOP выводит иконку критической ошибки. О других возможных флагах поговорим позже - когда они нам понадобятся.

Возвращаемое значение функции MessageBox() - число, определяющее, какую кнопку пользователь нажал для его закрытия.

Ну-с, приступим.


2.1 Главное меню окна

Вообще говоря, меню в Windows можно создать на этапе компиляции и поместить в ресурсы программы, а можно и сформировать в run-time. В случае главного меню окна мы воспользуемся первым способом.

Для начала определим несколько констант, которые будут идентифицировать наши команды. Причем, сделаем это в отдельном модуле.

unit Commands;

interface

const
  CMD_FIRST  = $101;
  CMD_SECOND = $102;
  CMD_EXIT   = $201;

implementation

end.

Константы должны быть целыми числами и располагаться в диапазоне $0000..$FFFF. Форма записи - десятичная или шестнадцатиричная - значения не имеет. То, что я начал нумерацию не с нуля, тоже не принципиально. Такая традиция выработалась в связи с тем, что в больших сложных меню удобно разбивать команды на группы, которые отличаются друг от друга старшим байтом, тогда как команды внутри группы - только младшим.

Теперь напишем файл определения ресурсов. Это простой текстовый файл с расширением .rc. Можно использовать и другие расширения, но зачем?

Во-первых, подключим к нему модуль, в котором мы определили константы команд:

#include "commands.pp"

Теперь - собственно меню:

MAINMENU MENU
  BEGIN
  POPUP "Команды"
    BEGIN
    MENUITEM "Первая команда", CMD_FIRST
    MENUITEM "Вторая команда", CMD_SECOND
    MENUITEM SEPARATOR
    MENUITEM "Выход", CMD_EXIT
    END
  END

MAINMENU в нашем случае - имя, идентифицирующее данный ресурс. Далее пишем MENU и, между BEGIN и END, содержимое этого меню. POPUP определяет подменю. В выражении MENUITEM первый параметр - название пункта, то есть - текст, который будет выводиться, а второй параметр - идентификатор команды. Можно, конечно, использовать вместо заранее определенной константы и просто число, но тогда это же число нам придется писать и в тексте программы. Лично у меня желание запоминать кучу значений вместо понятных идентификаторов - не возникает.

Компилируем rc-файл программой windres, которая входит в дистрибутив Free Pascal для Win32. Формат вызова следующий:

windres --preprocessor fprcp -i <имя файла>.rc -o <имя файла>.res

Здесь --preprocessor fprcp - вызов препроцессора, который подставит вместо имен констант их значения, поскольку сам windres паскалевские модули не понимает.

Подключение ресурсов к программе производится следующей директивой компилятора:

{$R <файл ресурсов>}

Директиву лучше всего расположить где-нибудь между program и uses.

Теперь нам понадобится подключить меню к окну. Делается это просто: можно при регистрации класса окна указать 'MAINMENU' в качестве имени ресурса меню - lpszMenuName, или же при создании окна указать параметр hMenu, загрузив дескриптор окна функцией LoadMenu().

function LoadMenu (hInstance : THandle; lpszMenuName : pchar) : HMenu;

Первый параметр - дескриптор программного модуля, в ресурсах которого содержится искомое меню, а второй - имя ресурса, или же приведенный к типу pchar целочисленный идентификатор ресурса. Возвращает функция дескриптор (handle) выбранного меню. В нашем случае уместно будет использовать вызов вида LoadMenu (HInstance, 'MAINMENU').

В любом случае, меню присвоенное окну в дальнейшем управляется автоматически и каких-либо действий по своему отображению от нас не потребует. Правда, существует возможность создать отрисовываемое меню, которое будет требовать от нас явной перерисовки, но займемся мы им еще ой как не скоро.


2.2 Контестное меню

Контекстное меню в отличие от главного меню окна не занимает какого-то определенного места, да и вообще большую часть времени на экране отсутствует. Как правило, оно создается и отображается только в ответ на определенные действия пользователя - щелчок правой кнопки мыши или нажатие специальной клавиши на клавиатуре. Впрочем, создать его можно и заранее, но мы так делать не будем.

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

  • xPos - содержится в loword(lParam) - горизонтальная позиция курсора в экранных координатах на момент сообщения.
  • yPos - содержится в hiword(lParam) - соответственно, вертикальная позиция.

Реакцию на данное сообщение мы вынесем в отдельную процедуру, которую будем вызывать из оконной функции. Вот ее код:

procedure ProcessContextMenu (Wnd : HWnd; xPos, yPos : word);
 var
   Menu : HMenu;
   Dummy : PRect;
 begin
 Dummy := nil;
 Menu := CreatePopupMenu ();
 AppendMenu (Menu, MF_STRING, CMD_FIRST, 'Первая команда');
 AppendMenu (Menu, MF_STRING, CMD_SECOND, 'Вторая команда');
 TrackPopupMenu (Menu,
                 TPM_LEFTALIGN or TPM_LEFTBUTTON,
                 xPos, yPos,
                 0,
                 Wnd,
                 Dummy^);
 DestroyMenu (Menu)
 end;

Для начала поясню странную переменную Dummy. Дело в том, что последний параметр функции TrackPopupMenu() на самом деле - указатель на структуру, причем WinAPI допускает, чтобы он был нулевым. Более того, именно нулевой указатель чаще всего и нужен. Однако, Free Pascal почему-то объявляет его как var. Передача параметра при этом приходит правильно, однако для того, чтобы его занулить приходится вот так извращаться.

А далее по порядку.

Функция CreatePopupMenu() создает новое пустое меню.

Функция AppendMenu() добавляет в меню новый пункт. Можно для этого использовать и другие функции: AppendMenu() - самая простая. Ее параметры следующие:

  • hMenu : HMenu - дескриптор (handle) меню, к которому мы добавляем новый пункт.
  • uFlags : cardinal - флаги, определяющие свойства данного пункта. Пока мы используем только один - MF_STRING, который указывает, что данный пункт отображается просто как строка, передаваемая четвертым параметром.
  • uIDNewItem : cardinal - целочисленный идентификатор команды, соответствующей данному пункту меню. Не мудрствуя лукаво, мы используем для контекстного меню те же команды, что и для меню окна.
  • lpNewItem : pchar - данные об отображении нового пункта. Для MF_STRING (то есть - в нашем случае) - это указатель на строку.

Функция TrackPopupMenu() выводит созданное меню. Параметры:

  • hMenu : HMenu - дескриптор меню.
  • uFlags : cardinal - флаги, управляющие отображением меню. Представляет собой комбинацию двух флагов, первый из которых определяет позицию (выравнивание) меню относительно курсора (точнее - позиции, указанной параметрами x и y) - в нашем случае, TMP_LEFTALIGN, а второй - то, какой кнопкой мыши будет выбираться команда - мы использовали стандартный вариант - TPM_LEFTBUTON.
  • x, y : longint - позиция в которой будет выводиться меню.
  • nReserved : longint - зарезервировано для неизвестных целей, согласно справке - данный параметр должен быть равен нулю.
  • hWnd : HWnd - окно, которое будет владельцем меню, то есть то, которому уйдет сообщение о выбранной команде.
  • prcRect - указатель на структуру типа TRect, определяющую прямоугольник, за пределами которого клик мыши будет рассматриваться как отмена. Передача нулевого указателя означает, что данная область должна совпадать с областью меню, что обычно и требуется, однако из-за неудачного var-объявления во Free Pascal приходится извращаться (см. выше).

Ну, и функция DestroyMenu() уничтожает созданное меню, высвобождая задействованные для него ресурсы системы.


2.3 Кнопки

С меню мы худо-бедно разобрались. Теперь рассмотрим следующий распространенный способ командовать программой - кнопки.

Кнопка в Windows - это дочернее окно определенного класса и с некоторыми определенными стилями. Посему для создания кнопки мы воспользуемся все той же функцией CreateWindowEx():

. . . . .
var
  btn : HWnd;
. . . . .
btn := CreateWindowEx (0,
                       'BUTTON',
                       'Первая команда',
                       WS_CHILD or WS_VISIBLE
                                or BS_PUSHBUTTON or BS_CENTER or BS_VCENTER,
                       200, 30,
                       120, 24,
                       Wnd,
                       CMD_FIRST,
                       HInstance,
                       nil
                       );
. . . . .

Расширенных стилей мы не используем - незачем. Все, чего мы смогли бы от них добиться - сделать кнопку вжато-выпуклой - душераздирающее зрелище.

Класс окна. Стандартные кнопки принадлежат к классу "BUTTON". Однако, к тому же классу принадлежат и многие другие стандартные элементы управления Windows, о которых мы поговорим в следующий раз. Различия между ними опредеяются стилями.

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

Теперь рассмотрим использованные нами стили окна:

  • WS_CHILD - определяет, что создаваемое окно является дочерним. Дочерние окна отображаются только в пределах клиентской области родительского окна. Если мы уменьшим родительское окно, то увидим, что дочернее выглядит как бы обрезанным. Все элементы управления обязаны создаваться с этим стилем.
  • WS_VISIBLE - окно отображается сразу после создания.
  • BS_PUSHBUTTON - определяет то, что это именно кнопка, а не переключатель, например. Обратите внимание на префикс: WS_ (window style) заменился на BS_ (button style). Применение такой константы к окнам другого класса, не "BUTTON", может дать совсем не ожидаемые эффекты.
  • BS_CENTER - текст на кнопке центрируется посередине (по горизонтали).
  • BS_VCENTER - текст на кнопке центрируется посередине по вертикали.

Дальше все более-менее ясно. Следует обратить внимание на то, что мы указываем в качестве родительского окна наше основное, а не 0.

Особый момент обнаруживается при задании параметра hMenu. Особенность элементов управления Windows, в том, что меню в них не отображается, а данный атрибут используется в качестве некоего идентификатора элемента, который, в случае нажатия кнопки, и будет отправлен как команда основному окну.

hInstance и дополнительные параметры передаются уже знакомые нам по созданию основного окна.

В общем-то, в создании кнопок на этом можно поставить точку. Но уж очень страшный шрифт система предлагает для них по умолчанию. Пожалуй, мы его изменим.

. . . . .
var
   lF : TLogFont;
   hF : HFont;
. . . . .
SystemParametersInfo (SPI_GETICONTITLELOGFONT, 0, @lF, 0);
hF := CreateFontIndirect (@lf);
. . . . .
SendMessage (btn, WM_SETFONT, hF, 0);
. . . . .

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

Функция SystemParametersInfo() вообще-то весьма многофункциональна. В данном случае, константа SPI_GETICONTITLELOGFONT заполняет структуру, адрес которой мы передаем в третьем параметре, данными о нужном шрифте. Разбираться подробнее с полями этой структуры мы будем гораздо позднее.

Затем мы получаем дескриптор этого шрифта, или, иначе говоря, загружаем данный шрифт. Почему в терминологии Windows это называется "создать" ("create"), мне не совсем понятно. Ну, да и шут с ним. Загружает в нашем случае функция CreateFontIndirect().

Для того, чтобы наша кнопка отображалась выбранным шрифтом мы посылаем ей сообщение WM_SETFONT. При этом дескриптор шрифта передается в качестве wParam, а в lParam указывается флаг перерисовки - если lParam не равен нулю, то окно, которому посылается сообщение, должно немедленно перерисоваться. Мы не пользуемся этим, поскольку создание кнопок производим в ответ на сообщение о создании главного окна (а вы что думали?), то есть до того, как вообще что-то отрисуется.

Посылку сообщения в нашем случае осуществляет функция SendMessage(), которая должна быть вызывана сразу после создания кнопки. Вообще, в Windows используются две функции для передачи сообщений - SendMessage() и PostMessage(). Параметры обеих соответствуют аргументам оконной функции. Разница между ними в том, что SendMessage() - синхронная, то есть передает сообщение немедленно и дожидается его обработки, тогда как PostMessage() всего лишь ставит сообщение в очередь и немедленно возвращает управление, то есть - асинхронная.


2.4 Сообщение WM_COMMAND

Итак, мы научились создавать элементы управления, которые будут посылать команды нашему окну. Теперь пора научиться их обрабатывать.

Рассмотрим параметры сообщения WM_COMMAND:

  • wNotifyCode - старшее слово wParam. Если сообщение отправлено в результате нажатия "горячей клавиши", то данный параметр равен 1, во все других случаях - 0. Честно говоря, не совсем очевидно, зачем это вообще нужно. Мы будем обходиться без него, и не только сейчас (пока мы вообще не рассматривали "горячие клавиши").
  • wID - младшее слово wParam. Самый важный параметр - идентификатор команды.
  • hwndCtl - lParam - дескриптор окна элемента управления, если команда отправлена таковым. Напрмер, кнопкой.

Обработку сообщения WM_COMMAND мы вынесем в отдельную процедуру для ясности и удобства. Внутри оконной функции оставим только ее вызов: ProcessCommand (Wnd, loword(wParam)).

procedure ProcessCommand (Wnd : HWnd; Command : word);
 begin
 case Command of
      CMD_EXIT : PostMessage (Wnd, WM_CLOSE, 0, 0);
      CMD_FIRST : MessageBox (Wnd, 'Первая команда', 'Команда', MB_ICONASTERISK);
      CMD_SECOND : MessageBox (Wnd, 'Вторая команда', 'Команда', MB_ICONASTERISK)
      end;
 end;

В случае команды CMD_EXIT, мы посылаем окну сообщение WM_CLOSE, которое используется, например, и тогда, когда пользователь кликнул на крестик справа от заголовка. В общем - мы попросту закрываем окно. Две другие команды вызывают соответствующее сообщение для пользователя.


2.5 Резюме

Итак, мы рассмотрели управление программой при помощи команд, которые посредством элементов управления подает пользователь. В примитивнейшем, конечно, виде, но суть, думаю, понятна.

Законченную программу см. в архиве.

Ну и несколько скриншотов:


Рис. 2-1. Главное меню и кнопки


Рис. 2-2. Контекстное меню


Рис. 2-3. Реакция

Актуальные версии
FPC3.2.2release
Lazarus3.2release
MSE5.10.0release
fpGUI1.4.1release