Теперь мы займемся прорисовкой на экране игровых элементов.
Продолжаем нашу работу. В предыдущих трех статьях мы познакомились с основами программирования в среде Visual C++ на простом примере создания Windows-программы игры "крестики-нолики" (англ. название - tic-tac-toe). Действие происходит на квадратном игровом поле в виде сетки 5х5, и цель игры - выстроить в линию по горизонтали, вертикали или диагонали последовательность из четырех значков Х или О.
Сначала мы запустили "мастера" AppWizard, который подготовил исходный текст, используемый в дальнейшем в качестве прототипа нашей программы. Затем мы дополнили его view-класс обработчиком событий OnLButtonDown, который при щелчках левой клавишей мыши на клетках игрового поля выводит на экран некоторое сообщение. Изменениям подвергся также класс document, в который были включены средства для записи сведений о вводимых пользователями крестиках и ноликах и по отслеживанию очередности хода. В данной статье мы ставим перед собой задачу организовать взаимодействие классов document и view, чтобы при щелчке на пустой клетке игрового поля в ней появлялся значок Х или О. На этом разработка нашей программы будет в основном завершена, за исключением некоторых деталей, речь о которых пойдет в следующем выпуске.
Если мы хотим, чтобы наша программа рисовала значки Х или О в ответ на щелчки мышью, необходимо внести некоторые изменения в функцию OnLButtonDown класса view. В ней уже предусмотрены средства для проверки попадания указателя мыши на игровое поле, что позволяет программе не реагировать на щелчки за его пределами. Теперь ту часть программы, которая при щелчке на какой-то клетке выводит на экран сообщение, необходимо переделать, чтобы она заполняла пустую клетку значком Х или О.
На лист. 1 показан окончательный вариант обработчика OnLButtonDown. Теперь при щелчке на клетке игрового поля обновленная версия этого обработчика обращается к функции GetSquare класса document, чтобы выяснить, оставлен ли в ней значок Х или О. Если GetSquare передает в качестве результата 0, значит клетка пуста, и OnLButtonDown вызывает функцию IsItXsTurn, чтобы получить информацию о том, какой значок нужно вставить - Х или О. Далее с помощью функций AddX или AddO класса document в клетку заносится крестик или нолик. Затем выполняется функция DrawX или DrawO класса view.
Лист. 1. Окончательный вариант функции OnLButtonDown.
void CTicView::OnLButtonDown(UINT nFlags, CPoint point) { // // Получить указатель на класс document. // CTiDoc* pDoc = GetDocument (); // // Получить контекст устройства (DC) и // преобразовать записанные в переменной point значения // к логической системе координат. // CClientDC dc (this) dc.SetMapMode (MM_LOENGLISH); dcDPtoLP (&point); // // Проверить попадание указателя мыши // на клетку игрового поля. // Если да, то нарисовать Х или О. // BOOL bQuit = FALSE; for (int i=0; i<5 && !bQuit; i++) { for (int j=0; j<5 &&!bQuit; j++) { if (m_rect[i][j].PtInRect (point)) { if (pDoc->GetSquare (i, j) == 0 { if (pDoc->IsItXsTurn ()) { pDoc->AddX (i, j); DrawX (&dc, &m_rect[i][j]); } else { pDoc->Add0 (i, j); Draw0 (&dc, &m_rect[i][j]); } } bQuit = TRUE; } } } // // Выполнить функцию базового класса. // CView::OnLButtonDown(nFlags, point); }
Если теперь после внесения показанных на лист. 1 исправлений заново скомпоновать программу, то компилятор выдаст сообщения об ошибках, поскольку нет определений для двух функций класса view - DrawX и DrawO. Теперь мы займемся ими.
На лист. 2 показан окончательный вид функций DrawX и DrawO. Обе они входят в view-класс CTicView, а в процессе рисования используют функции вывода класса CDC. Сначала DrawX создает инструмент для рисования - красный карандаш (pen) для линий толщиной 10 условных единиц (для типа отображения MM_LOENGLISH одна условная единица эквивалентна 0,1 логического дюйма):
CPen pen (PS_SOLID, 10, RGB(255, 0, 0));
Лист. 2. Функции DrawX и DrawO класса view.
void CTicView::DrawX (CDC* pDC, CRect* pRect) { // Скопировать параметры переданного прямоугольника и // уменьшить его размеры. // CRect rect; rect.CopyRect (pRect); rect.DeflateRect (10, 10); // // Создать красный карандаш и нарисовать им Х. // CPen pen (PS_SOLID, 10, RGB (255, 0, 0)); CPen* pOldPen = pDC->SelectObject (&pen); pDC->MoveTo (rect.left, rect.top); pDC->LineTo (rect.right, rect.bottom); pDC->MoveTo (rect.left, rect.bottom); pDC->LineTo (rect.right, rect.top); pDC->SelectObject (pOldPen); } void CTicView::Draw0 (CDC* pDC, CRect* pRect) { // Скопировать параметры переданного прямоугольника и // уменьшить его размеры. // CRect rect; rect.CopyRect (pRect); rect.DeflateRect (10, 10); // // Создать синий карандаш и нарисовать им 0. // CPen pen (PS_SOLID, 10, RGB (0, 0, 255)); CPen* pOldPen = pDC->SelectObject (&pen); pDC->SelectStockObject (NULL_BRUSH); pDC->Ellipse (rect); pDC->SelectObject (pOldPen); }
Затем с помощью следующих предложений рисуются две пересекающиеся линии:
pDC->MoveTo (rect.left, rect.top); pDC->LineTo (rect.right, rect.bottom); pDC->MoveTo (rect.left, rect.bottom); pDC->LineTo (rect.right, rect.top);
Аналогичным образом функция DrawO создает свой инструмент для рисования - карандаш синего цвета для линий толщиной 10 условных единиц:
CPen pen (PS_SOLID, 10, RGB(0, 0, 255));
и рисует O с помощью MFC-функции CDC::Ellipse
pDC->Ellipse (rect);
В обоих случаях до начала каких-либо операций рисования нужный карандаш выбирается в контекст устройства с помощью функции CDC::SelectObject и возвращается обратно по завершении работ с ним:
CPen* pOldPen = pDC->SelectObject (&pen); . . . pDC->SelectObject (pOldPen);
CPen - это отдельный MFC-класс, отвечающий за работу с GDI-карандашами (pen) - логическими объектами для рисования прямых и кривых линий. Если нужный карандаш еще не выбран в контекст устройства, то при работе функций LineTo, Ellipse и других CDC-функций будет использоваться карандаш, принятый для него по умолчанию, который вычерчивает линию черного цвета толщиной всего 1 пиксел. Если в контекст устройства выбирается новый карандаш или любой другой GDI-объект, то обязательно следует удалить его; это нужно сделать, пока соответствующий этому контексту CDC-объект еще не удален и доступен. Освободить текущий контекст от своего карандаша означает просто выбрать вместо него другой. Именно поэтому в DrawX и DrawO сохраняется CPen-указатель, полученный при обращении к функции SelectObject. Он является ссылкой на карандаш, предварительно выбранный для данного контекста. Передача этого указателя в SelectObject до окончания работы данной функции позволяет выбрать для текущего контекста карандаш, принятый для него по умолчанию, и тем самым освободить свой CPen-инструмент.
Когда функция CDC::Ellipse рисует круг или эллипс, то его контур проводится выбранным в данный контекст устройства карандашом; а внутренняя область заполняется текущей кистью. Кисть (brush) - это специальный GDI-объект, предназначенный для закрашивания областей на экране. Так же, как карандаш, она должна быть выбрана в контекст устройства, прежде чем будут вызваны использующие ее функции, например CDC::Ellipse. Мы хотим, чтобы внутренняя область значка О не закрашивалась, поэтому в view-функции DrawO перед обращением к CDC::Ellipse выбирается в контекст NULL-кисть - не заполняющая область рисования:
pDC->SelectStockObject (NULL_BRUSH);
NULL_BRUSH - это лишь один из целой группы возможных объектов для рисования, который можно выбрать с помощью функции CDC::SelectStockObject для рабочего контекста. После завершения работ с кистью в DrawO никаких действий с отменой выбора этой кисти не предпринимается. Дело в том, что в отличие от обычных инструментов для рисования делать это для штатных (stock) инструментов не обязательно.
Дополнив view-класс функциями DrawX и DrawO, представленными на лист. 2, можно повторить компоновку программы и посмотреть, как она работает. Теперь при щелчке левой клавишей мыши на пустой клетке игрового поля должен появляться красный значок Х или голубой значок О. При щелчках на уже заполненной клетке не должно быть никакой реакции.
Однако есть еще одна небольшая проблема. Сделайте следующее: проставьте на игровом поле несколько значков Х и О, а затем уменьшите окно. Если теперь восстановить окно в прежних размерах, то окажется, что все значки Х и О пропали. В чем причина? Все очень просто: мы учли необходимость прорисовки значков Х и О функцией OnLButtonDown в ответ на щелчки мышью. Однако необходимо еще обновить view-функцию OnDraw, чтобы эти значки возобновлялись при восстановлении окна. Все необходимые средства уже имеются. Можно воспользоваться функцией GetSquare класса document, чтобы выяснить содержимое каждой клетки и отобразить нужный значок с помощью уже подготовленных функций DrawX и DrawO.
На лист. 3 показана следующая редакция функции OnDraw. В прежнем варианте во вложенном цикле for просто перебирались и заново прорисовывались путем обращения к функции CDC::Rectangle все клетки. Теперь он сначала перерисовывает квадраты, а затем, если от функций CTicDoc::GetSquare получено ненулевое значение, обращается к функции DrawX или DrawO. Теперь, когда изменения в OnDraw внесены, попробуйте вновь: вставьте несколько значков Х и О, сверните, а затем восстановите окно, чтобы программа смогла выполнить повторную прорисовку. На этот раз окно должно принять прежний вид, такой, каким он был до свертывания.
Лист. 3. Окончательный вариант функции OnDraw.
void CTicView::OnDraw(CDC* pDC) { CTicDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // // Задать режим отображения MM_LOENGLISH, // в котором за единицу измерения толщины принимается 0,01 дюйма. // pDC->SetMapMode (MM_LOENGLISH); // // Прорисовать игровое поле. // for (int i=0; i<5; i++) { for (int j=0; j<5; j++) { pDC->Rectangle (m_rect[i][j]); BYTE bVal = pDoc->GetSquare (i,j); if (bVal == 1) // Проставить Х DrawX (pDC, &m_rect[i][j]); else if (bVal == 2) // Проставить О Draw0 (pDC, &m_rect[i][j]); } } }
Наш долгий путь, наконец, увенчался созданием работающей программы! В самом деле, как ни странно, практически все уже сделано. Например, работает команда New из меню File, хотя специально для этого мы не составили ни единой строчки. Попробуйте выбрать ее. В ответ будут удалены все присутствовавшие на игровом поле значки Х и О. Эта команда действует благодаря наличию в MFC обработчика команды File | New, который самостоятельно вызывает функцию OnNewDocument, предусмотренную в нашем классе document. Как мы упоминали в предыдущей статье, CTicDoc::OnNewDocument освобождает все клетки игрового поля. После исполнения OnNewDocument функция MFC очищает экран и повторяет его прорисовку. Теперь view-функция OnDraw рисует пустые клетки. Работают команды Print (Печать) и Preview (Предварительный просмотр), также благодаря средствам MFC.
В следующей статье этой серии мы завершим разработку нашей программы. Мы займемся командами меню File - Open (Открыть), Save (Сохранить) и Save As (Сохранить как). Основную нагрузку возьмет на себя MFC; наша же задача будет на удивление простой.