Sudoku, a complete MFC application.  Part 2

AndyAinscowFreelance programmer / Consultant
CERTIFIED EXPERT
Published:

Introduction:

Dynamic window placements and drawing on a form, simple usage of windows registry as a storage place for information.

Continuing from the first article about sudoku.  There we have designed the application and put a lot of user interface elements into place.  Now open the project in visual studio.

Pressing the F5 key to test I get the following on my system:

Basic appNote the scroll bar to the right - not nice is it.  So how do we fix that?

From the class view select CSudokuView in the top tree view and in the list below double click the OnInitialUpdate.  You should spring to the code editor and see the following code:

void CSudokuView::OnInitialUpdate()
                      {
                          CFormView::OnInitialUpdate();
                          GetParentFrame()->RecalcLayout();
                          ResizeParentToFit();
                      }

Open in new window


Now change the line ResizeParentToFit(); to ResizeParentToFit(FALSE);  The default parameter (TRUE) will instruct the system to shrink the frame to fit but not to enlarge the frame should that be required.  Changing the parameter to FALSE instructs the function to make the frame fit even if it must be enlarged.

void CSudokuView::OnInitialUpdate()
                      {
                          CFormView::OnInitialUpdate();
                          GetParentFrame()->RecalcLayout();
                          ResizeParentToFit(FALSE);
                      }

Open in new window


Now run it again – no scroll bar, but still doesn’t look nice.  Let’s finish the layout with some code to demonstrate how one can do some things outside of the resource editor.

First the buttons will be positioned to be in their 3x3 groupings.  This will be done in the OnInitialUpdate of the View.  Now for an SDI type application (like this) the OnInitialUpdate is called every time a new document is created or a document is loaded – we only need to position the buttons once.  In the header file we will add a boolean variable to act as a flag to control this so the positioning only occurs once.  This is just for neatness (efficiency) as the first repositioning will move the buttons to the correct position, any later times the code is run will not actually move them.  We also want an array to hold the buttons – later we will subclass the buttons to a custom class for some owner drawing.

In the SudokuView.h we add the following declaration just before the final }; in the file

private:
                          bool m_bInitialised;
                          CButton m_arWndButtons[81];
                      };

Open in new window


In the constructor of the view we set it to an initial value

