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

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

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

Creating the Document and Improving the View

Map Collection: CMap<KeyType, KeyType&, ObjectType, ObjectType&> aMap

Key argument type

Object argument type

Key argument type

Type of object to be stored

 

 

 

Key1

 

Object1

aMap[Key2]=Object2;

Adds an object

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Key2

 

 

Object2

 

Accesses the object

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Key3

 

Object3

corresponding to the key

 

 

 

 

 

 

 

 

 

 

Stores the object

 

 

 

 

 

Key4

Object4

LookUp(Key3,AnObject);

Figure 15-4

The hashing process may not produce a unique hash value from a key, in which case an element — the key together with the associated object — is entered and linked to whatever element or elements were previously stored with the same hashed key value (often as a list). Of course, the fewer unique hash values generated, the less efficient the retrieval process from your map is because searching is required to retrieve elements that have the same hash value.

There are four arguments necessary when you declare a map:

CMap<LONG, LONG&, CPoint, CPoint&> PointMap;

The first two specify the key type and how it is passed as an argument. Usually, it is passed as a reference. The second pair of arguments specifies the object type and how the object is passed as an argument, as you have previously seen.

You can store an object in a map by using the [] operator, as shown in Figure 15-4. You can also use the SetAt() member function to store an object, where you supply the key value and the object as arguments. Note that you cannot use the [] operator on the right side of an assignment to retrieve an object, as this version of the operator is not implemented in the class.

To retrieve an object, use the LookUp() function shown in Figure 15-4. This retrieves the object corresponding to the key specified; the function returns TRUE if the object was found and FALSE otherwise. You can also iterate through all the objects in a map using a variable of type POSITION, although the sequence in which objects are retrieved is unrelated to the sequence in which they were added to

the map. This is because objects are stored in a map in locations determined by the hash value, not by the sequence in which they were entered.

769

Chapter 15

Helper Functions Used by CMap

As well as the helper functions that have been discussed in the context of arrays and lists, map collection classes also use a global function HashKey(), which is defined by this template:

template<class ARG_KEY>

UINT HashKey(ARG_KEY key);

This function converts your key value to a hash value of type UINT, which is equivalent to unsigned int. The default version does this by simply shifting your key value right by 4 bit positions. You need to implement your own version of this function if the default operation isn’t suited to your key type.

There are different techniques used for hashing that vary depending on the type of data being used as a key, and the number of elements you are likely to want to store in your map. The likely number of elements to be stored indicates the number of unique hash values you need. A common method for hashing a numeric key value is to compute the hash value as the value of the key modulo N, where N is the number of different values you want. For reasons it would take too long to explain here, N needs to be prime for this to work well. Didn’t you just know that program to calculate primes from way back in Chapter 4 would turn out to be useful after all?

You can get an appreciation of the principles of the mechanism used here with a simple example. Suppose you expect to store up to 100 different entries in a map using a key value, Key. You could hash the key with the statement:

HashValue = Key%101;

This results in values for the HashValue between 0 and 100, which is exactly what you need to calculate the address for an entry. Assuming your map is stored at some location in memory, Base, and the memory required to store the object along with its key is Length bytes, then you can store an entry that produces the hash value HashValue at the location Base+HashValue*Length. With the hashing process mentioned previously, you can accommodate up to 101 entries at unique positions in the map.

Where a key is a character string, the hashing process is rather more complicated, particularly with long or variable length strings; however, a method commonly used involves using numerical values derived from characters in the string. This typically involves assigning a numerical value to each character, so if your string was lowercase letters plus spaces, you could assign each character a value between 0 and 26, with space as 0, a as 1, b as 2, and so on. The string can then be treated as the representation of a number to some base, 32 say. The numerical value for the string ‘fred’, for instance, is

6*323+18*322+5*321+4*320

and, assuming you expected to store 500 strings, you could calculate the hashed value of the key as:

6*323+18*322+5*321+4*320 mod 503

770

Creating the Document and Improving the View

