C/C++ Q & A

Paul DiLascia

Paul DiLascia is a freelance software consultant specializing in training and software development in C++ and Windows. He is the author of Windows++: Writing Reusable Code in C++ (Addison-Wesley, 1992).

Click to open or copy the VIEW3 project files.

QI am developing a Windows® 3.1 MDI application with Visual C++™ and MFC. Each document can have three views: a text view, a contour plot view, and a 3D surface plot view of data from a manufacturing platform. The view is selected by the user when defining a document. The type of view is then stored in the document when it's saved. When the document is restored, how can I determine the correct view to invoke based on the saved view type? The way the document/view architecture is defined, this looksimpossible.

Roger Vaught

AThe easiest and most straightforward way to implement multiple views is to store an ID in your view class that identifies which "mode" the view is in, and act accordingly.


 enum { VIEW_TEXT, VIEW_CONTOUR, VIEW_3D };

void CMyView::OnDraw(CDC* pDC)
{
switch (m_nMode) {
case VIEW_TEXT:
OnDrawText(pDC);
break;
case VIEW_CONTOUR:
OnDrawContour(pDC);
break;
case VIEW_3D:
OnDraw3D(pDC);
break;
default:
ASSERT(FALSE);
}
}

This approach works well if the differences among your views are limited to a few places, such as drawing. But if each view has different commands and different behavior in a number of places, the switch (or if) statements will grow quickly tiresome, not to mention inelegant. Whenever you have a class with member functions that look like the fragment above, you really want several classes. After all, that's what C++ and virtual functions are for.


 class CViewText : public CView {
virtual void OnDraw(CDC* pDC); . . . };
class CViewContour : public CView {
virtual void OnDraw(CDC* pDC); . . . };
class CView3D : public CView {
virtual void OnDraw(CDC* pDC); . . . };

With these classes defined, when you or the framework calls pView->OnDraw, the right OnDraw function is invoked for whichever class pView really points to. That's what virtual functions do; it's their whole role in life. The only question is, how and when do you create the appropriate view?

The problem is that MFC assumes that there is one and only one view class associated with each kind of document. This is reflected in the definition of CDocTemplate, which stores three pointers for the document, frame, and view classes.


 class CDocTemplate : public CCmdTarget {

               .
               .
               .
  CRuntimeClass* m_pDocClass;   // for new docs
  CRuntimeClass* m_pFrameClass; // for new frames
CRuntimeClass* m_pViewClass; // for new views
};

(There are other classes too, for OLE frames and such, but you needn't worry about those here.) These pointers are initialized when you create the document template, usually in your app's InitInstance function.


 AddDocTemplate(new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CMyDoc),
RUNTIME_CLASS(CMyMainFrame),
RUNTIME_CLASS(CMyView)));

You only get to specify one view class. Not two, not three, not seventeen. One. When MFC decides it's time to create a new frame on the fly—such as whenever the user opens a document in a MDI app—MFC uses the CRuntimeClass pointers in CDocTemplate to create the objects it needs.


 // Create new doc 
// (in CDocTemplate::CreateNewDocument)
pDocument =
(CDocument*)m_pDocClass->CreateObject(); // Create new frame
// (in CDocTemplate::CreateNewFrame)
CFrameWnd* pFrame =
(CFrameWnd*)m_pFrameClass->CreateObject();

This is why views, frames, and docs must be declared with DECLARE_DYNCREATE (or DECLARE_SERIAL): so MFC can create them through its run-time class mechanism, CRuntimeClass::CreateObject. The view is created in similar fashion, but indirectly. The doc template creates the frame, and the frame in turn creates the view in its OnCreate handler. The frame knows which view class to create because CDocTemplate passes m_pViewClass inside an intermediate struct, CCreateContext. Since there's just one m_pViewClass pointer, you can have only one kind of view.

At least, that's how MFC's doc/view UI model is supposed to work. In practice, you can always do whatever you want—if you're sneaky enough. But before you go violating the nice pretty model, it's a good idea to think about what you're doing. Usually, whenever you bump into some sort of fundamental limitation, it's a sure sign that something deep is going on. What are the UI implications of having more than one view per document?

A number of questions arise. How do users select which view they want? If you save the view type in the document, does that mean changing views counts as modifying the document? If not, the change may not get saved. And what happens if there are two views open on the same document? Most MDI apps have a "New Window" command that lets users create another window (frame/view) on the current document. AppWizard even generates this command for you. Unless you explicitly remove it, users can create multiple views on the same doc. If there are two views on a document—say one is contour and one is 3D—which one do you save? The active one? Both? If both, should you also save the positions of the windows? If you save the positions and types of all the views, you're moving toward a workspace UI model, like the one Visual C++ 4.0 uses. In this model, there's a file called the "project" or "workspace" that references other files. The project provides a place to store the positions of all the document windows and other supra-document information.

My point is just that whether you go for a full-blown workspace UI model or not, you enter a different world as soon as you allow more than one view per document, so you need to sit down and explore all the implications and design your user interface carefully, before you write any code. This is one situation where adding what seems like an innocent feature in fact opens a whole can of worms.

