CS-360
Fall, 2000
Class 15
R. Eckert

MFC WINDOWS PROGRAMMING (DOCUMENT/VIEW APPROACH)

The approach taken in the last notes was to write an MFC program that
creates application and window objects. This is referred to as the
APP/WINDOW approach.

This mirrors the way Win32 API programs are organized. The main difference
is that MFC automates and masks many details.

But in the APP/WINDOW approach, data and rendering of data are intertwined.
Frequently, data members exist within the window class. (For example in
MSG1.CPP, the output string (program data) and the x,y position where the
data is to be displayed on the screen are both defined in the window-based
class.) This class also determines how the data is displayed.

But conceptually data is different from the rendering of the data. In an
APP/WINDOW they are mixed together in the same window class.

Frequently we need to have different views of the same data (e.g.,
displaying data in a window or on a printer). So it would be good to
separate the data and the presentation of the data.

The DOCUMENT/VIEW approach achieves this separation by encapsulating the
the data in a CDocument class object and encapsulating the mechanism that
displays that data in a CView class object.

Classes derived from CDocument should handle commands that affect the
application's data, while classes derived from CView should handle the
display of the data and user interactions with that display.

We still need to create a CFrameWnd class and a CWinApp class, but in the
DOCUMENT/VIEW approach their roles are reduced.

Document--Any and all forms of data associated with the application; pure
data. In spite of the name, a document is not limited to text. The data
could be anything--game data, graphical data, etc. The term 'document' is
just a label for the application data in a program treated as a unit.

Document Interfaces--If a program deals with just one document at a time,
it is referred to as being a Single Document interface (SDI) application.
All the programs we've done in this class have been SDI programs. If, on
the other hand, a program is organized to handle multiple documents at the
same time, it is referred to as being a Multiple Document Interface (MDI)
application. The multiple documents open at the same time can be of the
same or different types. An example of an MDI application is Microsoft
Word.

View--A rendering of a document; a physical representation of the data. A
view is an object that provides a mechanism for displaying some or all of
the data stored in a document. It defines how the data is to be displayed
in a window and how the user can interact with it.

Frame Window--The window in which a view appears is called a frame window.

A document can have multiple views associated with it (different ways of
looking at the same data). But a view has only one document associated with
it. (See Figure below.)


The coordination between documents, views, and frame windows is handled by
an MFC template class object.

In general (loosely), we can say that an application object creates a
template which coordinates the display of a document's data in a view
inside a frame window. (See following diagram.)

Serialization--A mechanism whereby the current state of an object (e.g., a
document) can be stored and retrieved, usually to a disk file. The
CDocument class has serialization built into it. This means that in a
DOCUMENT/VIEW application, saving and storing data is very straightforward.

Dynamic Creation--In the DOCUMENT/VIEW approach, objects are dynamic. In
other words, when a DOCUMENT/VIEW program is run, its frame window,
document, and view are created dynamically. This is necessary because
DOCUMENT/VIEW objects need to be synthesized from the data that is read
from the file. They need to be created at the time they are loaded from
disk. To allow for dynamic creation, the following macros must be used in
classes derived from CFrameWnd, CDocument, and CView:

  DECLARE_DYNCREATE(class_name)  // in declaration (.h file)

  IMPLEMENT_DYNCREATE(class_name, parent_class_name)  // (in .cpp file)

After the IMPLEMENT_DYNCREATE() macro is invoked, the class is enabled for
dynamic creation. At that point a template can be created.

Initializing a DOCUMENT/VIEW Application [the application's InitInstance()
override]--

Because of the addition of the CDocument and CView classes, there is a lot
more that has to be done by our application's InitInstance() override.
First we must set up the document template.

The Document Template--As mentioned above the document template links
together the document, view, and frame window classes, and allows them to
work together as a unit. For an SDI application, it is created by invoking
the CSingleDocTemplate class constructor, which returns a pointer to an
allocated CSingleDocTemplate structure. For example, for an SDI application
containing a CDocument class called CSketchDoc, a CFrameWnd class called
CMainFrame, and a CView class called CSketchView, the template object could
be created with:

CSingleDocTemplate *DocPtr = new CSingleDocTemplate(IDR_MAINFRAME,
                                      RUNTIME_CLASS(CSketchDoc),
                                      RUNTIME_CLASS(CMainFrame),
                                      RUNTIME_CLASS(CSketchView));