The value of 503 for N is the smallest prime greater than the likely number of entries. The base chosen to evaluate a hash value for a string is usually a power of 2 that corresponds to the minimum value that is greater than or equal to the number of possible different characters in a string. For long strings, this can generate very large numbers, so special techniques are used to compute the value modulo N. Detailed discussion of these techniques is beyond the scope of this book, but you can find numerous Web references by searching on “hashing.”

The Typed Pointer Collections

The typed pointer collection class templates store pointers to objects, rather than objects themselves. This is the primary difference between these class templates and the template classes just discussed. Take a look at how the CTypedPtrList class template is used, because you’ll use this as a basis for managing elements in your document class, CSketcherDoc.

The CTypedPtrList Template Class

You can declare a typed pointer list class with a statement of the form:

CTypedPtrList<BaseClass, Type*> ListName;

The first argument specifies a base class that must be one of two pointer list classes defined in MFC, either CObList or CPtrList. Your choice depends on how your object class has been defined. Using the CObList class creates a list supporting pointers to objects derived from CObject, while CPtrList supports lists of void* pointers. Because the elements in the Sketcher example have CObject as a base class, I’ll concentrate on how CObList is used.

The second argument to the template is the type of the pointers to be stored in the list. In the example, this is going to be CElement* because all your shapes have CElement as a base class and CElement is derived from CObject. Thus, the declaration of a class for storing shapes is:

CTypedPtrList<CObList, CElement*> m_ElementList;

You could have used CObList* types to store the pointers to our elements, but then the list could contain an object of any class that has CObject as a base. The declaration of m_ElementList ensures that only pointers to objects of the class CElement can be stored. This provides a greatly increased level of security in the program.

CTypePtrList Operations

The functions provided in the CTypedPtrList based classes are similar to those supported by CList, except of course that all operations are with pointers to objects rather than with objects, so you need to tabulate them. They fall into two groups: those that are defined in CTypedPtrList, and those that are inherited from the base class — CObList in this case.

771

Chapter 15

The functions defined in CTypedPtrList are:

Function

Description

 

 

GetHead()

Returns the pointer at the head of the list. You should use IsEmpty() to

 

verify that the list is not empty before calling this function.

GetTail()

Returns the pointer at the tail of the list. You should use IsEmpty() to

 

verify that the list is not empty before calling this function.

RemoveHead()

Removes the first pointer in the list. You should use IsEmpty() to ver-

 

ify that the list is not empty before calling this function.

RemoveTail()

Removes the last pointer in the list. You should use IsEmpty() to verify

 

that the list is not empty before calling this function.

GetNext()

Returns the pointer at the position indicated by the variable of type

 

POSITION passed as a reference argument. The variable is updated to

 

indicate the next element in the list. When the end of the list is reached,

 

the position variable is set to NULL. This function can be used to iterate

 

forwards through all the pointers in the list.

GetPrev()

Returns the pointer at the position indicated by the variable of type

 

POSITION passed as a reference argument. The variable is updated to

 

indicate the previous element in the list. When the beginning of the list

 

is reached, the position variable is set to NULL. This function can be used

 

to iterate backwards through all the pointers in the list.

GetAt()

Returns the pointer stored at the position indicated by the variable of

 

type POSITION passed as an argument, which isn’t changed. Because

 

the function returns a reference, as long as the list is not defined as

 

const, this function can be used on the left of an assignment operator to

 

modify a list entry.

 

 

The functions in CTypedPtrList inherited from CObList are:

Function

Description

 

 

AddHead()

Adds the pointer passed as an argument to the head of the list and

 

returns a value of type POSITION that corresponds to the new element.

 

There is another version of this function that can add another list to the

 

head of the list.

AddTail()

Adds the pointer passed as an argument to the tail of the list and returns

 

a value of type POSITION that corresponds to the new element. There is

 

another version of this function that can add another list to the tail of the

 

list.

RemoveAll()

Removes all the elements from the list. Note that this doesn’t delete the

 