That said, I implemented a simple multiview app, VIEW3, that implements three different views on a document (see Figures 1 and 2). VIEW3 documents store a single text string, which VIEW3 lets users view either as normal ASCII text, binary zeroes and ones, or as "binary stripes": the ones and zeroes are converted to black and white vertical stripes that resemble a bar code. Three commands, View As ASCII, View As Binary, and View As Binary Stripes, let users change the view. VIEW3 stores the type of whichever view was active when the document was last saved, and restores it when that document is opened.

Figure 1 Three, three, three views in one!

Figure 2 VIEWS

DYNTEMPL.H


 ////////////////////////////////////////////////////////////////
// Definition for CDynViewDocTemplate, a document template class that
// implements "dynamic views", i.e., the ability to change the view
// class dynamically, while the app is running. See DynTempl.cpp.
// CDynViewDocTemplate is designed to work in a MDI app.
//
// Copyright 1996 Microsoft Systems Journal. If this code works, 
// it was written by Paul DiLascia. If not, I don't know who wrote it.

////////////////
// This structure associates a view class with 
// a display name and index (position in table).
//
struct DYNAMICVIEWINFO {
   CRuntimeClass* rtc;     // MFC runtime class
   LPCSTR         name;    // display name of view
};

//////////////////
// CDynViewDocTemplate manages multiple views of the same doc. 
// It handles the "View As" commands, and restoring of initial 
// view when a document is loaded.
//
class CDynViewDocTemplate : public CMultiDocTemplate {
public:
   CDynViewDocTemplate(UINT nIDResource, CRuntimeClass* pDocClass,
      CRuntimeClass* pFrameClass, 
      const DYNAMICVIEWINFO* pViewInfo);
   virtual ~CDynViewDocTemplate();

   virtual void InitialUpdateFrame(CFrameWnd* pFrame, CDocument* pDoc,
      BOOL bMakeVisible = TRUE);

   int GetViewID(CDocument *pDoc);  // <== NOTE: You must implement this!
   int GetViewID(CView* pView);
   const DYNAMICVIEWINFO* GetViewInfo(CView* pView);

protected:
   DECLARE_DYNAMIC(CDynViewDocTemplate)
   const DYNAMICVIEWINFO*  m_pViewInfo;
   UINT                    m_nViewClasses;

   // Helper fn to change the view
   CView* ReplaceView(CFrameWnd* pFrame, int iNewView, BOOL bMakeVis=TRUE);

   //{{AFX_MSG(CDynViewDocTemplate)
   afx_msg BOOL OnViewAs(UINT nCmdID);
   afx_msg void OnUpdateViewAs(CCmdUI* pCmdUI);
   //}}AFX_MSG
   DECLARE_MESSAGE_MAP()
};

DYNTEMPL.CPP


 ////////////////////////////////////////////////////////////////
// Implementation for CDynViewDocTemplate, a document template that
// supports multiple views per doc.
//
// Copyright 1996 Microsoft Systems Journal. If this code works, 
// it was written by Paul DiLascia. If not, I don't know who wrote it.
//
#include "stdafx.h"
#include "doc.h"
#include "dyntempl.h"
#include "resource.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

IMPLEMENT_DYNAMIC(CDynViewDocTemplate, CMultiDocTemplate)

//////////////////
// NOTE: You must #define ID_VIEW_AS_BEGIN and ID_VIEW_AS_END in your 
// resource.h file. CDynViewDocTemplate uses RANGE handlers to handle 
// the "View As" commands and command update, in the given range.
//
BEGIN_MESSAGE_MAP(CDynViewDocTemplate, CDocTemplate)
   //{{AFX_MSG_MAP(CDynViewDocTemplate)
   ON_UPDATE_COMMAND_UI_RANGE(ID_VIEW_AS_BEGIN,ID_VIEW_AS_END,OnUpdateViewAs)
   ON_COMMAND_EX_RANGE(ID_VIEW_AS_BEGIN, ID_VIEW_AS_END, OnViewAs)
   //}}AFX_MSG_MAP
END_MESSAGE_MAP()

//////////////////
// Constructor is like CMultiDocTemplate, but takes an array of 
// DYNAMICVIEWINFO's instead of a single runtime class for the view.
//
CDynViewDocTemplate::CDynViewDocTemplate(UINT nIDResource, 
   CRuntimeClass* pDocClass, 
   CRuntimeClass* pFrameClass, 
   const DYNAMICVIEWINFO* pViewInfo)
   : CMultiDocTemplate(nIDResource, pDocClass, pFrameClass, NULL)
{
   ASSERT(pViewInfo);
   m_pViewInfo = pViewInfo;
   m_pViewClass = pViewInfo[0].rtc;

#ifdef _DEBUG
   // Check that # of entries in table matches begin/end command IDs
   for (int n=0; pViewInfo[n].name; n++)
      ;
   if (n!=ID_VIEW_AS_END-ID_VIEW_AS_BEGIN+1) {
      TRACE0("Number of view classes in table does not match command ID\n");
      TRACE0("range ID_VIEW_AS_BEGIN to ID_VIEW_AS_END!\n");
      ASSERT(FALSE);
   }
#endif
}

