Sudoku, a complete MFC application.  Part 10

AndyAinscowFreelance programmer / Consultant
CERTIFIED EXPERT
Published:

Introduction:

Dialogs (1) modal - maintaining the database.

Continuing from the ninth article about sudoku.  

You might have heard of modal and modeless dialogs.  Here with this Sudoku application will we use one of each type: a modal dialog to help with maintaining the database and a modeless dialog for helping ‘solve’ a game.  The solving procedure will be the subject of the next article so we will just concentrate on the modal dialog and the database maintenance in this article.

Add a new dialog resource in the resource editor and change the ID to IDD_DLG_MAINTAIN and the caption to Maintenance (properties of the dialog).  Then add a list control, three buttons and a picture control.  Select the list control and in the properties make certain ‘Always Show Selection’ is true, ‘No Sort Header’ is true, ‘Single Selection’ is false, ‘Sort’ is none and ‘View’ is report.  Select the first button, change the ID to IDC_BUTTON_EXPORT and the caption to Export.  The second button has the ID to be IDC_BUTTON_IMPORT and the caption as Import.  The third button is IDC_BUTTON_DELETE and Delete as the caption.  The picture control we have an ID of IDC_STATIC_BOUNDARY and make certain the ‘Type’ is Frame.

Resource editor
You can use the buttons in the resource editor to help align (all four sides) and position controls.  The picture above is how I have the dialog layout in the resource editor.  Also delete the two buttons (OK and Cancel) that were on the default dialog resource template - we don't need them.

Now right click on the dialog background (NOT a control) and select add new class.  We require an MFC based class with the name CDlgMaintain and select CDialog as the base class.  In the resource editor open the menu we have for the application, select the edit point then the ‘Database Maintenance’ sub menu item.  Now right click and ‘Add Event Handler’.  We want to add a command handler to the CSudokuDoc class.

void CSudokuDoc::OnEditDatabasemaintenance()
                      {
                          //Import and Export to database, remove unwanted entries from the database
                          CDlgMaintain dlg(m_pDB);
                          dlg.DoModal();
                      }
                      

Open in new window


We also require the #include “DlgMaintain.h” at the top of the file (otherwise it won’t compile).

Note the dlg(m_pDB) as we declare the variable.  Somehow we need to pass a pointer to the database to this dialog so that it can perform operations on the database.  I have made a custom constructor for the dialog class which requires a pointer to the database.  This can be useful in that it reduces the risk of forgetting to set the database pointer in a later line of code after declaring a variable of this type.  The following is the code we need to add in the DlgMaintain.cpp / .h files to the wizard generated code:

#pragma once
                      #include "afxcmn.h"
                      #include "afxdao.h"
                      #pragma warning(disable : 4995)
                      
                          //CDlgMaintain(CWnd* pParent = NULL);   //Remove the standard code, we do NOT allow this
                          CDlgMaintain(CDaoDatabase* pDB, CWnd* pParent = NULL);   
                      
                      private:
                          CDaoDatabase* m_pDB;
                      public:
                          virtual void OnOK() {};
                      

Open in new window


And in .cpp file

CDlgMaintain::CDlgMaintain(CDaoDatabase* pDB, CWnd* pParent /*=NULL*/)
                          : CDialog(CDlgMaintain::IDD, pParent)
                          , m_pDB(pDB)
                      {
                      }
                      

Open in new window


The application should now compile and the maintenance dialog is available.  Try pressing the return key on the keyboard – nothing happens – that is because of the virtual function OnOK.  The default behaviour of the dialog is to respond to the return key by running the OnOK function.  The base class implementation will dismiss the dialog – this line of code stops that happening.

Now we need to get things implemented in it.  From the resource editor right click the mouse on the list control and add a variable.  We want a control type variable (CListCtrl) and call it m_wndList – make it a private variable, no other class should require access to this.  In the header declare two new private functions – PrepareColumnHeaders, no parameters and returning void and AddData, no parameters and also returning void.

private:
                          CDaoDatabase* m_pDB;
                          CListCtrl m_wndList;
                          void PrepareColumnHeaders();
                          void AddData();
                      

Open in new window


Modify the OnInitDialog function (if it isn’t there then use the properties window of the dialog from the class window view to select the overrides and add the override for OnInitDialog).

