Multiple Document Interface (MDI) Applications

                 Serialization and Multiple View support

           

MDI applications are designed using the document-view architecture in which there could be many views associated with a document object. Further, the application can open multiple document objects at the same time. This logical breakdown allows the data related functions to be assigned to the document class object including serialization (saving/retrieving data to/from the hard disk) and the view related functions to be assigned to the view class object. The view object is contained inside a childframe object which in turn is contained inside a mainframe object.

                                                                                                            Child frames

 

 

 

 


                                                                                                           

                       

Serialization                                                                                        Main frame

 

 

 

 

 


As discussed previously, MFC framework provides several support functions (referred to as the Document-View interfaces). These are:

OnNewDocument –    called when application starts or from the menu File->New is selected. We often write document object (data) initialization code here.

OnInitialUpdate -       called one time for a view object when a new view is created. This could be when the application starts or if the user creates a new view by selecting Window->New from the menu.

UpdateAllViews –     triggers a call to OnUpdate in each view class object connected to the current document. UpdateAllViews needs to be called when a user updates the data through one of the views. The code in the OnUpdate will usually then read the latest data from the document object and call the OnDraw to display it.

 

The view class provides a member function called GetDocument which returns a pointer to the associated document object.

Note that the different views connected to the document object can be of different types. For example, one of the views could be of CView type (default view type in an SDI or MDI application) while the other view could be of CFormView type with dialog controls. We will see how to associate multiple views with the same document object through the following example.

Create an MFC Appwizard(exe) type of project. Name the project mdimview. Select all the defaults.

 

In the last wizard screen, you will notice that there are 5 classes created for the project as shown below. MDI applications contain a childframe inside a mainframe and thus make it possible to associate multiple views with a document (or to open multiple documents at the same time – each with possibly multiple views)

 

Add a new header file called Student.h to the project. Type in the following code.

//--------Student.h------------

#ifndef STUDENT_H

#define STUDENT_H

class Student : public CObject {

               DECLARE_SERIAL(Student);

 

               CString m_szName;

               int m_nID;

public:

               Student();

               Student(const CString & nm, int id);

 

               void set(const CString & nm, int id);

               CString GetName() const;

               int GetID() const;

 

               virtual void Serialize(CArchive & ar); // override serialize

};

#endif

 

Add another cpp file called Student.cpp to the project with the following code:

//-----Student.cpp-----------

#include "stdafx.h"

#include "Student.h"

 

IMPLEMENT_SERIAL (Student,CObject,1);

 

Student::Student() { }

 

Student::Student(const CString & nm, int id) {

               m_szName = nm;

               m_nID = id;

}

 

void Student::set(const CString & nm, int id) {

               m_szName = nm;

               m_nID = id;

}

 

CString Student::GetName() const { return m_szName;}

 

int Student::GetID() const { return m_nID;}

 

void Student::Serialize(CArchive & ar) {

               if (ar.IsLoading())

                              ar >> m_szName >> m_nID ;

               else

                              ar << m_szName << m_nID ;

}

 

Modify the mdimviewDoc.h file to as shown below. The additions are shown in bold.

// mdimviewDoc.h : interface of the CMdimviewDoc class

//

/////////////////////////////////////////////////////////////////////////////

 

#if !defined(AFX_MDIMVIEWDOC_H__A9BFF795_B1C1_4DC7_8594_FFC9C05777C1__INCLUDED_)

#define AFX_MDIMVIEWDOC_H__A9BFF795_B1C1_4DC7_8594_FFC9C05777C1__INCLUDED_

 

#if _MSC_VER > 1000

#pragma once

#endif // _MSC_VER > 1000

 

#include "Student.h"

#include <afxtempl.h>

 

void AFXAPI SerializeElements(CArchive & ar, Student **pSt, int cnt);

 

class CMdimviewDoc : public CDocument

{

protected: // create from serialization only

               CMdimviewDoc();

               DECLARE_DYNCREATE(CMdimviewDoc)

 

// Attributes

public:

 

// Operations

public:

            int GetStudentCount() const;

            Student * GetStudent(int num) const;

protected:

            CArray <Student *, Student * &> m_setofStudents;

 

Modify the mdimviewDoc.cpp file to add the SerializeElements function. Also add the code for GetStudentCount and GetStudent member functions.

 

void AFXAPI SerializeElements(CArchive & ar, Student **pSt, int cnt) {

               for (int i = 0; i < cnt; i++, pSt++)

               {

                              if (ar.IsStoring())

                                             (*pSt)->Serialize(ar);

                              else

                              {

                                             Student * pNewSt = new Student;

                                             pNewSt ->Serialize(ar);

                                             *pSt = pNewSt;

                              }

               }

}

 

int CMdimviewDoc::GetStudentCount() const

{

               return m_setofStudents.GetSize();

}

 

Student * CMdimviewDoc::GetStudent(int num) const {

               Student * pst = 0;

               if (num < m_setofStudents.GetSize())

                              pst = m_setofStudents.GetAt(num);

               return pst;

}

 

Modify the menu resource (IDR_MDIMVITYPE) to add a top level menu item called Student. Add a menu underneath it called Add Student with an ID of ID_STUDENT_ADD.

 

Insert a new dialog type resource in the project. The dialog design looks like:

The two edit boxes have IDs of IDC_NAME and IDC_ID respectively.

 

When you choose view->Class wizard, it will prompt you to assign a new class to the dialog resource. Give it a class name of CAddStudent.

 

Through the class wizard, associate two variables to the two edit boxes. For the name edit box, associate it with a CString variable called m_szName. Similarly, associate the ID edit box with an int variable called m_nID.

 

Add an event handler to the document class for the Add Student menu item. The code for the handler is:

 

void CMdimviewDoc::OnStudentAdd()

{

               // TODO: Add your command handler code here

                              // TODO: Add your command handler code here

               CAddStudent d1;

               if (d1.DoModal() == IDOK)

               {

                              Student * pst = new Student(d1.m_szName,d1.m_nID);

                              m_setofStudents.Add(pst);

                              UpdateAllViews(NULL);

                              SetModifiedFlag();

               }

              

}

 

Modify the document cpp file to as shown below.

 

 

// mdimviewDoc.cpp : implementation of the CMdimviewDoc class

//

 

#include "stdafx.h"

#include "mdimview.h"

 

#include "mdimviewDoc.h"

#include "AddStudent.h"

#include "Student.h"

 

#ifdef _DEBUG

….

….

void CMdimviewDoc::Serialize(CArchive& ar)

{

               if (ar.IsStoring())

               {

                              // TODO: add storing code here

                              m_setofStudents.Serialize(ar);

               }

               else

               {

                              // TODO: add loading code here

                              m_setofStudents.Serialize(ar);

               }

}

 

Through the class wizard add the code for OnDraw function to as shown below.

 

void CMdimviewView::OnDraw(CDC* pDC)

{

               CMdimviewDoc* pDoc = GetDocument();

               ASSERT_VALID(pDoc);

               // TODO: add draw code for native data here

               TEXTMETRIC tm;

               pDC->GetTextMetrics(&tm);

               int nLineHeight = tm.tmHeight + tm.tmExternalLeading;

               CPoint ptText(0,0);

               for (int i = 0; i < pDoc->GetStudentCount(); i++) {

                              CString s1;

                              Student * pst = pDoc->GetStudent(i);

                              s1.Format("Student Name = %s, ID = %d\n",pst->GetName(),

                                                                                                                        pst->GetID());

                              pDC->TextOut(ptText.x, ptText.y,s1);

                              ptText.y += nLineHeight;

               }

                  

}

 

Also add the code for OnUpdate function (through class wizard):

void CMdimviewView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint)

{

               // TODO: Add your specialized code here and/or call the base class

              

               InvalidateRect(NULL);

}

 

Build and execute the program. Test the serialization capabilities also.

 

 

You can add the OnNewDocument function to the document class to initialize the document data.

BOOL CMdimviewDoc::OnNewDocument()

{

               if (!CDocument::OnNewDocument())

                              return FALSE;

 

               // TODO: add reinitialization code here

               // (SDI documents will reuse this document)

                              m_setofStudents.RemoveAll();

                              Student * pst = new Student("First Student     ",100);

                              m_setofStudents.Add(pst);

 

               return True;

}

 

 

