Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Beginning Visual C++ 2005 (2006) [eng]-1

.pdf
Скачиваний:
108
Добавлен:
16.08.2013
Размер:
18.66 Mб
Скачать

Drawing in a Window

If you take a look at the end of the code for the CSketcherView class definition, you’ll see that three function declarations have been added:

// Generated message map functions protected:

DECLARE_MESSAGE_MAP() public:

afx_msg void OnLButtonDown(UINT nFlags, CPoint point); afx_msg void OnLButtonUp(UINT nFlags, CPoint point); afx_msg void OnMouseMove(UINT nFlags, CPoint point);

};

These identify the functions that you added as message handlers.

Now you have an understanding of the information passed to the message handlers you have created, you can start adding your own code to make them do what you want.

Drawing Using the Mouse

For the WM_LBUTTONDOWN message, you want to record the cursor position as the first point defining an element. We also want to record the position of the cursor after a mouse move. The obvious place to store these is in the CSketcherView class, so you can add data members to the class for these. Rightclick the CSketcherView class name in Class View and select Add > Add Variable from the pop-up. You’ll then be able to add details of the variable to be added to the class, as Figure 14-12 shows.

Figure 14-12

729

Chapter 14

The drop-down list of types only includes fundamental type so to enter the type as CPoint you just highlight the type displayed by double-clicking it and then key in the type name you want. The new data member should be protected to prevent direct modification of it from outside the class. When you click the Finish button the variable will be created and an initial value will be set arbitrarily as 0 in the initialization list for the constructor. You’ll need to amend the initial value to CPoint(0,0) so the code is:

// CSketcherView construction/destruction

CSketcherView::CSketcherView(): m_FirstPoint(CPoint(0,0))

{

// TODO: add construction code here

}

This initializes the member to a CPoint object at position (0,0). You can now add m_SecondPoint as a protected member of type CPoint to the CSketcherView class that stores the next point for an element. You should also amend the initialization list for the constructor to initialize it to CPoint(0,0).

You can now implement the handler for the WM_LBUTTONDOWN message as:

void CSketcherView::OnLButtonDown(UINT nFlags, CPoint point)

{

// TODO: Add

your message handler code

here and/or call default

m_FirstPoint

= point;

//

Record the cursor position

 

 

 

 

}

All it does is note the coordinates passed by the second argument. You can ignore the first argument in this situation altogether.

You can’t complete WM_MOUSEMOVE message handler yet, but you can have a stab at writing the code for it in outline:

void CSketcherView::OnMouseMove(UINT nFlags, CPoint point)

{

// TODO: Add your message handler code here and/or call default if(nFlags & MK_LBUTTON) // Verify the left button is down

{

m_SecondPoint = point;

// Save the current cursor position

// Test for a previous temporary element

{

//We get to here if there was a previous mouse move

//so add code to delete the old element

}

//Add code to create new element

//and cause it to be drawn

}

}

It’s important to check that the left mouse button is down because you only want to handle the mouse move when this is the case. Without the check, you would be processing the event when the right button was down or when the mouse was moved with no buttons pressed.

730

Drawing in a Window

The first thing that the handler does after verifying the left mouse button is down is to save the current cursor position. This is used as the second defining point for an element. The rest of the logic is clear in general terms, but you need to establish a few more things before you can complete the function. You have no means of defining an element — you’ll want to define an element as an object of a class so some classes must be defined. You also need to devise a way to delete an element and get one drawn when you create a new one. A brief digression is called for.

Getting the Client Area Redrawn

Drawing or erasing elements involves redrawing all or part of the client area of a window. As you’ve already discovered, the client area gets drawn by the OnDraw() member function of the CSketcherView class, and this function is called when a WM_PAINT message is received by the Sketcher application. Along with the basic message to repaint the client area, Windows supplies information about the part of the client area that needs to be redrawn. This can save a lot of time when you’re displaying complicated images because only the area specified actually needs to be redrawn, which may be a very small proportion of the total area.

You can tell Windows that a particular area should be redrawn by calling the InvalidateRect() function that is an inherited member of your view class. The function accepts two arguments, the first of which is a pointer to a RECT or CRect object that defines the rectangle in the client area to be redrawn. Passing null for this parameter causes the whole client area to be redrawn. The second parameter is a BOOL value, which is TRUE if the background to the rectangle is to be erased and FALSE otherwise. This argument has a default value of TRUE because you normally want the background erased before the rectangle is redrawn, so you can ignore it most of the time. BOOL is a Windows API type representing Boolean values and can be assigned the values TRUE or FALSE.

