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

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

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

Creating the Document and Improving the View

This maintains the mapping mode as MM_TEXT and defines the total extent that you can draw on as 20000 pixels in each direction.

This is enough to get the scrolling mechanism working after a fashion. Build the program and execute it with these additions, and you’ll be able to draw a few elements and then scroll the view. However, although the window scrolls OK, if you try to draw more elements with the view scrolled, things don’t

work as they should. The elements appear in a different position from where you draw them and they’re not displayed properly. What’s going on?

Logical Coordinates and Client Coordinates

The problem is the coordinate systems that you’re using — and that plural is deliberate. You’ve actually been using two coordinate systems in all the examples up to now, although you may not have noticed. As you saw in the previous chapter, when you call a function such as LineTo(), it assumes that the arguments passed are logical coordinates. The function is a member of the CDC class that defines a device context, and the device context has its own system of logical coordinates. The mapping mode, which is a property of the device context, determines what the unit of measurement is for the coordinates when you draw something.

The coordinate data that you receive along with the mouse messages, on the other hand, has nothing to do with the device context or the CDC object — and outside of a device context, logical coordinates don’t apply. The points passed to the OnLButtonDown() and OnMouseMove() handlers have coordinates that are always in device units, that is, pixels, and are measured relative to the upper-left corner of the client area. These are referred to as client coordinates. Similarly, when you call InvalidateRect(), the rectangle is assumed to be defined in terms of client coordinates.

In MM_TEXT mode, the client coordinates and the logical coordinates in the device context are both in units of pixels, and so they’re the same(as long as you don’t scroll the window. In all the previous examples there was no scrolling, so everything worked without any problems. With the latest version of Sketcher, it all works fine until you scroll the view, whereupon the logical coordinates origin (the 0,0 point) is moved by the scrolling mechanism, so it’s no longer in the same place as the client coordinates origin. The units for logical coordinates and client coordinates are the same here, but the origins for the two coordinates systems are different. This situation is illustrated in Figure 15-11.

The left side shows the position in the client area where you draw, and the points that are the mouse positions defining the line. These are recorded in client coordinates. The right side shows where the line is actually drawn. Drawing is in logical coordinates, but you have been using client coordinate values. In the case of the scrolled window, the line appears displaced due to the logical origin being relocated.

This means that you are actually using the wrong values to define elements in the Sketcher program, and when you invalidate areas of the client area to get them redrawn, the rectangles passed to the function are also wrong — hence, the weird behavior of the program. With other mapping modes it gets worse, because not only are the units of measurement in the two coordinate systems different, but also the y axes may be in opposite directions!

789

Chapter 15

 

 

Drawing in an Unscrolled Window

Logical

 

 

0,0

 

 

Left Button

Left Button

 

down

up

 

Line is drawn here

Line appears here

in client coordinates

in logical coordinates

 

 

Drawing in a Scrolled Window

Logical

 

 

0,0

 

View is scrolled so

 

 

origin is now here

Left Button down

Left Button up

 

Line is drawn here

Line appears here

in client coordinates

in logical coordinates

Figure 15-11

 

 

Dealing with Client Coordinates

Consider what needs to be done to fix the problem. There are two things you may have to address:

You need to convert the client coordinates that you got with mouse messages to logical coordinates before you can use them to create elements.

You need to convert a bounding rectangle that you created in logical coordinates back to client coordinates if you want to use it in a call to InvalidateRect().

This amounts to making sure you always use logical coordinates when using device context functions, and always use client coordinates for other communications about the window. The functions you have to apply to do the conversions are associated with a device context, so you need to obtain a device context whenever you want to convert from logical to client coordinates, or vice versa. You can use the coordinate conversion functions of the CDC class inherited by CClientDC to do the work.

790

Creating the Document and Improving the View

The new version of the OnLButtonDown() handler incorporating this is:

// Handler for left mouse button down message

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

{

CClientDC aDC(this);

// Create a device context

OnPrepareDC(&aDC);

// Get origin adjusted

aDC.DPtoLP(&point);

// convert point to Logical

m_FirstPoint = point;

// Record the cursor position

SetCapture();

// Capture subsequent mouse messages

}

You obtain a device context for the current view by creating a CClientDC object and passing the pointer this to the constructor. The advantage of CClientDC is that it automatically releases the device context when the object goes out of scope. It’s important that device contexts are not retained, as there are a limited number available from Windows and you could run out of them. If you use CClientDC, you’re always safe.

As you’re using CScrollView, the OnPrepareDC() member function inherited from that class must be called to set the origin for the logical coordinate system in the device context to correspond with the scrolled position. After you have set the origin by this call, you use the function DPtoLP(), which converts from Device Points to Logical Points, to convert the point value that’s passed to the handler to logical coordinates. You then store the converted point, ready for creating an element in the

OnMouseMove() handler.

The new code for the OnMouseMove() handler is as follows:

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

{

CClientDC aDC(this);

// Device context for the current view

OnPrepareDC(&aDC);

// Get origin adjusted

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

{

 

aDC.DPtoLP(&point);

// convert point to Logical

m_SecondPoint = point;

// Save the current cursor position

// Rest of the function as before...

}

The code for the conversion of the point value passed to the handler is essentially the same as in the previous handler, and that’s all you need here for the moment. The last function that you must change is easy to overlook: the OnUpdate() function in the view class. This needs to be modified to:

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)

{

CClientDC aDC(this);

// Create a device context

791

// Get the area redrawn
// Invalidate the client area

Chapter 15

OnPrepareDC(&aDC);

// Get origin adjusted

// Get the enclosing rectangle and convert to client coordinates CRect aRect=((CElement*)pHint)->GetBoundRect(); aDC.LPtoDP(aRect);

InvalidateRect(aRect);

}

else InvalidateRect(0);

}

