
Beginning Visual C++ 2005 (2006) [eng]-1
.pdf
Creating the Document and Improving the View
Figure 15-7
Of course, like the other elements you can draw, the curves are not persistent. As soon as you cause a WM_PAINT message to be sent to the application, by resizing the view for instance, they disappear. After you can store them in the document object for the application, though, they will be a bit more permanent, so take a look at that next.
Creating the Document
The document in the Sketcher application needs to be able to store an arbitrary collection of lines, rectangles, circles, and curves in any sequence, and an excellent vehicle for handling this is a list. Because all the element classes that you’ve defined include the capability for the objects to draw themselves, drawing the document is easily accomplished by stepping through the list.
Using a CTypedPtrList Template
You can declare a CTypedPtrList that stores pointers to instances of the shape classes as CElement pointers. You just need to add the list declaration as a new member in the CSketcherDoc class definition:
// SketcherDoc.h : interface of the CSketcherDoc class
//
#pragma once
class CSketcherDoc: public CDocument
{
protected: // create from serialization only CSketcherDoc(); DECLARE_DYNCREATE(CSketcherDoc)
// Rest of the class as before...
protected:
779

Chapter 15
COLORREF |
m_Color; |
// |
Current |
drawing |
color |
unsigned |
int m_Element; |
// |
Current |
element |
type |
CTypedPtrList<CObList, CElement*> m_ElementList; // Element list
// Rest of the class as before...
};
The CSketcherDoc class now refers to the CElement class and normally a forward declaration of the CElement class before the CSketcherDoc class definition would be enough for Sketcher to compile correctly, but not in this case. The compiler needs to know about the base class for the CElement class to compile the CTypedPtrList template instance correctly. This is only possible if the definition of the CElement class is available at this point. You have two ways to achieve this. You can make sure that every #include directive for the SketcherDoc.h header is preceded by an #include directive for CElement, or you can simply add an #include directive for Elements.h before the CSketcherDoc class definition. The latter course is the easiest and saves you from hunting for #include directives for SketcherDoc.h in the source files.
You’ll also need a member function to add an element to the list and AddElement() is a good, if unoriginal, name for this. You create shape objects on the heap, so you can just pass a pointer to the function. Because all it does is add an element, you might just as well put the implementation in the class definition:
class CSketcherDoc: public CDocument
{
// Rest of the class as before... |
|
// Operations |
|
public: |
|
unsigned int GetElementType() |
// Get the element type |
{ return m_Element; } |
|
COLORREF GetElementColor() |
// Get the element color |
{ return m_Color; } |
|
void AddElement(CElement* pElement) |
// Add an element to the list |
{m_ElementList.AddTail(pElement); }
//Rest of the class as before...
};
Adding an element to the list only requires one statement that calls the AddTail() member function. That’s all you need to create the document, but you still have to consider what happens when a document is closed. You must ensure that the list of pointers and all the elements they point to are destroyed properly. To do this, you need to add code to the destructor for CSketcherDoc objects.
Implementing the Document Destructor
In the destructor, you’ll first go through the list deleting the element pointed to by each entry. After that is complete, you must delete the pointers from the list. The code to do this is:
780

Creating the Document and Improving the View
CSketcherDoc::~CSketcherDoc(void)
{
// Get the position at the head of the list
POSITION aPosition = m_ElementList.GetHeadPosition();
// Now delete the element pointed to by each list entry
while(aPosition)
delete m_ElementList.GetNext(aPosition);
m_ElementList.RemoveAll(); |
// Finally delete all pointers |
}
You use the GetHeadPosition() function to obtain the position value for the entry at the head of the list, and initialize the variable aPosition with this value. You then use aPosition in the while loop to walk through the list and delete the object pointed to by each entry. The GetNext() function returns the current pointer entry and updates the aPosition variable to refer to the next entry. When the last entry is retrieved, aPosition is set to NULL by the GetNext() function and the loop ends. After you have deleted all the element objects pointed to by the pointers in the list, you can delete the pointers themselves. You delete the whole lot in one go by calling the RemoveAll() function for the list object.
You should add this code to the definition of the destructor in SketcherDoc.cpp. You can go directly to the code for the destructor through the Class View.
Drawing the Document
As the document owns the list of elements, and the list is protected, you can’t use it directly from the view. The OnDraw() member of the view does need to be able to call the Draw() member for each of the elements in the list, though, so you need to consider how best to do this. Take a look at the options:
You could make the list public, but this defeats the object of maintaining protected members of the document class because it exposes all the function members of the list object.
You could add a member function to return a pointer to the list, but this effectively makes the list public and also incurs overhead in accessing it.
You could add a public function to the document that calls the Draw() member for each element. You could then call this member from the OnDraw() function in the view. This wouldn’t be a bad solution because it produces what you want and still maintains the privacy of the list. The only thing against it is that the function needs access to a device context, and this is really the domain of the view.
You could make the OnDraw() function a friend of CSketcherDoc, but this exposes all of the members of the class, which isn’t desirable, particularly with a complex class.
You could add a function to provide a POSITION value for the first list element, and a second member to iterate through the list elements. This doesn’t expose the list, but it makes the element pointers available.
781