CDynViewDocTemplate::~CDynViewDocTemplate()
{
}

//////////////////
// Get the 0-based view class ID given a ptr to a view.
//
int CDynViewDocTemplate::GetViewID(CView* pView)
{
   for (int i=0; m_pViewInfo[i].name; i++) {
      if (m_pViewInfo[i].rtc == pView->GetRuntimeClass())
         return i;
   }
   return -1; // (invalid)
}

//////////////////
// Get class info about a view.
//
const DYNAMICVIEWINFO* CDynViewDocTemplate::GetViewInfo(CView* pView)
{
   ASSERT_VALID(pView);
   int iViewID = GetViewID(pView);
   return iViewID >=0 ? &m_pViewInfo[iViewID] : NULL;
}

//////////////////
// Override to change the view class when a document is opened,
// to use whichever view class was saved with the doc.
//
void CDynViewDocTemplate::InitialUpdateFrame(CFrameWnd* pFrame, 
   CDocument* pDoc, BOOL bMakeVisible)
{
   CMultiDocTemplate::InitialUpdateFrame(pFrame, pDoc, bMakeVisible);
   ReplaceView(pFrame, GetViewID(pDoc), bMakeVisible);
}

//////////////////
// Handle "View As Xxx" command to change view class. Both this and
// InitialUpdateFrame call a common helper, ReplaceView.
//
BOOL CDynViewDocTemplate::OnViewAs(UINT nCmdID) 
{
   CFrameWnd* pFrame = (CFrameWnd*)AfxGetMainWnd();
   ASSERT_VALID(pFrame);
   ASSERT(pFrame->IsFrameWnd());
   pFrame = pFrame->GetActiveFrame();
   ReplaceView(pFrame, nCmdID-ID_VIEW_AS_BEGIN, TRUE);
   return FALSE;
}

///////////////////
// Replace the current view with a one of a different class
//
CView* CDynViewDocTemplate::ReplaceView(CFrameWnd* pFrame, 
   int iNewViewType, BOOL bMakeVisible)
{
   ASSERT(ID_VIEW_AS_BEGIN<=iNewViewType+ID_VIEW_AS_BEGIN && 
      iNewViewType+ID_VIEW_AS_BEGIN<=ID_VIEW_AS_END);

   CView* pView = pFrame->GetActiveView();
   ASSERT_VALID(pView);

   CRuntimeClass* pViewClass = m_pViewInfo[iNewViewType].rtc;
   if (!pView->IsKindOf(pViewClass)) {

      // Tell MFC not to delete the doc when the view is destroyed
      CDocument* pDoc = pView->GetDocument();
      BOOL bSaveAutoDelete = pDoc->m_bAutoDelete;
      pDoc->m_bAutoDelete = FALSE;
      pFrame->SetActiveView(NULL);  // it's safer

#ifndef DONT_RECYCLE_HWND
      // This implementation reuses the actual (Windows) window
      //
      HWND hwnd = pView->Detach();  // remove window from view
      delete pView;                 // destroy C++ obj & remove from doc too
      pView = (CView*)pViewClass->CreateObject();  // create new view
      ASSERT(pView);
      ASSERT_KINDOF(CView, pView);
      pView->Attach(hwnd);          // reuse the same HWND !
      pDoc->AddView(pView);         // add view to doc
      pFrame->SetActiveView(pView); // make it the active view
#else
      // This implementation create a whole new (Windows) window
      //
      CRect rcView;
      pView->GetWindowRect(&rcView);// save original view window size
      pView->DestroyWindow();       // destroy old view

      // Create new view using CFrameWnd::CreateView, 
      // which requires a "CCreateContext"
      CCreateContext context;
      context.m_pNewViewClass = pViewClass;
      context.m_pCurrentDoc   = pDoc;
      pView = (CView*)pFrame->CreateView(&context);
      ASSERT(pView);
      ASSERT_VALID(pView);

      // Set view window size to same as old view
      pFrame->ScreenToClient(&rcView);
      pView->MoveWindow(&rcView, FALSE);
#endif

      pDoc->m_bAutoDelete = bSaveAutoDelete;

      // This will do good stuff, like update the title 
      // and send WM_INITIALUPDATE to the view.
      //
      pFrame->InitialUpdateFrame(pDoc, bMakeVisible);
   }
   return pView;
}

//////////////////
// Update "View As Xxx" command: set radio button for whichever kind
// of view is currently active.
//
void CDynViewDocTemplate::OnUpdateViewAs(CCmdUI* pCmdUI) 
{
   ASSERT(ID_VIEW_AS_BEGIN<=pCmdUI->m_nID && pCmdUI->m_nID<=ID_VIEW_AS_END);

   CFrameWnd* pFrame = (CFrameWnd*)AfxGetMainWnd();
   ASSERT_VALID(pFrame);
   ASSERT(pFrame->IsFrameWnd());
   CView* pView = pFrame->GetActiveFrame()->GetActiveView();
   pCmdUI->SetRadio(pView && 
      pView->IsKindOf(m_pViewInfo[pCmdUI->m_nID-ID_VIEW_AS_BEGIN].rtc));
}