objects pointed to by elements in the list. You need to take care of this

 

yourself.

 

 

772

 

 

Creating the Document and Improving the View

 

 

 

 

 

 

 

Function

Description

 

 

 

 

GetHeadPosition()

Returns the position of the element at the head of the list.

 

GetTailPosition()

Returns the position of the element at the tail of the list.

 

SetAt()

Stores the pointer specified by the second argument at the position in

 

 

the list defined by the first argument. An invalid position value causes

 

 

an error.

 

RemoveAt()

Removes the pointer from the position in the list specified by the argu-

 

 

ment of type POSITION. An invalid position value causes an error.

 

InsertBefore()

Inserts a new pointer specified by the second argument before the posi-

 

 

tion specified by the first argument. The position of the new element is

 

 

returned.

 

InsertAfter()

Inserts a new pointer specified by the second argument after the posi-

 

 

tion specified by the first argument. The position of the new element is

 

 

returned.

 

Find()

Searches for a pointer in the list that is identical to the pointer specified

 

 

as an argument. Its position is returned if it is found. NULL is returned

 

 

otherwise.

 

FindIndex()

Returns the position of a pointer in the list specified by a zero-based

 

 

integer index argument.

 

GetCount()

Returns the number of elements in the list.

 

IsEmpty()

Returns TRUE if there are no elements in the list, and FALSE otherwise.

 

 

 

You’ll see some of these member functions in action a little later in this chapter in the context of implementing the document class for the Sketcher program.

Using the CList Template Class

You can use of the CList collection template in the definition of the curve object in our Sketcher application. A curve is defined by two or more points, so storing these in a list would be a good method of handling them. You first need to define a CList collection class object as a member of the CCurve class.

You’ll use this collection to store points. You’ve already looked at the CList template class in some detail, so this should be easy.

The CList template class has two parameters, so the general form of declaring a collection class of this type is:

CList<YourObjectType, FunctionArgType> ClassName;

The first argument, YourObjectType, specifies the type of object that you want to store in the list. The second argument specifies the argument type to be used in function members of the collection class when referring to an object. This is usually specified as a reference to the object type to minimize copying

773

Chapter 15

of arguments in a function call. Declare a collection class object to suit your needs in the CCurve class as:

class CCurve: public CElement

{

// Rest of the class definition...

protected:

CCurve(void); // Default constructor - should not be used CList<CPoint, CPoint&> m_PointList; // Type safe point list

};

You can either add this manually to the class definition or use the Add > Add Variable menu item that you’ve used before from Class View. I have omitted the rest of the class definition here because you’re not concerned with it for now. The collection declaration is shaded. It declares the collection m_PointList that stores CPoint objects in the list, and its functions use reference arguments to CPoint objects.

The CPoint class doesn’t allocate memory dynamically, so you won’t need to implement

ConstructElements() or DestructElements(), and because you don’t need to use the Find() member function, you can forget about CompareElements() as well.

Drawing a Curve

Drawing a curve is different from drawing a line or a circle. With a line or a circle, as you move the cursor with the left button down, you are creating a succession of different line or circle elements that share a common reference point — the point where the left mouse button was pressed. This is not the case when you draw a curve, as shown in Figure 15-5.

X-Axis

 

 

Minimum y

The first two points

 

define a basic

 

curve

 

Y-Axis

 

 

Maximum y

Each additional point defines

 

another segment

 

Minimum x

Maximum x

Drawing a curve with MM_TEXT mapping mode

Figure 15-5

 

774

Creating the Document and Improving the View

When you move the cursor while drawing a curve, you’re not creating a sequence of new curves but rather extending the same curve, so each successive point adds another segment to the curve’s definition. You therefore need to create a curve object as soon as you have the two points from the WM_LBUTTONDOWN message and the first WM_MOUSEMOVE message. Points defined with subsequent mouse move messages then define additional segments to the existing curve object. You’ll need to add a function, AddSegment(), to the CCurve class to extend the curve once it has been created by the constructor.