Chapter 15
The last option looks to be the best choice, so go with that. You can extend the document class definition to:
class CSketcherDoc: public CDocument
{
// Rest of the class as before... |
|
// Operations |
|
public: |
|
unsigned int GetElementType() |
// Get the element type |
{ return m_Element; } |
|
COLORREF GetElementColor() |
// Get the element color |
{ return m_Color; } |
|
void AddElement(CElement* pElement) // Add an element to the list |
|
{ m_ElementList.AddTail(pElement); } |
|
POSITION GetListHeadPosition() |
// return list head POSITION value |
{ return m_ElementList.GetHeadPosition(); } |
|
CElement* GetNext(POSITION& aPos) |
// Return current element pointer |
{return m_ElementList.GetNext(aPos); }
//Rest of the class as before...
};
By using the two functions you have added to the document class, the OnDraw() function for the view will be able to iterate through the list, calling the Draw() function for each element. The implementation of OnDraw() to do this is:
void CSketcherView::OnDraw(CDC* pDC)
{
CSketcherDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc);
if(!pDoc)
return;
POSITION aPos = pDoc->GetListHeadPosition();
while(aPos) // Loop while aPos is not null
{
pDoc->GetNext(aPos)->Draw(pDC); |
// Draw the current element |
}
}
This implementation of the OnDraw() function always draws all the elements the document contains. The statement in the while loop first gets a pointer to an element from the document with the expression pDoc->GetNext(). The pointer that is returned is used to call the Draw() function for that element. The statement works this way without parentheses because of the left to right associativity of the -> operator. The while loop plows through the list from beginning to end. You can do it better though, and make the program more efficient.
Frequently, when a WM_PAINT message is sent to your program, only part of the window needs to be redrawn. When Windows sends the WM_PAINT message to a window, it also defines an area in the client area of the window, and only this area needs to be redrawn. The CDC class provides a member function,
782

Creating the Document and Improving the View
RectVisible(), which checks whether a rectangle that you supply to it as an argument overlaps the area that Windows requires to be redrawn. You can use this to make sure you only draw the elements that are in the area Windows wants redrawn, thus improving the performance of the application:
void CSketcherView::OnDraw(CDC* pDC)
{
CSketcherDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc);
if(!pDoc)
return;
POSITION aPos = pDoc->GetListHeadPosition();
CElement* pElement = 0; |
// Store for an element pointer |
while(aPos) |
// Loop while aPos is not null |
{ |
|
pElement = pDoc->GetNext(aPos); |
// Get the current element pointer |
// If the element is visible... |
|
if(pDC->RectVisible(pElement->GetBoundRect())) |
|
pElement->Draw(pDC); |
// ...draw it |
} |
|
}
You get the position for the first entry in the list and store it in aPos. You use the value stored in aPos to control the while loop that retrieves each pointer entry in turn so the loop continues until aPos is NULL. You retrieve the bounding rectangle for each element using the GetBoundRect() member of the object and pass it to the RectVisible() function in the if statement. As a result, only elements that overlap the area that Windows has identified as invalid are drawn. Drawing on the screen is a relatively expensive operation in terms of time, so checking for just the elements that need to be redrawn, rather than drawing everything each time, improves performance considerably.
Adding an Element to the Document
The last thing you need to do to have a working document in our program is to add the code to the OnLButtonUp() handler in the CSketcherView class to add the temporary element to the document:
void CSketcherView::OnLButtonUp(UINT nFlags, CPoint point)
{
if(this == GetCapture()) |
|
ReleaseCapture(); |
// Stop capturing mouse messages |
// If there is an element, add it to the document if(m_pTempElement)
{
GetDocument()->AddElement(m_pTempElement);
InvalidateRect(0); // Redraw the current window m_pTempElement = 0; // Reset the element pointer
}
}
Of course, you must check that there really is an element before you add it to the document. The user might just have clicked the left mouse button without moving the mouse. After adding the element to the list in the document, you call InvalidateRect() to get the client area for the current view redrawn.
783

