На предыдущем уроке мы занимались частью Document архитектуры «Document/View», на этом же уроке мы займемся частью View, начиная отрисовку нашей игры.
The View: Отрисовка игры SameGame
Теперь, когда документ содержит инициализированный объект игрового поля, нам нужно отобразить эту информацию пользователю. Именно здесь уже можно заметить, что наша игра начинает «оживать».
Первым шагом является добавление кода для изменения параметров окна до нужного размера. Сейчас окно имеет размер, заданный по умолчанию, это не является тем, что нам нужно. Мы исправим это в переопределяемом методе OnInitialUpdate(). Класс View наследует базовый метод OnInitialUpdate(), который задает представление нашего документа, и мы должны переопределить этот метод, чтобы получить возможность изменять размеры окна. Для того чтобы это реализовать, нам нужно открыть окно свойств заголовочного файла CSameGameView (который фактически будет называться SameGameView.h): для этого нажмите Alt+Enter
или в меню "Вид" > "Окно свойств"
(в других версиях Visual Studio может быть следующее: "Вид" > "Другие окна" > "Окно свойств"
). Найдите опцию OnInitialUpdate
и выберите <Add> OnInitialUpdate
, как показано на скриншоте ниже:
Тем самым мы добавим переопределенный метод OnInitialUpdate() к нашему View с небольшим содержанием по умолчанию для вызова функции ResizeWindow(). Таким образом, заголовочный файл SameGameView.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 |
#pragma once class CSameGameView : public CView { protected: CSameGameView(); DECLARE_DYNCREATE(CSameGameView) // Атрибуты public: CSameGameDoc* GetDocument() const; // Переопределения public: virtual void OnDraw(CDC* pDC); // переопределяем, чтобы нарисовать этот View virtual BOOL PreCreateWindow(CREATESTRUCT& cs); protected: // Реализация public: void ResizeWindow(); virtual ~CSameGameView(); #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif // Генерируем функцию сообщений protected: DECLARE_MESSAGE_MAP() public: virtual void OnInitialUpdate(); }; #ifndef _DEBUG // версия debug в SameGameView.cpp inline CSameGameDoc* CSameGameView::GetDocument() const { return reinterpret_cast<CSameGameDoc*>(m_pDocument); } #endif |
Помимо этого, нам также нужно будет добавить код отрисовки в класс CSameGameView
. Заголовочные и исходные файлы для View уже содержат переопределение функции OnDraw(). Здесь мы и поместим наш код. Ниже приведен полный исходный код для SameGameView.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 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
#include "stdafx.h" #include "SameGame.h" #include "SameGameDoc.h" #include "SameGameView.h" #ifdef _DEBUG #define new DEBUG_NEW #endif // CSameGameView IMPLEMENT_DYNCREATE(CSameGameView, CView) BEGIN_MESSAGE_MAP(CSameGameView, CView) END_MESSAGE_MAP() // Конструктор CSameGameView CSameGameView::CSameGameView() { } // Деструктор CSameGameView CSameGameView::~CSameGameView() { } BOOL CSameGameView::PreCreateWindow(CREATESTRUCT& cs) { return CView::PreCreateWindow(cs); } // Отрисовка игры void CSameGameView::OnDraw(CDC* pDC) // MFC закомментирует имя аргумента по умолчанию. Раскомментируйте это { // В начале создаем указатель на Document CSameGameDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if (!pDoc) return; // Сохраняем текущее состояние контекста устройства int nDCSave = pDC->SaveDC(); // Получаем размеры клиентской области CRect rcClient; GetClientRect(&rcClient); COLORREF clr = pDoc->GetBoardSpace(-1, -1); // Сначала отрисовываем фон pDC->FillSolidRect(&rcClient, clr); // Создаем кисть для рисования CBrush br; br.CreateStockObject(HOLLOW_BRUSH); CBrush* pbrOld = pDC->SelectObject(&br); // Рисуем блоки for (int row = 0; row < pDoc->GetRows(); row++) { for (int col = 0; col < pDoc->GetColumns(); col++) { clr = pDoc->GetBoardSpace(row, col); // Вычисляем размер и позицию игрового пространства CRect rcBlock; rcBlock.top = row * pDoc->GetHeight(); rcBlock.left = col * pDoc->GetWidth(); rcBlock.right = rcBlock.left + pDoc->GetWidth(); rcBlock.bottom = rcBlock.top + pDoc->GetHeight(); // Заполняем блок соответствующим цветом pDC->FillSolidRect(&rcBlock, clr); // Рисуем контур pDC->Rectangle(&rcBlock); } } // Восстанавливаем контекст устройства pDC->RestoreDC(nDCSave); br.DeleteObject(); } // Диагностика CSameGameView #ifdef _DEBUG void CSameGameView::AssertValid() const { CView::AssertValid(); } void CSameGameView::Dump(CDumpContext& dc) const { CView::Dump(dc); } // Версия non-debug CSameGameDoc* CSameGameView::GetDocument() const { ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CSameGameDoc))); return (CSameGameDoc*)m_pDocument; } #endif //_DEBUG void CSameGameView::OnInitialUpdate() { CView::OnInitialUpdate(); // Изменяем размеры окна ResizeWindow(); } void CSameGameView::ResizeWindow() { // Создаем указатель на Document CSameGameDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if (!pDoc) return; // Получаем размеры клиентской области CRect rcClient, rcWindow; GetClientRect(&rcClient); GetParentFrame()->GetWindowRect(&rcWindow); int nWidthDiff = rcWindow.Width() - rcClient.Width(); int nHeightDiff = rcWindow.Height() - rcClient.Height(); // Изменяем размеры окна, исходя из размеров нашей доски rcWindow.right = rcWindow.left + pDoc->GetWidth() * pDoc->GetColumns() + nWidthDiff; rcWindow.bottom = rcWindow.top + pDoc->GetHeight() * pDoc->GetRows() + nHeightDiff; // Функция MoveWindow() изменяет размер окна фрейма GetParentFrame()->MoveWindow(&rcWindow); } |
Нарисовать игровую доску очень просто: мы будем перебирать каждую строку столбец за столбцом и рисовать цветной прямоугольник. У функции OnDraw() есть один аргумент — указатель на CDC
. Класс CDC
является базовым классом для всех контекстов устройства. Контекст устройства — это обобщённый интерфейс устройства вывода, такого как экран или принтер.
Вначале мы инициализируем указатель на Document, чтобы иметь возможность получить данные игрового поля. Далее мы вызываем функцию SaveDC() из контекста устройства. Эта функция сохраняет состояние контекста устройства, чтобы мы могли восстановить его после того, как закончим.
1 2 3 4 5 6 7 8 9 |
// Получаем клиентский прямоугольник CRect rcClient; GetClientRect(&rcClient); // Получаем фоновый цвет доски COLORREF clr = pDoc->GetBoardSpace(-1, -1); // Сначала рисуем фон pDC->FillSolidRect(&rcClient, clr); |
Затем нам нужно покрасить фон клиентской области в черный цвет. Для этого нам нужно получить размеры клиентской области — вызываем GetClientRect(). Вызов GetBoardSpace(-1,-1)
в Document возвратит цвет фона, а FillSolidRect() заполнит клиентскую область фоновым цветом.
1 2 3 4 5 6 7 8 9 |
// Создаём кисть для рисования CBrush br; br.CreateStockObject(HOLLOW_BRUSH); CBrush* pbrOld = pDC->SelectObject(&br); ... // Восстановление настроек контекста устройства pDC->RestoreDC(nDCSave); br.DeleteObject(); |
Теперь пришло время нарисовать отдельные прямоугольники. Для этого нам нужно сначала нарисовать цветной прямоугольник, а затем обвести его черным контуром. Нам нужно создать объект кисти, чтобы сделать контур. Кисть HOLLOW_BRUSH
, которую мы создаем, называется hollow (в переводе «пустой»), потому что, когда мы рисуем прямоугольник, MFC захочет заполнить его внутренность каким-нибудь цветом. Мы не хотим этого, поэтому будем использовать HOLLOW_BRUSH
. Создание кисти приводит к выделению GDI-памяти, которую позднее нам нужно будет очистить.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// Рисуем квадраты for (int row = 0; row < pDoc->GetRows(); row++) { for (int col = 0; col < pDoc->GetColumns(); col++) { // Получаем цвет для пространства доски clr = pDoc->GetBoardSpace(row, col); // Рассчитываем размер и положение пространства доски CRect rcBlock; rcBlock.top = row * pDoc->GetHeight(); rcBlock.left = col * pDoc->GetWidth(); rcBlock.right = rcBlock.left + pDoc->GetWidth(); rcBlock.bottom = rcBlock.top + pDoc->GetHeight(); // Заполняем блок правильным цветом pDC->FillSolidRect(&rcBlock, clr); // Рисуем контур блока pDC->Rectangle(&rcBlock); } } |
Вложенные циклы for очень просты, они перебирают строку за строкой, столбец за столбцом, получая цвет соответствующего пространства доски из Document с помощью функции GetBoardSpace(), вычисляя размер прямоугольника, который нужно закрасить, а затем выполняется сам процесс закрашивания блока. При отрисовке используются два метода:
метод FillSolidRect() — для заполнения цветной части блока;
метод Rectangle() — для рисования контура блока.
Последняя функция, которую мы вставили во View, изменяет размер окна в зависимости от размера игрового поля. На следующих уроках мы добавим дополнительный функционал, чтобы пользователь мог изменять количество и размер блоков, так что эта функция нам еще пригодится. Как и в прошлый раз, мы начинаем с получения указателя на Document, а затем получаем размер текущей клиентской области и текущего окна.
1 2 3 4 |
// Получаем размер клиентской области и окна CRect rcClient, rcWindow; GetClientRect(&rcClient); GetParentFrame()->GetWindowRect(&rcWindow); |
Вычисление разницы между этими двумя значениями дает нам площадь пространства, используемого строкой заголовка, меню и границами окна.
1 2 3 4 5 6 7 8 9 |
// Вычисляем разницу int nWidthDiff = rcWindow.Width() - rcClient.Width(); int nHeightDiff = rcWindow.Height() - rcClient.Height(); // Изменяем размер окна в соответствии с размером нашей доски rcWindow.right = rcWindow.left + pDoc->GetWidth() * pDoc->GetColumns() + nWidthDiff; rcWindow.bottom = rcWindow.top + pDoc->GetHeight() * pDoc->GetRows() + nHeightDiff; |
Наконец, функция GetParentFrame() возвращает указатель на класс CMainFrame, который является фактическим окном нашей игры, и мы изменяем размер окна, вызывая MoveWindow().
1 |
GetParentFrame()->MoveWindow(&rcWindow); |
Сейчас ваше приложение должно выглядеть примерно следующим образом:
Заключение
На этом и на предыдущих двух уроках мы рассмотрели некоторые основы MFC и архитектуру «Document/View». Мы собрали объект игрового поля, который содержит наши данные, и создали представление, которое отображает эти данные пользователю. На следующих уроках мы рассмотрим обработку событий.
GitHub / Исходный код — Урок №3: Отрисовка игры «SameGame» на C++/MFC
У меня не выводится окно "Переопределения" в свойствах представления класса. С чем это может быть связано?
https://yadi.sk/i/aXSVqrNLM8O7yA
Добавил в ручную в определение класса. Теперь работает.
При выборе SameGameView.h в окне свойств не отображаются события для выбора.
Не могу понять в чем проблема и почему не отображаются свойства.
Можете помочь?
https://i.imgur.com/8RgTUIa.png
У вас на скриншоте слева (где куча разных заголовочных файлов) — окно "Обозреватель решений". Это не то окно.
Нужно открыть окно "Представление Классов" (можно сделать через меню Вид->Классы). Откроется окно "Представление Классов" (как на самой первой картинке в статье). Далее щелкаете правой кнопкой мыши на CSameGameView и выбираете "Свойства".
Вот картинка для наглядности:
https://imgur.com/a/tUfRZLg
Премного благодарен, Дмитрий!
Всегда пожалуйста 🙂