Using different kind of View:

            The default base class for the view is CView for the SDI and MDI applications. One problem with CView is that it does not allow you to add controls to the client area. If you need controls in the client area, then the base class for the view should be CFormView. Here is how the view class hierarchy is designed in MFC.

                                    CView

 


                        CScrollView               CEditView

 

 


                        CFormView

 

To change the view class to another type, you could choose a different base class when going through the application wizard steps. In the last dialog of the wizard, when it displays the different classes that will be created in the project, you can select a particular class and change its base class. Once an application is created, then the only way to change a view’s behavior is by modifying the “document template” which holds the relationship between a document and its associated view. We will explain this by modifying the current example to display the students in a CFormView type of object (instead of the CView).

 

Insert another dialog resource to the project. Design the dialog as shown below. Since this dialog will be used as a view, the entire dialog should have the properties of:

            Style                Child

            Border             None

            Visible            UnChecked

            Titlebar           Unchecked

 

The two edit boxes in the dialog have IDs of IDC_NAME and IDC_ID respectively. The List Control has an ID of IDC_LIST and the two buttons have IDs of IDC_CLOSE and IDC_APPLY.

 

Also after designing the dialog when the class wizard asks you to create a new class for the dialog, its base class should be CFormView, as shown below.

 

 

Through the class wizard associate a variable called m_lbDisplay of category control to the List control and m_szName (CString variable) to the first edit box, and m_nID (int variable) to the second edit box.

 

Write the following event handlers for the Apply button, close button, OnInitialUpdate and OnUpdate handlers in the CFormTest class.

void CFormTest::OnApply()

{

               // TODO: Add your control notification handler code here

               CMdimviewDoc * pDoc;

               pDoc = (CMdimviewDoc *) GetDocument();

               ASSERT_VALID(pDoc);

              

               UpdateData(TRUE);

               if (m_szName.GetLength() > 0)  // edit box is not empty

               {

                              Student * pst = new Student(m_szName,m_nID);

                              int index = pDoc->m_setofStudents.Add(pst);

                              CString s1;

                              s1.Format("%s %d",m_szName,m_nID);

                              m_lbDisplay.InsertItem(index,s1,0);

                              pDoc->SetModifiedFlag(TRUE);

                              pDoc->UpdateAllViews(this);

               }

 

}

 

void CFormTest::OnClose()

{

               // TODO: Add your control notification handler code here

               PostMessage(WM_COMMAND, ID_FILE_CLOSE);

}

 

void CFormTest::OnInitialUpdate()

{

               CFormView::OnInitialUpdate();

              

               // TODO: Add your specialized code here and/or call the base class

               CMdimviewDoc * pDoc = (CMdimviewDoc *) GetDocument();

               ASSERT_VALID(pDoc);

               m_lbDisplay.DeleteAllItems();

               for (int i = 0; i < pDoc->GetStudentCount(); i++)

               {

                              CString nm = (pDoc->GetStudent(i))->GetName();

                              int id = (pDoc->GetStudent(i))->GetID();

                              CString s1;

                              s1.Format("%s %d",nm,id);

                              m_lbDisplay.InsertItem(i,s1,0);

               }

}

 

void CFormTest::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint)

{

               // TODO: Add your specialized code here and/or call the base class

                              // TODO: Add your specialized code here and/or call the base class

    CMdimviewDoc * pDoc = (CMdimviewDoc *) GetDocument();

               ASSERT_VALID(pDoc);

               m_lbDisplay.DeleteAllItems();

               for (int i = 0; i < pDoc->GetStudentCount(); i++)

               {

                              CString nm = (pDoc->GetStudent(i))->GetName();

                              int id = (pDoc->GetStudent(i))->GetID();

                              CString s1;

                              s1.Format("%s %d",nm,id);

                              m_lbDisplay.InsertItem(i,s1,0);

               }

              

}

 

 

Add the following include files to the top of FormTest.cpp file.

 

#include "Student.h"

#include "mdimviewDoc.h"

 

You need to modify the document template in the mdimview.cpp file (InitInstance function) to as shown below.

               pDocTemplate = new CMultiDocTemplate(

                              IDR_MDIMVITYPE,

                              RUNTIME_CLASS(CMdimviewDoc),

                              RUNTIME_CLASS(CChildFrame), // custom MDI child frame

            //          RUNTIME_CLASS(CMdimviewView));

                        RUNTIME_CLASS(CFormTest));

               AddDocTemplate(pDocTemplate);

 

 

 