CSudokuView::CSudokuView()
                          : CFormView(CSudokuView::IDD)
                          , m_bInitialised(false)
                      {

Open in new window


Note I have done it before the body of the constructor – in most cases it doesn’t make any real difference if done the way shown here or in the body of the constructor.  

And in the OnInitialUpdate we need the following code after the ResizeParentToFit function call:
    if(!m_bInitialised)
                          {
                              //Set the flag - prevent reentry and duplication of effort.
                              m_bInitialised = true;
                      
                              //Attach the controls in the dialog editor to our array of buttons
                              for(int i = 0; i < 81; i++)
                                  m_arWndButtons[i].SubclassDlgItem(IDC_BUTTON1 + i, this);
                      
                              //Get width and height of a button
                              CRect rcBtn;
                              GetDlgItem(IDC_BUTTON1)->GetWindowRect(rcBtn);
                              int iBtnWidth = rcBtn.Width();
                              int iBtnHeight = rcBtn.Height();
                      
                              //Get the position of the frame
                              CRect rcFrame;
                              GetDlgItem(IDC_STATIC_BOUNDARY)->GetWindowRect(rcFrame);
                              //Convert from screen to client area of the application coordinates
                              ScreenToClient(&rcFrame);
                      
                              //Size of the 'mini' Frame blocks
                              int iOffsetX = rcFrame.Width() / 3;
                              int iOffsetY = rcFrame.Height() / 3;
                      
                              //Calculate the spacing inside the mini blocks
                              int iSpaceX = (iOffsetX - 3*iBtnWidth)/2;
                              int iSpaceY = (iOffsetY - 3*iBtnHeight)/2;
                      
                              //First row is offset from the frame
                              int x, y = rcFrame.top + iSpaceY;
                      
                              //Position one row at a time
                              for(int row = 0; row < 9; row++)
                              {
                                  //Location of first button, offset a bit from the frame
                                  x = rcFrame.left + iSpaceX;
                                  
                                  //Now process the nine buttons in the row
                                  for(int col = 0; col < 9; col++)
                                  {
                                      //NULL - A window used in the tab ordering, SWP_NOZORDER tells system to ignore that parameter
                                      m_arWndButtons[row * 9 + col].SetWindowPos(NULL, x, y, iBtnWidth-1, iBtnHeight-1, SWP_NOZORDER);
                      
                                      //This is to provide a little extra spacing to display groups of three
                                      if(col == 2)
                                          x = rcFrame.left + iOffsetX + iSpaceX;
                                      else if (col == 5)
                                          x = rcFrame.left + 2*iOffsetX + iSpaceX;
                                      else
                                          x += iBtnWidth;
                                  }
                      
                                  //This is to provide a little extra spacing to display groups of three
                                  if(row == 2)
                                      y = rcFrame.top + iOffsetY + iSpaceY;
                                  else if(row == 5)
                                      y = rcFrame.top + 2*iOffsetY + iSpaceY;
                                  else
                                      y += iBtnHeight;
                              }
                          }

Open in new window


Important.  The above code uses a loop to subclass the controls on the dialog to the array of buttons.  This will only work if the ID’s of the buttons are in a series – which is why they were all added one after the other in the copy/paste when using the resource editor.

<If necessary we could manually edit the resource.h file to achieve the series.  In fact if you look into the .rc file (where resources are stored by the dialog editor) you will find it is a text file and can be edited with notepad for example – make a copy first.>

To complete the grid we would like some vertical and horizontal lines.  We will draw these ourselves.  This we can not do in the OnInitialUpdate because they must be drawn everytime the window needs to redrawn eg. After being behind another window this window is brought to the front.  In the class view select the CSudokuView, right click to get the context menu and select properties.  On the properties view there will be a button (overrides) which we click to get a list of various functions that can be overridden, scroll down until OnDraw is visible, click the blank field to the right and select <Add> OnDraw from the combobox. There should now be the following in the CSudokuView code window:

void CSudokuView::OnDraw(CDC* /*pDC*/)
                      {
                          // TODO: Add your specialized code here and/or call the base class
                      }

Open in new window


This needs to be as follows:

void CSudokuView::OnDraw(CDC* pDC)
                      {
                          CRect rcFrame;
                          GetDlgItem(IDC_STATIC_BOUNDARY)->GetWindowRect(&rcFrame);
                          ScreenToClient(&rcFrame);
                          int iOffsetX = rcFrame.Width() / 3;
                          int iOffsetY = rcFrame.Height() / 3;
                      
                          pDC->MoveTo(rcFrame.left + iOffsetX, rcFrame.top);
                          pDC->LineTo(rcFrame.left + iOffsetX, rcFrame.bottom);
                      
                          pDC->MoveTo(rcFrame.left + 2*iOffsetX, rcFrame.top);
                          pDC->LineTo(rcFrame.left + 2*iOffsetX, rcFrame.bottom);
                      
                          pDC->MoveTo(rcFrame.left, rcFrame.top + iOffsetY);
                          pDC->LineTo(rcFrame.right, rcFrame.top + iOffsetY);
                      
                          pDC->MoveTo(rcFrame.left, rcFrame.top + 2*iOffsetY);
                          pDC->LineTo(rcFrame.right, rcFrame.top + 2*iOffsetY);
                      }

Open in new window


Now press F5 again to compile and run – we should now see a much more organised grid, looking rather like a Sudoku grid apart from all the buttons having the same text (Button1) on them.

You have probably seen that the sudoku app starts at a new location in windows everytime you run it.  For those of you with a multi monitor system then always on the main monitor (typically in front of visual studio – a nuisance for debugging isn’t it).  Let’s make it ‘remember’ where you move it to, even if on another monitor.

When the frame window is being shown initially we will attempt to read the settings from the registry, if we don’t find any then we just center the window in the main monitor.

From the class view select the CMainFrame class and double click the OnCreate function.  I have the following:

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
                      {
                          if (CFrameWnd::OnCreate(lpCreateStruct) == -1)
                              return -1;
                          
                          if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP
                              | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) ||
                              !m_wndToolBar.LoadToolBar(IDR_MAINFRAME))
                          {
                              TRACE0("Failed to create toolbar\n");
                              return -1;      // fail to create
                          }
                      
                          if (!m_wndStatusBar.Create(this) ||
                              !m_wndStatusBar.SetIndicators(indicators,
                                sizeof(indicators)/sizeof(UINT)))
                          {
                              TRACE0("Failed to create status bar\n");
                              return -1;      // fail to create
                          }
                      
                          // TODO: Delete these three lines if you don't want the toolbar to be dockable
                          m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
                          EnableDocking(CBRS_ALIGN_ANY);
                          DockControlBar(&m_wndToolBar);
                      
                          return 0;
                      }