BOOL CDlgMaintain::OnInitDialog()
                      {
                          CDialog::OnInitDialog();
                      
                          PrepareColumnHeaders();    //Columns into the list control
                          AddData();    //Fill from the database
                      
                          return TRUE;  // return TRUE unless you set the focus to a control
                          // EXCEPTION: OCX Property Pages should return FALSE
                      }
                      
                      void CDlgMaintain::PrepareColumnHeaders()
                      {
                          CRect rect;
                          m_wndList.GetClientRect(&rect);
                      
                          //Width of control MINUS one fixed width column plus space for vertical scroll bar
                          int cx = rect.Width() - 40 - GetSystemMetrics(SM_CXVSCROLL);    
                      
                          m_wndList.InsertColumn(0, "ID", LVCFMT_LEFT, 40);
                          m_wndList.InsertColumn(1, "Game", LVCFMT_LEFT, cx);    //Fills the control with this column
                      }
                      
                      void CDlgMaintain::AddData()
                      {
                          //Remove any entries first
                          m_wndList.DeleteAllItems();
                      
                          CDaoRecordset RS(m_pDB);
                          RS.Open(AFX_DAO_USE_DEFAULT_TYPE, "SELECT ID, GameDetail FROM Games ORDER BY ID");
                      
                          //Just in case there are no games in the database - nothing to put in the list control
                          if(RS.IsBOF() && RS.IsEOF())
                              return;
                      
                          RS.MoveFirst();
                          UINT nItem = -1;
                          while(!RS.IsEOF())
                          {
                              LVITEM lvItem = {0};    //Set all members to be zero initially
                              lvItem.mask = LVIF_TEXT;
                              lvItem.iItem = nItem;
                      
                              //ID of the item in the DB, put into a string
                              COleVariant var = RS.GetFieldValue("ID");
                              CString szID;
                              szID.Format("%ld", var.lVal);
                              lvItem.pszText = szID.GetBuffer();
                      
                              //nItem is the index of the newly added item
                              nItem = m_wndList.InsertItem(&lvItem);
                      
                              //Now the game 'details'
                              var = RS.GetFieldValue("GameDetail");
                              //It is a string in the table, now convert it to a string we can use
                              CString szGame(var.pbVal);
                              m_wndList.SetItemText(nItem, 1, szGame.GetBuffer());
                      
                              //Next record and increment the positioning counter for the InsertItem (so appears at end of the list control)
                              RS.MoveNext();
                              nItem++;
                          }
                      
                          //If there are any items then select the first in the list
                          if(nItem >= 0)
                              m_wndList.SetItemState(0, LVIS_FOCUSED | LVIS_SELECTED, LVIS_FOCUSED | LVIS_SELECTED);
                          
                          RS.Close();
                      }
                      

Open in new window


If you compile and run you should see a picture similar to the following when you select Database Maintenance from the edit menu.

List view with games in database
Obvious how the game is laid out in the grid isn’t it (well, not really, do you understand the string of digits? ).  This is what we have the frame underneath the list control for.  Inside that frame we will display the selected game, but only if one and one only is selected in the list control.  Add a new class to the project form the class view.  It is a C++ (not MFC) class and name it CSudokuDCPainter.  This will handle the painting of the game inside this rectangle – we are delegating the painting to another class because we will also require a game to be displayed in the solve dialog (code reuse).

class CSudokuDCPainter
                      {
                      public:
                          CSudokuDCPainter(void) {};
                          ~CSudokuDCPainter(void) {};
                          void Paint(CDialog* pDlg, CDC* pDC, UINT nBoundary, CString szGame);
                      };
                      

Open in new window


And