Providing Different View Types to a Document:

            Note that the application class stores pointer(s) to the CMultiDocTemplate object and passes it to the CMainFrame class when a new view needs to be generated. Thus to provide different kinds of views to a document, the first thing we need to do is to create different CMultiDocTemplate objects in the InitInstance of the application class.

 

            Modify the application class (mdimview.h file) to add two CDocTemplate pointers and the corresponding get functions as shown below.

               CDocTemplate * GetCViewTemplate() const;

               CDocTemplate * GetCFormViewTemplate() const;

private:

               CDocTemplate * m_pCViewTemplate;

               CDocTemplate * m_pCFormViewTemplate;

 

 

Then modify the InitInstance function to create two CMultiDocTemplate objects as shown below.

 

Write the code for GetCViewTemplate( ) and GetCFormViewTemplate( ) functions in the mdimview.cpp file, as shown below.

//             CMultiDocTemplate* pDocTemplate;  before two multidoc objects

 

               m_pCFormViewTemplate = new CMultiDocTemplate(

                              IDR_MDIMVITYPE,

                              RUNTIME_CLASS(CMdimviewDoc),

                              RUNTIME_CLASS(CChildFrame), // custom MDI child frame

               //             RUNTIME_CLASS(CMdimviewView));

                              RUNTIME_CLASS(CFormTest));

 

               AddDocTemplate(m_pCFormViewTemplate);

               m_pCViewTemplate = new CMultiDocTemplate(

                              IDR_MDIMVITYPE,

                              RUNTIME_CLASS(CMdimviewDoc),

                              RUNTIME_CLASS(CChildFrame), // custom MDI child frame

                              RUNTIME_CLASS(CMdimviewView));

               AddDocTemplate(m_pCViewTemplate);

 

Note that, if you had started with a CFormView type of view and wanted to add a CView type of view, you could have added a new class to the application through the class wizard. This new class would be given a name of CMdimviewView and its base class would be CView. However, in our current example, if you recall, we had started with a standard MDI application (which will have a CView type of view), then we had added another dialog class to it (derived from CFormView), so we have already both the view classes, and all we need to do is add the document templates and associated code.

 

Write the Get functions for the two document template objects in the mdimview.cpp file, as shown below.

 

CDocTemplate * CMdimviewApp::GetCFormViewTemplate() const {

               return m_pCFormViewTemplate ;

}

CDocTemplate * CMdimviewApp::GetCViewTemplate() const {

               return m_pCViewTemplate ;

}

 

If you build the program so far and try to run it, you will get a dialog in the beginning as shown below.

If you choose the top choice, a form view MDI GUI will be displayed, as shown below (because the first document template is a form view).

 

If you stop and execute the program again, and choose the second Mdimvi choice in the dialog, the program will run with a CView type of view as shown below.

Now we will add menu options to choose the type of view we want. Note that in an MDI application, each child window can have its own set of menus (possibly different from the main frame). In our application, we already have two menu resources so far, called:

IDR_MAINFRAME and IDR_MVITYPE,

 

Select the IDR_MVITYPE resource and from the edit menu, choose copy, then paste it to create another copy of the resource. Change the name of the resource to IDR_MDIFORMTYPE. Add two menu items underneath the window menu in both the IDR_MVITYPE and IDR_MDIFORMTYPE menus. The menus have IDs of ID_WINDOW_FORMDISPLAY (caption “Form View Display”) and ID_WINDOW_CVIEWDISPLAY (caption “CView Display”). Add the following handler to the two menu items in the mainframe class:

 

void CMainFrame::OnWindowFormdisplay()

{

               // TODO: Add your command handler code here

               CMDIChildWnd * pActiveChild = MDIGetActive();

               if (pActiveChild != 0) {

                              CDocument * pDocument = pActiveChild->GetActiveDocument();

                              if(pDocument != 0)

                              {

                                             CMdimviewApp * pApp = (CMdimviewApp *) AfxGetApp();

                                             CDocTemplate * pTemp;

                                             CFrameWnd * pFrame;

                                             pTemp = pApp->GetCFormViewTemplate();

                                             pFrame=pTemp->CreateNewFrame(pDocument,pActiveChild);

                                             if (pFrame != 0)

                                               pTemp->InitialUpdateFrame(pFrame,pDocument);

                              }

               }

}

 

