Крестики-нолики — одна из старейших игр, известных человеку. Она не сложна, и я думаю, что большинство людей знакомы с ее правилами. В нашей версии крестиков-ноликов (программа будет называться Tic-Tac-Toe — таково английское на звание игры) мы изучим подробнее внутренние особенности графики в CBuilder, процесс пользовательского ввода/вывода и возможности формы в рисовании и отображении объектов.
На рис. 3.4 показана законченная форма с расположенными на ней компонентами VCL, необходимыми для успешного функционирования приложения. Ну да, она пуста. Вся работа по прорисовке формы будет сделана самим приложением.
Рис. 3.4. Форма программы Tic-Tac-Toe (крестики-нолики)
Замечание
Исходный текст программы Tic-Tac-Toe (крестики-нолики) находится на прилагаемом компакт- диске.
Шаг первый: создаем изображения
В этом примере мы собираемся динамически создавать растровые рисунки и их образы (images) в программе. Сами рисунки предельно просты (собственно, это пресловутые крестик и нолик), но на их примере мы узнаем, как можно использовать объект TBitmap для создания собственных рисунков непосредственно в приложении, не полагаясь на внешние файлы или другие ресурсы. Для начала надо объявить некоторые переменные в заголовочном файле приложения. Добавьте в заголовочный файл Unit1.h следующие описания:
class TForm1 : public TForm
{
__published: // IDE-managed Components void __fastcall FormPaint(TObject *Sender;
void __fastcall FormMouseDown(TObject *Sender,
Пример номер два: крестики-нолики TMouseButton Button, TShiftState Shift, int X, int Y); private: // User declarations
Graphics::TBitmap *FpXBitmap; Graphics::TBitmap *FpYBitmap; int FnGameBoard[3][3];
int FnWhichPlayer; public: // User declarations
__fastcall TForm1(TComponent* Owner);
};
Два объекта TBitmap будут использованы для внутренней отрисовки растровых рисунков и их отображения на форме в соответствии с ходами игроков. Массив FnGameBoard используется для хранения текущих выбранных клеток игрового поля и принадлежности этих клеток. И наконец, переменная класса FnWhichPlayer используется для отслеживания, чей ход.
После того как вы добавили все переменные, пришло время обратиться к конструктору формы. В нем мы инициализируем все переменные — члены класса и проделаем работу по инициализации объектов — растровых рисунков (TBitmap) в системе. Давайте сначала взглянем на код, а потом будем разбираться, как он работает:
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
// Присваиваем право хода первому игроку
FnWhichPlayer = 1;
// Инициализируем игровое поле FpXBitmap = new Graphics::TBitmap; FpXBitmap->Width = ClientWidth/3 – 2; FpXBitmap->Height = ClientHeight/3 – 2;
// Заливаем рисунок цветом формы
FpXBitmap->Canvas->Brush->Color = Color; Trect r;
r.Left = 0;
r.Top = 0;
r.Right = FpXBitmap->Width; r.Bottom = FpXBitmap->height; FpXBitmap->Canvas->FillRect( r); FpYBitmap = new Graphics::TBitmap;
FpYBitmap->Width = ClientWidth/3 – 2; FpYBitmap->Height = ClientHeight/3 – 2;
// Заливаем рисунок цветом формы FpYBitmap->Canvas->Brush->Color = Color; FpYBitmap->Canvas->FillRect( r);
// Отображаем рисунок X (крестик) FpXBitmap->Canvas->MoveTo(0,0);
FpXBitmap->Canvas->LineTo(FpXBitmap->Width, FpXBitmap->Height); FpXBitmap->Canvas->MoveTo(FpXBitmap->Width,0);
FpXBitmap->Canvas->LineTo(0, FpXBitmap->Height);
// Отображаем рисунок Y (нолик) FpYBitmap->Canvas->
Ellipse(0,0,FpYBitmap->Width,FpYBitmap->Height);
}
Первая пара блоков конструктора сравнительно понятна. Перед тем как двигаться дальше, мы присваиваем переменным класса разумные начальные значения. Трудная часть начинается с создания объекта TBitmap.
Жизнь и приключения объекта TBitmap C++ Builder
Почему мы должны устанавливать все эти свойства TBitmap при его создании? Раньше все, что мы делали, — это создавали новый объект и потом либо загружали его с диска, либо ассоциировали с ним растровый рисунок.
Как и все порядочные растровые рисунки в Windows, объект TBitmap начинает жизнь с растрового рисунка Windows 1ґ1, и, чтобы что-нибудь в нем нарисовать, мы должны «растянуть» его до размера, который собираемся использовать. В данном случае мы должны увеличить каждый из двух растровых рисунков до одной трети клиентской области нашей формы. Почему клиентской? Дело в том, что свойство Width (ширина) формы включает в себя рамки вокруг формы, а свойство Height (высота) формы включает в себя панель заголовка. Если использовать эти свойства, первые две кнопки выглядели бы шире, чем третья. Поскольку это не то, что нам надо, придется поискать другие пути. Объекты CBuilder имеют два типа высоты и ширины. Свойства ClientWidth и ClientHeight представляют собой соответственно ширину и высоту клиентской области формы. Клиентская область — это то, что осталось бы на форме, если бы из нее удалили меню, рамки, заголовки и панель состояния.
После того как мы определили свойства Width и Height, объекты — растровые рисунки (TBitmap) можно рассматривать как простые графические объекты, которые ожидают, чтобы мы их заполнили. При создании они имеют белый фон (background). Поскольку это будет плохо смотреться на стальном фоне нашей формы, мы должны переустановить цвет фона, используя свойство Brush (дословно — кисть) рисунка, у которого есть собственное свойство Color (цвет). Свойство Color свойства Brush будет использовано для фона при всех операциях, связанных с рисованием, для данного рисунка. В нашем случае мы должны залить белый рисунок цветом фона формы. Это сделано посредством метода FillRect. Для использования FillRect инициализируйте прямоугольник (объект TRect) с границами, которые хотите заполнить. В данном случае мы используем границы самого рисунка, поскольку хотим залить его целиком.
r.Left = 0;
r.Top = 0;
r.Right = FpXBitmap->Width; r.Bottom = FpXBitmap->Height;
После того как мы получили прямоугольник для заливки и цвет, указанный в свойстве Brush свойства Canvas объекта, который мы хотим залить (в нашем случае растрового рисунка), мы возбуждаем метод FillRect для этого объекта (Bitmap->Canvas), и все остальное делается автоматически.
Следующим шагом после заливки рисунка будет рисование того образа (image), который будет отображен на рисунке. Для этого вы можете использовать любой из определенных методов рисования Canvas или же передать свойство Canvas->Handle любой стандартной программе
Windows, которая окажется под рукой. Это удобно, когда вы хотите использовать графическую функцию третьей программы в вашем приложении на CBuilder.
Для растрового рисунка X (крестик) мы используем стандартные команды Canvas MoveTo и LineTo для рисования двух диагональных линий, образующих требующийся нам крест. Для растрового рисунка Y (нолик) используется другой метод объекта Canvas, который называется Ellipse. Ellipse отобразит эллиптическую фигуру на заданном Canvas. Поскольку наши рисунки почти квадратные, результат функции Ellipse будет очень похож на круг. Чем ближе будут значения Width и Heidht, тем более приближенным к кругу будет и результат.
Итак, теперь мы имеем законченные растровые рисунки, которые можно использовать так же, как если бы мы загрузили их из файла или ассоциировали в редакторе компонентов. Это очаровательная черта объектно-ориентированного программирования. Вне зависимости от того, как вы добились промежуточного результата, исходить из него вы будете совершенно одинаковым образом. В такой гибкости и есть мощь объектно-ориентированного программирования.
Замечание
CBuilder также располагает методами для сохранения вашего рисунка на диске. Вы можете создать настоящий графический редактор на основе системы растровых рисунков CBuilder. Для сохранения растрового рисунка на диске создайте объект — поток файлового ввода/вывода (file stream), используя класс TFileStream, а потом используйте метод SaveToStream объекта TBitmap для записи последнего на диск.
В конструкторе также вызывается вспомогательная функция ResetBoard. В ней нет никаких сюрпризов. Все, что мы делаем, — это присваиваем всем клеткам игрового поля определенное сигнальное значение, в данном случае 0.
void TForm1::ResetBoard(void)
{
// Инициализируем игровое поле
for ( int i=0, i<3, ++i ) for ( int j=0, j<3, ++j ) FnGameBoard[i][j] = 0;
}
Худшая часть задачи осталась позади. Мы динамически создали объект — растровый рисунок, сделали его нужного размера и нарисовали в его поле (Canvas) те изображения, которые должен видеть пользователь. Теперь следующий шаг — это показать рисунок пользователю. Сначала давайте займемся рисованием формы, а потом перейдем к обработке ввода пользователя.
Добавьте в форму обработчик для события OnPaint. Самый простой способ это сделать — перейти на страницу Events в Object Inspector, найти там событие OnPaint и дважды щелкнуть мышью в клеточке сетки справа от него. CBuilder автоматически создаст новый обработчик с корректным названием. В общем случае, присвоенное ему имя будет Formxxx, где xxx — это имя события без «On». Поэтому для события OnPaint корректное имя будет FormPaint. Вот как выглядит код для обработчика события FormPaint:
void __fastcall TForm1::FormPaint(TObject *Sender)
{
// Сначала рисуем вертикальные линии на форме
Canvas->MoveTo( ClientWidth/3,0 );
Canvas->LineTo( ClientWidth/3,ClientHeight );
Canvas->MoveTo( (2*ClientWidth)/3,0);
Canvas->LineTo( (2*ClientWidth)/3, ClientHeight );
// Теперь горизонтальные
Canvas->MoveTo( 0, ClientHeight/3 );
Canvas->LineTo( ClientWidth, ClientHeight/3 ); Canvas->MoveTo( 0, (2*ClientHeight)/3 );
Canvas->LineTo( ClientWidth, (2*ClientHeight)/3 );
// Отображаем рисунки по клеткам
for ( int i=0, i<3, ++i ) for ( int j=0, j<3, ++j )
{
int nXPos = (ClientWidth/3)*j + 1; int nYPos = (ClientHeight/3)*i + 1;
// Если в клетке – крестик
if ( FnGameBoard[i][j] == 1 )
{
Canvas->Draw(nXPos, nYPos, FpXBitmap );
}
else
// Если в клетке – нолик
if (FnGameBoard[i][j] == 2 )
{
Canvas->Draw(nXPos, nYPos, FpYBitmap );
}
}
}
Никаких особых сюрпризов в этом коде нам не встретилось. Мы рисуем игровое поле, передвигаясь по клиентской области формы и рисуя горизонтальные и вертикальные линии. Потом мы проверяем клетки на предмет их принадлежности первому или второму игроку. Если клетка принадлежит первому игроку, мы рисуем в ней крестик, используя для этого растровый рисунок X, созданный нами в конструкторе. Если клетка принадлежит второму игроку, мы рисуем в ней нолик, используя для этого растровый рисунок Y, созданный нами в конструкторе. Позиция рисунка — это позиция квадрата со смещением в один пиксел, чтобы не затереть линии поля.
Обработка щелчков мыши
На данном этапе мы завершили ту часть, которая связана с выводом на экране. Теперь перейдем к обработке собственно процесса игры. Нам надо отследить каждый щелчок мыши на игровом поле, определить клетку, в которой была нажата мышь, и закрепить эту клетку за тем игроком, чей ход. Конечно, при этом надо проверить, не занята ли уже клетка.
Форма не содержит события щелчка мыши1. Тем не менее существуют события нажатия кнопки мыши и отпускания нажатой кнопки мыши. Мы привяжем наш обработчик к событию MouseDown (нажатие кнопки мыши). Обычно не имеет значения, которое событие вы обрабатываете, если, конечно, у вас не предусмотрена разная конкретная реакция на события MouseDown и MouseUp (как было у нас в примере Scribble из предыдущей главы). Обращение к