The modification here creates a CClientDC object and uses the LPtoDP() function member to convert the rectangle for the area that’s to be redrawn to client coordinates.

If you now compile and execute Sketcher with the modifications I have discussed and are lucky enough not to have introduced any typos, it will work correctly, regardless of the scroller position.

Using MM_LOENGLISH Mapping Mode

Now look into what you need to do to use the MM_LOENGLISH mapping mode. This provides drawings in logical units of 0.01 inches, and also ensures that the drawing size is consistent on displays at different resolutions. This makes the application much more satisfactory from the users’ point of view.

You can set the mapping mode in the call to SetScrollSizes() made from the OnInitialUpdate() function in the view class. You also need to specify the total drawing area, so, if you define it as 3000 by 3000, this provides a drawing area of 30 inches by 30 inches, which should be adequate. The default scroll distances for a line and a page is satisfactory, so you don’t need to specify those. You can use Class View to get to the OnInitialUpdate() function and then change it to the following:

void CSketcherView::OnInitialUpdate(void)

{

CScrollView::OnInitialUpdate();

//Define document size as 30x30ins in MM_LOENGLISH CSize DocSize(3000,3000);

//Set mapping mode and document size. SetScrollSizes(MM_LOENGLISH, DocSize);

}

You just alter the arguments in the call to SetScrollSizes() for the mapping mode and document the size that you want. That’s all that’s necessary to enable the view to work in MM_LOENGLISH, but you still need to fix how you deal with rectangles.

Note that you are not limited to setting the mapping mode once and for all. You can change the mapping mode in a device context at any time and draw different parts of the image to be displayed using different mapping modes. A function SetMapMode() is used to do this, but I won’t be going into this any further here. You can get your application working just using MM_LOENGLISH. Whenever you create a CClientDC object for the view and call OnPrepareDC(), the device context that it owns has the mapping mode you’ve set in the OnInitialUpdate() function.

792

// Get the area redrawn

Creating the Document and Improving the View