Here the first parameter is the identifier for the resources (menu, icon,
etc.) used by the template. Notice that the RUNTIME_CLASS() macro has been
used to obtain the required pointers to the application's document class, 
its frame window class, and its view class. To be able to use this macro, 
the dynamic declaration and dynamic creation macros explained above have 
to have been invoked in the declarations and implementations of the three
classes.

The pointer returned by the CSingleDocTemplate constructor can now be used
in a call to AddDocTemplate(), which adds the template to the list of
document templates supported by the application. After the document
template has been added, InitInstance() should call EnableShellOpen() and
RegisterShellFileTypes(). The first allows the user to start the
application by clicking on one of its document files, and the second
registers the application's document in the system registry.

After these steps have been performed, the application's InitInstance()
usually looks to see if there are command line parameters. This is done by
calling ParseCommandLine(). Then a call is made to ProcessCommandLine() to
determine the validity of any user commands. If this function returns 0
(invalid), InitInstance() must return false. If not, all is well, and we
can proceed with the usual calls to ShowWindow() and UpdateWindow().

[For details of this process, look at the SKETCH application (explained
below). Specifically, look at the CSketchApp's InitInstance() override in
the sketch.cpp file.]


Steps in Creating a Document/View Program Framework--

1. Derive application, frame window, document, and view classes. These
are typically placed in four different .h/.cpp files. The last three must
be created dynamically, so we must use the macros DECLARE_DYNCREATE() in
the class declaration (.h) file(s) and IMPLEMENT_DYNCREATE() in the class
implementation (.cpp) file(s).