void CSudokuDCPainter::Paint(CDialog* pDlg, CDC* pDC, UINT nBoundary, CString szGame)
                      {
                          //We have a block of code in the view - with a copy/paste 
                          //we can use that with only minor changes here (adding a pDlg->)
                          //for drawing the cell boundaries.  
                      
                          CRect rcFrame;
                          pDlg->GetDlgItem(nBoundary)->GetWindowRect(&rcFrame);    //Get where it is to be drawn
                      
                          pDlg->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);
                      
                          //Do we have a game to paint?
                          if(szGame.IsEmpty())
                              return;
                      
                          //Now split into the 'digits' for display - only digits 1..9
                          //We did something similar for the hints in the grid button DrawItem routine
                      
                          //We have an iOffsetX & Y for the 3x3 grid spacers - However we now require 9 rows
                          //so recalculate a miini offset inside the larger offsets
                          int iOffsetXmini = iOffsetX / 3;
                          int iOffsetYmini = iOffsetY / 3;
                      
                          CRect rcClient;
                          CString s;
                      
                          //Otherwise the digits appear with a backgound colour
                          pDC->SetBkMode(TRANSPARENT);
                      
                          //Use the same font as the dialog, rather than the default font in the DC
                          CFont* pOldFont = pDC->SelectObject(pDlg->GetFont());
                      
                          for(int iRow = 0; iRow < 9; iRow++)
                          {
                              //every third row reset to the internal spacers 
                              //(remember integral math for the division, box might not be multiple of 9)
                              if((iRow % 3) == 0)
                                  rcClient.top = rcFrame.top + (iRow / 3) * iOffsetY;
                              else
                                  rcClient.top += iOffsetYmini;
                      
                              rcClient.bottom = rcClient.top + iOffsetYmini;
                      
                              for(int iCol = 0; iCol < 9; iCol++)
                              {
                                  if((iCol % 3) == 0)
                                      rcClient.left = rcFrame.left + (iCol / 3) * iOffsetX;
                                  else
                                      rcClient.left += iOffsetXmini;
                      
                                  rcClient.right = rcClient.left + iOffsetXmini;
                      
                                  s = szGame[iRow*9 + iCol];
                      
                                  //Only display if non zero
                                  if(s.Compare("0") != 0)
                                      pDC->DrawText(s, s.GetLength(), &rcClient, DT_SINGLELINE|DT_VCENTER|DT_CENTER);
                              }
                          }
                      
                          //reset the dc to the original - it might be reused elsewhere
                          pDC->SelectObject(pOldFont);
                      }
                      

Open in new window


Nothing particularly special in the code above so I am not going to add any more description to it.  Is there a reason we drew the frame directly in the view but here used a helper class?  Basically, yes there is - just to make the explanation simpler in the earlier article.  Note that as a result we now have a poorer design because code is being duplicated.

Now we need to modify the OnPaint function of the Maintenance dialog (if it doesn’t exist add a handler for the WM_PAINT message via properties of the class view – we’ve done similar tasks in the earlier articles) as follows:

void CDlgMaintain::OnPaint()
                      {
                          CPaintDC dc(this); // device context for painting
                          // TODO: Add your message handler code here
                          // Do not call CDialog::OnPaint() for painting messages
                      
                          CString szGame;
                      
                          //Get the selected game (if any, and also if only one selected) for painting onto this dialog
                          POSITION pos = m_wndList.GetFirstSelectedItemPosition();
                          if(pos != NULL) //Is something selected in the list
                          {
                              //Get some info about the selected item
                              int iIndex = m_wndList.GetNextSelectedItem(pos);
                      
                              if(pos == NULL)    //Must only be one selected in the list control
                              {
                                  //Get the game details from the list control
                                  szGame = m_wndList.GetItemText(iIndex, 1);
                              }
                          }
                      
                          CSudokuDCPainter painter;
                          painter.Paint(this, &dc, IDC_STATIC_BOUNDARY, szGame);
                      }
                      

Open in new window


Here we have a multi-selection list control.  Using the GetFirstSelectedItemPosition and GetNextSelectedItem functions is how one can iterate through the items that have been selected in a list control.  If you compile and build you should see something like the following:  (Note if you make a multiple selection of items in the list then the grid is empty).

Maintenance dialog
Unfortunately if you change the selection in the list control then the display of the grid is not updated to reflect that.  We need to force the grid to be updated when the selection in the list is changed.  So in the resource editor select view this maintenance dialog template and select the list control then view the properties.  Click the ‘control events’ button and add a new function for the LVN_ITEMCHANGED event.  The auto generated function will be as follows:

void CDlgMaintain::OnLvnItemchangedList1(NMHDR *pNMHDR, LRESULT *pResult)
                      {
                          LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
                          // TODO: Add your control notification handler code here
                          *pResult = 0;
                      }
                      

Open in new window


We need to add an extra line of code before the function exits.  Simplest (but not the most efficient) is

void CDlgMaintain::OnLvnItemchangedList1(NMHDR *pNMHDR, LRESULT *pResult)
                      {
                          LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
                          // TODO: Add your control notification handler code here
                          *pResult = 0;
                      
                          //Force a redraw - for the details of the game
                          Invalidate();
                      }
                      