CHILDFRM.H


 /////////////////
// MDI child frame overrides OnUpdateFrameTitle to 
// show which view is displayed.
//
class CChildFrame : public CMDIChildWnd {
   DECLARE_DYNCREATE(CChildFrame)
public:
   CChildFrame();
   virtual ~CChildFrame();
   virtual void OnUpdateFrameTitle(BOOL bAddToTitle);
protected:
   //{{AFX_MSG(CChildFrame)
   //}}AFX_MSG
   DECLARE_MESSAGE_MAP()
};

CHILDFRM.CPP


 #include "stdafx.h"
#include "view3.h"
#include "doc.h"
#include "view.h"
#include "ChildFrm.h"
#include "DynTempl.h"

IMPLEMENT_DYNCREATE(CChildFrame, CMDIChildWnd)

BEGIN_MESSAGE_MAP(CChildFrame, CMDIChildWnd)
   //{{AFX_MSG_MAP(CChildFrame)
   //}}AFX_MSG_MAP
END_MESSAGE_MAP()

CChildFrame::CChildFrame()
{
}

CChildFrame::~CChildFrame()
{
}

//////////////////
// Display the view type in the window title
//
void CChildFrame::OnUpdateFrameTitle(BOOL bAddToTitle)
{
   // First let MFC do standard title
   CMDIChildWnd::OnUpdateFrameTitle(bAddToTitle);
   
   CDocument* pDoc = GetActiveDocument();
   if (pDoc) {
      CDynViewDocTemplate* pTemplate = 
         (CDynViewDocTemplate*)pDoc->GetDocTemplate();
      ASSERT_KINDOF(CDynViewDocTemplate, pTemplate);
      ASSERT_VALID(pTemplate);

      CBaseView* pView = (CBaseView*)GetActiveView();
      const DYNAMICVIEWINFO* pInfo = pTemplate->GetViewInfo(pView);
      if (pInfo) {
         char text[256];
         GetWindowText(text, sizeof(text));  // Get MFC title..
         strcat(text, " As ");               // and append " As [viewName]"
         strcat(text, pInfo->name);          // ..
         SetWindowText(text);                // change frame title
      }
   }
}

DOC.H


 //////////////////
// Document class holds a string of text and what 
// kind of view last viewed the doc.
//
class CView3Doc : public CDocument {
   DECLARE_DYNCREATE(CView3Doc)
public:
   virtual  ~CView3Doc();
   void     SetText(const char* pszText);
   LPCSTR   GetText()      { return m_sText; }
   int      GetViewID()    { return m_nViewID; }
   virtual  void Serialize(CArchive& ar);
protected:
   CView3Doc();
   CString  m_sText;       // contents of doc
   int      m_nViewID;     // identifies view that last saved the doc
   //{{AFX_MSG(CView3Doc)
   //}}AFX_MSG
   DECLARE_MESSAGE_MAP()
};

DOC.CPP


 #include "stdafx.h"
#include "view3.h"
#include "doc.h"
#include "view.h"
#include "dyntempl.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

IMPLEMENT_DYNCREATE(CView3Doc, CDocument)

BEGIN_MESSAGE_MAP(CView3Doc, CDocument)
   //{{AFX_MSG_MAP(CView3Doc)
   //}}AFX_MSG_MAP
END_MESSAGE_MAP()

CView3Doc::CView3Doc() : m_sText("Use \"File Edit\" to change this text.")
{
   m_nViewID = 0; // default view = 1st view in table
}

CView3Doc::~CView3Doc()
{
}

/////////////////
// Change document text
//
void CView3Doc::SetText(const char* pszText)
{ 
   m_sText = pszText; 
   SetModifiedFlag();
   UpdateAllViews(NULL);
}

////////////////
// Serialization: Save doc text and class "ID" of current active view.
// The ID is the 0-based index into the doc template's view class table.
//
void CView3Doc::Serialize(CArchive& ar)
{
   if (ar.IsStoring()) {
      ar << m_sText;
      POSITION pos = GetFirstViewPosition();
      ASSERT(pos);
      m_nViewID = ((CDynViewDocTemplate*)GetDocTemplate())->
         GetViewID(GetNextView(pos));
      ar << m_nViewID;
   } else {
      ar >> m_sText;       // restore text and view class ID
      ar >> m_nViewID;     // CDynViewDocTemplate will change the view
   }
}

///////////////////
// This function should be in dyntempl.cpp, but since it's the only
// function that needs to know about the document, it's easier to put it
// here and require that the "programmer" implement it. Otherwise, I'd
// have to implement a new class CDynViewDoc, and require apps to derive
// their doc from it. 
//
int CDynViewDocTemplate::GetViewID(CDocument *pDoc) 
{
   ASSERT_KINDOF(CView3Doc, pDoc);
   return ((CView3Doc*)pDoc)->GetViewID();
}

