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).