Open in new window


Compile and test it – it should update the grid as you change selections in the list.  Why isn’t it efficient?  The complete dialog client area is being redrawn, including elements (buttons, list) that have not been changed.  So instead of Invalidate one can use the following to invalidate just a section of the dialog.  However for this to work efficiently in the OnPaint (or OnDraw) one needs to find just what area has been changed and just redraw that area.

    //Force a redraw - for the details of the game
                          CRect rc;
                          GetDlgItem(IDC_STATIC_BOUNDARY)->GetWindowRect(&rc);
                          ScreenToClient(&rc);
                          InvalidateRect(rc);
                      

Open in new window


We have to code the actual database maintenance now, the visual effects are finished.  Add handlers for the button clicks for the Delete, Export and Import buttons using the wizard (resource editor – context menu – add event handler).

Maybe one can reduce the amount of duplicate code in the following (in some respects both delete and export are similar in the looping) but I’m going to duplicate code for simplicity.

To delete, we need to get the ID’s of the items to delete and then remove them from the database.  I’ll use an SQL statement to perform the deletion.  

void CDlgMaintain::OnBnClickedButtonDelete()
                      {
                          //Get the selected game if any, exit routine if nothing selected
                          POSITION pos = m_wndList.GetFirstSelectedItemPosition();
                          if(pos == NULL)
                              return;
                      
                          //Ask for confirmation - there is no undo
                          if(AfxMessageBox(IDS_QRY_DELETE_GAMES, MB_YESNO | MB_ICONQUESTION) != IDYES)
                              return;
                      
                          int iIndex;
                          CString s, szRecordKeys;
                          while(pos != NULL) //Is something selected in the list
                          {
                              iIndex = m_wndList.GetNextSelectedItem(pos);
                              s = m_wndList.GetItemText(iIndex, 0);    //The ID of the game selected
                              szRecordKeys += s + _T(",");    //Add the index and append a comma
                          }
                      
                          //Remove the final comma from the string containing the keys
                          szRecordKeys.TrimRight(_T(','));
                      
                          //Prepare the delete command
                          s.Format(_T("DELETE * FROM Games WHERE ([ID] IN (%s))"), szRecordKeys);
                          
                          //Run the delete command
                          m_pDB->Execute(s);
                      
                          //Refill the list and redisplay
                          AddData();
                          Invalidate();
                      }
                      

Open in new window


Note that I have added a resource into the string table in the resource editor to provide the text for confirming the delete.  In theory there is a minor problem with this code.  The SQL statement – there is a limit on how many characters it can contain so in a production environment one ought to check for that and perform the action in a loop if the size is overrun.

The export operation is next, we want to write out to a file that will be ‘understood’ by the import operation.  Basically all we require is the game to be exported, each game on a new line in the file.  We need to prompt for a file name (and location) but most of this work is done for us in a common dialog – the CFileDialog class in MFC.

void CDlgMaintain::OnBnClickedButtonExport()
                      {
                          //Get the selected game if any, exit routine if nothing selected
                          POSITION pos = m_wndList.GetFirstSelectedItemPosition();
                          if(pos == NULL)
                              return;
                      
                          //Get the file name - we will use sdl (sudoku LIST) as the extension
                          CFileDialog dlg(FALSE        //save as
                                          , _T("sdl")    //default extension
                                          , NULL        //no file name 
                                          , OFN_OVERWRITEPROMPT    //if file exist warn about replacing it
                                          , _T("Sudoku Lists (*.sdl)|*.sdl||")    //Filter for the file extensions - allows only sdl file extensions
                                          , this); //Parent window
                      
                      
                          if(dlg.DoModal() != IDOK)    //display and exit function if cancel was used
                              return;
                      
                          //open a stdio file for writing the exported items
                          CStdioFile file(dlg.GetPathName(), CFile::modeCreate | CFile::modeWrite);
                      
                          int iIndex;
                          while(pos != NULL) //Is something selected in the list
                          {
                              iIndex = m_wndList.GetNextSelectedItem(pos);
                              file.WriteString(m_wndList.GetItemText(iIndex, 1));    //Write the 'game' into the file
                              
                              //Append a carriage return new line pair IF there is another game to export after this one
                              if(pos != NULL)
                                  file.WriteString(_T("\r\n"));
                          }
                          file.Close();
                      }
                      

Open in new window


