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

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

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

15

Creating the Document and

Improving the View

In this chapter, you’ll look into the facilities offered by MFC for managing collections of data items. You’ll use these to complete the class definition and implementation for the curve element that was left open in the last chapter. You’ll also extend the Sketcher application to store data in a document, and make the document view more flexible, introducing several new techniques in the process.

In this chapter, you’ll learn about:

Collections and what you can do with them

How to use a collection to store point data for a curve

How to use a collection to store document data

How to implement drawing a document

How to implement scrolling in a view

How to create a context menu at the cursor

How to highlight the element nearest the cursor to provide feedback to the user for moving and deleting elements

How to program the mouse to move and delete elements

What Are Collection Classes?

By the nature of Windows programming, you’ll frequently need to handle collections of data items where you have no advance knowledge of how many items you will need to manage, or even what particular type they are going to be. This is clearly illustrated by the Sketcher application. The user can draw an arbitrary number of elements which can be lines, rectangles, circles and curves, and in any sequence. MFC provides a group of collection classes designed to handle

Chapter 15

exactly this sort of problem — a collection being an aggregation of an arbitrary number of data items organized in a particular way.

Types of Collection

MFC provides you with a large number of collection classes for managing data. You’ll use just a couple of them in practice, but it would be helpful to understand the types of collections available. MFC supports three kinds of collections, differentiated by the way in which the data items are organized. The way a collection is organized is referred to as the shape of the collection. The three types of organization, or shape, are:

Shape

How information is organized

 

 

Array

An array in this context is just like the array you have seen in the C++ lan-

 

guage. It’s an ordered arrangement of elements, where any element is

 

retrieved by using an integer index value. An array collection can automati-

 

cally grow to accommodate more data items; however, one of the other collec-

 

tion types is generally preferred, because array collections can be rather slow

 

in operation.

List

A list collection is an ordered arrangement of data items, where each item has

 

two pointers associated with it that point to the next and previous items in the

 

list. You saw a linked list in Chapter 7, when I discussed structures. The list

 

here is called a doubly linked list because it has backward as well as forward-

 

pointing links. It can be searched in either direction and, like an array, a list

 

collection grows automatically when required. A list collection is easy to use,

 

and fast when it comes to adding items. Searching for an item can be slow,

 

though, if there are a lot of data items in the list.

Map

A map is an unordered collection of data items, where each item is associated

 

with a key that is used to retrieve the item from the map. A key is usually a

 

string, but it can also be a numeric value or any type of object. Maps are fast in

 

storing data items and in searching because a key takes you directly to the

 

item you need. This sounds as though maps are always the ideal choice, and

 

this is often the case, but for sequential access arrays are faster. You also

 

have the problem of choosing a key for your object that’s unique for each item

 

in the list.

 

 

MFC collection classes provide two approaches to implementing each type of collection. One approach is based on the use of class templates and provides you with type-safe handling of data in a collection. Type-safe handling means that the data passed to a function member of the collection class are checked to ensure that it’s of a type that can be processed by the function.

The other approach makes use of a range of collection classes (rather than templates), but these perform no data checking. If you want your collection classes to be type-safe, you have to include code yourself to assure this. These latter classes were available in older versions of Visual C++ under Windows, but the template collection classes were not. I’ll concentrate on the template-based versions because these provide the best chance of avoiding errors in our application.

760

Creating the Document and Improving the View

The Type-Safe Collection Classes

The template-based type-safe collection classes support collections of objects of any type, and collections of pointers to objects of any type. Collections of objects are supported by the template classes CArray, CList and CMap, and collections of pointers to objects are supported by the template classes CTypedPtrArray, CTypedPtrList and CTypedPtrMap. I won’t go into the detail of all of these, just the two that you’ll use in the Sketcher program. You’ll use one to store objects and the other to store pointers to objects so you’ll get a feel for both sorts of collection.

Collections of Objects

The template classes for defining collections of objects are all derived from the MFC class CObject. They are defined this way so that they inherit the properties of the CObject class, which are particularly useful for a number of things These include file input and output operations for objects generally referred to as serialization, and you’ll look at this in Chapter 17.

These template classes can store and manage any kind of object, including all the C++ basic data types, plus any classes or structures that you or anybody else might define. Because these classes store objects, whenever you add an element to a list, an array, or a map, the class template object needs to make a copy of your object. Consequently, any class type that you want to store in any of these collections must have a copy constructor. The copy constructor for your class is used to create a duplicate of the object that you want to store in the collection.

Take a look at the general properties of each of the template classes that provide type-safe management of objects. This is not an exhaustive treatment of all the member functions provided. Rather, it’s intended to give you a sufficient flavor of how they work to enable you to decide if you want to use them or not. You can get information on all of the member functions by using Help to get to the template class definition.