The problem you have with rectangles is that the element classes all assume the mapping mode is MM_TEXT, and in MM_LOENGLISH the rectangles are upside-down because of the reversal of the y axis. When you apply LPtoDP() to a rectangle, it is assumed to be oriented properly with respect to the MM_LOENGLISH axes. Because yours are not, the function mirrors the rectangles in the x axis. This creates a problem when you call InvalidateRect() to invalidate an area of a view because the mirrored rectangle in device coordinates is not recognized by Windows as being inside the visible client area.

You have two options for dealing with this. You can modify the element classes so that the enclosing rectangles are the right way up for MM_LOENGLISH, or you can re-normalize the rectangle that you intend to pass to the InvalidateRect() function. The latter is the easiest course because you only need to modify one member of the view class, OnUpdate():

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)

{

CClientDC aDC(this);

//

Create a device context

OnPrepareDC(&aDC);

//

Get origin adjusted

// Get the enclosing rectangle and convert to client coordinates CRect aRect=((CElement*)pHint)->GetBoundRect(); aDC.LPtoDP(aRect);

aRect.NormalizeRect();

InvalidateRect(aRect);

}

else InvalidateRect(0);

}

That should do it for the program as it stands. If you rebuild Sketcher, you should have scrolling working, with support for multiple views. You’ll need to remember to re-normalize any rectangle that you convert to device coordinates for use with InvalidateRect() in the future. Any reverse conversions are also affected.

Deleting and Moving Shapes

Being able to delete shapes is a fundamental requirement in a drawing program. One question relating to this is how you’re going to select the element you want to delete. Of course, after you decide how to select an element, this applies equally well if you want to move an element, so you can treat moving and deleting elements as related problems. But first consider how you’re going to bring move and delete operations into the program.

A neat way of providing move and delete functions would be to have a pop-up context menu appear at the cursor position when you click the right mouse button. You could then put Move and Delete as items on the menu. A pop-up that works like this is a very handy facility that you can use in lots of different situations.

793

Chapter 15

How should the pop-up be used? The standard way that context menus work is that the user moves the mouse over a particular object and right-clicks on it. This selects the object and pops up a menu containing a list of items which relate to actions that can be performed on that object. This means that different objects can have different menus. You can see this in action in Developer Studio itself. When you rightclick on a class icon in Class View, you get a menu that’s different from the one you get if you right-click on the icon for a member function. The menu that appears is sensitive to the context of the cursor, hence the term “context menu.” You have two contexts to consider in Sketcher. You could right-click with the cursor over an element, and you could right-click when there is no element under the cursor.

So, how can you implement this functionality in the Sketcher application? You can do it simply by creating two menus: one for when you have an element under the cursor, and one for when you don’t. You can check if there’s an element under the cursor when the user presses the right mouse button. If there is an element under the cursor, you can highlight the element so that the user knows exactly which element the context pop-up is referring to.

Take a look at how you can create a pop-up at the cursor and, after that works, come back to how to implement the detail of the move and delete operations.

Implementing a Context Menu

The first step is to create a menu containing two pop-ups: one containing Move and Delete as items, the other a combination of items from the Element and Color menus. So, change to Resource View and expand the list of resources. Right-click on the Menu folder to bring up a context menu — another demonstration of what you are now trying to create in the Sketcher application. Select Insert Menu to create a new menu. This has a default ID IDR_MENU1 assigned, but you can change this. Select the name of the new menu in the Resource View and display the Properties window for the resource by pressing Alt+Enter (This is a shortcut to the View > Other Windows > Properties Window menu item). You can then edit the resource ID in the Properties window by clicking the value for the ID. You could change it to something more suitable, such as IDR_CURSOR_MENU, in the right column. Note that the name for a menu resource must start with IDR. Pressing the Enter key saves the new name.