1 Совершенно непонятно, почему автор так считает. У формы ЕСТЬ событие OnClick (возникает при щелчке мыши). В последующем коде все ссылки на событие OnMouseDown можно заменить на OnClick. Тем не менее пускай все останется как есть. — Примеч. перев.
событию MouseDown обусловит чуть более быструю реакцию программы, чем обращение к событию MouseUp, но это не будет заметно среднему пользователю.
Добавьте обработчик для события MouseDown, дважды щелкнув на этом событии в окне Object Inspector. Следующий код добавьте в созданный при этом в исходном файле обработчик события FormMouseDown:
void fastcall TForm1::FormMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int x, int y)
{
int nRow, int nCol,
// Определяем, что за клетка
if ( X < ClientWidth/3 ) nCol = 0;
else
if ( X < (2*ClientWidth)/3 ) nCol = 1;
else
nCol = 2;
if ( Y < ClientHeight/3 ) nRow = 0;
else
if ( Y < (2*ClientHeight)/3 ) nRow = 1;
else
nRow = 2;
// Проверяем, не занята ли клетка
if ( FnGameBoard[nRow][nCol] != 0 )
{
MessageBeep(0);
}
else // Нет – присваиваем клетку этому игроку
{
FnGameBoard[nRow][nCol] = FnWhichPlayer;
// Передаем ход
if ( FnWhichPlayer == 1 ) FnWhichPlayer = 2;
Пример программы игры в крестики-нолики | Программирование ... |
Мы рассмотрим простейшую программу игры в крестики-нолики. Матрица
для игры в крестики-нолики имеет вид двумерного массива символов 3 на 3. http://www.c-cpp.ru/books/primer-programmy-igry-v-krestiki-noliki |
else
FnWhichPlayer = 1; Invalidate();
// Проверим, не выиграл ли кто
if ( CheckForWinner() )
{
ResetBoard(); FnWhichPlayer = 1; Invalidate();
}
}
}
В этом обработчике сначала проверяется, какую клетку выбрал пользователь. Это выполняется сравнением X и Y координат , которые передает методу CBuilder, с клетками нашей сетки. Мы просто проверяем, где произошел щелчок — в первой, второй или третьей трети сетки по вертикали, а потом и по горизонтали. Преобразовав полученные значения, мы получаем две координаты — строку и столбец в сетке. Это отлично подходит для нашего метода хранения данных, которые представлены в виде двумерного массива.
После того как определены строка и столбец, игровое поле проверяется на предмет того, не занята ли уже выбранная клетка. Если это так, то вызывается небезызвестный вам метод MessageBeep, показывающий пользователю недопустимость повторного выбора. Если же клетка пуста, она присваивается ходящему игроку. Наконец, после этого мы вызываем еще один метод формы для проверки, не победил ли кто. Вот код для этого метода:
BOOL Tform1::CheckForWinner(void)
{
int Winner;
for ( int nPlayer = 1; nPlayer<3; ++nPlayer )
{
// Проверяем построчно
for ( int nRow = 0; nRow<3; ++nRow)
{
// Предполагаем, что этот игрок выиграл
nWinner = nPlayer;
// Проверяем все столбцы. Если хотя бы в одном
// нет идентификатора игрока, он не победил здесь
for (int nCol = 0; nCol<3; ++nCol )
if ( FnGameBoard[nRow][nCol] != nPlayer ) nWinner = 0;
if ( nWinner != 0 )
{
String s = "Игрок " + String(nWinner) + " выиграл"; MessageBox(NULL, s.c_str(), "Победитель", MB_OK ); return true;
}
}
// Теперь проверяем по столбцам
for ( int nCol = 0; nCol<3; ++nCol )
{
// Предполагаем, что этот игрок выиграл
nWinner = nPlayer;
// Проверяем все строки. Если хотя бы в одной
// нет идентификатора игрока, он не победил здесь
for ( int nRow = 0; nRow<3; ++nRow )
if FnGameBoard[nRow][nCol] != nPlayer ) nWinner = 0;
if ( nWinner != 0 )
{
String s = "Игрок " + String(nWinner) + " выиграл"; MessageBox(NULL, s.c_str90, "Победитель", MB_OK ); return true;
}
}
// Наконец, проверяем диагонали
if ( FnGameBoard[0][0] == nPlayer && FnGameBoard[1][1] == nPlayer && FnGameBoard[2][2] == nPlayer )
{
String s = "Игрок " + String(nPlayer) + " выиграл"; MessageBox(NULL, s.c_str90, "Победитель", MB_OK ); return true;
}
if ( FnGameBoard[0][2] == nPlayer && FnGameBoard[1][1] == nPlayer && FnGameBoard[2][0] == nPlayer )
{
String s = "Игрок " + String(nPlayer) + " выиграл"; MessageBox(NULL, s.c_str90, "Победитель", MB_OK ); return true;
}
}
// Если дошли досюда, проверяем, есть ли еще
// пустые клетки (не конец ли игры) for ( int nRow = 0; nRow<3; ++nRow )
{
for ( int nCol = 0; nCol <3; ++nCol )
if ( FnGameBoard[nRow][nCol] == 0 ) return false;
}
// Уведомляем о конце игры
MessageBox(NULL, "Боевая ничья!", "Конец игры", MB_OK ); return true;
}
Этот метод интересен лишь с точки зрения использования в нем объекта String (строка) для формирования уведомления победителю. String — один из самых полезных классов в CBuilder, вам надо привыкнуть к нему. Объекты String гораздо более гибкие, чем char* или массив символов (char []), которые вам, наверное, приходилось использовать в C или C++ в других средах. String также является частью библиотеки стандартных шаблонов, о которой мы поговорим гораздо подробнее чуть ниже.
Самой приятной из возможностей String является возможность преобразовывать в строку вещи, не являющиеся строками или символами. Передав аргумент типа integer в конструктор String, мы получаем представление этого числа в виде строки. Два объекта String могут быть объединены с помощью оператора «+». Используя две эти возможности, мы и создали строку вывода о победе первого игрока из строк «Игрок» и «выиграл!» и цифры 1, что должно показаться вам очарователь ным, если вы привыкли использовать sprintf и волноваться о типах аргументов и длине строк.
Последняя проверка в методе проводится на предмет конца игры. Если все клетки заполнены, то игра окончена вничью. К сожалению, это очень частый исход в игре крестики-нолики — я уверен, вы знаете это не хуже меня.
Наша игра уже более-менее закончена, но нам надо освободить ресурсы, отведенные для TBitmap в конструкторе объекта. Лучшее место для этого — деструктор объекта, и это как раз то, что мы должны сделать. Вот код деструктора:
__fastcall TForm1::~TForm1(void)
{
delete FpXBitmap; delete FpYBitmap;
}
Заметьте, что мы используем модификатор fastcall для деструктора. ~TForm1 — это переопределение деструктора базового класса TForm. Поскольку TForm — объект VCL, а все методы VCL должны быть определены с модификатором __fastcall, то вам также надо добавлять это в свои переопределенные методы. Если вы этого не сделаете, то компилятор выдаст вам сообщение об ошибке «Virtual function TForm1::~TForm1 conflicts with base class Forms::TForm» («Виртуальная функция конфликтует с базовым классом»). В случае появления таких ошибок проверьте модификаторы.
Все, что нам осталось сделать, — это дополнить заголовочный файл описаниями методов, которые мы добавили в форму в исходном файле. Вот обновленный заголовочный файл Unit1.h, все изменения в котором показаны подсветкой:
//———————————————————
#ifndef Unit1H
#define Unit1H
//———————————————————
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
//———————————————————
class TForm1 : public TForm
{
__published: // IDE-managed components void __fastcall FormPaint(TObject *Sender);
void __fastcall FormMouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y);
private: // User declarations
Graphics::TBitmap *FpXBitmap;
Graphics::TBitmap *FpYBitmap;
int FnGameBoard[3][3];
int FnWhichPlayer;
void ResetBoard(void);
BOOL CheckForWinner(void);
public: // User declarations
__fastcall TForm1(TComponent *Owner);
virtual fastcall ~TForm1(void);
};
//——————————————————–
extern TForm1 *Form1;
//——————————————————–
#endif
Запускаем игру
Последний шаг в программировании игр всегда
самый веселый, ибо
это тестирование
программного продукта. Запустите программу и заполните несколько клеток. Попробуйте выбрать
одну и ту же клетку дважды. Заполните клетки так, чтобы победная комбинация находилась в
строке, потом в столбце, потом на диагонали. Убедитесь, что все варианты работают. Наконец, заполните все игровое поле так, чтобы на нем не было победной комбинации, чтобы удостовериться в том, что проверка окончания игры работает. После того как все проверили, пригласите друга или ребенка на игру и наслаждайтесь. На рис. 3.5 показан типичный пример игры в процессе, с несколькими крестиками и ноликами, а на рис. 3.6 показана игра, завершившаяся победой одного из игроков.
Рис. 3.5. Типичный пример игры крестики-нолики
Рис. 3.6. Победа первого игрока
Что мы узнали в этой главе?
CBuilder предоставляет богатый ассортимент графических объектов и компонентов, которые позволяют легко определять и отображать на экране в вашем приложении растровые рисунки, иконки и другую графику.
Вот кое-что из того, что мы освоили в этой главе:
· Загрузка растровых рисунков (bitmap) прямо из файла во время исполнения программы.
· Отображение растровых рисунков на экране при посредстве объекта Canvas.
· Создание растрового рисунка во время исполнения и рисование в его поле.
· Обработка вывода графики на экран во время исполнения посредством рисования прямо в поле формы.
· Я надеюсь, вы кое-что усвоили о графической подсистеме CBuilder. В следующей главе мы поглубже исследуем VCL (Visual Component Library, библиотека визуальных компонентов) CBuilder и приоткроем новые тайны компонентов.
Источник: Теллес М. – Borland C++ Builder. Библиотека программиста – 1998