The CArray Template Class

You can use this template to store any kind of object in an array and have the array automatically grow to accommodate more elements when necessary. An array collection is illustrated in Figure 15-1.

As with the arrays that you’ve seen in native C++, elements in array collections are indexed from 0. The declaration of an array collection takes two arguments. The first argument is the type of the object to be stored so, if your array collection is to store objects of type CPoint, for example, you specify CPoint as the first argument. The second argument is the type used in member function calls. To avoid the overhead in copying objects when passed by value, the second argument is usually a reference, so an example of an array collection declaration to hold CPoint objects is:

CArray<CPoint, CPoint&> PointArray;

This defines the array collection class object, PointArray, that stores CPoint objects. When you call function members of this object, the argument is a reference, so to add a CPoint object, you would write

PointArray.Add(aPoint);

and the argument aPoint is passed as a reference.

761

Chapter 15

Array Collection: CArray<ObjectType, ObjectType&> anArray

Type of object to be

 

Argument type to be used

stored

 

 

 

 

 

Index

 

 

 

0

 

 

 

Object1

 

 

1

 

 

Object returned

Object2

SetSize(5)

2

 

GetAt(2)

Object3

Defines the

3

 

initial size

At this index

Object4

 

 

4

Object5

 

 

5

Object6

 

Index returned

6

AnObject

 

 

 

Add (AnObject)

Increases are

automatic

 

 

Stores the object

Figure 15-1

After you have declared an array collection, it’s important to call the SetSize() member function to fix the initial number of elements that you require before you use it. It still works if you don’t do this, but the initial allocation of elements and subsequent increments are small, resulting in inefficient operation and frequent reallocation of memory for the array. The initial number of elements that you should specify depends on the typical size of array you expect to need, and how variable the size is. If you expect that the minimum your program required is of the order of 400 to 500 elements, for example, but with expansion up to 700 or 800, an initial size of 600 is suitable.

To retrieve the contents of an element, you can use the GetAt() function, as shown in Figure 15-1. To store the third element of PointArray in a variable aPoint, you write:

aPoint = PointArray.GetAt(2);

The class also overloads the [] operator, so you could retrieve the third element of PointArray by using PointArray[2]. For example, if aPoint is a variable of type CPoint, you could write:

aPoint = PointArray[2];

// Store a copy of the third element

For array collections that are not const, this notation can also be used instead of the SetAt() function to set the contents of an existing element. The following two statements are, therefore, equivalent:

PointArray.SetAt(3,NewPoint);

//

Store NewObject in the 4th element

PointArray[3] = NewPoint;

//

Same as previous line of code

762

Creating the Document and Improving the View

Here, NewPoint is an object of the type used to declare the array. In both cases, the element must already exist. You cannot extend the array by this means. To extend the array, you can use the Add() function shown in the diagram, which adds a new element to the array. There is also a function Append() to add an array of elements to the end of the array.

Helper Functions

Whenever you call the SetSize() function member of an array collection, a global function, ConstructElements(), is called to allocate memory for the number of elements you want to store in the array collection initially. This is called a helper function because it helps in the process of setting the size of the array collection. The default version of this function sets the contents of the allocated memory to zero and doesn’t call a constructor for your object class, so you’ll need to supply your own version of this helper function if this action isn’t appropriate for your objects. This is the case if space for data members of objects of your class is allocated dynamically, or if there is other initialization required. ConstructElements() is also called by the member function InsertAt(), which inserts one or more elements at a particular index position within the array.

Members of the CArray collection class that remove elements call the helper function DestructElements(). The default version does nothing, so if your object construction allocates any memory on the heap, you must override this function to release the memory properly.

The CList collection template makes use of a helper function when searching the contents of a list for a particular object. I’ll discuss this further in the next section. Another helper function,

SerializeElements(), is used by the array, list, and map collection classes, and I’ll discuss this when I explain how you can write a document to file.

The CList Template Class

Take a look at the list collection template in some detail because you’ll apply it in your Sketcher program. The parameters to the CList collection class template are the same as those for the CArray template:

CList<ObjectType, ObjectType&> aList;

You need to supply two arguments to the template when you declare a list collection: the type of object to be stored, and the way an object is to be specified in function arguments. The example shows the second argument as a reference because this is used most frequently. It doesn’t necessarily have to be a reference, though — you could use a pointer, or even the object type (so objects would be passed by value), but this would be slow.

You can use a list to manage a curve in the Sketcher program. You could declare a list collection to store the points specifying a curve object with the statement:

CList<CPoint, CPoint&> PointList;