A typical situation in which you’d want to cause an area to be redrawn would be where something has changed that necessitates the contents of the area being recreated — moving a displayed entity might be an example. In this case, you want to erase the background to remove the old representation of what was displayed before you draw the new version. When you want to draw on top of an existing background, you just pass FALSE as the second argument to InvalidateRect().

Calling the InvalidateRect() function doesn’t directly cause any part of the window to be redrawn; it just communicates to Windows the rectangle that you would like to have it redraw at some time. Windows maintains an update region — actually a rectangle — that identifies the area in a window that needs to be redrawn. The area specified in your call to InvalidateRect() is added to the current update region, so the new update region encloses the old region plus the new rectangle you have indicated as invalid. Eventually a WM_PAINT message is sent to the window, and the update region is passed to the window along with it. When processing of the WM_PAINT message is complete, the update region is reset to the empty state.

Thus, all you have to do to get a newly created shape drawn is:

1.Make sure that the OnDraw() function in your view includes the newly-created item when it redraws the window.

2.Call InvalidateRect() with a pointer to the rectangle bounding the shape to be redrawn passed as the first argument.

Similarly, if you want a shape removed from the client area of a window, you need to do the following:

731

Chapter 14

1.Remove the shape from the items that the OnDraw() function will draw.

2.Call InvalidateRect() with the first argument pointing to the rectangle bounding the shape that is to be removed.

Because the background to the rectangle specified is automatically erased, as long as the OnDraw() function doesn’t draw the shape again, the shape disappears. Of course, this means that you need to be able to obtain the rectangle bounding any shape that you create, so you’ll include a function to provide this as a member of the classes that define the elements that can be drawn by Sketcher.

Defining Classes for Elements

Thinking ahead a bit, you will need to store elements in a document in some way. You must also to be able to store the document in a file for retrieval subsequently if a sketch is to have any permanence. I’ll deal with the details of file operations later on, but for now it’s enough to know that the MFC class CObject includes the tools for us to do this, so you’ll use CObject as a base class for the element classes.

You also have the problem that you don’t know in advance what sequence of element types the user will create. The Sketcher program must be able to handle any sequence of elements. This suggests that using a base class pointer for selecting a particular element class function might simplify things a bit. For example, you don’t need to know what an element is to draw it. As long as you are accessing the element through a base class pointer, you can always get an element to draw itself by using a virtual function. This is another example of the polymorphism I talked about when I discussed virtual functions. All you need to do to achieve this is to make sure that the classes defining specific elements share a common base class and that in this class you declare all the functions you want to be selected automatically at run time as virtual. This indicates that the element classes could be organized could be as shown in Figure 14-13.

CObject

CElement

CLine

CRectangle

CCircle

CCurve

Figure 14-13

The arrows in the diagram point towards the base class in each case. If you need to add another element type, all you need to do is derive another class from CElement. Because these classes are closely related, you’ll be putting the definitions for all these classes in a single new .h file that you can call Elements.h.

732

Drawing in a Window

You can create the new CElement class by right-clicking Sketcher in Class View and selection Add > Class from the pop-up. Select the class category as MFC and the template as MFC Class. When you click the Add button in the dialog, another dialog displays in which you can specify the class name, as shown in Figure 14-14.

Figure 14-14

I have already filled in the class name as CElement and selected the base class to be CObject from the drop-down list. I have also adjusted the names of the source files to be Elements.h and Elements.cpp because eventually these files will contain definitions for the other element classes I need. When you click the Finish button, the code for the class definition is generated:

#pragma once

// CElement command target

class CElement : public CObject

{

public:

CElement(); virtual ~CElement();

};

The only members that have been declared for you are a constructor and a virtual destructor and skeleton definitions for these appear in the Elements.cpp file. You can see that the Class wizard has included a #pragma once directive to ensure that the contents of the header file cannot be included into a .cpp file more than once. Save all files to get the Class View tab updated.

733

Chapter 14