Note the filter - "Sudoku Lists (*.sdl)|*.sdl||". This is a string where one can define which extensions will appear in the ‘file type’ combo on the common dialog.  Specifically take note of the | character.  A single one acts as a delimiter for the different extension types to be displayed in  the combo, a double one instructs the common dialog that it is the end of the listing.  (Again a common fault resulting in questions like why doesn’t the following code work correctly? ).  The code is again fairly simple to understand.

Now to import a file in and store into the database.  We need to prompt for the file (CFileDialog again), open and parse the file, storing the individual games.  Ahhh, but what if a game is already in the database?  In the document we already encountered a similar thing of directly saving a game after entering by the keyboard – a try…catch block

void CDlgMaintain::OnBnClickedButtonImport()
                      {
                      	//Get the file name - we will use sdl (sudoku LIST) as the extension
                      	CFileDialog dlg(TRUE		//open
                      					, _T("sdl")	//default extension
                      					, NULL		//no file name 
                      					, NULL		//no restrictions
                      					, _T("Sudoku Lists (*.sdl)|*.sdl||")	//Filter for the file extensions - allows only sdl file extensions
                      					, this); //Parent window
                      
                      
                      	if(dlg.DoModal() != IDOK)	//display and exit function if cancel was used
                      		return;
                      
                      	//open a stdio file for reading the games to import
                      	CStdioFile file(dlg.GetPathName(), CFile::modeRead);
                      	CString s, szSQL;
                      	while(file.ReadString(s))	//keep reading from the file, return true if a line was read
                      	{
                      		szSQL.Format(_T("INSERT INTO Games (GameDetail) SELECT '%s' AS gd"), s.Left(81));	//Game is 81 chars long - removes any new line coding
                      
                      		try
                      		{
                      			m_pDB->Execute(szSQL);
                      		}
                      		catch(CDaoException* pe)
                      		{
                      			if(pe->m_pErrorInfo->m_lErrorCode != 3022)	//duplicate values into a field defined as unique
                      			{
                      				AfxMessageBox(pe->m_pErrorInfo->m_strDescription, MB_ICONEXCLAMATION);
                      			}
                      			pe->Delete();
                      		}
                      	}
                      	file.Close();
                      
                      	//Refill the list and redisplay
                      	AddData();
                      	Invalidate();
                      
                      	AfxMessageBox(_T("Import completed"));
                      }
                      

Open in new window


OK, the Import completed should be a resource string, maybe it would be nice to keep count of how many entries were added successfully and how many failed – let's do that just for completeness.

Here is the code

    int iAdded = 0, iFailed = 0;
                          while(file.ReadString(s))    //keep reading from the file, return true if a line was read
                          {
                              szSQL.Format(_T("INSERT INTO Games (GameDetail) SELECT '%s' AS gd"), s.Left(81));    //Game is 81 chars long - removes any new line coding
                      
                              try
                              {
                                  m_pDB->Execute(szSQL);
                                  iAdded++;
                              }
                              catch(CDaoException* pe)
                              {
                                  iFailed++;
                                  if(pe->m_pErrorInfo->m_lErrorCode != 3022)    //duplicate values into a field defined as unique
                                  {
                                      AfxMessageBox(pe->m_pErrorInfo->m_strDescription, MB_ICONEXCLAMATION);
                                  }
                                  pe->Delete();
                              }
                          }
                          file.Close();
                      
                          //Refill the list and redisplay
                          AddData();
                          Invalidate();
                      
                          s.Format(IDS_IMPORT_RESULTS, iAdded, iFailed);
                          AfxMessageBox(s);
                      

Open in new window


And here is how IDS_IMPORT_RESULTS is defined in the string resources: Import completed, %d games added, %d games were already in the database.  Note that the Format function of the CString can also work with a resource string, you don’t have to hard code it in the implementation.

Conclusion:

We have seen a modal dialog in use.
We have customised dialog constructors to pass information (instead of via member functions after construction).
We have seen how to use a (report style) list control.
We have implemented data manipulation in a database via SQL commands.
We have used a common file dialog (CFileDialog).


Click here for the source code for this article


Previous article in the series is here:  Sudoku in MFC: Part 9
There we implemented a simple undo mechanism.

Next article in the series is here:  Sudoku in MFC: Part 11
Here we will be working with a modeless dialog and a worker thread.  We will also share data between threads and work with a recursive function for solving a game of sudoku.


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.
0
4,936 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.