Шаг 7.
Библиотека OWL.
Пример приложения

    На этом шаге мы рассмотрим игру "Поймай окно!".

    Поскольку теперь мы умеем создавать окна и обрабатывать сообщения 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 - адрес функции, вызываемой при возникновении события. Если этот параметр равен нулю, то это сообщение помещается в очередь приложения.


    Замечание. Windows поддерживает несколько таймеров, используемых для получения события WM_TIMER через заданные интервалы времени. В стандартных Windows-программах установка таймера производится вызовом функции Windows API SetTimer (). В качестве аргументов функция требует дескриптор окна для получения события WM_TIMER, номер ID идентификатора таймера, количество миллисекунд между событиями и адрес функции, вызываемой при получении события. Если адрес функции равен NULL, Windows помещает сообщения таймера в очередь сообщений приложения. Снятие таймера осуществляется вызовом функции Windows API KillTimer(), которая требует в качестве единственного аргумента ID таймера. Обе функции, SetTimer() и KillTimer(), включены в OWL-класс TWindow, поэтому в OWL-цриложениях нет надобности вызывать API-версии этих функций.

    Когда пользователь нажимает правую кнопку мыши, попадая при этом в игровое окно, событие получает функция 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.

    Со следующего шага мы начнем знакомиться с оконной графикой.




Предыдущий шаг Содержание Следующий шаг