"The Same Game": A Simple Game from Start to FinishBy Ben MarchantThe View: Drawing Your GameNow that the document contains an initialized game board object we need to display this information to the user. This is where we can actually start to see our game come to life. In the screenshot my cursor is hovering over the "Overrides" section; click on it. Look for the OnInitialUpdate option, click on it, click the dropdown as shown in the screenshot below and select "<Add> OnInitialUpdate". This will add the OnInitialUpdate override to your view with some default code in it to call the CView implementation of the function. Then we just add a call to the ResizeWindow function that we will write. So this leaves us with the following in the header file (changes bolded). #pragma once class CSameGameView : public CView { protected: // create from serialization only CSameGameView(); DECLARE_DYNCREATE(CSameGameView) // Attributes public: CSameGameDoc* GetDocument() const; // Overrides public: virtual void OnDraw(CDC* pDC); // overridden to draw this view virtual BOOL PreCreateWindow(CREATESTRUCT& cs); protected: // Implementation public: void ResizeWindow(); virtual ~CSameGameView(); #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif // Generated message map functions protected: DECLARE_MESSAGE_MAP() public: virtual void OnInitialUpdate(); }; #ifndef _DEBUG // debug version in SameGameView.cpp inline CSameGameDoc* CSameGameView::GetDocument() const { return reinterpret_cast<CSameGameDoc*>(m_pDocument); } #endif While we're adding in the resize code, we also need to add drawing code to the CSameGameView class. The header and source files for the view already contain a function override called OnDraw. This is where we'll put the drawing code. Here is the full source code for the view (changes bolded). #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 construction/destruction CSameGameView::CSameGameView() { } CSameGameView::~CSameGameView() { } BOOL CSameGameView::PreCreateWindow(CREATESTRUCT& cs) { return CView::PreCreateWindow(cs); } // CSameGameView drawing void CSameGameView::OnDraw(CDC* pDC) // MFC will comment out the argument name by default; uncomment it { // First get a pointer to the document CSameGameDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if(!pDoc) return; // Save the current state of the device context int nDCSave = pDC->SaveDC(); // Get the client rectangle CRect rcClient; GetClientRect(&rcClient); // Get the background color of the board COLORREF clr = pDoc->GetBoardSpace(-1, -1); // Draw the background first pDC->FillSolidRect(&rcClient, clr); // Create the brush for drawing CBrush br; br.CreateStockObject(HOLLOW_BRUSH); CBrush* pbrOld = pDC->SelectObject(&br); // Draw the squares for(int row = 0; row < pDoc->GetRows(); row++) { for(int col = 0; col < pDoc->GetColumns(); col++) { // Get the color for this board space clr = pDoc->GetBoardSpace(row, col); // Calculate the size and position of this space CRect rcBlock; rcBlock.top = row * pDoc->GetHeight(); rcBlock.left = col * pDoc->GetWidth(); rcBlock.right = rcBlock.left + pDoc->GetWidth(); rcBlock.bottom = rcBlock.top + pDoc->GetHeight(); // Fill in the block with the correct color pDC->FillSolidRect(&rcBlock, clr); // Draw the block outline pDC->Rectangle(&rcBlock); } } // Restore the device context settings pDC->RestoreDC(nDCSave); br.DeleteObject(); } // CSameGameView diagnostics #ifdef _DEBUG void CSameGameView::AssertValid() const { CView::AssertValid(); } void CSameGameView::Dump(CDumpContext& dc) const { CView::Dump(dc); } // non-debug version is inline CSameGameDoc* CSameGameView::GetDocument() const { ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CSameGameDoc))); return (CSameGameDoc*)m_pDocument; } #endif //_DEBUG void CSameGameView::OnInitialUpdate() { CView::OnInitialUpdate(); // Resize the window ResizeWindow(); } void CSameGameView::ResizeWindow() { // First get a pointer to the document CSameGameDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if(!pDoc) return; // Get the size of the client area and the window CRect rcClient, rcWindow; GetClientRect(&rcClient); GetParentFrame()->GetWindowRect(&rcWindow); // Calculate the difference int nWidthDiff = rcWindow.Width() - rcClient.Width(); int nHeightDiff = rcWindow.Height() - rcClient.Height(); // Change the window size based on the size of the game board rcWindow.right = rcWindow.left + pDoc->GetWidth() * pDoc->GetColumns() + nWidthDiff; rcWindow.bottom = rcWindow.top + pDoc->GetHeight() * pDoc->GetRows() + nHeightDiff; // The MoveWindow function resizes the frame window GetParentFrame()->MoveWindow(&rcWindow); } It is very simple to draw the game board, we are just going to loop through each row, column by column, and draw a colored rectangle. There is one argument to the OnDraw function and that is a CDC pointer. The CDC class is the base class for all device contexts. A device context is the generic interface to a device such as the screen or a printer. Here we'll use it to draw to the screen. We first start the function by getting a pointer to the document so that we can get the board information. Next we call the SaveDC function from the device context. This function saves the state of the device context so that we can restore it after we are done. // Get the client rectangle CRect rcClient; GetClientRect(&rcClient); // Get the background color of the board COLORREF clr = pDoc->GetBoardSpace(-1, -1); // Draw the background first pDC->FillSolidRect(&rcClient, clr); Next we need to color the background of the client area black so we get the dimensions of the client area by calling GetClientRect. A call to GetBoardSpace(-1,-1) on the document will return the background color and FillSolidRect will fill the client area with that background color. // Create the brush for drawing CBrush br; br.CreateStockObject(HOLLOW_BRUSH); CBrush* pbrOld = pDC->SelectObject(&br); ... // Restore the device context settings pDC->RestoreDC(nDCSave); br.DeleteObject(); Now it is time to draw the individual rectangles. This is accomplished by drawing a colored rectangle and then drawing a black outline around it. We now have to create a brush object to do the outline. The type of brush we are creating, HOLLOW_BRUSH, is called hollow because when we draw a rectangle MFC will want to fill in the middle with some pattern. We don't want this so we'll use a hollow brush so that the previously drawn colored rectangle will show through. Creating a brush allocates GDI memory that we have to later delete so that we don't leak GDI resources. // Draw the squares for(int row = 0; row < pDoc->GetRows(); row++) { for(int col = 0; col < pDoc->GetColumns(); col++) { // Get the color for this board space clr = pDoc->GetBoardSpace(row, col); // Calculate the size and position of this space CRect rcBlock; rcBlock.top = row * pDoc->GetHeight(); rcBlock.left = col * pDoc->GetWidth(); rcBlock.right = rcBlock.left + pDoc->GetWidth(); rcBlock.bottom = rcBlock.top + pDoc->GetHeight(); // Fill in the block with the correct color pDC->FillSolidRect(&rcBlock, clr); // Draw the block outline pDC->Rectangle(&rcBlock); } } The nested for loops are very simple, they iterate row by row, column by column, getting the color of the corresponding board space from the document using the GetBoardSpace function from the document, calculating the size of the rectangle to color and then drawing the block. The drawing uses two functions, FillSolidRect() to fill in the colored portion of the block and Rectangle() to draw the outline of the block. This is what draws all of the blocks in the client area of our view. The last function that we've inserted into the view is one to resize the window based on the dimensions of the game board. In later articles we'll allow the user to change the number of blocks and size of the blocks so this function will come in handy then too. Again we start by getting a pointer to the document followed by getting the size of the current client area and the current window. // Get the size of the client area and the window CRect rcClient, rcWindow; GetClientRect(&rcClient); GetParentFrame()->GetWindowRect(&rcWindow); Finding the difference between these two gives us the amount of space used by the title bar, menu and borders of the window. We can then add the differences back onto the size of the desired client area (# of blocks by # of pixels per block) to get the new window size. // Calculate the difference int nWidthDiff = rcWindow.Width() - rcClient.Width(); int nHeightDiff = rcWindow.Height() - rcClient.Height(); // Change the window size based on the size of the game board rcWindow.right = rcWindow.left + pDoc->GetWidth() * pDoc->GetColumns() + nWidthDiff; rcWindow.bottom = rcWindow.top + pDoc->GetHeight() * pDoc->GetRows() + nHeightDiff; // The MoveWindow function resizes the frame window Finally the GetParentFrame function returns a pointer to the CMainFrame class that is the actual window for our game and we resize the window by calling MoveWindow. GetParentFrame()->MoveWindow(&rcWindow); Your game should now look similar to this: ConclusionIn this article we've gone over some of the basics of the Microsoft Foundation Classes and the Document/View architecture that it provides. We've assembled a game board object that contains our game data and constructed a view that renders the data to the user. In our next article we'll go over event driven programming, respond to events from the user such as mouse clicks and finish up with a "playable" version of our game. Source Code from Part 1Continue to Part 2: Creating a Playable Game |