RESOURCE.H


 //{{NO_DEPENDENCIES}}
// Microsoft Developer Studio generated include file.
// Used by view3.rc
//
#define IDD_ABOUTBOX                    100
#define IDR_MAINFRAME                   128
#define IDR_VIEW3TYPE                   129
#define IDD_FILEEDIT                    130
#define IDC_EDIT1                       1000
#define ID_VIEW_AS_TYPE1                32771
#define ID_VIEW_AS_TYPE2                32772
#define ID_VIEW_AS_TYPE3                32773
#define IDD_FILE_EDIT                   32774
#define ID_FILE_EDIT                    32775

#define ID_VIEW_AS_BEGIN                ID_VIEW_AS_TYPE1
#define ID_VIEW_AS_END                  ID_VIEW_AS_TYPE3

// Next default values for new objects
// 
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_3D_CONTROLS                     1
#define _APS_NEXT_RESOURCE_VALUE        131
#define _APS_NEXT_COMMAND_VALUE         32776
#define _APS_NEXT_CONTROL_VALUE         1001
#define _APS_NEXT_SYMED_VALUE           101
#endif
#endif

VIEW.CPP


 ////////////////////////////////////////////////////////////////
// Implementation of the three view classes in VIEW3
//
#include "stdafx.h"
#include "view3.h"
#include "Doc.h"
#include "View.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

IMPLEMENT_DYNAMIC(CBaseView, CView)

BEGIN_MESSAGE_MAP(CBaseView, CView)
   //{{AFX_MSG_MAP(CBaseView)
   ON_COMMAND(ID_FILE_EDIT, OnFileEdit)
   //}}AFX_MSG_MAP
END_MESSAGE_MAP()

CBaseView::CBaseView()
{
}

CBaseView::~CBaseView()
{
}

//////////////////
// Dialog to edit the document contents (a text string).
//
class CFileEditDlg : public CDialog {
public:
   CString  m_sText;
   CFileEditDlg(CWnd* pParent = NULL) : CDialog(IDD_FILEEDIT, pParent) { }
   virtual void DoDataExchange(CDataExchange* pDX) {
      CDialog::DoDataExchange(pDX);
      DDX_Text(pDX, IDC_EDIT1, m_sText);
      DDV_MaxChars(pDX, m_sText, 255);
   }
};

//////////////////
// Handle "File Edit" command
//
void CBaseView::OnFileEdit() 
{
   CView3Doc* pDoc = GetDocument();
   ASSERT_VALID(pDoc);
   CFileEditDlg dlg;
   dlg.m_sText = pDoc->GetText();
   if (dlg.DoModal() == IDOK)
      pDoc->SetText(dlg.m_sText);
}

//////////////////////////////////////////////////////////////////////
// The three views follow. Each one implements OnDraw a different way.

IMPLEMENT_DYNCREATE(CView1, CBaseView)
IMPLEMENT_DYNCREATE(CView2, CBaseView)
IMPLEMENT_DYNCREATE(CView3, CBaseView)

//////////////////
// Draw document contents as ASCII text
//
void CView1::OnDraw(CDC* pDC)
{
   CView3Doc* pDoc = GetDocument();
   ASSERT_VALID(pDoc);
   CString s = pDoc->GetText();
   CRect rc;
   GetClientRect(&rc);
   pDC->DrawText(s, &rc, DT_CENTER|DT_VCENTER|DT_SINGLELINE);
}

//////////////////
// Draw document contents as binary 1's and 0's
//
void CView2::OnDraw(CDC* pDC)
{
   CView3Doc* pDoc = GetDocument();
   ASSERT_VALID(pDoc);

   // It's overkill to recompute this on every draw, but who cares? 
   // CPU cycles are cheap, and you gotta give that fan a reason to spin!
   //
   CString s;
   for (LPCSTR p = pDoc->GetText(); *p; p++) {  // for each character:
      for (int bit=0; bit<8; bit++)             //   for each bit:
         s += (*p & 1<<bit) ? '1' : '0';        //     append 0 or 1
      s+= ' ';                                  //   space between chars
   }
   CRect rc;
   GetClientRect(&rc);
   pDC->DrawText(s, s.GetLength(), &rc, DT_LEFT|DT_WORDBREAK);
}

//////////////////
// Draw document contents as vertical stripes like a bar code
//
void CView3::OnDraw(CDC* pDC)
{
   CView3Doc* pDoc = GetDocument();
   ASSERT_VALID(pDoc);
   CRect rc;
   GetClientRect(&rc);
   int x=0, bottom = rc.Height();
   for (LPCSTR p = pDoc->GetText(); *p; p++) {  // for each character:
      for (int bit=0; bit<8; bit++) {           //   for each bit:
         if (*p & 1<<bit) {                     //     if bit is ON:
            pDC->MoveTo(x, 0);                  //       draw a..
            pDC->LineTo(x, bottom);             //       ..vertical line
         }
         x++;                                   //   next x-pixel
      }
   }
}