You can add the other elements classes using essentially the same process. Because the other element classes have CElement as the base class rather than an MFC class, you should choose the class category to be C++ and the template as C++ class. For the CLine class the Class Wizard window should look as shown in Figure 14-15.

Figure 14-15

The Class wizard supplies the name of the header files and .cpp file as Line.h and Line.cpp by default but you can change these to use different names for the files or use existing files. To have the CLine class code inserted in the files for the CElement class, just click the button alongside the file name and select the appropriate file to be used. I have already done this in Figure 14-14. You may need to erase the default file name that is supplied in the dialog and reselect the Sketcher directory to get the list of files displayed. You’ll see a dialog displayed when you click the Finish button that asks you to confirm that you want to merge the new class into the existing header file. Just click the Yes button to confirm this and do the same for the dialog that appears relating to the Elements.cpp file. When you have created the CLine class definition, do the same for CRectangle, CCircle and CCurve. When you are done, you should see the definitions of all four subclasses of CElement in the Elements.h file, each with a constructor and a virtual destructor declared.

Storing a Temporary Element in the View

When I discussed how shapes would be drawn, it was evident that as the mouse was dragged after pressing the left mouse button, a series of temporary element objects would be created and drawn. Now that you know that the base class for all the shapes is CElement, you can add a pointer to the view class that you’ll use to store the address of the temporary element. Right-click the CSketcherView class once more and select the Add > Add Variable option once again. The m_pTempElement should be of type CElement* and be protected like the previous two data members that you added earlier, as illustrated in Figure 14-16.

734

Drawing in a Window

Figure 14-16

The Add Member Variable Wizard ensures that the new variable is initialized when the view object is constructed and the default value of NULL that is set will do nicely.

CSketcherView::CSketcherView() :m_FirstPoint(CPoint(0,0))

,m_SecondPoint(CPoint(0,0))

,m_pTempElement(NULL)

{

// TODO: add construction code here

}

You’ll be able to use the m_pTempElement pointer in the WM_MOUSEMOVE message handler as a test for previous temporary elements because you’ll arrange for it to be null when there are none.

If you check what has been added to the SketcherView.h header file, you’ll see at the beginning there is the line:

#include “atltypes.h”

This has been inserted because the wizard has assumed the CElement type is an ATL type. You can delete this line as it is not required. For the CSketcherView class to compile correctly, you must add the following statement immediately following the #pragma once directive:

class CElement;

// Forward class declaration

735

Chapter 14

This just identifies the CElement identifier as the name of a class that is defined elsewhere so the compiler will process it as such.

Because you are creating CElement class objects in the view class member functions, and you refer to the CElement class in defining the data member that points to a temporary element, you should ensure that the definition of the CElement class is included before the CSketcherView class definition wherever SketcherView.h is included into a .cpp file. You can do this for CSketcherView by adding an

#include directive for Elements.h to the SketcherView.cpp file before the #include directive for

SketcherView.h:

#include “Elements.h” #include “SketcherView.h”

Sketcher.cpp also has an #include directive for SketcherView.h, so you should add an #include for Elements.h to this file too.

The CElement Class

You can now start to fill out the element class definitions. You’ll be doing this incrementally as you add more and more functionality to the Sketcher application — but what do you need right now? Some data items, such as color, are clearly common to all types of element so you can put those in the CElement class so that they are inherited in each of the derived classes; however, the other data members in the classes that define specific element properties will be quite disparate, so you’ll declare these members in the particular derived class to which they belong.

Thus the CElement class contains only virtual functions that are replaced in the derived classes, plus data and function members that are the same in all the derived classes. The virtual functions are those that are selected automatically for a particular object through a pointer. You could use the Add Member wizard you’ve used previously to do this, but modify the class manually for a change. For now, you can modify the CElement class to the following:

class CElement: public CObject

{

protected:

COLORREF m_Color; // Color of an element

public:

virtual

~CElement();

virtual

void Draw(CDC* pDC) {} // Virtual draw operation

CRect GetBoundRect();

// Get the bounding rectangle for an element

 

 

protected:

 

CElement();

// Here to prevent it being called

};

 

I have changed the access for the constructor from public to protected to prevent it from being called from outside the class. At the moment, the members to be inherited by the derived classes are a data member storing the color, m_Color, and a member function that calculates the rectangle bounding an element, GetBoundRect(). This function returns a value of type CRect that is the rectangle bounding the shape.