This declares a list called PointList that stores CPoint objects that are passed to functions in the class by reference. You’ll come back to this when you fill out more detail of the Sketcher program in this chapter.

763

Chapter 15

Adding Elements to a List

You can add objects at the beginning or at the end of the list by using the AddHead() or AddTail() member functions, as shown in Figure 15-2.

List Collection: CList<ObjectType, ObjectType&> aList

Type of object to be

Argument type to be used

stored

 

 

Stores the object

AddHead (ThisObject)

 

 

ThisObject

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

pointer

 

pointer

 

 

 

 

 

 

 

 

 

 

Object1

 

 

 

 

 

 

 

 

 

 

 

 

 

 

pointer

 

 

 

 

 

pointer

 

 

 

 

 

 

 

 

 

Object2

 

Increases in size

 

 

 

 

 

 

 

 

 

 

pointer

 

 

 

 

pointer

 

are automatic

 

 

 

 

 

 

 

Object3

 

 

 

 

 

 

 

 

 

 

 

 

 

 

pointer

 

pointer

 

 

 

 

 

 

 

 

 

Object4

 

 

 

 

 

 

 

 

 

 

 

 

 

 

pointer

 

pointer

 

ThatObject

AddTail (ThatObject)

Stores the object

Figure 15-2

Figure 15-2 shows backward and forward pointers for each list element that “‘glue” the objects in the list together. These are internal links that you can’t access in any direct way, but you can do just about anything you want by using the functions provided in the public interface to the class.

To add the object aPoint to the tail of the list PointList, you write:

PointList.AddTail(aPoint);

// Add an element to the end

As new elements are added, the size of the list increases automatically.

Both the AddHead() and AddTail() functions return a value of type POSITION, which specifies the position of the inserted object in the list. The way in which a variable of type POSITION is used is shown in Figure 15-3.

764

Creating the Document and Improving the View

List Collection: CList<ObjectType, ObjectType&> aList

Type of object to be stored

The position of a particular element

 

is given by a value of type

aPos

POSITION

Returns Object1

Argument type to be used

ThisObject

pointer

Object1

 

Increments aPos

 

 

GetNext(aPos)

 

Object1

 

 

retrieves the element at

 

 

 

 

aPos and sets aPos to

 

 

pointer

 

the next element

 

Object2

Object3

Object4

ThatObject

Figure 15-3

You can use a value of type POSITION to retrieve the object at a given position in the list by using the GetNext() function. Note that you can’t perform arithmetic on values of type POSITION — you can only modify a position value through member functions of the list object. Furthermore, you can’t set a position value to a specific numerical value. POSITION variables can only be set through member functions of the list object.

As well as returning the object, the GetNext() function increments the position variable passed to it so that it points to the next object in the list. You can, therefore, use repeated calls to GetNext() to step through a list element by element. The position variable is set to NULL if you use GetNext() to retrieve the last object from the list, so you can use this to control your loop operation. You should always make sure that you have a valid position value when you call member functions of a list object.

You can insert an element in a list at a specific position as long as you have a POSITION value. To insert the object ThePoint in the list PointList immediately before an element at the position aPosition, use the statement:

PointList.InsertBefore(aPosition, ThePoint);

The function InsertBefore()also returns the position of the new object. To insert an element after the object at a given position, the function InsertAfter() is provided. These functions are often used with a list containing geometric elements displayed. Elements are drawn on the screen in the sequence that you traverse the list. Elements that appear later in the list overlay elements positioned earlier, so the

765

Chapter 15

order of elements determines what overlays what. You can therefore determine which of the existing elements a new element overlays by entering it at an appropriate position in the list.

When you need to set an existing object in a list to a particular value, you can use the function SetAt(), as long as you know the position value for the object:

PointList.SetAt(aPosition, aPoint);

There is no return value for this function. You must ensure that the POSITION value you pass to the function is valid. An invalid value causes an error. You should, therefore, only pass a POSITION value to this function that was returned by one of the other member functions, and you must have verified that it isn’t NULL.

Iterating through a List

If you want to get the POSITION value for the beginning or the end of the list, the class provides the member functions GetHeadPosition() and GetTailPosition(). Starting with the POSITION value for the head of the list, you can iterate through the complete list by calling GetNext() until the position value is NULL. You can see the typical code to do this using the list of CPoint objects that you declared earlier:

CPoint CurrentPoint(0,0);

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

while(aPosition)

// Loop while aPosition is not NULL

{

 

CurrentPoint = PointList.GetNext(aPosition); // Process the current object...

}

You can work through the list backwards by using another member function, GetPrev(), which retrieves the current object and then decrements the position indicator. Of course, in this case, you would start out by calling GetTailPosition().

After you know a position value for an object in a list, you can retrieve the object with the member function GetAt(). You specify the position value as an argument and the object is returned. An invalid position value causes an error.

