Some good examples of SDI applications are Notepad, WordPad, and Paint. All of these applications can do only one type of task and can only work on one task at a time. WordPad is almost like an SDI version of Word. It's able to perform a large number of the tasks that Word does, but although Word allows you to work on numerous documents at the same time, WordPad limits you to only one document.
Some of the things that you will learn today are
When you create an SDI application, more classes are created for an SDI application than for a dialog-style application. Each of these classes serves a specific purpose in how SDI applications operate. Ignoring the About window dialog class, four specific classes make up an SDI application:
The CWinApp class creates all the other components in the application. It is the class that receives all the event messages and then passes the messages to the CFrameView and CView classes.
The CFrameView class is the window frame. It holds the menu, toolbar, scrollbars, and any other visible objects attached to the frame. This class determines how much of the document is visible at any time. Very little (if any) of your programming efforts on SDI applications will require making any modifications or additions to either of these first two classes.
The CDocument class houses your document. This class is where you will build the data structures necessary to house and manipulate the data that makes up your document. This class receives input from the CView class and passes display information to the CView class. This class is also responsible for saving and retrieving the document data from files.
The CView class is the class that displays the visual representation of your document for the user. This class passes input information to the CDocument class and receives display information from the CDocument class. Most of the coding that you will do for this class consists of drawing the document for the user and handling the input from the user. The CView class has several descendent classes that can be used as the ancestor for the view class. These descendent classes are listed in Table 10.1.
Class | Description |
CEditView | Provides the functionality of a edit box control. Can be used to implement simple text-editor functionality. |
CFormView | The base class for views containing controls. Can be used to provide form-based documents in applications. |
CHtmlView | Provides the functionality of a Web browser. This view directly handles the URL navigation, hyperlinking, and so on. Maintains a history list for browsing forward and back. |
CListView | Provides list-control functionality in the Document/View architecture. |
CRichEditView | Provides character and paragraph formatting functionality. Can be used to implement a word-processor application. |
CScrollView | Provides scrolling capabilities to a CView class. |
CTreeView | Provides tree-control functionality in the Document/View architecture. |
All four of these classes work together to make up the full functionality of an SDI application, as shown in Figure 10.1. By taking advantage of this architecture, you can build powerful document-centric applications with relative ease.
FIGURE 10.1. The Document/View architecture.
NOTE: Don't let the term document mislead you. This doesn't mean that you can only create applications such as word processors and spreadsheets. In this situation, the term document refers to the data that is processed by your application, whereas view refers to the visual representation of that data. For instance, the Solitaire application could be implemented as a Document/View application, with the document being the cards and their position in the playing area. In this case, the view is the display of the cards, drawing each card where the document specifies it should be.
To get a good idea of how the Document/View architecture works, and of how you can use it to build applications, you will build a new version of the drawing application you created on Day 3, "Allowing User Interaction--Integrating the Mouse and Keyboard in Your Application." In this version, the user's drawing will be persistent, which means it is not erased each time another window is placed in front of the application. This version will also be able to save and restore drawings.
To create the application shell for today's application, follow these steps:
One of the first issues that you will need to tackle is how to represent your data in the document class. For the drawing application, you have a series of lines. Each line consists of a starting point and ending point. You might think that you can use a series of points for the data representation. If you do this, you also have to make special accommodations for where one series of lines between points ends and the next begins. It makes much more sense to represent the drawing as a series of lines. This allows you to store each individual line that is drawn on the window without having to worry where one set of contiguous lines ends and where the next begins.
Unfortunately, the Microsoft Foundation Classes (MFC) does not have a line object class, although it does have a point object class (CPoint). I guess you'll just have to create your own line class by following these steps:
FIGURE 10.2. The New Class Wizard.
FIGURE 10.3. Warning about including the base class definition.
NOTE: The appropriate header class is already included in the CLine class files. Until your compiler complains because it can't find the definition for the CObject class, don't worry about this message. However, if you are using a base class that's a bit further down the MFC class hierarchy, you might need to heed this message and add the appropriate header file to the include statements in the class source code file.
At this time, your CLine class needs to hold only two data elements, the two end points of the line that it represents. You want to add those two data elements and add a class constructor that sets both values when creating the class instance. To do this, follow these steps:
1: CLine::CLine(CPoint ptFrom, CPoint ptTo) 2: { 3: //Initialize the from and to points 4: m_ptFrom = ptFrom; 5: m_ptTo = ptTo; 6: }
In this object constructor, you are initializing the from and to points with the points that were passed in to the constructor.
To follow correct object-oriented design, your CLine class should be able to draw itself so that when the view class needs to render the line for the user, it can just pass a message to the line object, telling it to draw itself. To add this functionality, follow these steps:
1: void CLine::Draw(CDC * pDC) 2: { 3: // Draw the line 4: pDC->MoveTo(m_ptFrom); 5: pDC->LineTo(m_ptTo); 6: }
This function is taken almost directly from the application you built a week ago. It's a simple function that moves to the first point on the device context and then draws a line to the second point on the device context.
Now that you have an object to use for representing the drawings made by the user, you can store these CLine objects on the document object in a simple dynamic array. To hold this array, you can add a CObArray member variable to the document class.
The CObArray class is an object array class that dynamically sizes itself to accommodate the number of items placed in it. It can hold any objects that are descended from the CObject class, and it is limited in size only by the amount of memory in the system. Other dynamic array classes in MFC include CStringArray, CByteArray, CWordArray, CDWordArray, and CPtrArray. These classes differ by the type of objects they can hold.
Add the CObArray to CDay10Doc, using the Add Member Variable Wizard and giving it a name of m_oaLines.
The first functionality that you need to add to the document class is the ability to add new lines. This should be a simple process of getting the from and to points, creating a new line object, and then adding it to the object array. To implement this function, add a new member function to the CDay10Doc class, specifying the type as CLine* and the declaration as AddLine(CPoint ptFrom, CPoint ptTo) with public access. Edit the function, adding the code in Listing 10.3.
1: CLine * CDay10Doc::AddLine(CPoint ptFrom, CPoint ptTo) 2: { 3: // Create a new CLine object 4: CLine *pLine = new CLine(ptFrom, ptTo); 5: try 6: { 7: // Add the new line to the object array 8: m_oaLines.Add(pLine); 9: // Mark the document as dirty 10: SetModifiedFlag(); 11: } 12: // Did we run into a memory exception? 13: catch (CMemoryException* perr) 14: { 15: // Display a message for the user, giving him or her the 16: // bad news 17: AfxMessageBox("Out of memory", MB_ICONSTOP | MB_OK); 18: // Did we create a line object? 19: if (pLine) 20: { 21: // Delete it 22: delete pLine; 23: pLine = NULL; 24: } 25: // Delete the exception object 26: perr->Delete(); 27: } 28: return pLine; 29: }
At first, this function is understandable. You create a new CLine instance, passing the from and to points as constructor arguments. Right after that, however, you have something interesting, the following flow control construct:
1: try 2: { 3: . 4: . 5: . 6: } 7: catch (...) 8: { 9: . 10: . 11: . 12: }
What is this? This construct is an example of structured exception handling. Some code could fail because of a factor beyond your control, such as running out of memory or disk space, you can place a try section around the code that might have a problem. The try section should always be followed by one or more catch sections. If a problem occurs during the code in the try section, the program immediately jumps to the catch sections. Each catch section specifies what type of exception it handles (in the case of the AddLine function, it specifically handles memory exceptions only), and if there is a matching catch section for the type of problem that did occur, that section of code is executed to give the application a chance to recover from the problem. If there is no catch section for the type of problem that did occur, your program jumps to a default exception handler, which will most likely shut down your application. For more information on structured exception handling, see Appendix A, "C++ Review."
Within the try section, you add the new CLine instance to the array of line objects. Next, you call the SetModifiedFlag function, which marks the document as "dirty" (unsaved) so that if you close the application or open another file without saving the current drawing first, the application prompts you to save the current drawing (with the familiar Yes, No, Cancel message box).
In the catch section, you inform the user that the system is out of memory and then clean up by deleting the CLine object and the exception object.
Finally, at the end of the function, you return the CLine object to the calling routine. This enables the view object to let the line object draw itself.
The next item you will add to the document class is a function to return the number of lines in the document. This functionality is necessary because the view object needs to loop through the array of lines, asking each line object to draw itself. The view object will need to be able to determine the total number of lines in the document and retrieve any specific line from the document.
Returning the number of lines in the document is a simple matter of returning the number of lines in the object array, so you can just return the return value from the GetSize method of the CObArray class. To implement this function, add a new member function to the CDay10Doc class, specifying the type as int and the declaration as GetLineCount with public access. Edit the function, adding the code in Listing 10.4.
1: int CDay10Doc::GetLineCount() 2: { 3: // Return the array count 4: return m_oaLines.GetSize(); 5: }
Finally, you need to add a function to return a specific line from the document. This is a simple matter of returning the object at the specified position in the object array. To implement this function, add a new member function to the CDay10Doc class, specifying the type as CLine* and the declaration as GetLine(int nIndex) with public access. Edit the function, adding the code in Listing 10.5.
1: CLine * CDay10Doc::GetLine(int nIndex) 2: { 3: // Return a pointer to the line object 4: // at the specified point in the object array 5: return (CLine*)m_oaLines[nIndex]; 6: }
NOTE: Notice that the object being returned had to be cast as a pointer to a CLine object. Because the CObArray class is an array of CObjects, every element that is returned by the array is a CObject instance, not a CLine object instance.
Now that you have built the capability into the document class to hold the drawing, you need to add the functionality to the view object to read the user's drawing input and to draw the image. The mouse events to capture the user input are almost identical to those you created a week ago. The second part of the functionality that you need to implement is drawing the image. You will make an addition to a function that already exists in the view object class.
Before adding these functions, you need to add a member variable to the CDay10View class to maintain the previous mouse point, just as you did a week ago. Add a member variable to the CDay10View class through the workspace pane, specifying the type as CPoint, the name as m_ptPrevPos, and the access as private.
To add the mouse events to capture the user's drawing efforts, open the Class Wizard and add functions to the CDay10View class for the WM_LBUTTONDOWN, WM_LBUTTONUP, and WM_MOUSEMOVE event messages. Edit the functions as in Listing 10.6.
1: void CDay10View::OnLButtonDown(UINT nFlags, CPoint point) 2: { 3: // TODO: Add your message handler code here and/or call default 4: 5: /////////////////////// 6: // MY CODE STARTS HERE 7: /////////////////////// 8: 9: // Capture the mouse, so no other application can 10: // grab it if the mouse leaves the window area 11: SetCapture(); 12: // Save the point 13: m_ptPrevPos = point; 14: 15: /////////////////////// 16: // MY CODE ENDS HERE 17: /////////////////////// 18: 19: CView::OnLButtonDown(nFlags, point); 20: } 21: 22: void CDay10View::OnLButtonUp(UINT nFlags, CPoint point) 23: { 24: // TODO: Add your message handler code here and/or call default 25: 26: /////////////////////// 27: // MY CODE STARTS HERE 28: /////////////////////// 29: 30: // Have we captured the mouse? 31: if (GetCapture() == this) 32: // If so, release it so other applications can 33: // have it 34: ReleaseCapture(); 35: 36: /////////////////////// 37: // MY CODE ENDS HERE 38: /////////////////////// 39: 40: CView::OnLButtonUp(nFlags, point); 41: } 42: 43: void CDay10View::OnMouseMove(UINT nFlags, CPoint point) 44: { 45: // TODO: Add your message handler code here and/or call default 46: 47: /////////////////////// 48: // MY CODE STARTS HERE 49: /////////////////////// 50: 51: // Check to see if the left mouse button is down 52: if ((nFlags & MK_LBUTTON) == MK_LBUTTON) 53: { 54: // Have we captured the mouse? 55: if (GetCapture() == this) 56: { 57: // Get the Device Context 58: CClientDC dc(this); 59: 60: // Add the line to the document 61: CLine *pLine = GetDocument()->AddLine(m_ptPrevPos, point); 62: 63: // Draw the current stretch of line 64: pLine->Draw(&dc); 65: 66: // Save the current point as the previous point 67: m_ptPrevPos = point; 68: } 69: } 70: 71: ///////////////////////
72: // MY CODE ENDS HERE
73: /////////////////////// 74: 75: CView::OnMouseMove(nFlags, point); 76: }
In the OnLButtonDown function, the first thing you do is call the SetCapture function. This function "captures" the mouse, preventing any other applications from receiving any mouse events, even if the mouse leaves the window space of this application. This enables the user to drag the mouse outside the application window while drawing and then drag the mouse back into the application window, without stopping the drawing. All mouse messages are delivered to this application until the mouse is released in the OnLButtonUp function, using the ReleaseCapture function. In the meantime, by placing the GetCapture function in an if statement and comparing its return value to this, you can determine whether your application has captured the mouse. If you capture the mouse, you want to execute the rest of the code in those functions; otherwise, you don't.
In the OnMouseMove function, after you create your device context, you do several things in a single line of code. The line
CLine *pLine = GetDocument()->AddLine(m_ptPrevPos, point);
creates a new pointer to a CLine class instance. Next, it calls the GetDocument function, which returns a pointer to the document object. This pointer is used to call the document class's AddLine function, passing the previous and current points as arguments. The return value from the AddLine function is used to initialize the CLine object pointer. The CLine pointer can now be used to call the line object's Draw function.
NOTE: A pointer is the address of an object. It is used to pass an object more efficiently around a program. Passing a pointer to an object, instead of the object itself, is like telling someone that the remote control is "on the couch between the second and third cushion, beside the loose pocket change" instead of handing the remote to the person. Actually, in programming terms, handing the remote to the person requires making an exact copy of the remote and handing the copy to the other person. It is obviously more efficient to tell the person where to find the remote than to manufacture an exact copy of the remote.
The notation -> denotes that the object's functions or properties are accessed through a pointer, as opposed to directly through the object itself with the period (.) notation.
In the view class, the function OnDraw is called whenever the image presented to the user needs to be redrawn. Maybe another window was in front of the application window, the window was just restored from being minimized, or a new document was just loaded from a file. Why the view needs to be redrawn doesn't matter. All you need to worry about as the application developer is adding the code to the OnDraw function to render the document that your application is designed to create.
Locate the OnDraw function in the CDay10View class and add the code in Listing 10.7.
1: void CDay10View::OnDraw(CDC* pDC) 2: { 3: CDay10Doc* pDoc = GetDocument(); 4: ASSERT_VALID(pDoc); 5: 6: // TODO: add draw code for native data here 7: 8: /////////////////////// 9: // MY CODE STARTS HERE 10: /////////////////////// 11: 12: // Get the number of lines in the document 13: int liCount = pDoc->GetLineCount(); 14: 15: // Are there any lines in the document? 16: if (liCount) 17: { 18: int liPos; 19: CLine *lptLine; 20: 21: // Loop through the lines in the document 22: for (liPos = 0; liPos < liCount; liPos++) 23: { 24: // Get the from and to point for each line 25: lptLine = pDoc->GetLine(liPos); 26: // Draw the line 27: lptLine->Draw(pDC); 28: } 29: } 30: 31: /////////////////////// 32: // MY CODE ENDS HERE 33: /////////////////////// 34: }
In this function, the first thing you did was find out how many lines are in the document to be drawn. If there aren't any lines, then there is nothing to do. If there are lines in the document, you loop through the lines using a for loop, getting each line object from the document and then calling the line object's Draw function.
Before you can compile and run your application, you'll need to include the header file for the Cline class in the source code file for the document and view classes. To add this to your application, edit both of these files (Day10Doc.cpp and Day10View.cpp), adding the Line.h file to the includes, as shown in Listing 10.8.
1: #include "stdafx.h" 2: #include "Day10.h" 3: #include "MainFrm.h" 4: #include "Line.h" 5: #include "Day10Doc.h"
At this point, you should be able to compile and run your application, drawing figures in it as shown in Figure 10.4. If you minimize the window and then restore it, or if you place another application window in front of your application window, your drawing should still be there when your application window is visible again (unlike the application you built a week ago).
FIGURE 10.4. Drawing with your application.
Now that you can create drawings that don't disappear the moment you look away, it'd be nice if you could make them even more persistent. If you play with the menus on your application, it appears that the Open, Save, and Save As menu entries on the File menu activate, but they don't seem to do anything. The printing menu entries all work, but the entries for saving and loading a drawing don't. Not even the New menu entry works! Well, you can do something to fix this situation.
If you examine the CDay10Doc class, you'll see the OnNewDocument function that you can edit to clear out the current drawing. Wrong! This function is intended for initializing any class settings for starting work on a new drawing and not for clearing out an existing drawing. Instead, you need to open the Class Wizard and add a function on the DeleteContents event message. This event message is intended for clearing the current contents of the document class. Edit this new function, adding the code in Listing 10.9.
1: void CDay10Doc::DeleteContents() 2: { 3: // TODO: Add your specialized code here and/or call the base class 4: 5: /////////////////////// 6: // MY CODE STARTS HERE 7: /////////////////////// 8: 9: // Get the number of lines in the object array 10: int liCount = m_oaLines.GetSize(); 11: int liPos; 12: 13: // Are there any objects in the array? 14: if (liCount) 15: { 16: // Loop through the array, deleting each object 17: for (liPos = 0; liPos < liCount; liPos++) 18: delete m_oaLines[liPos]; 19: // Reset the array 20: m_oaLines.RemoveAll(); 21: } 22: 23: /////////////////////// 24: // MY CODE ENDS HERE 25: /////////////////////// 26: 27: CDocument::DeleteContents(); 28: }
This function loops through the object array, deleting each line object in the array. Once all the lines are deleted, the array is reset by calling its RemoveAll method. If you compile and run your application, you'll find that you can select File|New, and if you decide not to save your current drawing, your window is wiped clean.
Adding the functionality to save and restore your drawings is pretty easy to implement, but it might not be so easy to understand. That's okay; you'll spend an entire day on understanding saving and restoring files, also known as serialization, in three days. In the meantime, find the Serialize function in the CDay10Doc class. The function should look something like
1: void CDay10Doc::Serialize(CArchive& ar) 2: { 3: if (ar.IsStoring()) 4: { 5: // TODO: add storing code here 6: } 7: else 8: { 9: // TODO: add loading code here 10: } 11: }
Remove all the contents of this function, and edit the function so that it looks like Listing 10.10.
1: void CDay10Doc::Serialize(CArchive& ar) 2: { 3: /////////////////////// 4: // MY CODE STARTS HERE 5: /////////////////////// 6: 7: // Pass the serialization on to the object array 8: m_oaLines.Serialize(ar); 9: 10: /////////////////////// 11: // MY CODE ENDS HERE 12: /////////////////////// 13: }
This function takes advantage of the functionality of the CObArray class. This object array will pass down its array of objects, calling the Serialize function on each of the objects. This means that you need to add a Serialize function to the CLine class. Specify it as a void function type with the declaration of Serialize(CArchive& ar). Edit the function, adding the code in Listing 10.11.
1: void CLine::Serialize(CArchive &ar)
2: { 3: CObject::Serialize(ar); 4: 5: if (ar.IsStoring()) 6: ar << m_ptFrom << m_ptTo; 7: else 8: ar >> m_ptFrom >> m_ptTo; 9: }
This function follows basically the same flow that the original Serialize function would have followed in the CDay10Doc class. It uses the I/O stream functionality of C++ to save and restore its contents.
At this point, if you compile and run your application, you expect the save and open functions to work. Unfortunately, they don't (yet). If you run your application and try to save a drawing, a message box will tell you that the application was unable to save the file, as in Figure 10.5.
FIGURE 10.5. Unable to save drawings.
The reason that you are unable to save your drawing is that Visual C++ must be told that a class should be serializable. To do this, you add one line to the CLine class header file and one line to the CLine source code file. Open the CLine header file (Line.h), and add the DECLARE_SERIAL line in Listing 10.12 just after the first line of the class definition.
1: class CLine : public CObject 2: { 3: DECLARE_SERIAL (CLine) 4: public: 5: CLine(CPoint ptFrom, CPoint ptTo, UINT nWidth, COLORREF crColor);
Next, open the CLine source code file, and add the IMPLEMENT_SERIAL line in Listing 10.13 just before the class constructor functions.
1: // Line.cpp: implementation of the CLine class. 2: // 3: ////////////////////////////////////////////////////////////////////// 4: 5: #include "stdafx.h" 6: #include "Day10.h" 7: #include "Line.h" 8: 9: #ifdef _DEBUG 10: #undef THIS_FILE 11: static char THIS_FILE[]=__FILE__; 12: #define new DEBUG_NEW 13: #endif 14: 15: IMPLEMENT_SERIAL (CLine, CObject, 1) 16: ////////////////////////////////////////////////////////////////////// 17: // Construction/Destruction 18: ////////////////////////////////////////////////////////////////////// 19: 20: CLine::CLine() 21: { 22: 23: }
Now if you compile and run your application, you should be able to draw your own self-portrait and save it for posterity, as shown in Figure 10.6.
FIGURE 10.6. My self-portrait.
Now that you have a working drawing program, it would be nice if the user could choose the color with which she wants to draw. Adding this functionality requires making changes in the CLine class to associate the color with the line and to CDay10Doc to maintain the currently selected color. Finally, you need to add a pull-down menu to select the desired color.
The changes to the CLine class are fairly straightforward. The first thing that you need to do is to add another member variable to the CLine class to hold the color of each line. Next, you need to modify the class constructor to add color to the list of attributes to be passed in. Third, you need to modify the Draw function to use the specified color. Finally, you need to modify the Serialize function to save and restore the color information along with the point information. To do all these things, follow these steps:
1: CLine::CLine(CPoint ptFrom, CPoint ptTo, COLORREF crColor) 2: { 3: //Initialize the from and to points 4: m_ptFrom = ptFrom; 5: m_ptTo = ptTo; 6: m_crColor = crColor; 7: }
1: void CLine::Draw(CDC * pDC) 2: { 3: // Create a pen 4: CPen lpen (PS_SOLID, 1, m_crColor); 5: 6: // Set the new pen as the drawing object 7: CPen* pOldPen = pDC->SelectObject(&lpen); 8: // Draw the line 9: pDC->MoveTo(m_ptFrom); 10: pDC->LineTo(m_ptTo); 11: // Reset the previous pen 12: pDC->SelectObject(pOldPen); 13: }
1: void CLine::Serialize(CArchive &ar) 2: { 3: CObject::Serialize(ar); 4: 5: if (ar.IsStoring()) 6: ar << m_ptFrom << m_ptTo << (DWORD) m_crColor; 7: else 8: ar >> m_ptFrom >> m_ptTo >> (DWORD) m_crColor; 9: }
The only part of any of these steps that should be a surprise is that you are capturing the return value from the SelectObject function when you are specifying the pen to use in drawing the lines. You didn't do this last week. The return value from the SelectObject function is the pen that was in use before you changed it. This way, you can use the previous pen to restore it to the device context when you are done drawing.
The changes that you need to make to the CDay10Doc class are just slightly more extensive than those made to the CLine class. You need to add a member variable to hold the current color and a color table to convert color IDs into RGB values. You need to initialize the current color variable in the OnNewDocument function. Then, you need to modify the AddLine function to add the current color to the CLine constructor. Finally, you add a function to return the current color. That's all that you need to do for now until you start adding menu message handlers for setting the current color. To do these things, follow these steps:
1: //}}AFX_MSG_MAP 2: END_MESSAGE_MAP() 3: 4: const COLORREF CDay10Doc::m_crColors[8] = { 5: RGB( 0, 0, 0), // Black 6: RGB( 0, 0, 255), // Blue 7: RGB( 0, 255, 0), // Green 8: RGB( 0, 255, 255), // Cyan 9: RGB( 255, 0, 0), // Red 10: RGB( 255, 0, 255), // Magenta 11: RGB( 255, 255, 0), // Yellow 12: RGB( 255, 255, 255) // White 13: }; 14: 15: ////////////////////////////////////////////////////////////////////// 16: // CDay10Doc construction/destruction 17:
18: CDay10Doc::CDay10Doc()
19: . 20: . 21: . 22: }
1: BOOL CDay10Doc::OnNewDocument() 2: { 3: if (!CDocument::OnNewDocument()) 4: return FALSE; 5: 6: // TODO: add reinitialization code here 7: // (SDI documents will reuse this document) 8: 9: /////////////////////// 10: // MY CODE STARTS HERE 11: /////////////////////// 12: 13: // Initialize the color to black 14: m_nColor = ID_COLOR_BLACK - ID_COLOR_BLACK; 15: 16: /////////////////////// 17: // MY CODE ENDS HERE 18: /////////////////////// 19: 20: return TRUE; 21: }
1: CLine * CDay10Doc::AddLine(CPoint ptFrom, CPoint ptTo) 2: { 3: // Create a new CLine object 4: CLine *pLine = new CLine(ptFrom, ptTo, m_crColors[m_nColor]); 5: try 6: { 7: // Add the new line to the object array 8: m_oaLines.Add(pLine); 9: // Mark the document as dirty 10: SetModifiedFlag(); 11: } 12: // Did we run into a memory exception? 13: catch (CMemoryException* perr) 14: { 15: // Display a message for the user, giving him or her the 16: // bad news 17: AfxMessageBox("Out of memory", MB_ICONSTOP | MB_OK); 18: // Did we create a line object? 19: if (pLine) 20: { 21: // Delete it 22: delete pLine; 23: pLine = NULL; 24: } 25: // Delete the exception object 26: perr->Delete(); 27: } 28: return pLine; 29: }
1: UINT CDay10Doc::GetColor()
2: {
3: // Return the current color 4: return ID_COLOR_BLACK + m_nColor; 5: }
In the OnNewDocument and the GetColor functions, the color is added and subtracted from ID_COLOR_BLACK. This is the lowest numbered color menu ID when you add the menu entries. These calculations maintain the variable as a number between 0 and 7, but when working with the menus, they allow comparison with the actual menu IDs.
Now comes the fun part. You need to add a new pull-down menu to the main menu. You need to add menu entries for all the colors in the color table. You need to add message handlers for all the color menu entries. Finally, you need to add event handlers to check the menu entry that is the current color. To do all of this, follow these steps:
FIGURE 10.7. The Color menu as designed.
Object | Property | Setting |
Menu Entry | ID | ID_COLOR_BLACK |
Caption | &Black | |
Menu Entry | ID | ID_COLOR_BLUE |
Caption | B&lue | |
Menu Entry | ID | ID_COLOR_GREEN |
Caption | &Green | |
Menu Entry | ID | ID_COLOR_CYAN |
Caption | &Cyan | |
Menu Entry | ID | ID_COLOR_RED |
Caption | &Red | |
Menu Entry | ID | ID_COLOR_MAGENTA |
Caption | &Magenta | |
Menu Entry | ID | ID_COLOR_YELLOW |
Caption | &Yellow | |
Menu Entry | ID | ID_COLOR_WHITE |
Caption | &White |
1: void CDay10Doc::OnColorBlack() 2: { 3: // TODO: Add your command handler code here 4: 5: /////////////////////// 6: // MY CODE STARTS HERE 7: /////////////////////// 8: 9: // Set the current color to black 10: m_nColor = ID_COLOR_BLACK - ID_COLOR_BLACK; 11: 12: /////////////////////// 13: // MY CODE ENDS HERE 14: /////////////////////// 15: } 16: 17: void CDay10Doc::OnUpdateColorBlack(CCmdUI* pCmdUI) 18: { 19: // TODO: Add your command update UI handler code here 20: 21: /////////////////////// 22: // MY CODE STARTS HERE 23: /////////////////////// 24:
25: // Determine if the Black menu entry should be checked
26: pCmdUI->SetCheck(GetColor() == ID_COLOR_BLACK ? 1 : 0); 27: 28: /////////////////////// 29: // MY CODE ENDS HERE 30: /////////////////////// 31: }
1: void CDay10Doc::OnColorBlue() 2: { 3: // TODO: Add your command handler code here 4: 5: /////////////////////// 6: // MY CODE STARTS HERE 7: /////////////////////// 8: 9: // Set the current color to blue 10: m_nColor = ID_COLOR_BLUE - ID_COLOR_BLACK; 11: 12: /////////////////////// 13: // MY CODE ENDS HERE 14: /////////////////////// 15: } 16: 17: void CDay10Doc::OnUpdateColorBlue(CCmdUI* pCmdUI) 18: { 19: // TODO: Add your command update UI handler code here 20: 21: /////////////////////// 22: // MY CODE STARTS HERE 23: /////////////////////// 24: 25: // Determine if the Blue menu entry should be checked 26: pCmdUI->SetCheck(GetColor() == ID_COLOR_BLUE ? 1 : 0); 27: 28: /////////////////////// 29: // MY CODE ENDS HERE 30: /////////////////////// 31: }
In the first of the two menu functions, the COMMAND function, the current color variable is set to the new color. If you add the menu entries in the correct order, their ID numbers are sequential, starting with ID_COLOR_BLACK. Subtracting ID_COLOR_BLACK from the menu ID should always result in the correct position in the color table for the selected color. For example, the Black color is position 0 in the color table. ID_COLOR_BLACK - ID_COLOR_BLACK = 0. Blue is position 1 in the color table. Because ID_COLOR_BLUE should be one greater than ID_COLOR_BLACK, ID_COLOR_BLUE -- ID_COLOR_BLACK = 1.
The second function, the UPDATE_COMMAND_UI function, may need a little explaining. The UPDATE_COMMAND_UI event is called for each menu entry just before it is displayed. You can use this event message function to check or uncheck the menu entry, based on whether it is the current color. You can also use this event to enable or disable menu entries or make other modifications as necessary. The code in this function
pCmdUI->SetCheck(GetColor() == ID_COLOR_BLUE ? 1 : 0);
does several things. First, the pCmdUI object that is passed in as the only argument is a pointer to a menu object. The SetCheck function can check or uncheck the menu entry, depending on whether the argument passed is 1 or 0 (1 checks, 0 unchecks). The argument portion for the SetCheck function is a flow-control construct that can be somewhat confusing if you haven't spent a large amount of time programming in C/C++. The first half
GetColor() == ID_COLOR_BLUE
is a simple boolean conditional statement. It results in a true or false result. The portion following this conditional statement
? 1 : 0
is basically an if...else statement in shorthand. If the conditional statement is true, then the value is 1, and if the statement is false, the value is 0. This is a fancy way of placing an if..else flow control within the argument to another function.
If you compile and run your application, you should be able to change the color that you are drawing with. When you pull down the color menu, you should see the current drawing color checked on the menu, as in Figure 10.8.
FIGURE 10.8. Specifying the current color on the menu.
Whew! What a day! You learned quite a bit today because this was a packed chapter. You initially learned about the SDI style application and about a couple of standard applications that you have probably used that are SDI applications. You next learned about the Document/View architecture that Visual C++ uses for SDI applications. You learned to create a simple class of your own for use in your drawing application. You created a drawing application that can maintain the images drawn using it. You learned how you can save and restore documents in the Document/View architecture. You also learned about the CObArray object array class and how you can use it to create a dynamic object array for storing various classes. Finally, you learned how you can check and uncheck menu entries in MFC applications.
void CDay10Doc::OnColorCommand(UINT nID) { // TODO: Add your command handler code here /////////////////////// // MY CODE STARTS HERE /////////////////////// // Set the current color to blue m_nColor = nID - ID_COLOR_BLACK; /////////////////////// // MY CODE ENDS HERE /////////////////////// }
void CDay10Doc::OnUpdateColor(CCmdUI* pCmdUI) { // TODO: Add your command update UI handler code here /////////////////////// // MY CODE STARTS HERE /////////////////////// // Determine if the Blue menu entry should be checked pCmdUI->SetCheck(GetColor() == pCmdUI->m_nID ? 1 : 0); /////////////////////// // MY CODE ENDS HERE /////////////////////// }
The Workshop provides quiz questions to help you solidify your understanding of the material covered and exercises to provide you with experience in using what you've learned. The answers to the quiz questions and exercises are provided in Appendix B, "Answers."
Add another pull-down menu to control the width of the pen used for drawing. Give it the following settings:
Menu Entry | Width Setting |
Very Thin | 1 |
Thin | 8 |
Medium | 16 |
Thick | 24 |
Very Thick | 32 |
TIP: In the pen constructor, the second argument is the width.
© Copyright, Macmillan Computer Publishing. All rights reserved.