На этом шаге мы рассмотрим игру "Поймай окно!".
Поскольку теперь мы умеем создавать окна и обрабатывать сообщения Windows, настало время написать программу, в которой не только суммируется вce, чему мы научились, но также демонстрируется несколько новых полезных приемов. Следующая программа представляет собой простую игру, в которой нужно мышкой поймать прыгающее окно.
#include <owl\framewin.h> #include <owl\applicat.h> #include <stdlib.h> #include <time.h> // Класс приложения. class TCatchApp : public TApplication { public: TCatchApp() : TApplication() {} void InitMainWindow(); }; // Класс главного окна. class TCatchWindow : public TFrameWindow { protected: int count, // Сколько прыжков совершило окно. timer, // Идентификатор таймера. score, // Счет игрока. game_over; // Флаг управления игрой. char score_string[20]; // Строка сообщений. public: TCatchWindow(TWindow *parent, const char far *title); ~TCatchWindow(); protected: void SetupWindow(); BOOL CanClose(); void EvLButtonDown(UINT, TPoint&); void EvRButtonDown(UINT, TPoint&); void EvTimer(UINT); DECLARE_RESPONSE_TABLE(TCatchWindow); }; DEFINE_RESPONSE_TABLE1(TCatchWindow,TFrameWindow) EV_WM_LBUTTONDOWN, EV_WM_RBUTTONDOWN, EV_WM_TIMER, END_RESPONSE_TABLE; // Реализация класса TCatchWindow. // TCatchWindow::TCatchWindow() // Это конструктор главного окна. TCatchWindow::TCatchWindow(TWindow *parent, const char far *title) : TFrameWindow (parent, title) { // Убрать различные ненужные части окна. Attr.Style &= ~(WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_THICKFRAME); // Установить начальное расположение и размеры окна. Attr.X = 100; Attr.Y = 100; Attr.W = 300; Attr.H = 100; } // TCatchWindow::~TCatchWindow() // Эта функция - деструктор основного окна, // вызываемый в момент его закрытия. // Здесь программа должна выполнить завершающие // действия по очистке. TCatchWindow::~TCatchWindow() { //Избавиться от игрового таймера. KillTimer(timer); } // TCatchWindow::SetupWindow() // Эта функция переопределяет функцию SetupWindow() // класса TFrameWindow и вызывается в промежутке // между созданием и отображением окна. // SetupWindow() - удобное место для завершения // действий по инициализации, что здесь и // продемонстрировано. void TCatchWindow::SetupWindow () { // ВНИМАНИЕ! Необходимо выполнить обычные установки. TFrameWindow::SetupWindow(); // Инициализировать датчик случайных чисел и флаг игры, randomize(); game_over = TRUE; // Установить таймер Windows. timer = SetTimer(1, 500, 0); if (timer == 0) MessageBox("Таймер не установлен!", "Ошибка", MB_OK); } //TCatchWindow::EvRButtonDown() // Эта функция отвечает на сообщение WM_RBUTTONDOWN // путем перезапуска игры. void TCatchWindow::EvRButtonDown(UINT, TPoint&) { // Начать игру, только если она еще не была запущена ранее. if (game_over) { // Сбросить флаг game_over и инициализирова переменные. game_over = FALSE; score = count = 0; // Изменить строку заголовка. SetWindowText("Поймай меня!"); } } // TCatchWindow::EvLButtonDown() // Эта функция обрабатывает сообщение WM_LBOTTONDOWN, // получаемое, только когда играющий щелкнул мышкой // внутри прыгающего окна. void TCatchWindow::EvLButtonDown(UINT, TPoint&) { // Реагировать на нажатие левой кнопки мыши, только // если игра не закончена. if (!game_over) { // Увеличить счет и подать звуковой сигнал. ++score; MessageBeep (0); } } // TCatchWindow::EvTimer() // Эта функция реагирует на сообщение WM_TIMER // путем перемещения окна в случайную позицию при // каждом событии, генерируемом таймером. Функция // также заканчивает игру, устанавливая флаг // game_over в значение TRUE после того, как // окно прыгнет 30 раз. void TCatchWindow::EvTimer(UINT) { // Переместить окно, только если игра не окончена. if (!game_over) { // Обеспечить, чтобы окно появилось поверх // всех других открытых окон. SetActiveWindow(); // Определять ширину и высоту экрана. int maxx = GetSystemMetrics(SM_CXSCREEN); int maxy = GetSystemMetrics(SM_CXSCREEN); // Вычислить случайную позицию на экране. int xpos = random(maxx-300); int ypos = random(maxy-100); // Переместить окно в новую позицию. MoveWindow(xpos, ypos, 300, 100, TRUE); // Закончить игру, если окно прыгнуло 30 раз. if (++count == 30) { // Установить флаг game_over и отобразить // окно сообщений по завершении игры. game_over = TRUE; wsprintf(score_string, "Количество попаданий: %d", score); MessageBox(score_string, "Игра окончена", MB_OK); // Вернуть окно к исходной позиции и размерам и сбросить текст. MoveWindow(100, 100, 300, 100, TRUE); SetWindowText("Щелкните правой кнопкой мыши для начала игры"); } } } // TCatchWindow::CanClose() // Функция, переопределяющая функцию CanClose() // класса TWindow и позволяющая пользователю // подтвердить, что он хочет закончить игру. BOOL TCatchWindow::CanClose() { // Спросить пользователя, закончить игру или нет? int result = MessageBox("Закончить игру?", "Выход", MB_YESNO | MB_ICONQUESTION); // Если CanClose(} возвращает TRUE, игра заканчивается. if (result == IDYES) return TRUE; else return FALSE; } // Реализация класса TCatchApp. // TCatchApp:: InitMainWindow() // Эта функция создает основное окно приложения. void TCatchApp::InitMainWindow() { TFrameWindow *wndw = new TCatchWindow(0, "Старт - щелчок правой клавишей мыши"); SetMainWindow(wndw); } int OwlMain (int, char*[]) { return TCatchApp().Run(); }
Когда вы запустите эту программу, то увидите окно, показанное на рисунке 1. В начале игры минимизируйте все окна, кроме игрового. Затем, находясь внутри игрового окна, нажмите правую кнопку мыши. Когда нажмете, окно изменит заголовок на "Поймай меня!" и начнет плясать по экрану. Ваша цель - поймать окно нажатием левой кнопки мыши, попав при этом в область окна пользователя. При каждой удачной попытке раздается сигнал и ваш счет увеличивается. После того, как окно совершит 30 прыжков, игра заканчивается, а в окне сообщений появляется ваш окончательный счет.
Рис.1. Результат работы приложения
Хотя эта игра кажется хитроумной, программировать ее просто. Все действия "зашиты" внутри класса TCatchWindow основного окна:
class TCatchWindow : public TFrameWindow { protected: int count, // Сколько прыжков совершило окно. timer, // Идентификатор таймера. score, // Счет игрока. game_over; // Флаг управления игрой. char score_string[20]; // Строка сообщений. public: TCatchWindow(TWindow *parent, const char far *title); ~TCatchWindow(); protected: void SetupWindow(); BOOL CanClose(); void EvLButtonDown(UINT, TPoint&); void EvRButtonDown(UINT, TPoint&); void EvTimer(UINT); DECLARE_RESPONSE_TABLE(TCatchWindow); }; DEFINE_RESPONSE_TABLE1(TCatchWindow,TFrameWindow) EV_WM_LBUTTONDOWN, EV_WM_RBUTTONDOWN, EV_WM_TIMER, END_RESPONSE_TABLE;
Этот класс содержит несколько защищенных членов-данных, хранящих число оконных перемещений, идентификатор таймера Windows, счет игрока и флаг, показывающий, активна ли игра в данный момент. Кроме того, внутри класса объявлен символьный массив, в котором хранится строка для окна сообщений, появляющегося в конце игры.
Помимо конструктора, в TCatchWindow объявлен деструктор, выполняющий необходимый сброс полей по завершении игры. TCatchWindow включает, кроме того, защищенные функции-члены, отвечающие на сообщения WM_RBUTTONDOWN, WM_LBUTTONDOWN и WM_TIMER, а также функции SetupWindow() и CanClose(). Назначение функции SetupWindow() будет объяснено позже. И наконец, класс объявляет и определяет таблицу откликов на сообщения WM_LBUTTONDOWN, WM_RBUTTONDOWN и WM_TIMER.
Начнем с конструктора основного окна. Он устанавливает местоположение и размеры окон. Он также отключает кнопки минимизации, максимизации и широкую рамку с помощью логического умножения инвертированных флагов управления стилем на поле Attr.Style:
Attr.Style &= ~(WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_THICKFRAME);
SetupWindow() является последней функцией, вызываемой перед появлением окна на экране. Это делает SetupWindow() удобным средством для всех видов инициализации, особенно инициализации, которую необходимо выполнить после построения окна:
void TCatchWindow::SetupWindow () { // ВНИМАНИЕ! Необходимо выполнить обычные установки. TFrameWindow::SetupWindow(); // Инициализировать датчик случайных чисел и флаг игры, randomize(); game_over = TRUE; // Установить таймер Windows. timer = SetTimer(1, 500, 0); if (timer == 0) MessageBox("Таймер не установлен!", "Ошибка", MB_OK); }
Класс TCatchWindow наследует функцию SetupWindow() из класса TFrameWindaw, который сам наследует ее из TWindow. При обращении TFrameWindow: : SetupWindow() функция вызывает датчик случайных чисел randowize() и устанавливает флаг game_over в значение TRUE.
Далее SetupWindow() запускает таймер Windows с помощью вызова OWL-функции SetTimer(). Эта функция работает идентично версии из Windows API, за исключением того, что не требует дескриптора окна. OWL добавляет дескриптор автоматически, когда передает запрос Windows-версии функции SetTimer(). Заметьте, что таймер нельзя запустить из конструктора окна, потому что построение окна еще должно быть завершено и оно пока не имеет дескриптора. Таймер, устанавливаемый в SetWindow(), посылает окну сообщение WM_TIMER каждые полсекунды.
Общий вид этой функции следующий:
uint SetTimer(uint timerId, uint timeout, TIMERPROC proc = 0);
где timerId - идентификатор таймера, timeout - количество милисекунд между событиями, proc - адрес функции, вызываемой при возникновении события. Если этот параметр равен нулю, то это сообщение помещается в очередь приложения.
Когда пользователь нажимает правую кнопку мыши, попадая при этом в игровое окно, событие получает функция EvRButtonDown() :
void TCatchWindow::EvRButtonDown(UINT, TPoint&) { // Начать игру, только если она еще не была запущена ранее. if (game_over) { // Сбросить флаг game_over и инициализирова переменные. game_over = FALSE; score = count = 0; // Изменить строку заголовка. SetWindowText("Поймай меня!"); } }
Эта функция сперва проверяет флаг game_over, чтобы убедиться, что игра еще активна. Если game_over равен FALSE, нажатие правой клавиши игнорируется (мы не хотим, чтобы пользователь нечаянно перезапустил игру, которая уже запущена). Если game_over равен TRUE, функция присваивает game_over значение FALSE, инициализирует счетчики score и count и вызывает SetWindowText() для изменения заголовка окна на "Поймай меня!". Функция SetWindowText(), унаследованная из TWindow, изменяет текущий текст в строке заголовка окна на текст, передаваемый ей в качестве единственного аргумента.
После того, как функция EvRButtonDown() присвоит флагу game_over значение FALSE, оконная функция EvTimer() может приступить к обработке сообщения WM_TIMER:
void TCatchWindow::EvTimer(UINT) { // Переместить окно, только если игра не окончена. if (!game_over) { // Обеспечить, чтобы окно появилось поверх // всех других открытых окон. SetActiveWindow(); // Определять ширину и высоту экрана. int maxx = GetSystemMetrics(SM_CXSCREEN); int maxy = GetSystemMetrics(SM_CXSCREEN); // Вычислить случайную позицию на экране. int xpos = random(maxx-300); int ypos = random(maxy-100); // Переместить окно в новую позицию. MoveWindow(xpos, ypos, 300, 100, TRUE); // Закончить игру, если окно прыгнуло 30 раз. if (++count == 30) { // Установить флаг game_over и отобразить // окно сообщений по завершении игры. game_over = TRUE; wsprintf(score_string, "Количество попаданий: %d", score); MessageBox(score_string, "Игра окончена", MB_OK); // Вернуть окно к исходной позиции и размерам и сбросить текст. MoveWindow(100, 100, 300, 100, TRUE); SetWindowText("Щелкните правой кнопкой мыши для начала игры"); } } }
Эта функция сначала проверяет значение флага game_over. Если игра неактивна, что соответствует значению TRUE флага, функция игнорирует любые сообщения таймера. В противном случае функция вызывает функцию SetActiveWindow(), наследуемую из TWindow, чтобы обеспечить активность игрового окна. (Тем самым исключается вероятность того, что игровое окно попадет под другое окно.) Затем с помощью двух обращений к Windows API-функции GetSystemMetrics() определяется ширина и высота экрана. Затем вычисляется новое случайное местоположение окна - и вызывается функция MoveWindow(), также унаследованная из TWindow, для перемещения окна в новую позицию. MoveWindow() требует в качестве параметров координаты х и уокна, его ширину, высоту и флаг, указывающий, требуется или нет перерисовка окна. MoveWindow() может быть вызвана также со ссылкой на объект TRect, содержащий координаты окна:
MoveWindow(const TRect&, BOOL repaint = FALSE);
Поскольку логический аргумент функции MoveWindow() принимает по умолчанию значение FALSE, он может быть опущен при вызове функции.
После перемещения окна EvTimer() увеличивает счетчик перемещений и проверяет, не было ли это перемещение последним. Если да, то флажку game_over присваивается значение TRUE, выводится окно сообщений, содержащее счет игры, и устанавливаются исходные позиции, размер окна и его заголовок "Щелкните правой кнопкой мыши для начала игры".
Когда пользователю удается щелкнуть левой кнопкой мыши внутри скачущего окна, начинает работу функция EvLButtonDown():
void TCatchWindow::EvLButtonDown(UINT, TPoint&) { // Реагировать на нажатие левой кнопки мыши, только // если игра не закончена. if (!game_over) { // Увеличить счет и подать звуковой сигнал. ++score; MessageBeep (0); } }
Если флаг game_over равен FALSE, означая тем самым, что игра еще неокончена, EvLButtonDown() увеличивает счет пользователя и вызывает Windows API-функцию MessageBeep(), посылающую звуковой сигнал. Если game_over равен TRUE, никаких действий не производится.
Когда пользователь решает закончить игру, деструктор окна получает возможность выполнить завершающие действия по очистке полей данных. В этом случае деструктор сбрасывает таймер с помощью вызова KillTimer():
TCatchWindow::~TCatchWindow()
{
//Избавиться от игрового таймера.
KillTimer(timer);
}
Заметим, что SetTimer(), KillTimer(), SetWindowText(), SetActiveWindow() и MoveWindow() являются OWL-версиями функций Windows API, определенными в классе TWindow, в то время как GetSystemMetrics() и MessageBeep() входят в состав Windows API. В своих многочисленных классах OWL содержит большинство, но не все функции Windows API.
Со следующего шага мы начнем знакомиться с оконной графикой.