Searching a List

You can find the position of an element that’s stored in a list by using the member function Find():

POSITION aPosition = PointList.Find(ThePoint);

This searches for the object specified as an argument by calling a global template function CompareElements() to compare the objects in the list with the argument. This is the helper function I referred to earlier that aids the search process. The default implementation of this function compares the address of the argument with the address of each object in the list. This implies that if the search is to be successful, the argument must actually be an element in the list — not a copy. If the object is found in the

766

Creating the Document and Improving the View

list, the position of the element is returned. If it isn’t found, NULL is returned. You can specify a second argument to define a position value where the search should begin.

If you want to search a list for an object that is equal to another object, you must implement your own version of CompareElements() that performs a proper comparison. The function template is of the form:

template<class TYPE, class ARG_TYPE> BOOL CompareElements(

const TYPE* pElement1, const ARG_TYPE* pElement2);

where pElement1 and pElement2 are pointers to the objects to be compared. For the PointList collection class object, the prototype of the function generated by the template would be:

BOOL CompareElements(CPoint* pPoint1, CPoint* pPoint2);

To compare the CPoint objects, you could implement this as:

BOOL CompareElements(CPoint* pPoint1, CPoint* pPoint2)

{ return *pPoint1 == *pPoint2; }

This uses the operator==() function implemented in the CPoint class. In general you would need to implement the operator==() function for your own class in this context. You could then use it to implement the helper function CompareElements().

You can also obtain the position of an element in a list by using an index value. The index works in the same way as for an array, with the first element being at index 0, the second at index 1, and so on. The function FindIndex() takes an index value of type int as an argument and returns a value of type POSITION for the object at the index position in the list. If you want to use an index value, you are likely to need to know how many objects there are in a list. The GetCount() function returns this for you:

int ObjectCount = PointList.GetCount();

Here, the integer count of the number of elements in the list is stored in the variable ObjectCount.

Deleting Objects from a List

You can delete the first element in a list using the member function RemoveHead(). This function will return the object that is the new head of the list. To remove the last object, you can use the function RemoveTail(). Both of these functions require that there should be at least one object in the list, so you should use the function IsEmpty() first, to verify that the list is not empty. For example:

if(!PointList.IsEmpty())

PointList.RemoveHead();

The function IsEmpty() returns TRUE if the list is empty, and FALSE otherwise.

If you know the position value for an object that you want to delete from the list, you can do this directly:

PointList.RemoveAt(aPosition);

767

Chapter 15

There’s no return value from this function. It’s your responsibility to ensure that the position value you pass as an argument is valid. If you want to delete the entire contents of a list, you use the member function RemoveAll():

PointList.RemoveAll();

This function also frees the memory that was allocated for the elements in the list.

Helper Functions for a List

You have already seen how the CompareElements() helper function is used by the Find() function for a list. Both the ConstructElements() and DestructElements() global helper functions are also used by members of a CList template class. These are template functions that are declared using the object type you specify in your CList class declaration. The template prototypes for these functions are:

template< class TYPE > void ConstructElements(TYPE* pElements, int nCount);

template< class TYPE > void DestructElements(TYPE* pElements, int nCount);

To obtain the function that’s specific to your list collection, just plug in the type for the objects you are storing. For example, the prototypes for the PointList class for these are:

void ConstructElements(CPoint* pPoint, int PointCount);

void DestructElements(CPoint* pPoint, int PointCount);

Note that the parameters here are pointers. I mentioned earlier that arguments to the PointList member functions would be references, but this doesn’t apply to the helper functions. The parameters to both functions are the same: the first is a pointer to an array of CPoint objects, and the second is a count of the number of objects in the array.

The ConstructElements() function is called whenever you enter an object in the list, and the DestructElements() function is called when you remove an object. As for the CArray template class, you need to implement your versions of these functions if the default operation is not suitable for your object class.

The CMap Template Class

Because of the way they work, maps are particularly suited to applications where your objects obviously have a unique key associated with them, such as a customer class where each customer has an associated customer number or a name and address class where the name might be used as a key. The organization of a map is shown in Figure 15-4.

A map stores an object and key combination. The key is used to determine where in the block of memory allocated to the map the object is to be stored. The key, therefore, provides a means of going directly to an object stored, as long as the key is unique. The process of converting a key to an integer that can be used to calculate the address of an entry in a map is called hashing.

The hashing process applied to a key produces an integer called a hash value. This hash value is typically used as an offset to a base address to determine where to store the key and its associated object in the map. If the memory allocated to the map is at address Base, and each entry requires Length bytes, the entry producing the hash value HashValue is stored at Base+HashValue*Length.

768