VIEW3.H


 ////////////////////////////////////////////////////////////////
// VIEW3 Copyright 1996 Microsoft Systems Journal. 
// If this program works, it was written by Paul DiLascia.
// If not, I don't know who wrote it.
// See VIEW3.CPP for Description of program.
#include "resource.h"

//////////////////
// Application class is generic
class CApp : public CWinApp {
public:
   virtual BOOL InitInstance();
   //{{AFX_MSG(CApp)
   afx_msg void OnAppAbout();
   //}}AFX_MSG
   DECLARE_MESSAGE_MAP()
};

VIEW3.CPP


 ////////////////////////////////////////////////////////////////
// VIEW3 Copyright 1996 Microsoft Systems Journal. 
// If this program works, it was written by Paul DiLascia.
// If not, I don't know who wrote it.
// VIEW3 Shows how to use CDynViewDocTemplate to implement an app that
// supports different views of the same document.
// This program requires VC++ 4.0 or later.
//
#include "stdafx.h"
#include "view3.h"
#include "MainFrm.h"
#include "ChildFrm.h"
#include "Doc.h"
#include "View.h"
#include "DynTempl.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

BEGIN_MESSAGE_MAP(CApp, CWinApp)
   //{{AFX_MSG_MAP(CApp)
   ON_COMMAND(ID_APP_ABOUT, OnAppAbout)
   //}}AFX_MSG_MAP
   ON_COMMAND(ID_FILE_NEW,  CWinApp::OnFileNew)
   ON_COMMAND(ID_FILE_OPEN, CWinApp::OnFileOpen)
END_MESSAGE_MAP()

// To add another view, put it in this table.
// Default view is first, must be in same order as "View As" menu commands.
static const DYNAMICVIEWINFO MyViewClasses[] = {
   { RUNTIME_CLASS(CView1), "ASCII" },
   { RUNTIME_CLASS(CView2), "Binary" },
   { RUNTIME_CLASS(CView3), "Binary Stripes" },
   { NULL, NULL }
};

CApp theApp;

BOOL CApp::InitInstance()
{
   // Create "dynamic" document template. 
   AddDocTemplate(new CDynViewDocTemplate(IDR_VIEW3TYPE,
      RUNTIME_CLASS(CView3Doc),
      RUNTIME_CLASS(CChildFrame),
      MyViewClasses)); // array of view classes

   // create main MDI Frame window (standard MFC)...
   CMainFrame* pMainFrame = new CMainFrame;
   if (!pMainFrame->LoadFrame(IDR_MAINFRAME))
      return FALSE;
   m_pMainWnd = pMainFrame;

   // Standard AppWiz junk...
   CCommandLineInfo cmdInfo;
   ParseCommandLine(cmdInfo);
   if (!ProcessShellCommand(cmdInfo))
      return FALSE;
   pMainFrame->ShowWindow(m_nCmdShow);
   pMainFrame->UpdateWindow();

   return TRUE;
}

void CApp::OnAppAbout()
{
   CDialog aboutDlg(IDD_ABOUTBOX);
   aboutDlg.DoModal();
}

There are several different ways you could implement a program like VIEW3. I already outlined the type-code-and-switch-statements approach. You could also implement the three views as three different child window classes contained within a single parent CView class. The view would handle some operations itself, and delegate others (such as drawing) to the child windows. You could even keep all the windows alive at the same time, but only show whichever one the user selects. The main advantage with this approach is that from MFC's perspective, there's still only one view class, so you don't have to delve into MFC architecture to make it work. The drawback is that it's a bit cumbersome. You have to create and destroy the child windows, you have to size them whenever the view gets an OnSize message, you have to route commands to the current active child window, and you may have to handle messages like as OnActivate and OnSetFocus that MFC normally takes care of invisibly.

The only really satisfying approach, the one I chose for VIEW3, is to implement three different view classes. I mean, if you have three different views of your document, you should be able to implement them that way, right? The challenge is figuring out how to fake MFC into using multiple view classes when it really wants to use just one. It's a little hairy, so now might be a good time for a caffeine supplement.

In VIEW3, there are two places where the view can change: when the user invokes one of the View As commands and when the user opens a new document. You might guess from my opening discussion that all you have to do to make VIEW3 fly is change CDocTemplate::m_pViewClass.JustinterceptOnFileOpen,setpDocTemplate->m_pViewClass to whichever view class you want, and then let CWinApp::OnFileOpen continue on its merry way. In fact, this will work fine. There's just one problem: you don't know which view class to use until the document is loaded—which happens in CWinApp::OnFileOpen. By then, it's too late—MFC has already created the frame and view! Moreover, setting m_pViewClass won't help for the View As commands, since these commands don't involve creating a new frame or view, at least not as far as MFC knows.

Youmightbetemptedtowriteacommandhandlerlikeso.


 void CMyFrame::OnViewAs3D()
{
CView* pView = GetActiveView();
delete pView;
pView = new CView3D;
SetActiveView(pView);
}