Open in new window


First we will comment out the final three lines (before the line return 0) concerning the toolbar  – to keep the toolbar fixed in one location in our application.

Now add the following code befoer the line return 0:

    WINDOWPLACEMENT wp;
                          if(ReadWindowPlacement(&wp))
                          {
                              wp.length = sizeof(wp);
                              SetWindowPlacement(&wp);
                          }
                          else
                          {
                              CenterWindow();
                          }

Open in new window


We also need a new function for the ReadWindowPlacement.  There is a wizard to assist one in adding a new function (class view, select class, right mouse click for context menu, add) but I don’t like it for simple things like this.  I’m going to add the function directly into the .h/.cpp files myself because I find it quicker than using the wizard.   Open MainFrm.h and add the following at the end of the class declaration, directly before the }; to give

protected:
                          bool ReadWindowPlacement(WINDOWPLACEMENT *pWP);
                      };

Open in new window


Now add the following to the MainFrm.cpp file

bool CMainFrame::ReadWindowPlacement(WINDOWPLACEMENT *pWP)
                      {
                          CRegKey regKey;
                          long lRes;
                          CString szRegKey;
                          szRegKey.Format("%s\\%s", "SOFTWARE\\AndyAinscow", "Sudoku");
                      
                          char szBuffer[sizeof("-32767")*8 + sizeof("65535")*2 + sizeof(" ")];
                          ZeroMemory(szBuffer, sizeof(szBuffer));
                          DWORD dwSize;
                      
                          lRes = regKey.Open(HKEY_CURRENT_USER, szRegKey, KEY_READ);
                          if(lRes == ERROR_SUCCESS)
                          {
                              dwSize = sizeof(szBuffer)/sizeof(TCHAR);
                              lRes = regKey.QueryStringValue("MainWindow", szBuffer, &dwSize);
                              if(lRes != ERROR_SUCCESS)
                              {
                                  return false;
                              }
                          }
                          else
                              return false;
                      
                          char szFormat[] = "%u,%u,%d,%d,%d,%d,%d,%d,%d,%d";
                          int nRead = sscanf_s(szBuffer, szFormat,
                              &pWP->flags, &pWP->showCmd,
                              &pWP->ptMinPosition.x, &pWP->ptMinPosition.y,
                              &pWP->ptMaxPosition.x, &pWP->ptMaxPosition.y,
                              &pWP->rcNormalPosition.left, &pWP->rcNormalPosition.top,
                              &pWP->rcNormalPosition.right, &pWP->rcNormalPosition.bottom);
                      
                          if (nRead != 10)
                              return false;
                      
                          return true;
                      }

Open in new window


We also need to add #include “atlbase.h” at the top of this file (after #include “MainFrm.h”) so that the compiler knows what the CRegKey class is – this is an MFC class to provide support for the registry.

Press F7 to compile and – oops, error messages.

I see an Error C2664 about a CStringT….

This is our ‘mistake’ in generating the project where we kept the default setting of UNICODE.  We are trying to mix ‘wide’ chars and ‘narrow’ chars (ANSI) with the strings in the code.  

For certain reasons not yet explained I don’t want a UNICODE based app so we need to change a project setting.  Go to the solution explorer,  there will be a tree view with a solution Sudoku and this contains a project also called Sudoku. Right click with the mouse on the project and select properties.  You should get dialog called Sudoku Property Pages.  

Look to the top of the dialog and see a combobox labelled Configuration.  That should contain Active(Debug) – change that to All Configurations.  In the tree view at the left expand the  Configuration Properties then select the General node.  To the right you should see a number of settings, fairly close to the bottom is Character Set, change that from ‘Use Unicode Character Set’ to ‘Use Multi-Byte Character Set’ then click the Apply button.  

Have a browse through the settings available – this is command central for how the applications code is converted into the exe (dll…) by Visual Studio.  Pathways for libraries and other files, predefined variables, ability to run programs before and after compilation, well way too much to describe here.  We have updated the project with the apply button so now hit cancel – just in case you accidentally made a change to something else.