A further point to consider is how you are to calculate the enclosing rectangle. This is defined by getting the minimum x and minimum y pair from all the defining points to establish the upper-left corner of the rectangle, and the maximum x and maximum y pair for the bottom right. This involves going through all the points in the list. You will, therefore, compute the enclosing rectangle incrementally in the AddSegment() function as points are added to the curve.

Defining the CCurve Class

With the constructor and the AddSegment() function added, the complete definition of the CCurve class is:

class CCurve: public CElement

 

{

 

public:

 

~CCurve(void);

 

virtual void Draw(CDC* pDC);

// Function to display a curve

// Constructor for a curve object

CCurve(CPoint FirstPoint, CPoint SecondPoint, COLORREF aColor);

void AddSegment(CPoint& aPoint); //Add a segment to the curve

protected:

CCurve(void); // Default constructor - should not be used CList<CPoint, CPoint&> m_PointList; // Type safe point list

};

You should modify the definition of the class in Elements.h to correspond with the previous code. The constructor has the first two defining points and the color as parameters, so it only defines a curve with one segment. This is called in the CreateElement() function invoked by the OnMouseMove() function in the view class the first time a WM_MOUSEMOVE message is received for a curve, so don’t forget to modify the definition of the CreateElement() function in CSketcherView to call the CCurve class constructor with the correct arguments. The statement using the CCurve constructor in the switch in the CreateElement() function should be changed to:

case CURVE:

return new CCurve(m_FirstPoint, m_SecondPoint, pDoc->GetElementColor());

After the constructor has been called, all subsequent WM_MOUSEMOVE messages results in the AddSegment() function being called to add a segment to the existing curve, as shown in Figure 15-6:

775

Chapter 15

 

 

 

 

 

 

 

 

OnLButtonUp() called

OnLbuttonDown()

 

 

 

11

 

stores point x1,y1

 

 

 

 

 

 

 

1

 

x1,y1

10

 

OnMouseMove()

 

 

 

 

 

 

 

 

 

OnMouseMove()

 

 

 

 

x2,y2

 

 

calls AddSegment() with the

 

 

 

 

 

 

point x10,y10

calls CreateElement(), which

2

 

 

 

 

 

 

will call the constructor with

 

 

 

 

x3,y3

 

 

 

the points x1,y1 and x2,y2

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

3

 

 

x4,y4

 

 

 

OnMouseMove()

 

 

 

 

 

 

 

 

calls AddSegment() with the

 

4

 

 

 

 

point x3,y3

 

 

 

 

 

 

 

OnMouseMove()

 

 

 

 

 

 

 

 

 

 

 

 

 

 

calls AddSegment() with the

 

 

 

 

point x4,y4

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Figure 15-6

This shows the complete sequence of message handler calls for a curve comprised of nine segments. The sequence is indicated by the numbered arrows. The code for the OnMouseMove() function in CSketcherView needs to be updated as follows:

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

{

CClientDC aDC(this);

// Device context for the current view

if((nFlags&MK_LBUTTON)&&(this==GetCapture()))

 

{

 

 

m_SecondPoint = point;

// Save the current cursor position

if(m_pTempElement)

 

 

{

 

 

if(CURVE == GetDocument()->GetElementType())

// Is it a curve?

{// We are drawing a curve

// so add a segment to the existing curve static_cast<CCurve*>(m_pTempElement)->AddSegment(m_SecondPoint);

m_pTempElement->Draw(&aDC);

// Now draw it

return;

// We are done

}

 

 

 

aDC.SetROP2(R2_NOTXORPEN);

// Set drawing mode

// Redraw the old element so it disappears from the view

m_pTempElement->Draw(&aDC);

 

delete m_pTempElement;

// Delete the old element

m_pTempElement = 0;

// Reset the pointer to 0

}

//Create an element of the type and color

//recorded in the document object

m_pTempElement = CreateElement(); m_pTempElement->Draw(&aDC);

}

}

776

Creating the Document and Improving the View