This is basically the right idea, but there are a few details to sweat before it'll fly. First of all, you have to do something with the window (HWND) associated with the view. And you have to be careful about destroying any object that MFC stores a pointer to. I originally tried destroying the view in my view's OnInitialUpdate function. Ignoring for a moment the fact that I was deleting the very object through which I was being called, effectively doing a "delete this" inside a member function (a stunt that's always socially unacceptable but not necessarily fatal, and even MFC does it sometimes), my code failed because it was called from some function deep in MFC that had a pView local variable pointing to the view I'd just deleted.


 // (pseudo-code in MFC)
pView = pFrame->GetActiveView();
pView->OnInitialUpdate(); // calls me to delete pView
pView->Activate(); // Oops!

Needless to say, things weren't very copacetic when my function returned and MFC tried to activate my deleted view! Sigh.

The only safe place to destroy the view is in some class outside it. Since the document template is the official coordinating entity for documents, frames, and views, it's the natural choice. For VIEW3, I derived a new doc template class, CDynViewDocTemplate, that implements "dynamic views" in a totally generic fashion. You should be able to simply plop DYNTEMPL.CPP and DYNTEMPL.H directly into your app and use CDynViewDocTemplate without altering it.

To use CDynViewDocTemplate, the first thing you have to do is create a table that tells it what view classes you have. VIEW3.CPP does it like this:


 static const DYNAMICVIEWINFO MyViewClasses[] = {
{ RUNTIME_CLASS(CView1), "ASCII" },
{ RUNTIME_CLASS(CView2), "Binary" },
{ RUNTIME_CLASS(CView3), "Binary Stripes" },
{ NULL, NULL }
};

DYNAMICVIEWINFO is defined in DYNTEMPL.H. It associates a display name with each run-time class. You can put as many views as you like in the table, but the order of the entries must match the order of View As menu command IDs and the last entry must contain NULLs. You pass this table when you create your document template in your app's InitInstance function.


 // Create "dynamic" document template. 
AddDocTemplate(new CDynViewDocTemplate(IDR_VIEW3TYPE,
RUNTIME_CLASS(CView3Doc),
RUNTIME_CLASS(CChildFrame),
MyViewClasses)); // array of view classes

This looks a lot like creating a normal CMultiDocTemplate, which is a good sign that the design is reasonable. The difference is that instead of specifying a single view class, you give a table of classes. CDynViewDocTemplate uses your table to swap views when the user invokes one of the View As commands. CDynViewDocTemplate uses ON_COMMAND_EX_RANGE and ON_UPDATE_COMMAND_UI_RANGE to handle commands in the range from ID_VIEW_AS_BEGIN to ID_VIEW_AS_END. (I used the _EXversionof ON_COMMAND_RANGE so other command targets in the app can handle the View As commands too.)


 BEGIN_MESSAGE_MAP(CDynViewDocTemplate, CDocTemplate)
ON_UPDATE_COMMAND_UI_RANGE
(ID_VIEW_AS_BEGIN, ID_VIEW_AS_END, OnUpdateViewAs)
ON_COMMAND_EX_RANGE
(ID_VIEW_AS_BEGIN, ID_VIEW_AS_END, OnViewAs)
END_MESSAGE_MAP()

You must define ID_VIEW_AS_BEGIN and ID_VIEW_AS_END in your RESOURCE.H file. I could have let the IDs be passed at run time, but I'd have had to override CDocTemplate::OnCmdMsg, or else handle the entire range of IDs from 0 to 0xFFFFFFFF and then examine the actual ID to see if it was mine—but both require writing several lines of code and cause a slight performance penalty, since every command would go through my handler, even ones I'm not interested in. It seemed better and not so terribly odious to simply hardcode a couple of #define symbols and make programmers use them. MFC's message map macro approach, where everything must be known at compile time, makes life easy for code generators but it's not particularly conducive to writing reusable code and class libraries. Such is life.

The OnViewAs command handler in DYNTEMPL.CPP converts the command ID to a zero-based offset from ID_VIEW_AS_BEGIN, and passes this view ID to a helper function, ReplaceView, which uses it as an index into the view class table to find out which view to use. If the requested view class differs from the current one, ReplaceView changes the view by destroying the old one and creating a new one.


 // Tell MFC not to delete my document
// when I destroy the last view.
CDocument* pDoc = pView->GetDocument();
BOOL bSaveAutoDelete = pDoc->m_bAutoDelete;
pDoc->m_bAutoDelete = FALSE; // Destroy old view and create new one,
// preserving the window (HWND)
pFrame->SetActiveView(NULL);
HWND hwnd = pView->Detach();
delete pView;
pView = (CView*)pViewClass->CreateObject();
pView->Attach(hwnd);
pDoc->AddView(pView);
pFrame->SetActiveView(pView); // Restore doc and do initial update
pDoc->m_bAutoDelete = bSaveAutoDelete;
pFrame->InitialUpdateFrame(pDoc, bMakeVisible);

The above may look a little hairy at first, but it's not really so bad. The basic idea is to reuse the window (HWND) instead of destroying it and creating a new one. This has the advantage of preserving the window size, style flags, and any other information stored in the window itself, and it's faster. Instead of using the normal MFC allocate/Create sequence for creating windows, I simply Detach the HWND from the old view, and reAttach it to the new one. Easy. Of course, if you're going to create a view in unorthodox ways, you've got to hook it up manually to its document and frame. CDocument::AddView and CFrameWnd::SetActiveView are the functions that do it. The only trick is that you have to set pDoc->m_bAutoDelete to FALSE before destroying the original view, because otherwise MFC will delete the document when you destroy the old view. (I found out the hard way.) Once the new view is installed, ReplaceView calls InitialUpdateFrame to do everything MFC would normally do when a new frame/view is created—like update the title and send WM_INITIALUPDATE to the view.

Of course, the decision to recycle the window (HWND) is justthat:adecision.Youcoulddoitdifferently.Forexample, you might use a different (Windows) window class for each view. In that case, you'd call DestroyWindow instead of Detach/delete to destroy the old view; and new/Create or even CFrameWnd::CreateView to create the new view, instead of new/Attach. DYNTEMPL.CPP actually contains both implementations. To select the non-window-recycling implementation, just #define DONT_RECYCLE_HWND.

OK, so much for the View As commands. To change the view when a document is opened, CDynViewDocTemplate overrides the virtual function CDocTemplate::InitialUpdateFrame. MFC calls this function after a new frame and view are created and the document has been loaded—but before the frame is displayed. (The CDynViewDocTemplate constructor initializes m_pViewClass to the first view class in your table.) It's the perfect time to change the view without the user noticing. The implementation is trivial, it calls the same ReplaceView function that OnViewAs calls. The only problem is: how does CDynViewDocTemplate get the ID of the view to use? I couldn't just write pDoc->GetViewID, because GetViewID is not a member of CDocument.

Being a firm believer in reusability, I wanted to make CDynViewDocTemplate as self-contained and generic as possible, so anyone could use it to implement apps that support multiple views per doc. The obvious thing to do is derive a new class, CDynViewDoc, with a pure virtual function, GetViewID, and make programmers derive their doc from that. But it seems excessive to derive a whole new class just for one virtual function—and what if you want to derive from COleDocument instead of CDocument? So I took an unorthodox approach. I introduced a new function, CDynViewDocTemplate::GetViewID(CDocument*). Instead of


 pDoc->GetViewID();

I use


 GetViewID(pDoc);

But that only begs the question: how do I implement CDynViewDocTemplate::GetViewID? I don't! I left it out of DYNTEMPL.CPP. In other words, someone else (ahem) has to implement it. Why not? There's no law that says all the functions for a class or library have to be implemented in the same file, or even implemented at all! GetViewID is like a "callback" that you supply at compile time instead of run time! In VIEW3, CDynViewDocTemplate::GetViewID is implemented in DOC.CPP:


 // (in VIEW3\Doc.cpp)
int CDynViewDocTemplate::GetViewID(CDocument *pDoc)
{
ASSERT_KINDOF(CView3Doc, pDoc);
return ((CView3Doc*)pDoc)->GetViewID();
}

It just returns the ID stored in a data member m_nViewID. Another app could implement CDynViewDocTemplate:: GetViewID differently. It doesn't matter how you implement it, as long as you return a valid index into the original view class table you supplied when you created the CDynViewDocTemplate.

The only other interesting thing in CDynViewDocTemplate is a command update handler that sets the radio buttons in the View As menu (see Figure 1). You can read the code yourself to see how it works; it's not rocket science.

The rest of VIEW3 is pretty simple. CView3Doc::Serialize saves the ID of whichever view was active when the user saved the document, and restores it on loading. VIEW3 has three different view classes, CView1, CView2 and CView3, each with a different OnDraw function that implements the view. The views all derive from a common CBaseView that implements a command to modify the document's content. Just for fun, I wrote my own CChildFrame::OnUpdateFrameTitle to include the name of the view type in the caption, as in Figure 1.

VIEW3 compiles with MFC 4.0 or later, but it would only require a few modifications to work in earlier versions. Now, if only I knew how bar codes really work, maybe I could sell come copies of VIEW3 and make a buck.

Have a question about programming in C or C++? You can mail it directly to C/C++ Q&A, Microsoft Systems Journal, 825 Eighth Avenue, 18th Floor, New York, New York 10019, or send it to MSJ (re: C/C++ Q&A) via:


CompuServe:


Internet:

Paul DiLascia
72400,2702

Eric Maffei
ericm@microsoft.com


This article is reproduced from Microsoft Systems Journal. Copyright © 1995 by Miller Freeman, Inc. All rights are reserved. No part of this article may be reproduced in any fashion (except in brief quotations used in critical articles and reviews) without the prior consent of Miller Freeman.

To contact Miller Freeman regarding subscription information, call (800) 666-1084 in the U.S., or (303) 447-9330 in all other countries. For other inquiries, call (415) 358-9500.