Now try to compile again (F7 key) and now it should compile without error.  (Sometimes I see a general error about the manifest – it seems to to be Visual Studio not behaving properly, just press Ctrl+ Alt+ F7 <note the Ctrl and Alt keys – this forces a complete rebuild> and it will compile properly this time without error).  If you ran the application it should at least display at the same location each time – now to store the position when the app is closed.

From the class view select the CMainFrame class then go to the properties.  This time we want a message to respond to (not an override as previously - different button to control which information is displayed in the IDE).  Select the WM_CLOSE message and add an OnClose handler.  Modify it to be as follows:

void CMainFrame::OnClose()
                      {
                          WINDOWPLACEMENT wp;
                          wp.length = sizeof wp;
                          if (GetWindowPlacement(&wp))
                              WriteWindowPlacement(&wp);
                      
                          CFrameWnd::OnClose();
                      }

Open in new window


Then add a new function as we did before for the WriteWindowPlacement function.  The code in the cpp file is (note this has no return value)

void CMainFrame::WriteWindowPlacement(WINDOWPLACEMENT *pWP)
                      {
                          char szFormat[] = "%u,%u,%d,%d,%d,%d,%d,%d,%d,%d";
                          char szBuffer[sizeof("-32767")*8 + sizeof("65535")*2 + sizeof(" ")];
                      
                          sprintf_s(szBuffer, sizeof(szBuffer), szFormat,
                              pWP->flags, pWP->showCmd,
                              pWP->ptMinPosition.x, pWP->ptMinPosition.y,
                              pWP->ptMaxPosition.x, pWP->ptMaxPosition.y,
                              pWP->rcNormalPosition.left, pWP->rcNormalPosition.top,
                              pWP->rcNormalPosition.right, pWP->rcNormalPosition.bottom);
                      
                          CRegKey regKey;
                          long lRes;
                          CString szRegKey;
                          szRegKey.Format("%s\\%s", "SOFTWARE\\AndyAinscow", "Sudoku");
                      
                          lRes = regKey.Create(HKEY_CURRENT_USER, szRegKey);
                          if(lRes == ERROR_SUCCESS)
                          {
                              lRes = regKey.SetStringValue("MainWindow", szBuffer);
                              if(lRes != ERROR_SUCCESS)
                              {
                                  ASSERT(FALSE);
                              }
                          }
                      }

Open in new window


If you compare the code in the ReadWindowPlacement and WriteWindowPlacement functions you should see a lot of similarities.  The size and location of the application is converted to a string then stored in the registry when the application is closed.  When it is started the values are read from the registry as a string, parsed into the WINDOWPLACEMENT structure and used to position the application where it was last time.

HINT: If you don’t understand a (windows) function select it with the cursor then press the F1 key.  The help file should start with the help about that function.  Read it and the options / default settings.  Failure to do that is a source of numerous questions here at EE, and think just how much quicker it is to do that than to ask a question.  If you don’t understand the help then you can still ask a question for more explanation about the part you don’t understand.

Press the F5 key to compile and run.  Move the Sudoku app somewhere then close it.  Run Sudoku again and it should start where you last closed it.

Conclusion:

We have moved window elements (buttons) into the place we want using code.
We have modified the settings for the solution - settings about how the source code of the app is compiled and linked.
We have used the registry to store information for positioning the main window where it was located the last time the app was run.


Click here for the source code for this article


Previous article in the series is here:  Sudoku in MFC: Part 1
There we used the project wizard to create a new application skeleton.  We also started using the resource editor to modify how the compiled application would look.

Next article in the series is here:  Sudoku in MFC: Part 3
There we will be looking at loading and saving to file on disc.  We also look at the interaction between the document and the view in the application architecture.


Two points to bear in mind.
You may use the code but you are not allowed to distribute the resulting application either for free or for a reward (monetary or otherwise).  At least not without my express permission.
I will perform some things to demonstrate a point – it is not to be taken as that meaning it is a ‘best’ practice, in fact an alternative might be simpler and suitable.  Some points in the code would even be called poor design and a possible source of errors.
1
5,476 Views
AndyAinscowFreelance programmer / Consultant
CERTIFIED EXPERT

Comments (0)

Have a question about something in this article? You can receive help directly from the article author. Sign up for a free trial to get started.