На этом уроке мы поговорим об архитектуре «Document/View» и о том, как и зачем она используется, а также начнем уже писать код нашей игры.
Архитектура «Document/View»
Архитектура «Document/View» — это очень интересная парадигма в программировании, в которой мы отделяем фактические данные нашего приложения от отображения их пользователю. Document содержит все данные, в то время как View получает эти данные из Document и отображает их пользователю в том или ином виде. Здесь наши данные — это игровая доска, время, необходимое для завершения игры, и другая соответствующая информация. Наш View отображает игровое поле в виде цветных блоков и позволяет пользователю кликать по блокам. View обрабатывает взаимодействие с пользователем и изменяет игровые данные в документе соответствующим образом, а затем обновляется, чтобы отразить изменения. Затем цикл повторяется.
При выборе архитектуры «Document/View» в мастере приложений MFC, автоматически создается вся необходимая кодовая база вместе со всеми нужными механизмами, относящимися к использованию данной технологии.
The Document: Хранение данных
Наконец-то пришло время начать писать исходный код нашей игры. Прежде, чем мы сможем отобразить что-либо на экране, нам понадобятся данные.
Сначала мы создаем класс, представляющий нашу игровую доску — CSameGameBoard
. Создайте новый класс, щелкнув правой кнопкой мыши по проекту SameGame в "Обозреватель решений"
и выбрав "Добавить" > "Класс..."
. Укажите CSameGameBoard
в качестве имени класса.
Теперь давайте заполним класс игровой доски. Вот код для заголовочного файла SameGameBoard.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
#pragma once class CSameGameBoard { public: // Конструктор по умолчанию CSameGameBoard(void); // Деструктор ~CSameGameBoard(void); // Функция для рандомной расстановки блоков в начале игры void SetupBoard(void); // Получаем цвет в определенном участке игрового поля COLORREF GetBoardSpace(int row, int col); // Геттеры для получения информации о параметрах игрового поля int GetWidth(void) const { return m_nWidth; } int GetHeight(void) const { return m_nHeight; } int GetColumns(void) const { return m_nColumns; } int GetRows(void) const { return m_nRows; } // Функция для удаления игрового поля и освобождения памяти void DeleteBoard(void); private: // Функция для создания игрового поля и выделения памяти под него void CreateBoard(void); // Указатель на двумерный массив int** m_arrBoard; // Список цветов: 0 – это цвет фона, 1-3 – это цвета блоков COLORREF m_arrColors[4]; // Информация о размере игрового поля int m_nColumns; int m_nRows; int m_nHeight; int m_nWidth; }; |
Этот класс сам по себе довольно простой. Он содержит указатель m_arrBoard
на двумерный массив целых чисел, которые представляют один из трех цветов (элементы с индексами 1-3), либо цвет фона (элемент с индексом 0). Затем мы добавляем переменные-члены, чтобы отслеживать строки (m_nRows
), столбцы (m_nColumns
), высоту (m_nHeight
) и ширину (m_nWidth
) в пикселях. Здесь также присутствуют функции для создания, настройки и удаления доски.
Метод CreateBoard() нужен для выделения памяти под двумерный массив, в котором будет храниться игровое поле, и для инициализации всех блоков значением фонового цвета. Метод SetupBoard() сбрасывает игровое поле путем случайного выбора цвета для каждого элемента поля на доске. Наконец, метод DeleteBoard() освобождает память, которую мы используем для игровой доски (в противном случае, это может привести к утечкам памяти).
В классе, представляющем игровую доску, также присутствует массив m_arrColors[]
элементов типа COLORREF
. Тип COLORREF
— это 32-битный целочисленный тип unsigned, который содержит значение цвета блока в формате RGBA. Элемент массива m_arrColors[0]
хранит фоновый цвет игровой доски, а элементы m_arrColors[1]
, m_arrColors[2]
и m_arrColors[3]
— цвет блоков. В конструкторе, приведенном ниже, мы будем использовать макрос RGB для создания значения COLORREF
из трех целых чисел, представляющих собой числовое обозначение красного, зеленого и синего цветов.
Ниже приведена реализация класса CSameGameBoard
в SameGameBoard.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
#include "stdafx.h" // в более новых версиях Visual Studio эта строка не нужна #include "SameGameBoard.h" CSameGameBoard::CSameGameBoard(void) : m_arrBoard(NULL), m_nColumns(15), m_nRows(15), m_nHeight(35), m_nWidth(35) { m_arrColors[0] = RGB(0, 0, 0); m_arrColors[1] = RGB(255, 0, 0); m_arrColors[2] = RGB(255, 255, 64); m_arrColors[3] = RGB(0, 0, 255); } CSameGameBoard::~CSameGameBoard(void) { // Просто удаляем нашу доску DeleteBoard(); } void CSameGameBoard::SetupBoard(void) { // При необходимости создаем доску if (m_arrBoard == NULL) CreateBoard(); // Устанавливаем каждому блоку случайный цвет for (int row = 0; row < m_nRows; row++) for (int col = 0; col < m_nColumns; col++) m_arrBoard[row][col] = (rand() % 3) + 1; } COLORREF CSameGameBoard::GetBoardSpace(int row, int col) { // Проверяем границы массива if (row < 0 || row >= m_nRows || col < 0 || col >= m_nColumns) return m_arrColors[0]; return m_arrColors[m_arrBoard[row][col]]; } void CSameGameBoard::DeleteBoard(void) { if (m_arrBoard != NULL) { for (int row = 0; row < m_nRows; row++) { if (m_arrBoard[row] != NULL) { // Сначала удаляем каждую отдельную строку delete[] m_arrBoard[row]; m_arrBoard[row] = NULL; } } // В конце удаляем массив, содержащий строки delete[] m_arrBoard; m_arrBoard = NULL; } } void CSameGameBoard::CreateBoard(void) { // Если у нас осталась доска с предыдущего раза, то удаляем её if(m_arrBoard != NULL) DeleteBoard(); // Создаем массив для хранения строк m_arrBoard = new int*[m_nRows]; // Создаем отдельно каждую строку for (int row = 0; row < m_nRows; row++) { m_arrBoard[row] = new int[m_nColumns]; // Устанавливаем для каждого блока значение цвета, равное цвету фона for (int col = 0; col < m_nColumns; col++) m_arrBoard[row][col] = 0; } } |
Теперь, когда наша игровая доска инкапсулирована в объект, мы можем создать экземпляр этого объекта в классе документа. Помните, что класс документа содержит все наши игровые данные и отделен от кода View.
Вот заголовочный файл SameGameDoc.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
#pragma once #include "SameGameBoard.h" class CSameGameDoc : public CDocument { protected: // создаем только из сериализации CSameGameDoc(); virtual ~CSameGameDoc(); DECLARE_DYNCREATE(CSameGameDoc) // Атрибуты public: // Операции public: // Геттеры для получения информации о параметрах игрового поля COLORREF GetBoardSpace(int row, int col) { return m_board.GetBoardSpace(row, col); } void SetupBoard(void) { m_board.SetupBoard(); } int GetWidth(void) { return m_board.GetWidth(); } int GetHeight(void) { return m_board.GetHeight(); } int GetColumns(void) { return m_board.GetColumns(); } int GetRows(void) { return m_board.GetRows(); } void DeleteBoard(void) { m_board.DeleteBoard(); } // Переопределения public: virtual BOOL OnNewDocument(); protected: // Экземпляр объекта нашей игровой доски CSameGameBoard m_board; // Генерация функции сообщений protected: DECLARE_MESSAGE_MAP() }; |
Большая часть этого кода покажется вам уже знакомой, за исключением нескольких вещей, специфичных для MFC. Пока мы не будем заострять внимание на строках с DECLARE_DYNCREATE
и DECLARE_MESSAGE_MAP
, так как они являются типичными директивами MFC.
Сейчас Document, по сути, является простой обёрткой для нашего класса. Мы добавили экземпляр нашего класса и семь функций, которые вызывают аналогичные функции класса SameGameBoard
. Благодаря этому View сможет получить доступ к данным игровой доски, хранящимся в Document. Исходный файл для Document (SameGameDoc.cpp) также очень прост, так как все функции, которые мы добавили, имеют свою реализацию:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
#include "stdafx.h" // в более новых версиях Visual Studio эта строка не нужна #include "SameGame.h" #include "SameGameDoc.h" #ifdef _DEBUG #define new DEBUG_NEW #endif // CSameGameDoc IMPLEMENT_DYNCREATE(CSameGameDoc, CDocument) BEGIN_MESSAGE_MAP(CSameGameDoc, CDocument) END_MESSAGE_MAP() // Создание CSameGameDoc CSameGameDoc::CSameGameDoc() { } // Уничтожение CSameGameDoc CSameGameDoc::~CSameGameDoc() { } BOOL CSameGameDoc::OnNewDocument() { if (!CDocument::OnNewDocument()) return FALSE; // Установка (или сброс) параметров доски m_board.SetupBoard(); return TRUE; } |
На самом деле всё, что мы добавили, — это вызов функции SetupBoard() в обработчике OnNewDocument. Всё это позволяет пользователю начать новую игру с помощью нажатия Ctrl+N
или из меню File > New
.
На следующих уроках мы добавим новые функции к игровому полю и к Document.
Потребовалось в указанных выше файлах добавить #include «pch.h» иначе вываливались ошибки.
Заменил указатель на двумерный массив вектором, так безопаснее, ИМХО.
Соответственно скорректировав файл SameGameBoard.cpp под вектор
Доброго времени суток. При создании класса CSameGameBoard у меня появился заголовочный файл CSameGameBoard.h, а не SameGameBoard.h. Также вместо SameGameBoard.cpp — CSameGameBoard.cpp. Это вообще критично? И почему так получилось.
Доброго.
>И почему так получилось.
Так работает Visual Studio :)/ Если мне не изменяет память, то приставка "C" в данном случае является сокращением от "Class". Показывая тем самым, что ваш заголовочный файл относится к описанию нового класса.
>Это вообще критично?
Нет, не критично. Просто учитывайте во время изучения уроков и вносите (если нужно) соответствующие коррективы. Например, если в статье идет подключение #include "SameGameBoard", то у вас должно быть #include "CSameGameBoard", ну и т.д.
В файле SameGameBoard.h visual studio ругался COLORREF — мол идентификатор не определен. Заinclude-ил "Windows.h" — все заработало
Может кому помог 🙂
Спасибо за информацию, чуть позже добавим её в статью.
P.S.: На момент написания статьи (да и сейчас, только что проверил) всё собирает и без явного инклуда именно "Windows.h". Видимо, что-то изменилось 🙂
Мне помогло, спасибо. Без этого ругается на COLORREF.
Почему в функции CSameGameBoard::DeleteBoard(void) необходимо удалять поочередно строки, а уже потом весь массив? Его нельзя сразу весь удалить
К сожалению — нельзя, так устроен C++. Сами выделяем память — значит сами за собой и прибираемся. Т.е., сколько раз использовали вызовов оператора new/new[] , то и столько же должно быть вызовов delete/delete []. Иначе получите утечки памяти.
Спасибо за ответ!