736

Drawing in a Window

You also have a virtual Draw() function that is implemented in the derived classes to draw the particular object in question. The Draw() function needs a pointer to a CDC object passed to it to provide access to the drawing functions that you saw earlier that allow drawing in a device context.

You might be tempted to declare the Draw() member as a pure virtual function in the CElement class — after all, it can have no meaningful content in this class. This would also force its definition in any derived class. Normally you would do this, but the CElement class inherits a facility from CObject called serialization that you’ll use later for storing objects in a file, and this requires that an instance of the CElement class can be created. A class with a pure virtual function member is an abstract class, and instances of an abstract class can’t be created. If you want to use MFC’s serialization capability for storing objects, your classes mustn’t be abstract. You must also supply a no-arg constructor for a class to be serializable.

You might also be tempted to declare the GetBoundRect() function as returning a pointer to a CRect object — after all, you’re going to pass a pointer to the InvalidateRect() member function in the view class; however, this could lead to problems. You’ll be creating the CRect object as local to the function, so the pointer would be pointing to a nonexistent object on return from the GetBoundRect() function. You could get around this by creating the CRect object on the heap, but then you’d need to take care that it’s deleted after use; otherwise, you’d be filling the heap with CRect objects — a new one for every call of GetBoundRect(). A further possibility is that you could store the bounding rectangle for an element as a class member and generate it when the element is created. This is a reasonable alternative, but if you changed an element subsequently, by moving it say, you would need to ensure the bounding rectangle was recalculated.

The CLine Class

You can amend the definition of the CLine class to:

class CLine: public CElement

 

{

 

public:

 

~CLine(void);

 

virtual void Draw(CDC* pDC);

// Function to display a line

// Constructor for a line object

CLine(CPoint Start, CPoint End, COLORREF aColor);

protected:

CPoint m_StartPoint;

// Start point of line

CPoint m_EndPoint;

// End point of line

CLine(void);

// Default constructor(should not be used

};

 

The data members that define a line are m_StartPoint and m_EndPoint, and these are both declared to be protected. The class has a public constructor that has parameters for the values that define a line, and the no-arg default constructor has been moved to the protected section of the class to prevent its use externally.

Implementing the CLine Class

You add the implementation of the member functions in the Elements.cpp that was created by the Class wizard when you created the CElement class. The stdafx.h file was included in this file to make

737

Chapter 14

the definitions of the standard system header files available in the file. You may need to add #include directives for the files containing definitions for Application wizard-generated classes if you use any in the code.

Of course, you’ll have to add each of the member function definitions to this file manually because Class wizard wasn’t involved in defining the classes. You’re now ready to add the constructor for the CLine class to the Elements.cpp file.

The CLine Class Constructor

The code for this is:

// CLine class constructor

CLine::CLine(CPoint Start, CPoint End, COLORREF aColor)

{

m_StartPoint = Start; m_EndPoint = End; m_Color = aColor;

}

//Set line start point

//Set line start point

//Set line color

You first store the start point in the m_StartPoint member that is inherited from the CElement class. Later you’ll add code to allow an element to be moved and to enable a line to be moved by just changing the start point and the end point must be defined relative to the start point. You do this by subtracting the x and y coordinate values for Start from those for End. Both x and y are public members of the CPoint class so you can refer to them directly. Finally, you store the color in the m_Color member that is inherited from the CElement class.

Drawing a Line

The Draw() function for the CLine class isn’t too difficult either, although you do need to take account of the color to be used when the line is drawn:

// Draw a CLine object void CLine::Draw(CDC* pDC)

{

//Create a pen for this object and

//initialize it to the object color and line width of 1 pixel CPen aPen;

if(!aPen.CreatePen(PS_SOLID, m_Pen, m_Color))

{

//Pen creation failed. Abort the program

AfxMessageBox(_T(“Pen creation failed drawing a line”), MB_OK); AfxAbort();

}

CPen* pOldPen = pDC->SelectObject(&aPen); // Select the pen

// Now draw the line pDC->MoveTo(m_StartPoint); pDC->LineTo(m_EndPoint);

pDC->SelectObject(pOldPen);

// Restore the old pen

}

738