You can now create two new items on the menu bar in the Editor pane. These can have any old captions because they won’t actually be seen by the user. They represents the two context menus that you provide with Sketcher, so you can name them element and no element, according to the situation in which the context menu will be used. Now you can add the Move and Delete items to the element pop-up. The default IDs of ID_ELEMENT_MOVE and ID_ELEMENT_DELETE will do fine, but you could change them if you wanted to in the Properties window for each item. Figure 15-12 shows how the new element menu looks.

Figure 15-12

794

Creating the Document and Improving the View

The second menu contains the list of available element types and colors, identical to the items on the Element and Color menus on the main menu bar, but here separated by a Separator. The IDs you use for these items must be the same as you applied to the IDR_SketcherTYPE menu. This is because the handler for a menu is associated with the menu ID. Menu items with the same ID use the same handlers, so the same handler is used for the Line menu item regardless of whether it’s invoked from the main menu pop-up or from the context menu.

You have a shortcut that saves you having to create all these menu items one-by-one. If you display the IDR_SketcherTYPE menu and extend the Element menu, you can select all the menu items by clicking the first item and then by clicking the last item while holding down the Shift key. You can then rightclick the selection and select Copy from the pop-up or simply press Ctrl+C. If you then return to the IDR_CURSOR_MENU and right-click the first item on the no-element menu, you can insert the complete contents of the Element menu by selecting Paste from the pop-up or by pressing Ctrl+V. The copied menu items will have the same IDs as the originals. To insert the separator, just right-click the empty menu item and select Insert Separator from the pop-up. Repeat the process for the Color menu items and you’re done(almost. Putting the items from the Element and Color menus together has created a conflict(both Rectangle and Red share the same shortcut. Changing &Red to Re&d here will fix it, and it’s a good idea to change it on in the IDRSketcherTYPE menu too for consistency. You do this by editing the Caption property for the menu item. The completed menu should look as shown in Figure 15-13.

Figure 15-13

Close the properties box and save the resource file. At the moment, all you have is the definition of the menu in a resource file. It isn’t connected to the code in the Sketcher program. You now need to associate this menu and its ID, IDR_CURSOR_MENU, with the view class. You also must create command handlers for the menu items in the pop-up corresponding to the IDs ID_MOVE and ID_DELETE.

Associating a Menu with a Class

To associate the context menu with the view class in Sketcher, go to the Class View pane and display the Properties window for CSketcherView by right-clicking the class name and selecting Properties from the pop-up. If you click the Messages button in the Properties window, you’ll be able to add a handler

795

Chapter 15

for the WM_CONTEXTMENU message by selecting <Add>OnContextMenu from the adjacent cell in the right column. You can then add the following code to the handler:

void CSketcherView::OnContextMenu(CWnd* pWnd, CPoint point)

{

CMenu menu; menu.LoadMenu(IDR_CURSOR_MENU); CMenu* pPopup = menu.GetSubMenu(0); ASSERT(pPopup != NULL);

//Load the context menu

//Get the first menu

//Ensure it’s there

// Display the popup menu

pPopup->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, this);

}

For now, this handler arbitrarily displays the first of the two context menus. You still need to figure out how you’ll determine whether or not the cursor is over an element to decide which menu to display, but we’ll come back to that a little later. Calling the LoadMenu() method for the menu object loads the menu resource corresponding to the ID supplied as the argument and attaches it to the CMenu object menu. The GetSubMenu() function returns a pointer to the pop-up menu corresponding to the integer argument that specifies the position of the pop-up, with 0 being the first pop-up, 1 being the second, and so on. After you ensure the pointer returned by GetSubMenu() is not NULL, you display the pop-up by calling TrackPopupMenu().

The first argument to the TrackPopupMenu() function consists of two flags ORed together. One flag specifies how the pop-up menu should be positioned and can be any of the following values:

Flag

Description

 

 

TPM_CENTERALIGN

Centers the pop-up horizontally relative to the x coordinate supplied

 

as the second argument to the function.

TPM_LEFTALIGN

Positions the pop-up so that the left side of the menu is aligned with

 

the x coordinate supplied as the second argument to the function.

TPM_RIGHTALIGN

Positions the pop-up so that the right side of the menu is aligned with

 

the x coordinate supplied as the second argument to the function.

 

 

The second flag specifies the mouse button and can be either of the following:

Flag

Description

TPM_LEFTMOUSEBUTTON Specifies that the pop-up tracks the left mouse button.

TPM_RIGHTMOUSEBUTTON Specifies that the pop-up tracks the right mouse button.

The next two arguments to the TrackPopupMenu() function specify the x and y coordinates of the popup menu on the screen respectively. The y coordinate determines to position of the top of the menu. The fourth argument specifies the window that owns the menu and that should receive all WM_COMMAND messages from the menu.

796

Creating the Document and Improving the View

Now you can add the handlers for the items in the first pop-up menu. Return to the Resource View and double-click on IDR_CURSOR_MENU. Right-click the Move menu item and then select Add Event Handler from the pop-up. You can then specify the handler in the dialog for the Event Handler wizard, as shown in Figure 15-14.

Figure 15-14

It’s a COMMAND handler and is to be created in the CSketcherView class. Click the Add and Edit button to create the handler function. You can follow the same procedure to create the hander for the Delete menu item.

You don’t have to do anything for the second context menu, as you already have handlers written for them in the document class. These take care of the messages from the pop-up items automatically.

Choosing a Context Menu

At the moment, the OnContextMenu() handler only displays the first context pop-up, no matter where the right button is clicked in the view. This isn’t really what you want it to do. The first context menu applies specifically to an element, whereas the second context menu applies in general. You want to display the first menu if there is an element under the cursor and to display the second menu if there isn’t.

You need two things to fix this up: You need a mechanism to find out which (if any) element is at the current cursor position, and you need to save the address of this element somewhere so you can use it in the OnContextMenu() handler. You can deal with saving the address of the element first because this is the easier bit.

797

Chapter 15

When you find out which element is under the cursor, you’ll store its address in a data member, m_pSelected, of the view class. This is available to the right mouse button handler because that’s in the same class. You can add the declaration for this variable to the protected section of the

CSketcherView class:

class CSketcherView: public CScrollView

{

// Rest of the class as before...

protected:

 

CPoint m_FirstPoint;

// First point recorded for an element

CPoint m_SecondPoint;

// Second point recorded for an element

CElement* m_pTempElement;

// Pointer to temporary element

CElement* m_pSelected;

// Currently selected element

// Rest of the class as before...

};

You can add this manually or alternatively, you can right-click the class name and select Add > AddVariable from the pop-up to open the dialog for adding a data member. If you add m_pSelected manually you’ll also need to initialize this element in the class constructor, so add the following code:

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

,m_SecondPoint(CPoint(0,0))

,m_pTempElement(NULL)

,m_pSelected(NULL)

{

// TODO: add construction code here

}

You’ll figure out how to decide when an element is under the cursor in a moment, but in the meantime you can use the m_pSelected member of the view in the implementation of the OnContextMenu() handler:

void CSketcherView::OnContextMenu(CWnd* pWnd, CPoint point)

{

CMenu menu; menu.LoadMenu(IDR_CURSOR_MENU);

CMenu* pPopup = menu.GetSubMenu(m_pSelected == 0 ? 1 : 0); ASSERT(pPopup != NULL);

pPopup->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, this);

}

The expression m_pSelected == 0 ? 1 : 0 results in 1 when the pointer is null and 0 otherwise; so you’ll select the first pop-up menu containing Move and Delete when m_pSelected is not null and the second pop-up when it is.

Identifying a Selected Element

To keep track of which element is under the cursor you can add code to the OnMouseMove() handler in the CSketcherView class. This handler is called every time the mouse cursor moves, so all you have to do is add code to test whether there’s an element under the current cursor position and set

798