Chapter 15
The argument of 0 invalidates the whole of the client area in the view. Because of the way the rubberbanding process works, some elements may not be displayed properly if you don’t do this. If you draw a horizontal line, for instance, and then rubberband a rectangle with the same color so that its top or bottom edge overlaps the line, the overlapped bit of line disappears. This is because the edge being drawn is XORed with the line underneath, so you get the background color back. You also reset the pointer m_pTempElement to avoid confusion when another element is created.
Exercising the Document
After saving all the modified files, you can build the latest version of Sketcher and execute it. You’ll now be able to produce art such as “the happy programmer” shown in Figure 15-8.
Figure 15-8
The program is now working more realistically. It stores a pointer to each element in the document object, so they’re all automatically redrawn as necessary. The program also does a proper cleanup of the document data when it’s deleted.
There are still some limitations in the program that you can address. For instance:
You can open another view window by using the Window > New Window menu option in the program. This capability is built in to an MDI application and opens a new view to an existing document, not a new document. If you draw in one window, however, the elements are not drawn in the other window. Elements never appear in windows other than the one where they were drawn, unless the area they occupy needs to be redrawn for some other reason.
You can only draw in the client area you can see. It would be nice to be able to scroll the view and draw over a bigger area.
Neither can you delete an element, so if you make a mistake, you either live with it or start over with a new document.
These are all quite serious deficiencies that, together, make the program fairly useless as it stands. You’ll overcome all of them before the end of this chapter.
784

Creating the Document and Improving the View
Improving the View
The first item that you can try to fix is the updating of all the document windows that are displayed when an element is drawn. The problem arises because only the view in which an element is drawn knows about the new element. Each view is acting independently of the others and there is no communication between them. You need to arrange for any view that adds an element to the document to let all the other views know about it, and they need to take the appropriate action.
Updating Multiple Views
The document class conveniently contains a function UpdateAllViews() to help with this particular problem. This function essentially provides a means for the document to send a message to all its views. You just need to call it from the OnLButtonUp() function in the CSketcherView class, whenever you have added a new element to the document:
void CSketcherView::OnLButtonUp(UINT nFlags, CPoint point)
{
if(this == GetCapture()) |
|
ReleaseCapture(); |
// Stop capturing mouse messages |
// If there is an element, add it to the document if(m_pTempElement)
{
GetDocument()->AddElement(m_pTempElement); GetDocument()->UpdateAllViews(0,0,m_pTempElement); // Tell all the views
m_pTempElement = 0; |
// Reset the element pointer |
} |
|
}
When the m_pTempElement pointer is not NULL, the specific action of the function has been extended to call the UpdateAllViews() member of your document class. This function communicates with the views by causing the OnUpdate() member function in each view to be called. The three arguments to UpdateAllViews() are described in Figure 15-9.
The first argument to the UpdateAllViews() function call is often the this pointer for the current view. This suppresses the call of the OnUpdate() function for the current view. This is a useful feature when the current view is already up to date. In the case of Sketcher, because you are rubber-banding you want to get the current view redrawn as well, so by specifying the first argument as 0 you get the OnUpdate() function called for all the views, including the current view. This removes the need to call
InvalidateRect() as you did before.
You don’t use the second argument to UpdateAllViews() here, but you do pass the pointer to the new element through the third argument. Passing a pointer to the new element allows the views to figure out which bit of their client area needs to be redrawn.
785