You have to treat an element of type CURVE as a special case after it has been created because on all subsequent calls of the OnMouseMove() handler, you want to call the AddSegment() function for the existing element, rather than construct a new one in place of the old. You don’t want to set the drawing mode in this instance because you don’t need to erase the previous curve each time. You take care of this by moving the call to SetROP2() to a position after the code processing a curve.

Adding the curve segment and drawing the extended curve is taken care of within the if statement you have added. Note that you must cast the m_pTempElement pointer to type CCurve* to use it to call AddSegment() for the old element because AddSegment() is not a virtual function. If you don’t add the cast, you’ll get an error because the compiler tries to resolve the call statically to a member of the

CElement class.

Implementing the CCurve Class

Write the code for the constructor; this should be added to Elements.cpp in place of the temporary constructor that you used in the last chapter. It needs to store the two points passed as arguments in the

CList data member, m_PointList:

CCurve::CCurve(CPoint FirstPoint,CPoint SecondPoint, COLORREF aColor)

{

m_PointList.AddTail(FirstPoint); m_PointList.AddTail(SecondPoint); m_Color = aColor;

m_Pen = 1;

//Add the 1st point to the list

//Add the 2nd point to the list

//Store the color

//Set the pen width

// Construct the enclosing rectangle assuming MM_TEXT mode m_EnclosingRect = CRect(FirstPoint, SecondPoint); m_EnclosingRect.NormalizeRect();

}

The points are added to the list, m_PointList, by calling the AddTail() member of the CList template class. This function adds a copy of the point passed as an argument to the end of the list. The enclosing rectangle is defined in exactly the same way that that defined it for a line.

You can add the AddSegment() function to Elements.cpp next. This function is called when additional curve points are recorded, after the first version of a curve object has been created. This member function is very simple:

void CCurve::AddSegment(CPoint& aPoint)

 

{

 

m_PointList.AddTail(aPoint);

// Add the point to the end

// Modify the enclosing rectangle for the new point

m_EnclosingRect = CRect(min(aPoint.x, m_EnclosingRect.left), min(aPoint.y, m_EnclosingRect.top), max(aPoint.x, m_EnclosingRect.right), max(aPoint.y, m_EnclosingRect.bottom));

}

The min() and max() functions you use here are standard macros that are the equivalent of using the conditional operator for choosing the minimum or maximum of two values. The new point is added to the tail of the list in the same way as in the constructor. It’s important that each new point is added to the

777

Chapter 15

list in a way that is consistent with the constructor because you’ll draw the segments using the points in sequence, from the beginning to the end of the list. Each line segment is drawn from the end point of the previous line to the new point. If the points are not in the right sequence, the line segments won’t be drawn correctly. After adding the new point, the enclosing rectangle for the curve is redefined, taking account of the new point.

The last member function you need to define for the interface to the CCurve class is Draw():

void CCurve::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. Close the program

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

}

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

//Now draw the curve

//Get the position in the list of the first element POSITION aPosition = m_PointList.GetHeadPosition();

//As long as it’s good, move to that point if(aPosition)

pDC->MoveTo(m_PointList.GetNext(aPosition));

//Draw a segment for each of the following points while(aPosition)

pDC->LineTo(m_PointList.GetNext(aPosition));

pDC->SelectObject(pOldPen);

// Restore the old pen

}

You draw the CCurve object by iterating through all the points in the list from the beginning, drawing each segment as you go. You get a POSITION value for the first element by using the function GetHeadPosition() and then use MoveTo() to set the first point as the current position in the device context. You then draw line segments in the while loop as long as aPosition is not NULL. The GetNext() function call that appears as the argument to the LineTo() function returns the current point and simultaneously increments aPosition to refer to the next point in the list.

Exercising the CCurve Class

With the changes I’ve just discussed added to the Sketcher program, you have implemented all the code necessary for the element shapes in your menu. You can now build the Sketcher program once more, and execute it. You should be able to create curves in all four colors. A typical application window is shown in Figure 15-7.

778