void CMainFrame::OnWindowCviewdisplay()

{

               // TODO: Add your command handler code here

               CMDIChildWnd * pActiveChild = MDIGetActive();

               if (pActiveChild != 0) {

                              CDocument * pDocument = pActiveChild->GetActiveDocument();

                              if(pDocument != 0)

                              {

                                             CMdimviewApp * pApp = (CMdimviewApp *) AfxGetApp();

                                             CDocTemplate * pTemp;

                                             CFrameWnd * pFrame;

                                             pTemp = pApp->GetCViewTemplate();

                                             pFrame=pTemp->CreateNewFrame(pDocument,pActiveChild);

                                             if (pFrame != 0)

                                               pTemp->InitialUpdateFrame(pFrame,pDocument);

                              }

               }            

}

 

The code in the mdimview.cpp file, InitInstance function can now be modified to point to the two new resources in the document templates.

//             CMultiDocTemplate* pDocTemplate;  before two multidoc objects

 

               m_pCFormViewTemplate = new CMultiDocTemplate(

                              IDR_MDIFORMTYPE,

                              RUNTIME_CLASS(CMdimviewDoc),

                              RUNTIME_CLASS(CChildFrame), // custom MDI child frame

               //             RUNTIME_CLASS(CMdimviewView));

                              RUNTIME_CLASS(CFormTest));

 

               AddDocTemplate(m_pCFormViewTemplate);

               m_pCViewTemplate = new CMultiDocTemplate(

                              IDR_MDIMVITYPE,

                              RUNTIME_CLASS(CMdimviewDoc),

                              RUNTIME_CLASS(CChildFrame), // custom MDI child frame

                              RUNTIME_CLASS(CMdimviewView));

//          AddDocTemplate(m_pCViewTemplate);

 

Note that the above code follows the general guidelines of:

1.     Get a pointer to the active child window.

2.     Get a pointer to the active document window.

3.     Get a pointer to the application.

4.     Using the application pointer, get the document template for the new view

5.     Using the document template, create a new frame.

6.     Update the frame

 

Through the class wizard, add the OnInitialUpdate and OnUpdate handlers for the view class, as shown below.

void CMdimviewView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint)

{

               // TODO: Add your specialized code here and/or call the base class

              

               InvalidateRect(NULL);

}

 

void CMdimviewView::OnInitialUpdate()

{

               CView::OnInitialUpdate();

              

               // TODO: Add your specialized code here and/or call the base class

               InvalidateRect(NULL);

}

 

 

We can assign a different icon and a string resource to the new document template corresponding to the form view. You can copy the icon resource to the clip board from the edit menu and paste it back. Change the resource name to IDR_MDIFORMTYPE. Double click on it to design a different icon for it. You should choose the number of pixels to be 16x16 for the icon when you are designing or changing it in the bit map editor.

 

When we have a dialog or form type of view for an MDI application, it is better if the dialog occupies the proper child frame area, i.e., the size of frame is equal to the size of the dialog.. To do this, add the following code to the OnInitialUpdate function in the CFormTest class (FormTest.cpp file).

 

void CFormTest::OnInitialUpdate()

{

               CFormView::OnInitialUpdate();

              

               // TODO: Add your specialized code here and/or call the base class

            ResizeParentToFit(FALSE);

            ResizeParentToFit(FALSE);

              

              

To prevent the child frame from being resized, you can add the following code to the CChildFrame class.

 

BOOL CChildFrame::PreCreateWindow(CREATESTRUCT& cs)

{

               // TODO: Modify the Window class or styles here by modifying

               //  the CREATESTRUCT cs

            cs.style &= ~WS_THICKFRAME;

            cs.style &= ~WS_MAXIMIZEBOX;

               if( !CMDIChildWnd::PreCreateWindow(cs) )

                              return FALSE;

 

               return TRUE;

}

 

Build and execute the program.