Chapter 15
This argument is a pointer to the current view. It suppresses calling of the OnUpdate() member funtion for the view.
LPARAM is a 32-bit Windows type that can be used to pass information about the region to be updated in the client area
This argument is a pointer to an object that can provide information about the area in the region to be updated in the client area.
void UpdateAllView( CView* pSender, LPARAM IHint = OL, CObject* pHint = NULL );
These two argument values are passed on to the OnUpdate() functions in the views
Figure 15-9
To catch the information passed to the UpdateAllViews() function, you add the OnUpdate() member function to the view class. You can do this from the Class wizard and looking at the properties for CSketcherView. As I’m sure you recall, you display the properties for a class by right-clicking the class name and selecting Properties from the pop-up. If you click the Overrides button in the Properties window, you’ll be able to find OnUpdate in the list of functions you can override. Click the function name, then the <Add> OnUpdate option that shows in the drop-down list in the adjacent column. If you close the Properties window, you’ll be able to edit the code for the OnUpdate() override you have added in
the Editor pane. You only need to add the highlighted code below to the function definition:
void CSketcherView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint)
{
//Invalidate the area corresponding to the element pointed to
//if there is one, otherwise invalidate the whole client area if(pHint)
InvalidateRect(((CElement*)pHint)->GetBoundRect()); else
InvalidateRect(0);
}
Note that you must uncomment the parameter names in the generated version of the function; otherwise, it won’t compile with the additional code here. The three arguments passed to the OnUpdate() function in the view class correspond to the arguments that you passed in the UpdateAllViews() function call. Thus, pHint contains the address of the new element. However, you can’t assume that this is
786

Creating the Document and Improving the View
always the case. The OnUpdate() function is also called when a view is first created, but with a NULL pointer for the third argument. Therefore, the function checks that the pHint pointer isn’t NULL and only then gets the bounding rectangle for the element passed as the third argument. It invalidates this area
in the client area of the view by passing the rectangle to the InvalidateRect() function. This area is redrawn by the OnDraw() function in this view when the next WM_PAINT message is sent to the view. If the pHint pointer is NULL, the whole client area is invalidated.
You might be tempted to consider redrawing the new element in the OnUpdate() function. This isn’t a good idea. You should only do permanent drawing in response to the Windows WM_PAINT message. This means that the OnDraw() function in the view should be the only place that’s initiating any drawing operations for document data. This ensures that the view is drawn correctly whenever Windows deems it necessary.
If you build and execute Sketcher with the new modifications included, you should find that all the views are updated to reflect the contents of the document.
Scrolling Views
Adding scrolling to a view looks remarkably easy at first sight; the water is in fact deeper and murkier that it at first appears, but jump in anyway. The first step is to change the base class for CSketcherView from CView to CScrollView. This new base class has the scrolling functionality built in, so you can alter the definition of the CSketcherView class to:
class CSketcherView: public CScrollView
{
// Class definition as before...
};
You must also modify two lines of code at the beginning of the SketcherView.cpp file which refer to the base class for CSketcherView. You need to replace CView with CScrollView as the base class:
IMPLEMENT_DYNCREATE(CSketcherView, CScrollView)
BEGIN_MESSAGE_MAP(CSketcherView, CScrollView)
However, this is still not quite enough. The new version of the view class needs to know some things about the area you are drawing on, such as the size and how far the view is to be scrolled when you use the scroller. This information has to be supplied before the view is first drawn. You can put the code to do this in the OnInitialUpdate() function in the view class.
You supply the information that is required by calling a function that is inherited from the CScrollView class: SetScrollSizes(). The arguments to this function are explained in Figure 15-10.
Scrolling a distance of one line occurs when you click on the up or down arrow on the scroll bar; a page scroll occurs when you click on the scrollbar itself. You have an opportunity to change the mapping mode here. MM_LOENGLISH would be a good choice for the Sketcher application, but first get scrolling working with the MM_TEXT mapping mode because there are still some difficulties to be uncovered.
787

Chapter 15
This defines the horizontal(cx) and vertical(cy) distances to scroll a page. This can be defined as:
CSize Page(cx, cy);
Default is 1/10 of the total area
This defines the horizontal(cx) and vertical(cy) distances to scroll a line. This can be defined as:
CSize Line(cx, cy);
Default is 1/10 of the total area
void SetScrollSizes(
int MapMode, SIZE Total, const SIZE& Page = sizeDefault, const SIZE& Line =
sizeDefault
);
Can be any of:
MM_Text MM_LOENGLISH MM_LOMETRIC
MM_TWIPS MM_HEINGLISH MM_HIMETRIC
This is the total drawing area and can be defined as:
CSize Total(cx,cy);
where cx is the horizontal extent and cy is the vertical extent in logical units.
Figure 15-10
To add the code to call SetScrollSizes(), you need to override the default version of the OnInitialUpdate() function in the view. You access this in the same way as for the OnUpdate() function override(through the Properties window for the CSketcherView class. After you have added the override, just add the code to the function where indicated by the comment:
void CSketcherView::OnInitialUpdate()
{
CScrollView::OnInitialUpdate();
//Define document size CSize DocSize(20000,20000);
//Set mapping mode and document size. SetScrollSizes(MM_TEXT,DocSize);
}
788