2. Create a template that links together the frame window, document, and
view classes so they can can work as a unit. Use the CSingleDocTemplate
constructor as described above. (This is done in the application's class.)

3. Initialize the application using CApp::InitInstance(). Follow the steps
outlined above (again in the application's class).

4. Overload the various class member functions and add appropriate message
handlers according to the tasks to be performed by the application.

But in practice few programmers code these steps--It is much easier to use
the Microsoft Developer Studio AppWizard and ClassWizard tools:

AppWizard--

A tool that generates a DOCUMENT/VIEW MFC program FRAMEWORK automatically.
This can be built on and customized by the programmer. It provides a fast
and efficient way of producing Microsoft Windows programs. For example,
all the steps outlined above for the application's CWinApp class will be
performed for you by AppWizard. In addition, fully functional skeletal
CMainFrame, CView, and CDocument classes will be created. After AppWizard
does it's thing, we can build the application and run it. We will get a
full-fledged window with all the common menu items, tools, etc. Of course
the window doesn't do anything.

Message Handling in a Framework-based MFC Application 
(ClassWizard)--

ClassWizard is a tool that connects resources (menus, dialog boxes, etc.)
and user-generated events to the code that implements the program's
response. It can write C++ skeleton routines to handle messages and insert
the code into appropriate spots in the program. The code can then be
customized by hand. It can also be used to help derive classes from MFC
base classes.

An Example of Using AppWizard and ClassWizard: A Simple 
Sketching Program (SKETCH)--

In this application, the user can use the mouse as a drawing pencil. When
the left mouse button is down a line follows the motion of the mouse.
When it is up, sketching stops. The color of the line is determined by a
"Drawing Color" menu item, which pops up the menu selections: "Red",
"Green", and "Blue". A "Clear" menu item allows the user to erase the
window's client area.

Since we are not going to save the data associated with the sketch in this
example, we will not do anything with the CDocument class (called
CSketchDoc in the files sketchDoc.h and sketchDoc.cpp) created by MFC's
AppWizard. We also won't touch the CWinApp class (called CSketch in the
files sketch.h and sketch.cpp) or the CFrameWnd class (called CMainFrame in
the files MainFrm.h and MainFrm.cpp) generated by AppWizard. The base
functionality provided by the AppWizard when it created these three classes
will be adequate for this application. We WILL use ClassWizard to help add
the sketching and Drawing color functionality to the CView class (called
CSketchView in the files sketchView.h and sketchView.cpp) created by
AppWizard.

1. Get into Developer Studio and use "File | New | Projects-tab" and
choose "MFC AppWizard (exe)". Enter the name of the project (here
"sketch").

2. In the resulting "MFC AppWizard-Step 1" window, choose the "Single
document" radio button. Press the "Next" button.

3. In the next two windows ("Step 2 of 6" and "Step 3 of 6"), just press
the "Next" button.

4. In the resulting "Step 4 of 6" window, uncheck the "Docking toolbar",
"Initial status bar", and "Printing and print preview" check boxes so that
only the "3D controls" check box remains checked. Then press the "Finish"
and "OK" buttons. AppWizard will create all the source files associated
with your MFC, SDI, DOCUMENT/VIEW application. If you want you can "Build"
the project (as always) and run the application. It's a full-fledged window
with a lot of functionality, but we now need to make it into what we want--
namely our sketching application. For that we'll use ClassWizard.

First, however, we need to know what is required. To use the mouse for
sketching, every time the mouse moves, if the left button is down, we
need to get the current point (from the mousemove message), get a device
context and pen, select the pen into the DC, move to the old point, and
draw a line from there to the current point. We then need to make the
current point become the old point and to select the pen out of the device
context. We'll use a boolean (TRUE/FALSE) m_butdn flag to indicate
whether the button is down or not. A couple of CPoint structures (m_ptold
and m_pt) will be used for the old and current points. We'll use a
pointer to a CDC object (*pDC) to be able to access the functions that
get us a device context and allow us to draw. Finally we'll use a
COLORREF variable (nColor) to hold the current drawing color, as selected
by the user from the "Drawing color" popup menu. So we will first use
Developer Studio to declare those variables in the CSketchView.h file.

Declaring Variables in the Sketchview.h File--

Choose the ClassView Icon in the project workspace window (to the left).
Expand it, and double click on CSketchView. This will bring the .h file
into the editing window (to the right). After the lines:

class CSketchView : public CView
{

enter the following to declare the variables we'll be using:

protected:
    CPoint    m_ptold, m_pt;
    BOOL      m_butdn;
    CDC       *pDC;
    COLORREF  nColor;

Changing the Menu--

Let's now make our changes to the menu. Choose the ResourceView icon in
the project workspace window. Expand the "sketch resources" folder and the
Menu subfolder. Double click on IDR_MAINFRAME. That will bring up the menu
editor. Notice that by default the application already has "File" and
"Edit" popup menus with several standard items. Since this application will
not be doing any file I/O or editing, use the <Delete> keyboard key to
delete all of the entries from "File" except "Exit" and to delete the
entire "Edit" popup menu. "Add a popup menu called "Drawing Color" with items
"Red" (ID=IDM_RED), "Green" (IDM_GREEN), and "Blue" (IDM_BLUE). Add another
menu item called "Clear" (IDM_CLEAR) to the main menu bar. Finally, drag
the "Help" menu item to the right side of the menu bar.

Removing the Accelerator Table--

Next we want to remove the accelerator table for this application (since
we have removed most of the menu items that accelerators are used with).
Close the menu editor and expand the "sketch resources" Accelerator subfolder
in the ResourceView. You will see the IDR_MAINFRAME accelerator ID. Delete
it by selecting it and using the <Delete> key.

Adding Code to the Sketchview.cpp File--

Now we want to add the code that will respond to WM_LBUTTONDOWN,
WM_LBUTTONUP, WM_MOUSEMOVE, and the various WM_COMMAND messages from the
menu. To do that we will use ClassWizard.

First, however, what should the code be? For the button down message, we
want to set m_butdn to TRUE and record the point in m_ptold. For the
button up message, we want to set m_butdn to FALSE. For the various color
choices from the menu we need to set nColor to the appropriate COLORREF.
For example for red, nColor=RGB(255,0,0). For the "Clear" menu choice, we
want to invalidate the window's client area to force a WM_PAINT message.

In response to the mouse move message, if the mouse button is down
(m_butdn==TRUE), we need to get pointer to a device context, pDC, with
GetDC(), set m_pt to the point coming from the mouse message, construct a
solid, 1-pixel-wide pen of color nColor using the CPen class constructor.
Then we can use pDC's SelectObject() member function to select the pen
into the device context, its MoveTo(m_ptold) member function to move it
to the last point, and its LineTo(m_pt) member function to draw the line
to the new point. Finally we set m_ptold equal to m_pt, and use pDC's
SelectObject() member function to select the pen out of the DC.
(Automatically the class destructor will destroy the pen and DC.)

So with all that in mind, we'll now use the ClassWizard to help us add
this code to the framework already generated by AppWizard. ClassWizard
can be started in several different ways. The easiest is to select the
ClassWizard toolbar button, which may or may not be visible on the
Developer Studio main menu bar.

The ClassWizard toolbar button is a "magic wand" hovering over a tiny 
triangle of yellow squares (see diagram below). If it is not displayed 
on the main menu bar, it can be added by right-clicking on the menu 
bar and selecting "Customize..." from the popup. Under the "Category:" 
combo box, select "View" and the ClassWizard "magic wand" will appear in 
the "Buttons" area. You can now drag this toolbar button to the main menu 
bar, and from that point, click it any time you need to bring up the 
ClassWizard.

 The ClassWizard Toolbar Button

If the ClassWizard toolbar button is not visible, you can invoke 
ClassWizard by pressing <Ctrl>-<w>, or by selecting "View | ClassWizard" 
from Developer Studio's menu bar. The resulting dialog box should have
"sketch" in the "Project:" box and "CSketchView" in the "Class name:"
box. If not, change to them. We can scroll through the "Messages" list to
find the messages we want to respond to. Scroll to WM_LBUTTONDOWN, select
it, and press the "Add Function" button. Class Wizard will automatically
add skeleton code in the right places to set up CSketchView's message map
so that it contains the OnLButtonDown() handler. If you now click the "Edit
Code" button, you will be taken to the exact place in the code where you
must make your additions. It even has a comment: //TODO: Add your message
handler code here, to remind you of what you need to do. So just add the
following after the "//TODO" line:

m_butdn = TRUE;
m_ptold = point;

You can see by looking at the OnLButtonDown() function definition that
the last parameter (provided with the message by Windows) is the CPoint
structure point. Our m_ptold variable is also of type CPoint, so we can
just copy point to m_ptold.

Go back to ClassWizard (<ctrl>-<w>) and use it in the same way to add the
WM_LBUTTONUP handler. Add the following line of code after the resulting
"//TODO":

m_butdn = FALSE;

Use ClassWizard to add the WM_MOUSEMOVE handler and add the following
code to it:

if (m_butdn)
{
    pDC = GetDC();
    m_pt = point;
    CPen pPen(PS_SOLID, 1, nColor);
    CPen *pPenOld = pDC->SelectObject(&pPen);
    pDC->MoveTo(m_ptold);
    pDC->LineTo(m_pt);
    m_ptold = m_pt;
    pDC->SelectObject(pPenOld);
}

The process of adding the WM_COMMAND menu item handlers is slightly
different (as you might expect if you recall the ON_COMMAND() message map
macro). Invoke ClassWizard and scroll the "ObjectIDs" list (not the
"Messages" list you used with the other messages) to "IDM_BLUE" that
Developer Studio's integrated environment was nice enough to insert for
you when you used the menu editor to add the "Drawing Color" popup menu.
Select it and choose "COMMAND" in the "Messages" list box. This option is
used to add WM_COMMAND handlers. (The other option "UPDATE_COMMAND_UI"
would be used to update the menu--change checked state of items, etc.)
Finally click the "Add Function" button and click "OK" in the resulting
"Add Member Function" dialog box to cause ClassWizard to generate the
On_Blue() handler to the message map. Press "Edit Code" to go to the
skeleton handler and add the code:

nColor = RGB(0,0,255);

The same procedure is used to add similar OnGreen() and OnRed() handlers.

We can add an OnClear() handler in the same way. But here the code to be
added will be the following:

Invalidate(TRUE);

One other thing needs to be done. The various data members we've added
to our CSketchView class need to be initialized. If not they will
contain garbage when the application begins. This initialization code is
best placed in the CSketchView constructor. So go back to the project
workspace window and choose again the ClassView icon. Expand the "sketch
classes" icon, and expand the CSketchView Class icon. (Notice that all
the handler functions you've added now appear.) Double click on the
CSketchView() constructor and add the following initialization code
under the "//TODO" comment:

m_pt = m_ptold = CPoint(0,0);
m_butdn = FALSE;
nColor = RGB(0,0,0);

(This initialization of nColor means that we're starting out with black
as the drawing color.)

Before continuing, you might want to take a look at the ClassView window
again. Expand the CSketchView class. Notice that in addition to the
functions provided by AppWizard, all the handler functions you added are
there. (These have pink cube icons alongside their names.) In addition
the data members you added are also there, with blue cube icons
alongside. Protected members have a key next to their icons.

You also might want to go to the FileView window and examine the various
source files to see what AppWizard and ClassWizard have helped you do.

Once you have completed all the above steps, you can build the project.
If you have not made any mistakes, you will have a functioning DOCUMENT/VIEW
application that allows the user to sketch in three different colors.


Using Modal Dialog Boxes in MFC Wizard-Generated Frameworks--

To create and display a modal dialog box within the framework generated by
AppWizard and ClassWizard we must do the following:

  Insert the dialog box template into the program's resources (as usual).
  Instantiate a CDialog-based object.
  Call the object's DoModal() function.

It would be possible to exchange information between a dialog box control
and CDialog member variables by getting a pointer to the control's ID with
CWnd::GetDlgItem() and then using that pointer to send the appropriate
messages to the control (as we did in the DIALG2 example from the last
class). However in a Wizard-generated application, it is much more
convenient to use something called the DDX (Dynamic Data Exchange)
mechanism. The DDX system moves data back and forth between dialog box
controls and variables any time a call is made to a CWnd member function
named UpdateData(). This function takes a single Boolean parameter that
indicates the direction in which the data is to be moved; TRUE means from
the controls to the variables, FALSE means in the opposite direction.

When ClassWizard is used to create a CDialog-based class, it automatically
generates three blocks of code that set up the DDX system. Basically the
first one (the AFX_DATA block in the CDialog's .h file) declares the
variables that are bound to the controls, the second one (the AFX_DATA_INIT
block in the CDialog's .cpp file) initializes the variables, and the last
one (the AFX_DATA_MAP block inside a function called DoDataExchange() in
the CDialog's .cpp file) provides information about the data exchange and
control IDs involved when it is called by UpdateData(). Although this
sounds complicated, most of the work is done transparently by ClassWizard
when the programmer creates the CDialog-based class. In the simplest
applications, we can use the fact that the MFC library's OnOK() member
function (which is called when the user clicks the "OK" button inside the
dialog box) automatically calls UpdateData(TRUE), which means that all the
data provided by the dialog box's controls will be transferred to the
variables--and is therefore available to our application. It then calls
CDialog::EndDialog(), which removes the dialog box and causes DoModal() to
return. It's destructor destroys the dialog box.

Adding a Text-Display Modal Dialog Box to SKETCH--

We now want to add a "Text" menu item (ID = IDM_TEXT) to the SKETCH
application discussed above. When the user clicks this item, a modal dialog
box should appear which allows the user to enter a line of text that is
to be displayed in the client area of the sketching window and the (x,y)
coordinates at which the text is to be displayed. The dialog box is to
contain three static controls (labeled "x", "y", and "Text String") and
three single-line edit controls (IDs = IDC_X, IDC_Y, and IDC_TEXTEDIT,
respectively) in addition to the standard "OK" and "Cancel" buttons. The
figure below shows the kind of dialog box we have in mind.




1. Use the ResourceView's menu editor to add the new "Text" (ID=IDM_TEXT)
menu item to the menu bar of the SKETCH program.

2. Use ResourceView's dialog box editor ("Insert | Resource | Dialog |
New") to set up the new IDD_TEXT ("Enter Text") dialog box with the
controls indicated above. In each case when you have positioned the control
in the dialog box, right click it and select "Properties" to enter its ID
and/or caption. If you double click, Developer Studio will ask you if you
want to create a new dialog class--which, of course, you will eventually
want to do. But don't do that until after you have the dialog box set up to
look the way you want. It can be tricky to change the dialog box and keep
the associated class in agreement.

3. Use ClassWizard to create the new class (<Ctrl>-<w>). You will be
prompted for the class name (use CTextDlg), and, by default it will be
based on CDialog.

4. Select the "Member Variables" tab, making sure that the "Class name:" is
CTextDlg. Notice that ClassWizard has already placed the IDs of the various
controls you indicated you wanted in a table. Select each control, choose
"Add Variable", and fill in the resulting "Add Member Variable" dialog box
with the name and type of the variable as shown below:


     ControlIDs       Type      Member
     ---------------------------------
     IDC_TEXTEDIT     CString   m_text
     IDC_X            UINT      m_x
     IDC_Y            UINT      m_y


5. Use ClassWizard to add an OnText() handler to respond to the new IDM_TEXT
menu item. The code that should be placed in this handler is:

        CTextDlg dlg;
        dlg.DoModal
        pDC = GetDC();
        pDC->SetTextColor(nColor);
        pDC->TextOut(dlg.m_x,dlg.m_y,dlg.m_text,strlen(dlg.m_text));

6. At the top of the SketchView.cpp file, with the other include
statements, type in:

 #include "TextDlg.h"

You should now be able to build and run the new version of SKETCH.


In the past two sets of notes I have only scratched the surface of MFC
programming. One of the best references I have seen on this topic is
"Beginning Visual C++5" by Ivor Horton (Wrox Press, 1997).