Chapter 17. Creating the Document and Improving the View

WHAT YOU WILL LEARN IN THIS CHAPTER

  • How to use an STL list container and an STL/CLR list container to store sketch data

  • How to implement drawing a sketch in the MFC and CLR versions of Sketcher

  • How to implement scrolling in a view in MFC Sketcher

  • 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

In this chapter, you'll extend the MFC version of Sketcher to make the document view more flexible, introducing several new techniques in the process. You'll also extend the MFC and CLR versions of Sketcher to store elements in an object that encapsulates a complete sketch.

CREATING THE SKETCH DOCUMENT

The document in the Sketcher application needs to be able to store a sketch consisting of an arbitrary collection of lines, rectangles, circles, and curves in any sequence, and an excellent vehicle for handling this is a list. Using a list will also enable you to change an element's position in the list or to delete an element easily if it becomes necessary to do so. Because all the element classes that you've defined include the capability for the objects to draw themselves, the user will be able to a draw a sketch easily by stepping through the list.

Using a list<T> Container for the Sketch

You can define an STL list<CElement*> container that stores pointers to instances of the shape classes that make up a sketch. You just need to add the list declaration as a new member in the CSketcherDoc class definition. You also need a member function to add an element to the list, and AddElement() is a good, if unoriginal, name for this:

// SketcherDoc.h : interface of the CSketcherDoc class
//

#pragma once
#include <list>
#include "Elements.h"
#include "SketcherConstants.h"

class CSketcherDoc: public CDocument
{
protected: // create from serialization only
   CSketcherDoc();
   DECLARE_DYNCREATE(CSketcherDoc)

// Attributes
public:

// Operations
public:
   unsigned int GetElementType() const       // Get the element type
      { return m_Element; }
   COLORREF GetElementColor() const          // Get the element color
      { return m_Color; }
   void AddElement(CElement* pElement)       // Add an element to the list
      { m_ElementList.push_back(pElement); }
// Rest of the class as before...

protected:
   ElementType m_Element;               // Current element type
   COLORREF m_Color;                    // Current drawing color
   std::list<CElement*> m_ElementList;  // List of elements in the sketch

// Rest of the class as before...

};

                                                                 
Using a list<T> Container for the Sketch

The CSketcherDoc class now refers to the CElement class, so there is an #include directive for Elements.h in SketcherDoc.h. There is also one for the list header because you are using the STL list<> template.

You create shape objects on the heap, so you can just pass a pointer to the AddElement() function. Because all this function does is add an element, you have put the implementation of AddElement() in the class definition. Adding an element to the list requires only one statement, which calls the push_back() member function. That's all you need to create the document, but you still have to consider what happens when a document is closed. You must ensure that the list of pointers and all the elements they point to are destroyed properly. To do this, you need to add code to the destructor for CSketcherDoc objects.

Implementing the Document Destructor

In the destructor, you'll first go through the list deleting the element pointed to by each entry. After that is complete, you must delete the pointers from the list. The code to do this is as follows:

CSketcherDoc::~CSketcherDoc(void)
{
  // Delete the element pointed to by each list entry
  for(auto iter = m_ElementList.begin() ; iter != m_ElementList.end() ; ++iter)
    delete *iter;

   m_ElementList.clear();   // Finally delete all pointers
}

You should add this code to the definition of the destructor in SketcherDoc.cpp. You can go directly to the code for the destructor through the Class View.

The for loop iterates over all the elements in the list. The auto keyword ensures the correct type is specified for the iterator iter. When the for loop ends, you call the clear() function for the list to remove all the pointers from the list.

Drawing the Document

Because the document owns the list of elements and the list is protected, you can't use it directly from the view. The OnDraw() member of the view does need to be able to call the Draw() member for each of the elements in the list, though, so you need to consider how best to arrange this. One possibility is to make some const iterators for the m_ElementList member of the document class available. This will allow the view class object to call the Draw() function for each element, but will prevent the list from being changed. You can add two more functions to the CSketcherDoc class definition to provide for this:

class CSketcherDoc: public CDocument
{

// Rest of the class as before...

// Operations
public:
   unsigned int GetElementType() const // Get the element type
      { return m_Element; }
   COLORREF GetElementColor() const    // Get the element color
      { return m_Color; }
   void AddElement(CElement* pElement) // Add an element to the list
      { m_ElementList.AddTail(pElement); }
   std::list<CElement*>::const_iterator begin() const // Get list begin iterator
                   { return m_ElementList.begin(); }
   std::list<CElement*>::const_iterator end() const   // Get list end iterator
                   { return m_ElementList.end(); }

// Rest of the class as before...

};

The begin() function returns an iterator that points to the first element in the list, and the end() function returns an iterator that points to one past the last element in the list. The iterator type is defined in the list<> class template as const_iterator, but because the iterator type is specific to a particular instance of the template, you must qualify the type name with the list instance type.

By using the two functions you have added to the document class, the OnDraw() function for the view will be able to iterate through the list, calling the Draw() function for each element. This implementation of OnDraw()is as follows:

void CSketcherView::OnDraw(CDC* pDC)
{
   CSketcherDoc* pDoc = GetDocument();
   ASSERT_VALID(pDoc);
   if(!pDoc)
     return;

  CElement* pElement(nullptr);
  for(auto iter = pDoc->begin() ; iter != pDoc->end() ; ++iter)
  {
    pElement = *iter;
    if(pDC->RectVisible(pElement->GetBoundRect())) // If the element is visible
         pElement->Draw(pDC);                      // ...draw it
  }
}
                                                               
Drawing the Document

Frequently, when a WM_PAINT message is sent to your program, only part of the window needs to be redrawn. When Windows sends the WM_PAINT message to a window, it also defines an area in the client area of the window, and only this area needs to be redrawn. The CDC class provides a member function, RectVisible(), which determines whether a rectangle you supply to it as an argument overlaps the area that Windows requires to be redrawn. Here, you use this function to make sure that you draw only the elements that are in the area Windows wants redrawn, thus improving the performance of the application.

You use the begin() and end() members of the document object in the for loop to iterate over all the elements in the list. The auto keyword determines the type of iter appropriately. Within the loop, you dereference iter and store it in the pElement variable. You retrieve the bounding rectangle for each element using the GetBoundRect() member of the object, and pass it to the RectVisible() function in the if statement. If the value returned by RectVisible() is true, you call the Draw() function for the current element pointed to by pElement. As a result, only elements that overlap the area that Windows has identified as invalid are drawn. Drawing on the screen is a relatively expensive operation in terms of time, so checking for just the elements that need to be redrawn, rather than drawing everything each time, improves performance considerably.

Adding an Element to the Document

The last thing you need to do to have a working document in our program is to add the code to the OnLButtonUp() handler in the CSketcherView class to add the temporary element to the document:

void CSketcherView::OnLButtonUp(UINT nFlags, CPoint point)
{
   if(this == GetCapture())
      ReleaseCapture();      // Stop capturing mouse messages

   // If there is an element, add it to the document
   if(m_pTempElement)
   {
      GetDocument()->AddElement(m_pTempElement);
      InvalidateRect(nullptr);  // Redraw the current window
      m_pTempElement = nullptr; // Reset the element pointer
   }
}

Of course, you must check that there really is an element before you add it to the document. The user might just have clicked the left mouse button without moving the mouse. After adding the element to the list in the document, you call InvalidateRect() to get the client area for the current view redrawn. The argument of 0 invalidates the whole of the client area in the view. Because of the way the rubber-banding process works, some elements may not be displayed properly if you don't do this. If you draw a horizontal line, for instance, and then rubber-band a rectangle with the same color so that its top or bottom edge overlaps the line, the overlapped bit of line disappears. This is because the edge being drawn is XORed with the line underneath, so you get the background color back. You also reset the pointer m_pTempElement to avoid confusion when another element is created. Note that the statement deleting m_pTempElement has been removed.

Exercising the Document

After saving all the modified files, you can build the latest version of Sketcher and execute it. You'll now be able to produce art such as "The Happy Programmer," shown in Figure 17-1.

FIGURE 17-1

Figure 17.1. FIGURE 17-1

The program is now working more realistically. It stores a pointer to each element in the document object, so they're all automatically redrawn as necessary. The program also does a proper cleanup of the document data when it's deleted.

There are still some limitations in the program that you can address. For instance:

  • You can open another view window by using the Window

    FIGURE 17-1
  • You can draw only in the client area you can see. It would be nice to be able to scroll the view and draw over a bigger area.

  • You can't delete an element, so if you make a mistake, you either live with it or start over with a new document.

These are all quite serious deficiencies that, together, make the program fairly useless as it stands. You'll overcome all of them before the end of this chapter.

IMPROVING THE VIEW

The first item that you can try to fix is the updating of all the document windows that are displayed when an element is drawn. The problem arises because only the view in which an element is drawn knows about the new element. Each view is acting independently of the others, and there is no communication between them. You need to arrange for any view that adds an element to the document to let all the other views know about it, and they need to take the appropriate action.

Updating Multiple Views

The document class conveniently contains a function, UpdateAllViews(), to help with this particular problem. This function essentially provides a means for the document to send a message to all its views. You just need to call it from the OnLButtonUp() function in the CSketcherView class whenever you have added a new element to the document:

void CSketcherView::OnLButtonUp(UINT nFlags, CPoint point)
{
   if(this == GetCapture())
      ReleaseCapture();                // Stop capturing mouse messages

   // If there is an element, add it to the document
   if(m_pTempElement)
   {
      GetDocument()->AddElement(m_pTempElement);
      GetDocument()->UpdateAllViews(nullptr, 0, m_pTempElement); // Tell the views
      m_pTempElement = nullptr;        // Reset the element pointer
   }
}

When the m_pTempElement pointer is not nullptr, the specific action of the function has been extended to call the UpdateAllViews() member of your document class. This function communicates with the views by causing the OnUpdate() member function in each view to be called. The three arguments to UpdateAllViews() are described in Figure 17-2.

FIGURE 17-2

Figure 17.2. FIGURE 17-2

The first argument to the UpdateAllViews() function call is often the this pointer for the current view. This suppresses the call of the OnUpdate() function for the current view. This is a useful feature when the current view is already up to date. In the case of Sketcher, because you are rubber-banding, you want to get the current view redrawn as well, so by specifying the first argument as 0 you get the OnUpdate() function called for all the views, including the current view. This removes the need to call InvalidateRect() as you did before.

You don't use the second argument to UpdateAllViews() here, but you do pass the pointer to the new element through the third argument. By passing a pointer to the new element, you allow the views to figure out which bit of their client area needs to be redrawn.

To catch the information passed to the UpdateAllViews() function, you must add an override for the OnUpdate() member function to the view class. You can do this from the Class View through the Properties window for CSketcherView. As I'm sure you recall, you display the properties for a class by right-clicking the class name and selecting Properties from the pop-up. If you click the Overrides button in the Properties window, you'll be able to find OnUpdate in the list of functions you can override. Click the function name, then the <Add> OnUpdate option that shows in the drop-down list in the adjacent column. You'll be able to edit the code for the OnUpdate() override you have added in the Editor pane. You only need to add the highlighted code below to the function definition:

void CSketcherView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint)
{
   // Invalidate the area corresponding to the element pointed to
   // if there is one, otherwise invalidate the whole client area
   if(pHint)
   {
      InvalidateRect(static_cast<CElement*>(pHint)->GetBoundRect());
   }
   else
   {
      InvalidateRect(nullptr);
   }
}

Note that you must uncomment at least the name of the third parameter in the generated version of the function; otherwise, it won't compile with the additional code here. The three arguments passed to the OnUpdate() function in the view class correspond to the arguments that you passed in the UpdateAllViews() function call. Thus, pHint contains the address of the new element. However, you can't assume that this is always the case. The OnUpdate() function is also called when a view is first created, but with a null pointer for the third argument. Therefore, the function checks that the pHint pointer isn't nullptr and only then gets the bounding rectangle for the element passed as the third argument. It invalidates this area in the client area of the view by passing the rectangle to the InvalidateRect() function. This area is redrawn by the OnDraw() function in this view when the next WM_PAINT message is sent to the view. If the pHint pointer is nullptr, the whole client area is invalidated.

You might be tempted to consider redrawing the new element in the OnUpdate() function. This isn't a good idea. You should do permanent drawing only in response to the Windows WM_PAINT message. This means that the OnDraw() function in the view should be the only thing that's initiating any drawing operations for document data. This ensures that the view is drawn correctly whenever Windows deems it necessary.

If you build and execute Sketcher with the new modifications included, you should find that all the views are updated to reflect the contents of the document.

Scrolling Views

Adding scrolling to a view looks remarkably easy at first sight; the water is, in fact, deeper and murkier than it at first appears, but jump in anyway. The first step is to change the base class for CSketcherView from CView to CScrollView. This new base class has the scrolling functionality built in, so you can alter the definition of the CSketcherView class to this:

class CSketcherView: public CScrollView
{
   // Class definition as before...
};

You must also modify two lines of code at the beginning of the SketcherView.cpp file that refer to the base class for CSketcherView. You need to replace CView with CScrollView as the base class:

IMPLEMENT_DYNCREATE(CSketcherView, CScrollView)

BEGIN_MESSAGE_MAP(CSketcherView, CScrollView)

However, this is still not quite enough. The new version of the view class needs to know some things about the area you are drawing on, such as the size and how far the view is to be scrolled when you use the scroller. This information has to be supplied before the view is first drawn. You can put the code to do this in the OnInitialUpdate() function in the view class.

You supply the information required by calling a function that is inherited from the CScrollView class: SetScrollSizes(). The arguments to this function are explained in Figure 17-3.

FIGURE 17-3

Figure 17.3. FIGURE 17-3

Scrolling a distance of one line occurs when you click on the up or down arrow on the scroll bar; a page scroll occurs when you click the scrollbar itself. You have an opportunity to change the mapping mode here. MM_LOENGLISH would be a good choice for the Sketcher application, but first get scrolling working with the MM_TEXT mapping mode because there are still some difficulties to be uncovered. (Mapping modes were introduced in Chapter 16.)

To add the code to call SetScrollSizes(), you need to override the default version of the OnInitialUpdate() function in the view. You access this in the same way as for the OnUpdate() function override — through the Properties window for the CSketcherView class. After you have added the override, just add the code to the function where indicated by the comment:

void CSketcherView::OnInitialUpdate()
{
   CScrollView::OnInitialUpdate();

   // Define document size
   CSize DocSize(20000,20000);

   // Set mapping mode and document size
   SetScrollSizes(MM_TEXT, DocSize, CSize(500,500), CSize(50,50));
}

This maintains the mapping mode as MM_TEXT and defines the total extent that you can draw on as 20,000 pixels in each direction. It also specifies a page scroll increment as 500 pixels in each direction and a line scroll increment as 50 pixels, because the defaults would be too large.

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 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 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 17-4.

FIGURE 17-4

Figure 17.4. FIGURE 17-4

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 because the logical origin has been 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 the y axes also may be in opposite directions!

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().

These amount 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.

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

// 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 coordinates
   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
   aDC.DPtoLP(&point);              // Convert point to logical coordinates
   if((nFlags&MK_LBUTTON)&&(this==GetCapture()))
   {
      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 the following:

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 = static_cast<CElement*>(pHint)->GetBoundRect();
      aDC.LPtoDP(aRect);
      InvalidateRect(aRect);          // Get the area redrawn
   }
else
  {
    InvalidateRect(nullptr);          // Invalidate the client area
  }
}

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, we will look into what you need to do to use the MM_LOENGLISH mapping mode. The advantage of using this mapping mode is that it provides drawings in logical units of 0.01 inches, which ensures that the drawing size is consistent on displays at different resolutions. This makes the application much more satisfactory from the user's 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: if you define it as 3,000 by 3,000, 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 are satisfactory, so you don't need to specify those. You can use the 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 30×30ins 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 the document size that you want. That's all that's necessary to enable the view to work in MM_LOENGLISH.

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.

That should do it for the program as it stands. If you rebuild Sketcher, you should have scrolling working, with support for multiple views.

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 you are going to select an element, your decision also applies to how you 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 user interface for 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 include Move and Delete as items on the menu. A pop-up that works like this is a very handy tool that you can use in lots of different situations.

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 it. This selects the object and pops up a menu containing a list of items relating 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 right-click a class icon in Class View, you get a menu that's different from the one you get if you right-click 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, or 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 clicks 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.

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

IMPLEMENTING A CONTEXT MENU

You will need two context menu pop-ups: one for when there is an element under the mouse cursor, and another for when there isn't.

The first step is to create a menu containing Move and Delete as menu items that will apply to the element under the cursor. The other pop-up will contain a combination of menu items from the Element and Color menus. So change to Resource View and expand the list of resources. Right-click 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

IMPLEMENTING A CONTEXT MENU

You can now create a new item on the menu bar in the Editor pane. This can have any old caption because it won't actually be seen by the user; the caption element will be OK. 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 can change them if you want in the Properties window for each item. Figure 17-5 shows how the new element menu looks.

FIGURE 17-5

Figure 17.5. FIGURE 17-5

Save the menu and create another with the ID IDR_NOELEMENT_MENU; you can make the caption no element this time. The second menu will contain 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 the ones 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 from 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 clicking the last item while holding down the Shift key. You can then right-click the selection and select Copy from the pop-up or simply press Ctrl+C. If you then return to the IDR_NOELEMENT_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.

Close the properties box and save the resource file. At the moment, all you have is the definition of the context menus in a resource file. It isn't connected to the code in the Sketcher program. You now need to associate these menus and their IDs with the view class. You also must create command handlers for the menu items in the pop-up corresponding to the IDs ID_ELEMENT_MOVE and ID_ELEMENT_DELETE.

Associating a Menu with a Class

Context menus are managed in an MFC program by an object of type CContextManager. The CSketcherApp class object already has a member of type CContextManager defined, and you can use this object to manage your context menus. Go to the CSketcherApp class in the Class View and go to the definition of the PreLoadState() member function. Add the following lines of code:

void CSketcherApp::PreLoadState()
{
/*
  BOOL bNameValid;
  CString strName;
  bNameValid = strName.LoadString(IDS_EDIT_MENU);
  ASSERT(bNameValid);
GetContextMenuManager()->AddMenu(strName, IDR_POPUP_EDIT);
*/

  GetContextMenuManager()->AddMenu(_T("Element menu"), IDR_ELEMENT_MENU);
  GetContextMenuManager()->AddMenu(_T("No element menu"), IDR_NOELEMENT_MENU);
}

                                                                     
Associating a Menu with a Class

Note

I have commented out the original lines of code that were there, to show that they are no longer required. You should remove these lines because they display the Edit menu as a context menu, and you don't want that to happen.

Calling the GetContextManager() function for the application object returns a pointer to the CContextManager object. The AddMenu() function adds the menu specified by the second argument to the CContextManager object. The first argument specifies a name for the new menu. You can specify the name explicitly, as in the code here, or you can specify a resource ID for the string that is the name.

Note how the text strings in the first argument to the AddMenu() function are enclosed within the _T() macro. This is because we are using the Unicode version of the MFC libraries. The _T() macro selects the correct type for the string, depending on whether or not the _UNICODE symbol is defined for the project. You may remember we left the Use Unicode libraries option checked when we first created the Sketcher project. You should always use the _T() macro when defining string literals in a Unicode program. When you are using the MFC Unicode libraries, you should also use the TCHAR type instead of the char type in your code, LPTSTR instead of char*, and LPCTSTR instead of const char*.

A context menu can be displayed in response to a WM_CONTEXTMENU message, but the default code generated by the wizard calls the OnContextMenu() handler from the OnRButtonUp() handler. If you look at the CSketcherView class implementation, there should already be a definition of the OnRButtonUp() handler and the OnContextMenu() handler, and this is where you'll put the code to display the new context menus you have just defined. Note that the default code for the OnRButtonUp() handler calls ClientToScreen() to convert the coordinates of the current cursor position to screen coordinates.

You can then add the following code to the OnContextMenu() handler:

void CSketcherView::OnContextMenu(CWnd* pWnd, CPoint point)
{
#ifndef SHARED_HANDLERS

// theApp.GetContextMenuManager()->ShowPopupMenu(IDR_POPUP_EDIT,
//                                           point.x, point.y, this, TRUE);
  if(m_pSelected)
    theApp.GetContextMenuManager()->ShowPopupMenu(IDR_ELEMENT_MENU,
                                             point.x, point.y, this);
  else
    theApp.GetContextMenuManager()->ShowPopupMenu(IDR_NOELEMENT_MENU,
                                             point.x, point.y, this);
#endif
}

The first line of code was already present, and I have commented it out to indicate that it is not required for Sketcher. You should remove this line because it displays the Edit menu as a context menu, and you don't want that to happen.

The new code chooses between the two context menus you have created based on whether or not the m_pSelected member is null. m_pSelected will store the address of the element under the mouse cursor, or nullptr if there isn't one. You will add this member to the view class in a moment. Calling the ShowPopupMenu() function for the CContextMenuManager object displays the pop-up identified by the first argument at the position in the client area specified by the second and third arguments.

Add m_pSelected as a protected member of the CSketcherView class as type CElement*. You can initialize this member to nullptr in the view class constructor.

Now, you can add the handlers for the items in the element pop-up menu. Return to the Resource View and double-click IDR_ELEMENT_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. It's a COMMAND handler and you create it 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 no element context menu, as you already have handlers written for it in the document class. These automatically take care of the messages from the pop-up items.

Identifying a Selected Element

We will create a very simple mechanism for selecting an element. An element in a sketch will be selected whenever the mouse cursor is within the bounding rectangle for an element. For this to be effective, Sketcher must track at all times, whether or not there is an element under the cursor.

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 determine whether there's an element under the current cursor position and set m_pSelected accordingly. The test for whether a particular element is under the cursor is simple: if the cursor position is within the bounding rectangle for an element, that element is under the cursor. Here's how you can modify the OnMouseMove() handler to determine whether there's an element under the cursor:

void CSketcherView::OnMouseMove(UINT nFlags, CPoint point)
{
  // Define a Device Context object for the view
  CClientDC aDC(this);              // DC is for this view
  OnPrepareDC(&aDC);                // Get origin adjusted
  aDC.DPtoLP(&point);               // Convert point to logical coordinates
  if((nFlags & MK_LBUTTON) && (this == GetCapture()))
  {
    // Code as before...
  }
else
  {  // We are not creating an element, so select an element
    CSketcherDoc* pDoc=GetDocument();         // Get a pointer to the document
    m_pSelected = pDoc->FindElement(point);   // Set selected element
  }
}

The new code is very simple and is executed only when we are not creating a new element. The else clause in the if statement calls the FindElement() function for the document object that you'll add shortly and stores the element address that is returned in m_pSelected. The FindElement() function has to search the document for the first element that has a bounding rectangle that encloses point. You can add FindElement() to the document class as a public member and implement it like this:

// Finds the element under the point
CElement* CSketcherDoc::FindElement(const CPoint& point)const
{
  for(auto rIter = m_ElementList.rbegin() ; rIter != m_ElementList.rend() ; ++rIter)
  {
    if((*rIter)->GetBoundRect().PtInRect(point))
      return *rIter;
  }
  return nullptr;
}

You use a reverse-iterator to search the list from the end, which means the most recently created and, therefore, last-drawn element will be found first. The PtInRect() member of the CRect class returns TRUE if the point you pass as the argument lies within the rectangle, and FALSE otherwise. Here, you use this function to test whether or not the point lies within any bounding rectangle for an element in the sketch. The address of the first element for which this is true is returned. If no element in the sketch is found, the loop will end and nullptr will be returned. The code is now in a state where you can test the context menus.

Exercising the Pop-Ups

You have added all the code you need to make the pop-ups operate, so you can build and execute Sketcher to try it out. When you right-click the mouse, or press Shift+F10, a context menu will be displayed. If there are no elements under the cursor, the second context pop-up appears, enabling you to change the element type and color. These options work because they generate exactly the same messages as the main menu options, and because you have already written handlers for them.

If there is an element under the cursor, the first context menu will appear with Move and Delete on it. It won't do anything at the moment, as you've yet to implement the handlers for the messages it generates. Try right-button clicks outside the view window. Messages for these are not passed to the document view window in your application, so the pop-up is not displayed.

Highlighting Elements

Ideally, the user will want to know which element is under the cursor before right-clicking to get the context menu. When you want to delete an element, you want to know which element you are operating on. Equally, when you want to use the other context menu — to change color, for example — you need to be sure no element is under the cursor when you right-click the mouse. To show precisely which element is under the cursor, you need to highlight it in some way before a right-button click occurs.

You can change the Draw() member function for an element to accommodate this. All you need to do is pass an extra argument to the Draw() function to indicate when the element should be highlighted. If you pass the address of the currently-selected element that you save in the m_pSelected member of the view to the Draw() function, you will be able to compare it to the this pointer to see if it is the current element.

All highlights will work in the same way, so let's take the CLine member as an example. You can add similar code to each of the classes for the other element types. Before you start changing CLine, you must first amend the definition of the base class CElement:

class CElement : public CObject
{
   protected:
     int m_PenWidth;                  // Pen width
     COLORREF m_Color;                // Color of an element
     CRect m_EnclosingRect;           // Rectangle enclosing an element
   public:
     virtual ~CElement();
     // Virtual draw operation
     virtual void Draw(CDC* pDC, CElement* pElement=nullptr) {}

     CRect GetBoundRect() const;     // Get the bounding rectangle for an element

   protected:
     CElement();                     // Here to prevent it being called
};                                                                                                                                   
Highlighting Elements

The change is to add a second parameter to the virtual Draw() function. This is a pointer to an element. The reason for initializing the second parameter to nullptr is that this allows the use of the function with just one argument; the second will be supplied as nullptr by default.

You need to modify the declaration of the Draw() function in each of the classes derived from CElement in exactly the same way. For example, you should change the CLine class definition to the following:

class CLine :
  public CElement
{
   public:
      CLine(void);
      // Function to display a line
      virtual void Draw(CDC* pDC, CElement* pElement=nullptr);

      // Constructor for a line object
      CLine(const CPoint& Start, const 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 implementation for each of the Draw() functions for the classes derived from CElement needs to be extended in the same way. The function for the CLine class is as follows:

void CLine::Draw(CDC* pDC, CElement* pElement)
{
   // Create a pen for this object and
   // initialize it to the object color and line width m_PenWidth
   CPen aPen;
   if(!aPen.CreatePen(PS_SOLID, m_PenWidth,this==pElement ? SELECT_COLOR : 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
}

                                                                
Highlighting Elements

This is a very simple change. The third argument to the CreatePen() function is now a conditional expression. You choose the color for the pen based on whether or not pElement is the same as the current CLine element pointer, this. If it is, you choose SELECT_COLOR for the pen rather than the original color for the line.

When you have updated the Draw() functions for all the subclasses of CElement, you can add the definition for SELECT_COLOR to the SketcherConstants.h file:

// SketcherConstants.h : Definitions of constants
//Definitions of constants

#pragma once

// Element type definitions
enum ElementType{LINE, RECTANGLE, CIRCLE, CURVE};

// Color values for drawing
const COLORREF BLACK = RGB(0,0,0);
const COLORREF RED = RGB(255,0,0);
const COLORREF GREEN = RGB(0,255,0);
const COLORREF BLUE = RGB(0,0,255);
const COLORREF SELECT_COLOR = RGB(255,0,180);
///////////////////////////////////
                                                                
Highlighting Elements

You have nearly implemented the highlighting. The derived classes of the CElement class are now able to draw themselves as selected when required — you just need a mechanism to cause an element to be selected. So where should you create this? You can determine which element, if any, is under the cursor in the OnMouseMove() handler in the CSketcherView class, so that's obviously the place to expedite the highlighting.

The amendments to the OnMouseMove() handler are as follows:

void CSketcherView::OnMouseMove(UINT nFlags, CPoint point)
{
  // Define a Device Context object for the view
  CClientDC aDC(this);                      // DC is for this view
  OnPrepareDC(&aDC);                        // Get origin adjusted
  aDC.DPtoLP(&point);                       // Convert point to logical coordinates
  if((nFlags & MK_LBUTTON) && (this == GetCapture()))
  {
    // Code as before...
  }
  else
   {  // We are not creating an element to do highlighting
    CSketcherDoc* pDoc=GetDocument();         // Get a pointer to the document
    CElement* pOldSelected(m_pSelected);
    m_pSelected = pDoc->FindElement(point);   // Set selected element
    if(m_pSelected != pOldSelected)
    {
      if(m_pSelected)
        InvalidateRect(m_pSelected->GetBoundRect(), FALSE);
      if(pOldSelected)
        InvalidateRect(pOldSelected->GetBoundRect(), FALSE);
      pDoc->UpdateAllViews(nullptr);
    }
  }
}

You must keep track of any previously highlighted element because, if there's a new one, you must un-highlight the old one. To do this, you save the value of m_pSelected in pOldSelected before you check for a selected element. You then store the address returned by the FindElement() function for the document object in m_pSelected.

If pOldSelected and m_pSelected are equal, then either they both contain the address of the same element or they are both zero. If they both contain a valid address, the element will already have been highlighted last time around. If they are both zero, nothing is highlighted and nothing needs to be done. Thus, in either case, you do nothing.

Thus, you want to do something only when m_pSelected is not equal to pOldSelected. If m_pSelected is not nullptr, you need to get it redrawn, so you call InvalidateRect() with the first argument as the bounding rectangle for the element, and the second argument as FALSE so as not to erase the background. If pOldSelected is also not null, you must un-highlight the old element by invalidating its bounding rectangle in the same way. Finally, you call UpdateAllViews() for the document object to get the views updated.

Drawing Highlighted Elements

You still need to arrange that the highlighted element is actually drawn highlighted. Somewhere, the m_pSelected pointer must be passed to the draw function for each element. The only place to do this is in the OnDraw() function in the view:

void CSketcherView::OnDraw(CDC* pDC)
{
  CSketcherDoc* pDoc = GetDocument();
  ASSERT_VALID(pDoc);
  if (!pDoc)
    return;

  CElement* pElement(nullptr);
  for(auto iter = pDoc->begin() ; iter != pDoc->end() ; ++iter)
  {
    pElement = *iter;
    if(pDC->RectVisible(pElement->GetBoundRect())) // If the element is visible
         pElement->Draw(pDC, m_pSelected);         // ...draw it
  }
}

You need to change only one line. The Draw() function for an element has the second argument added to communicate the address of the element to be highlighted.

Exercising the Highlights

This is all that's required for the highlighting to work all the time. It wasn't trivial but, on the other hand, it wasn't terribly difficult. You can build and execute Sketcher to try it out. Any time there is an element under the cursor, the element is drawn in magenta. This makes it obvious which element the context menu is going to act on before you right-click the mouse, and means that you know in advance which context menu will be displayed.

Servicing the Menu Messages

The next step is to provide code in the bodies of the handlers for the Move and Delete menu items that you added earlier. You can add the code for Delete first, as that's the simpler of the two.

Deleting an Element

The code that you need in the OnElementDelete() handler in the CSketcherView class to delete the currently selected element is simple:

void CSketcherView::OnElementDelete()
{
  if(m_pSelected)
  {
    CSketcherDoc* pDoc = GetDocument();// Get the document pointer
    pDoc->DeleteElement(m_pSelected);  // Delete the element
    pDoc->UpdateAllViews(nullptr);     // Redraw all the views
    m_pSelected = nullptr;             // Reset selected element ptr
  }
}

The code to delete an element is executed only if m_pSelected contains a valid address, indicating that there is an element to be deleted. You get a pointer to the document and call the function DeleteElement() for the document object; you'll add this member to the CSketcherDoc class in a moment. When the element has been removed from the document, you call UpdateAllViews() to get all the views redrawn without the deleted element. Finally, you set m_pSelected to nullptr to indicate that there isn't an element selected.

You can add a declaration for DeleteElement() as a public member of the CSketcherDoc class:

class CSketcherDoc : public CDocument
{
protected: // create from serialization only
       CSketcherDoc();
       DECLARE_DYNCREATE(CSketcherDoc)

// Attributes
public:

// Operations
public:
      unsigned int GetElementType() const      // Get the element type
         { return m_Element; }
      COLORREF GetElementColor() const         // Get the element color
         { return m_Color; }
   void AddElement(CElement* pElement)         // Add an element to the list
      { m_ElementList.push_back(pElement); }
   std::list<CElement*>::const_iterator begin() const
     { return m_ElementList.begin(); }
   std::list<CElement*>::const_iterator end() const
     { return m_ElementList.end(); }
  CElement* FindElement(const CPoint& point) const;

   void DeleteElement(CElement* pElement);     // Delete an element

// Rest of the class as before...
};

It accepts as an argument a pointer to the element to be deleted and returns nothing. You can implement it in SketcherDoc.cpp as the following:

// Delete an element from the sketch
void CSketcherDoc::DeleteElement(CElement* pElement)
{
  if(pElement)
  {
    m_ElementList.remove(pElement);    // Remove the pointer from the list
    delete pElement;                   // Delete the element from the heap
  }
}

You shouldn't have any trouble understanding how this works. You use pElement with the remove() member of the list<CElement*> object to delete the pointer from the list, then you delete the element pointed to by the parameter pElement from the heap.

That's all you need in order to delete elements. You should now have a Sketcher program in which you can draw in multiple scrolled views, and delete any of the elements in your sketch from any of the views.

Moving an Element

Moving the selected element is a bit more involved. As the element must move along with the mouse cursor, you must add code to the OnMouseMove() method to provide for this behavior. As this function is also used to draw elements, you need a mechanism for indicating when you're in "move" mode. The easiest way to do this is to have a flag in the view class, which you can call m_MoveMode. If you make it of type BOOL, the Windows API Boolean type, you use the value TRUE for when move mode is on, and FALSE for when it's off. Of course, you can also define it as the C++ fundamental type, bool, with the values true and false.

You'll also have to keep track of the cursor during the move, so you can add another data member in the view for this. You can call it m_CursorPos, and it will be of type CPoint. Another thing you should provide for is the possibility of aborting a move. To do this, you must remember the first position of the cursor when the move operation started, so you can move the element back when necessary. This is another member of type CPoint, and it is called m_FirstPos. Add the three new members to the protected section of the view 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
    bool m_MoveMode;           // Move element flag
    CPoint m_CursorPos;        // Cursor position
    CPoint m_FirstPos;         // Original position in a move

   // Rest of the class as before...
};

These must also be initialized in the constructor for CSketcherView, so modify it to the following:

CSketcherView::CSketcherView()
: m_FirstPoint(CPoint(0,0))
, m_SecondPoint(CPoint(0,0))
, m_pTempElement(nullptr)
, m_pSelected(nullptr)
, m_MoveMode(false)
, m_CursorPos(CPoint(0,0))
, m_FirstPos(CPoint(0,0))
{
  // TODO: add construction code here
}

The element move process starts when the Move menu item from the context menu is selected. Now, you can add the code to the message handler for the Move menu item to set up the conditions necessary for the operation:

void CSketcherView::OnElementMove()
{
   CClientDC aDC(this);
   OnPrepareDC(&aDC);              // Set up the device context
   GetCursorPos(&m_CursorPos);     // Get cursor position in screen coords
   ScreenToClient(&m_CursorPos);   // Convert to client coords
   aDC.DPtoLP(&m_CursorPos);       // Convert to logical
   m_FirstPos = m_CursorPos;       // Remember first position
   m_MoveMode = true;              // Start move mode
}

You are doing four things in this handler:

  1. Getting the coordinates of the current position of the cursor, because the move operation starts from this reference point.

  2. Converting the cursor position to logical coordinates, because your elements are defined in logical coordinates.

  3. Remembering the initial cursor position in case the user wants to abort the move later.

  4. Setting the move mode on as a flag for the OnMouseMove() handler to recognize.

The GetCursorPos() function is a Windows API function that stores the current cursor position in m_CursorPos. Note that you pass a reference to this function. The cursor position is in screen coordinates (that is, coordinates measured in pixels relative to the upper-left corner of the screen). All operations with the cursor are in screen coordinates. You want the position in logical coordinates, so you must do the conversion in two steps. The ScreenToClient() function (which is an inherited member of the view class) converts from screen to client coordinates, and then you apply the DPtoLP() function member of the aDC object to the result to convert to logical coordinates.

After saving the initial cursor position in m_FirstPos, you set m_MoveMode to true so that the OnMouseMove() handler can deal with moving the element.

Now that you have set the move mode flag, it's time to update the mouse move message handler to deal with moving an element.

Modifying the WM_MOUSEMOVE Handler

Moving an element only occurs when move mode is on and the cursor is being moved. Therefore, all you need to do in OnMouseMove() is add code to handle moving an element in a block that gets executed only when m_MoveMode is TRUE. The new code to do this is as follows:

void CSketcherView::OnMouseMove(UINT nFlags, CPoint point)
{
  CClientDC aDC(this);              // DC is for this view
  OnPrepareDC(&aDC);                // Get origin adjusted

  aDC.DPtoLP(&point);               // Convert point to logical coordinates
// If we are in move mode, move the selected element and return
  if(m_MoveMode)
  {
    MoveElement(aDC, point);        // Move the element
    return;
  }

   // Rest of the mouse move handler as before...
}

This addition doesn't need much explaining, really, does it? The if statement verifies that you're in move mode and then calls a function MoveElement(), which does what is necessary for the move. All you have to do now is implement this function.

Add the declaration for MoveElement() as a protected member of the CSketcherView class by adding the following at the appropriate point in the class definition:

void MoveElement(CClientDC& aDC, const CPoint& point); // Move an element

As always, you can also right-click the class name in Class View to do this, if you want to. The function needs access to the object encapsulating a device context for the view, aDC, and the current cursor position, point, so both of these are reference parameters. The implementation of the function in the SketcherView.cpp file is as follows:

void CSketcherView::MoveElement(CClientDC& aDC, const CPoint& point)
{
  CSize distance = point - m_CursorPos;   // Get move distance
  m_CursorPos = point;                    // Set current point as 1st for
                                          // next time

  // If there is an element selected, move it
  if(m_pSelected)
  {
    aDC.SetROP2(R2_NOTXORPEN);
    m_pSelected->Draw(&aDC, m_pSelected); // Draw the element to erase it
    m_pSelected->Move(distance);          // Now move the element
    m_pSelected->Draw(&aDC, m_pSelected); // Draw the moved element
  }
}

The distance to move the element that is currently selected is stored locally as a CSize object, distance. The CSize class is specifically designed to represent a relative coordinate position and has two public data members, cx and cy, which correspond to the x and y increments. These are calculated as the difference between the current cursor position, stored in point, and the previous cursor position, saved in m_CursorPos. This uses the subtraction operator, which is overloaded in the CPoint class. The version you are using here returns a CSize object, but there is also a version that returns a CPoint object. You can usually operate on CSize and CPoint objects combined. You save the current cursor position in m_CursorPos for use the next time this function is called, which occurs if there is a further mouse move message during the current move operation.

You implement moving an element in the view using the R2_NOTXORPEN drawing mode, because it's easy and fast. This is exactly the same as what you have been using during the creation of an element. You redraw the selected element in its current color (the selected color) to reset it to the background color, and then call the function Move() to relocate the element by the distance specified by distance. You'll add this function to the element classes in a moment. When the element has moved itself, you simply use the Draw() function once more to display it highlighted at the new position. The color of the element will revert to normal when the move operation ends, as the OnLButtonUp() handler will redraw all the windows normally by calling UpdateAllViews().

Getting the Elements to Move Themselves

Add the Move() function as a virtual member of the base class, CElement. Modify the class definition to the following:

class CElement : public CObject
{
   protected:
     int m_PenWidth;                   // Pen width
     COLORREF m_Color;                 // Color of an element
     CRect m_EnclosingRect;            // Rectangle enclosing an element
   public:
     virtual ~CElement();

     // Virtual draw operation
     virtual void Draw(CDC* pDC, CElement* pElement=nullptr) {}
     virtual void Move(const CSize& aSize){} // Move an element

     CRect GetBoundRect();             // Get the bounding rectangle for an element

   protected:
     CElement();                       // Here to prevent it being called
};

As discussed earlier in relation to the Draw() member, although an implementation of the Move() function here has no meaning, you can't make it a pure virtual function because of the requirements of serialization.

You can now add a declaration for the Move() function as a public member of each of the classes derived from CElement. It is the same in each:

virtual void Move(const CSize& aSize); // Function to move an element

Next, you can add the implementation of the Move() function in the CLine class to Elements.cpp:

void CLine::Move(const CSize& aSize)
{
  m_StartPoint += aSize;               // Move the start point
  m_EndPoint += aSize;                 // and the end point
  m_EnclosingRect += aSize;            // Move the enclosing rectangle
}

This is easy because of the overloaded += operators in the CPoint and CRect classes. They all work with CSize objects, so you just add the relative distance specified by aSize to the start and end points for the line and to the enclosing rectangle.

Moving a CRectangle object is even easier:

void CRectangle::Move(const CSize& aSize)
{
  m_EnclosingRect+= aSize;             // Move the rectangle
}

Because the rectangle is defined by the m_EnclosingRect member, that's all you need to move it.

The Move() member of the CCircle class is identical:

void CCircle::Move(const CSize& aSize)
{
  m_EnclosingRect+= aSize;             // Move rectangle defining the circle
}

Moving a CCurve object is a little more complicated, because it's defined by an arbitrary number of points. You can implement the function as follows:

void CCurve::Move(const CSize& aSize)
{
  m_EnclosingRect += aSize;            // Move the rectangle
  // Now move all the points
  std::for_each(m_Points.begin(), m_Points.end(),
                                 [&aSize](CPoint& p){ p += aSize; });
}

So that you don't forget about them, I have used an algorithm and a lambda expression here. You will need to add an #include directive for the algorithm header to Elements.cpp.

There's still not a lot to it. You first move the enclosing rectangle stored in m_EnclosingRect, using the overloaded += operator for CRect objects. You then use the STL for_each algorithm to apply the lambda expression that is the third argument to all the points defining the curve. The capture clause for the lambda expression accesses the aSize parameter by reference. Of course, the lambda parameter must be a reference so the original points can be updated.

Dropping the Element

Once you have clicked on the Move menu item, the element under the cursor will move around as you move the mouse. All that remains now is to add the capability to drop the element in position once the user has finished moving it, or to abort the whole move. To drop the element in its new position, the user clicks the left mouse button, so you can manage this operation in the OnLButtonDown() handler. To abort the operation, the user clicks the right mouse button — so you can add the code to the OnRButtonDown() handler to deal with this.

You can take care of the left mouse button first. You'll have to provide for this as a special action when move mode is on. The changes are highlighted in the following:

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

  if(m_MoveMode)
  {
    // In moving mode, so drop the element
    m_MoveMode = false;                // Kill move mode
    m_pSelected = nullptr;             // De-select the element
    GetDocument()->UpdateAllViews(0);  // Redraw all the views
    return;
  }
    m_FirstPoint = point;              // Record the cursor position
    SetCapture();                      // Capture subsequent mouse messages
}

The code is pretty simple. You first make sure that you're in move mode. If this is the case, you just set the move mode flag back to false and then de-select the element. This is all that's required because you've been tracking the element with the mouse, so it's already in the right place. Finally, to tidy up all the views of the document, you call the document's UpdateAllViews() function, causing all the views to be redrawn.

Add a handler for the WM_RBUTTONDOWN message to CSketcherView using the Properties window for the class. The implementation for aborting a move must do two things. It must move the element back to where it was and turn off move mode. You can move the element back in the right-button-down handler, but you must leave switching off move mode to the right-button-up handler to allow the context menu to be suppressed. The code to move the element back is as follows:

void CSketcherView::OnRButtonDown(UINT nFlags, CPoint point)
{
   if(m_MoveMode)
   {
     // In moving mode, so drop element back in original position
     CClientDC aDC(this);
     OnPrepareDC(&aDC);                      // Get origin adjusted
     MoveElement(aDC, m_FirstPos);           // Move element to original position
     m_pSelected = nullptr;                  // De-select element
     GetDocument()->UpdateAllViews(nullptr); // Redraw all the views
  }
}

You first create a CClientDC object for use in the MoveElement() function. You then call the MoveElement() function to move the currently selected element the distance from the current cursor position to the original cursor position that we saved in m_FirstPos. After the element has been repositioned, you just deselect the element and get all the views redrawn.

The last thing you must do is switch off move mode in the button-up handler:

void CSketcherView::OnRButtonUp(UINT /* nFlags */, CPoint point)
{
  if(m_MoveMode)
  {
    m_MoveMode = false;
    return;
  }
ClientToScreen(&point);
  OnContextMenu(this, point);
}

Now, the context menu gets displayed only when move mode is not in progress.

Exercising the Application

Everything is now complete for the context pop-ups to work. If you build Sketcher, you can select the element type and color from one context menu, or if you are over an element, you can move that element or delete it from the other context menu.

DEALING WITH MASKED ELEMENTS

There's still a limitation that you might want to get over. If the element you want to move or delete is enclosed by the rectangle of another element that is drawn after the element you want, you won't be able to highlight it because Sketcher always finds the outer element first. The outer element completely masks the element it encloses. This is a result of the sequence of elements in the list. You could fix this by adding a Send to Back item to the context menu that would move an element to the beginning of the list.

Add a separator and a menu item to the element drop-down in the IDR_ELEMENT_MENU resource, as shown in Figure 17-6.

FIGURE 17-6

Figure 17.6. FIGURE 17-6

You can add a handler for the item to the view class through the Properties window for the CSketcherView class. It's best to handle it in the view because that's where you record the selected element. Select the Events toolbar button in the Properties window for the class and double-click the message ID ID_ELEMENT_SENDTOBACK. You'll then be able to select COMMAND below and <Add>OnElementSendtoback in the right column. You can implement the handler as follows:

void CSketcherView:: OnElementSendtoback()
{
  GetDocument()->SendToBack(m_pSelected);  // Move element to start of list
}

You'll get the document to do the work by passing the currently selected element pointer to a public function, SendToBack(), that you implement in the CSketcherDoc class. Add it to the class definition with a void return type and a parameter of type CElement*. You can implement this function as follows:

void CSketcherDoc::SendToBack(CElement* pElement)
{
  if(pElement)
  {
m_ElementList.remove(pElement);      // Remove the element from the list
    m_ElementList.push_front(pElement);  // Put it back at the beginning of the list
  }
}

After checking that the parameter is not null, you remove the element from the list by calling remove(). Of course, this does not delete the element from memory; it just removes the element pointer from the list. You then add the element pointer back at the beginning of the list, using the push_front() function.

With the element moved to the beginning of the list, it cannot mask any of the others because you search for an element to highlight from the end. You will always find one of the other elements first if the applicable bounding rectangle encloses the current cursor position. The Send to Back menu option is always able to resolve any element masking problem in the view.

EXTENDING CLR SKETCHER

It's time to extend CLR Sketcher by adding a class encapsulating a complete sketch. You'll also implement element highlighting and a context menu with the ability to move and delete elements. You can adopt a different approach to drawing elements in this version of Sketcher that will make the move element operation easy to implement. Before you start extending CLR Sketcher and working with the element classes, let's explore another feature of the Graphics class that will be useful in the application.

Coordinate System Transformations

The Graphics class contains functions that can move, rotate, and scale the entire drawing coordinate system. This is a very powerful capability that you can use in the element drawing operations in Sketcher. The following table describes the most useful functions that transform the coordinate system.

TRANSFORM FUNCTION

DESCRIPTION

TranslateTransform(
  float dx, float dy)

Translates the coordinate system origin by dx in the x-direction and dy in the y-direction.

RotateTransform(
  float angle)

Rotates the coordinate system about the origin by angle degrees. A positive value for angle represents rotation from the x-axis toward the y-axis, a clockwise rotation in other words.

ScaleTransform(
  float scaleX,
  float scaleY)

Scales the x-axis by multiplying by scaleX and scales the y-axis by multiplying by scaleY.

ResetTransform()

Resets the current transform state for a Graphics object so no transforms are in effect.

You will be using the TranslateTransform() function in the element drawing operations. To draw an element, you can translate the origin for the coordinate system to the position specified by the inherited position member of the Element class, draw the element relative to the origin (0,0) and then restore the coordinate system to its original state. This process is illustrated in Figure 17-7, which shows how you draw a circle.

FIGURE 17-7

Figure 17.7. FIGURE 17-7

You can change the Draw() function in the Circle class to use the TranslateTransform() function:

virtual void Draw(Graphics^ g) override
    {
      g->TranslateTransform(safe_cast<float>(position.X),
                            safe_cast<float>(position.Y));
g->DrawEllipse(pen, 0, 0, width,height);
      g->ResetTransform();
    }

The TranslateTransform() function requires arguments of type float, so you cast the coordinates to this type. This is not absolutely necessary, but you will get warning messages from the compiler if you don't, so it is a good idea to always put the casts in.

To make the most efficient use of the TranslateTransform() function when drawing a line, you could change the Line class constructor to store the end point relative to position:

Line(Color color, Point start, Point end)
{
  pen = gcnew Pen(color);
  this->color = color;
  position = start;
  this->end = end - Size(start);
  boundRect = System::Drawing::Rectangle(Math::Min(position.X, end.X),
                                         Math::Min(position.Y, end.Y),
                Math::Abs(position.X - end.X), Math::Abs(position.Y - end.Y));

  // Provide for lines that are horizontal or vertical
  if(boundRect.Width < 2) boundRect.Width = 2;
  if(boundRect.Height < 2) boundRect.Height = 2;
}

The subtraction operator for the Point class enables you to subtract a Size object from a Point object. Here, you construct the Size object from the start object that is passed to the constructor. This results in the end member coordinates being relative to start.

Change the Draw() function in the Line class to the following:

virtual void Draw(Graphics^ g) override
{
  g->TranslateTransform(safe_cast<float>(position.X),
                           safe_cast<float>(position.Y));
  g->DrawLine(pen, 0, 0, end.X, end.Y);
  g->ResetTransform();
}

The DrawLine() function now draws the line relative to 0,0 after the TranslateTransform() function moves the origin of the client area to position, the start point for the line.

The Draw() function for the Rectangle class is very similar to that for the Circle class:

virtual void Draw(Graphics^ g) override
{
  g->TranslateTransform(safe_cast<float>(position.X),
                        safe_cast<float>(position.Y));
  g->DrawRectangle(pen, 0, 0, width, height);
  g->ResetTransform();
}

The Draw() function for the Curve class is simpler than before:

virtual void Draw(Graphics^ g) override
    {
      g->TranslateTransform(safe_cast<float>(position.X),
                            safe_cast<float>(position.Y));
      Point previous(0,0);
      for each(Point p in points)
      {
        g->DrawLine(pen, previous, p);
        previous = p;
      }
      g->ResetTransform();
 }

The points in the points vector all have coordinates relative to position, so once you transfer the client area origin to position using the TranslateTransform() method, each line can be drawn directly by means of the points in the points vector.

With the existing element classes updated, you are ready to define the object that will encapsulate a sketch.

Defining a Sketch Class

Using Solution Explorer, create a new header file with the name Sketch.h to hold the Sketch class; just right-click the Header Files folder and select from the pop-up. A sketch is an arbitrary sequence of elements of any type that has Element as a base class. An STL/CLR container looks a good bet to store the elements in a sketch, and this time you can use a list<T> container. To allow any type of element to be stored in the list, you can define the container class as type list<Element^>. Specifying that the container stores references of type Element^ will enable you to store references to objects of any type that is derived from Element.

Initially, the Sketch class will need a constructor, an Add() function that adds a new element to a sketch, and a Draw() function to draw a sketch. Here's what you need to put in Sketch.h:

// Sketch.h
// Defines a sketch

#pragma once
#include <cliext/list>
#include "Elements.h"

using namespace System;
using namespace cliext;

namespace CLRSketcher
{
 public ref class Sketch
  {
  private:
    list<Element^>^ elements;

  public:
    Sketch()
{
    elements = gcnew list<Element^>();
    }

    // Add an element to the sketch
    Sketch^ operator+=(Element^ element)
    {
      elements->push_back(element);
      return this;
    }

    // Remove an element from the sketch
    Sketch^ operator-=(Element^ element)
    {
      elements->remove(element);
      return this;
    }

    void Draw(Graphics^ g)
    {
      for each(Element^ element in elements)
        element->Draw(g);
    }
 };
}
                                                                     
Defining a Sketch Class

The #include directive for the <cliext/list> header is necessary for accessing the STL/CLR list<T> container template, and the #include for the Elements.h header enables you to reference the Element base class. The Sketch class is defined within the CLRSketcher namespace, so it can be referenced without qualification from anywhere within the CLRSketcher application. The elements container that the constructor creates stores the sketch. You add elements to a Sketch object by calling its += operator overload function, which calls the push_back() function for the elements container to store the new element. Drawing a sketch is very simple. The Draw() function for the Sketch object iterates over all the elements in the elements container and calls the Draw() function for each of them.

To make the current Sketch object a member of the Form1 class, add a member of type Sketch^ with the name sketch to the class. You can add the initialization for sketch to the Form1 constructor:

Form1(void) : drawing(false), firstPoint(0),
              elementType(ElementType::LINE), color(Color::Black),
              tempElement(nullptr), sketch(gcnew Sketch())
{
  InitializeComponent();
  //
  SetElementTypeButtonsState();
  SetColorButtonsState();
  //
}
                                                                   
Defining a Sketch Class

Don't forget to add an #include directive for the Sketch.h header at the beginning of the Form1.h header. Now that you have the Sketch class defined, you can modify the MouseUp event handler in the Form1 class to add elements to the sketch:

private: System::Void Form1_MouseUp(System::Object^  sender,

System::Windows::Forms::MouseEventArgs^  e)
{
  if(!drawing)
    return;
  if(tempElement)
  {
    sketch += tempElement;
    tempElement = nullptr;
    Invalidate();
  }
  drawing = false;
}

The function uses the operator+=() function for the sketch object when tempElement is not nullptr. After resetting tempElement to nullptr, you call the Invalidate() function to redraw the form. The final piece of the jigsaw in creating a sketch is the implementation of the Paint event handler to draw the sketch.

Drawing the Sketch in the Paint Event Handler

Amazingly, you need only one extra line of code to draw an entire sketch:

private: System::Void Form1_Paint(System::Object^  sender,
                                  System::Windows::Forms::PaintEventArgs^  e)
{
  Graphics^ g = e->Graphics;
  sketch->Draw(g);
  if(tempElement != nullptr)
    tempElement->Draw(g);
}

You pass the Graphics object, g, to the Draw() function for the sketch object to draw the sketch. This will result in g being passed to the Draw() function for each element in the sketch to enable each element to draw itself. Finally, if tempElement is not nullptr, you call the Draw() function for that, too. With the Paint() event handler complete, you should have a working version of CLR Sketcher.

A little later in this chapter, you will add the capability to pop up a context menu to allow elements to be moved as in the MFC version of Sketcher, but first, you need to get the element highlighting mechanism working.

Implementing Element Highlighting

You want to highlight an element when the mouse cursor is within the element's bounding rectangle, just like the MFC version of Sketcher. You will implement the mechanism to accomplish this in CLR Sketcher in a slightly different way.

The MouseMove event handler in Form1 is the prime mover in highlighting elements because it tracks the movement of the cursor, but highlighting should occur only when there is not a drawing operation in progress. The first thing you need to do, before you can modify the MouseMove handler, is to implement a way for an element to draw itself in a highlight color when it is under the cursor. Add highlighted as a public property of the Element class of type bool to record whether an element is highlighted.

public:
  property bool highlighted;

You can also add a protected member to the Element class to specify the highlight color for elements:

Color highlightColor;

Add a public constructor to the Element class to initialize the new members:

Element() : highlightColor(Color::Magenta) {   highlighted = false;  }

You cannot initialize a property in the initialization list, so it must go in the body of the constructor.

As you will see, you will need external access to the bounding rectangle for an element, so you can add a public property to the Element class to provide this:

property System::Drawing::Rectangle bound
{
  System::Drawing::Rectangle get() { return boundRect; }
}

This makes boundRect accessible without endangering its integrity, because there is no set() function for the bound property.

Now, you can change the implementation of the Draw() function in each of the classes derived from Element. Here's how the function looks for the Line class:

virtual void Draw(Graphics^ g) override
{
  pen->Color = highlighted ? highlightColor : color;
  g->TranslateTransform(safe_cast<float>(position.X),
                        safe_cast<float>(position.Y));
  g->DrawLine(pen, 0, 0, end.X, end.Y);
  g->ResetTransform();
}

The extra statement sets the Color property for pen to highlightColor whenever highlighted is true, or to color otherwise. You can add the same statement to the Draw() function for the other element classes.

Finding the Element to Highlight

To highlight an element, you must discover which element, if any, is under the cursor at any given time. It would be helpful if an element could tell you if a given point is within the bounding rectangle. This process will be the same for all types of element, so you can add a public function to the Element base class to implement this:

bool Hit(Point p)
 {
  return boundRect.Contains(p);
  }

The Contains() member of the System::Drawing::Rectangle structure returns true if the Point argument lies within the rectangle, and false otherwise. There are two other versions of this function: one accepts two arguments of type int that specify the x and y coordinates of a point, and the other accepts an argument of type System::Drawing::Rectangle and returns true if the rectangle for which the function is called contains the rectangle passed as an argument.

You can now add a public function to the Sketch class to determine if any element in the sketch is under the cursor:

Element^ HitElement(Point p)
{
      for (auto riter = elements->rbegin(); riter != elements->rend(); ++riter)
      {
        if((*riter)->Hit(p))
          return *riter;
      }
  return nullptr;
}

This function iterates over the elements in the sketch in reverse order, starting with the last one added to the container, and returns a reference to the first element for which the Hit() function returns true. If the Hit() function does not return true for any of the elements in the sketch, the HitElement() function returns nullptr. This provides a simple way to detect whether or not there is an element under the cursor.

Highlighting the Element

Add a private member, highlightedElement, of type Element^ to the Form1 class to record the currently highlighted element and initialize it to nullptr in the Form1 constructor. Because there's such a lot of code in the Form1 class, it is best to do this by right-clicking the class name and selecting Add

Highlighting the Element
private: System::Void Form1_MouseMove(System::Object^  sender,
   System::Windows::Forms::MouseEventArgs^  e)
{
  if(drawing)
  {
    if(tempElement)
       Invalidate(tempElement->bound);        // Invalidate old element area
    switch(elementType)
    {
      case ElementType::LINE:

        tempElement = gcnew Line(color, firstPoint, e->Location);
        break;
      case ElementType::RECTANGLE:
tempElement = gcnew Rectangle(color, firstPoint, e->Location);
        break;
      case ElementType::CIRCLE:
        tempElement = gcnew Circle(color, firstPoint, e->Location);
        break;
      case ElementType::CURVE:
        if(tempElement)
          safe_cast<Curve^>(tempElement)->Add(e->Location);
        else
          tempElement = gcnew Curve(color, firstPoint, e->Location);
        break;
    }
    Invalidate(tempElement->bound);           // Invalidate new element area
    Update();                                 // Repaint
  }
  else
  {
    // Find the element under the cursor - if any
    Element^ element(sketch->HitElement(e->Location));
    if(highlightedElement == element)         // If the old is same as the new
      return;                                 // there's nothing to do

    // Reset any existing highlighted element
    if(highlightedElement)
    {
      Invalidate(highlightedElement->bound);  // Invalidate element area
      highlightedElement->highlighted = false;
      highlightedElement = nullptr;
    }
    // Find and set new highlighted element, if any
    highlightedElement = element;
    if(highlightedElement)
    {
      highlightedElement->highlighted = true;
      Invalidate(highlightedElement->bound);  // Invalidate element area
    }
    Update();                                 // Send a paint message
  }
}

There's a change to the original code that is executed to improve performance when drawing is true. The old code redraws the entire sketch to highlight an element when, in fact, only the regions occupied by the un-highlighted element and the newly highlighted element need to be redrawn. The Invalidate() member of the System::Windows::Forms::Form class has an overloaded version that accepts an argument of type System::Drawing::Rectangle to add the rectangle specified by the argument to the currently invalidated region for the form. Thus, you can call Invalidate() repeatedly to accumulate a composite of rectangles to be redrawn.

Calling Invalidate() with a rectangle argument does not redraw the form, but you can get the invalid region redrawn by calling the Update() function for the form. You must pass the enclosing rectangle for an element to Invalidate(), rather than the bounding rectangle, to get elements redrawn properly, because the shapes extend beyond the right and bottom edges of the bounding rectangle by the thickness of the pen. You will therefore have to adjust the existing bounding rectangles to take the pen width into account if this is to work properly; you'll get to that in a moment.

You now pass the bounding rectangle of the old temporary element to the Invalidate() function to get just that area added to the region to be repainted. When you have created a new temporary element, you add its bounding rectangle to the region to be repainted. You finally call Update() for the Form1 object to send a WM_Paint message. The effect of all this is that only the region occupied by the old and new temporary elements will be repainted, so the operation will be much faster than drawing the whole of the client area.

If drawing is false, you execute the highlighting code in the else clause. First, you get a reference to a new element under the cursor by calling the HitElement() function for the sketch. If the return value is the same as highlightedElement, either they are both nullptr or both reference the same element — either way, there's nothing further to be done.

If you don't return from the handler, you test for an existing highlighted element, and if there is one, you invalidate the rectangle it occupies and reset it by setting the value of its highlighted property to false. You then set highlightedElement back to nullptr. You store the element value in highlightedElement, and if this is not null, you set the highlighted member for the element to true and call Invalidate() with the bounding rectangle as the argument. Finally, you call Update() to redraw the invalid region.

The highlight code will execute repeatedly for every movement of the mouse cursor, so it needs to be efficient.

Adjusting the Bounding Rectangle for an Element

The existing bounding rectangles for the elements need to be inflated by the width of the pen. The pen width is available as a floating point value, as the Width property of a Pen object. Here's how you can modify the Line class constructor to inflate the bounding rectangles appropriately:

Line(Color color, Point start, Point end)
    {
      pen = gcnew Pen(color);
      this->color = color;
      position = start;
      this->end = end - Size(start);
      boundRect = System::Drawing::Rectangle(Math::Min(position.X, end.X),
                                             Math::Min(position.Y, end.Y),
                    Math::Abs(position.X - end.X), Math::Abs(position.Y - end.Y));
      int penWidth(safe_cast<int>(pen->Width));        // Pen width as an integer
      boundRect.Inflate(penWidth, penWidth);
    }

There are just two additional statements. The first converts the Width property value for the pen to an integer and stores it locally. The second calls Inflate() for the Rectangle object. This inflates the width of the rectangle in both directions by the first argument value, and the height in both directions by the second argument value. Thus, both the width and height will increase by twice the pen width. Note that the statements that were there previously to ensure the width and height were greater than zero are no longer required, because both will always be at least twice the pen width.

You can add the same two statements to the end of the constructors for the Rectangle and Circle classes. The Curve constructor should be changed to the following:

Curve(Color color, Point p1, Point p2)
{
  pen = gcnew Pen(color);
  this->color = color;
  points = gcnew vector<Point>();
  position = p1;
  points->push_back(p2 - Size(position));

  // Find the minimum and maximum coordinates
  int minX = p1.X < p2.X ? p1.X : p2.X;
  int minY = p1.Y < p2.Y ? p1.Y : p2.Y;
  int maxX = p1.X > p2.X ? p1.X : p2.X;
  int maxY = p1.Y > p2.Y ? p1.Y : p2.Y;
  int width = maxX - minX;
  int height = maxY - minY;
  boundRect = System::Drawing::Rectangle(minX, minY, width, height);
  int penWidth(safe_cast<int>(pen->Width));        // Pen width as an integer
  boundRect.Inflate(penWidth, penWidth);
}

You also need to change the Add() member of the Curve class:

void Add(Point p)
{
  points->push_back(p - Size(position));

  // Modify the bounding rectangle to accommodate the new point
  int penWidth(safe_cast<int>(pen->Width));       // Pen width as an integer
  boundRect.Inflate(−penWidth, -penWidth);         // Reduce by the pen width
  if(p.X < boundRect.X)
  {
    boundRect.Width = boundRect.Right - p.X;
    boundRect.X = p.X;
  }
  else if(p.X > boundRect.Right)
    boundRect.Width = p.X - boundRect.Left;

  if(p.Y < boundRect.Y)
  {
    boundRect.Height = boundRect.Bottom - p.Y;
    boundRect.Y = p.Y;
  }
  else if(p.Y > boundRect.Bottom)
    boundRect.Height = p.Y - boundRect.Top;

  boundRect.Inflate(penWidth, penWidth);          // Inflate by the pen width
}

Once you have done this, you should find CLR Sketcher works with element highlighting in effect.

Creating Context Menus

You use the Design window to create context menus interactively by dragging a ContextMenuStrip control from the Toolbox window to the form. You need to display two different context menus, one for when there is an element under the cursor and one for when there isn't, and you can arrange for both possibilities using a single context menu strip. First, drag a ContextMenuStrip control from the Toolbox window to the form in the Design window; this will have the default name ContextMenuStrip1, but you can change this if you want. To make the context menu strip display when the form is right-clicked, display the Properties window for the form and set the ContextMenuStrip property in the Behavior group by selecting ContextMenuStrip1 from the drop-down in the value column.

The drop-down for the context menu is empty at present, and you are going to control what menu items it contains programmatically. When an element is under the cursor, the element-specific menu items should display, and when there is no element under the cursor, menu items equivalent to those from the Element and Color menus should display. First, add a Send to Back menu item, a Delete menu item, and a Move menu item to the drop-down for the context menu by typing the entries into the design window. Change the (name) properties for the items to sendToBackContextMenuItem, deleteContextMenuItem, and moveContextMenuItem.

A menu item can belong to only one menu strip, so you need to add new ones to the context menu matching those in the Element and Color menus. Add a separator and then add menu items Line, Rectangle, Circle, and Curve, and Black, Red, Green, and Blue. Change the (name) property for each in the same way as the others, to lineContextMenuItem, rectangleContextMenuItem, and so on. Also change the (name) property for the separator to contextSeparator so you can refer to it easily.

You need Click event handlers for all the menu items in the context menu, but the only new ones you need to create are for the Move, Delete, and Send to Back items; you can set the handlers for all the other menu items to be the same as the corresponding items in the Element and Color menus.

You can control what displays in the drop-down for the context menu in the Opening event handler for the context menu strip, because this event handler is called before the drop-down displays. Open the Properties window for the context menu strip by right-clicking it and selecting Properties. Click the Events button in the Properties window and double-click the Opening event to add a handler. You are going to add different sets of menu items to the context menu strip, depending on whether or not an element is under the cursor; so how can you determine if anything is under the cursor? Well, the MouseMove handler for the form has already taken care of it. All you need to do is check to see whether the reference contained in the highlightElement member of the form is nullptr or not.

You have added items to the drop-down for contextMenuStrip1 to add the code to create the objects in the Form1 class that encapsulates them, but you want to start with a clean slate for the drop-down menu when it is to display, so you can set it up the way you want. In the Opening event handler, you want to remove any existing menu items before you add back the specific menu items you want. A ContextMenuStrip object that encapsulates a context menu stores its drop-down menu items in an Items property that is of type ToolStripItemsCollection^. To remove existing items from the drop-down, simply call Clear() for the Items property. To add a menu item to the context menu, call the Add() function for the Items property with the menu item as the argument.

The implementation of the Opening handler for the context menu strip is as follows:

private: System::Void contextMenuStrip1_Opening(System::Object^  sender,

System::ComponentModel::CancelEventArgs^  e)
{
  contextMenuStrip1->Items->Clear();    // Remove existing items
  if(highlightedElement)
  {
    contextMenuStrip1->Items->Add(moveContextMenuItem);
    contextMenuStrip1->Items->Add(deleteContextMenuItem);
    contextMenuStrip1->Items->Add(sendToBackContextMenuItem);
  }
  else
  {
    contextMenuStrip1->Items->Add(lineContextMenuItem);
    contextMenuStrip1->Items->Add(rectangleContextMenuItem);
    contextMenuStrip1->Items->Add(circleContextMenuItem);
    contextMenuStrip1->Items->Add(curveContextMenuItem);
    contextMenuStrip1->Items->Add(contextSeparator);
    contextMenuStrip1->Items->Add(blackContextMenuItem);
    contextMenuStrip1->Items->Add(redContextMenuItem);
    contextMenuStrip1->Items->Add(greenContextMenuItem);
    contextMenuStrip1->Items->Add(blueContextMenuItem);

    // Set checks for the menu items
    lineContextMenuItem->Checked = elementType == ElementType::LINE;
    rectangleContextMenuItem->Checked = elementType == ElementType::RECTANGLE;
    circleContextMenuItem->Checked = elementType == ElementType::CIRCLE;
    curveContextMenuItem->Checked = elementType == ElementType::CURVE;
    blackContextMenuItem->Checked = color == Color::Black;
    redContextMenuItem->Checked = color == Color::Red;
    greenContextMenuItem->Checked = color == Color::Green;
    blueContextMenuItem->Checked = color == Color::Blue;
  }
}

After clearing the context menu, you add a set of items to it depending on whether or not highlightedElement is null. You set the checks for the element and color menu items in the same way you did for the main menu, by setting the Checked property for each of the menu items.

All the element and color menu items in the context menu should now work, so try it out. All that remains is to implement the element-specific operations. Let's do the easiest one first.

Implementing the Element Delete Operation

The overloaded -= operator in the Sketch class already provides you with a way to delete an element. You can use the operator-=() function in the implementation of the Click event handler for the Delete menu item:

private: System::Void deleteContextMenuItem_Click(System::Object^  sender,
                                                  System::EventArgs^  e)
{
  if(highlightedElement)
  {
    sketch -= highlightedElement;              // Delete the highlighted element
    Invalidate(highlightedElement->bound);
    highlightedElement = nullptr;
    Update();
  }
}

As well as deleting the element from the sketch, the event handler also invalidates the region it occupies to remove it from the display, and resets highlightedElement back to nullptr.

Implementing the "Send to Back" Operation

The Send to Back operation works much as in MFC Sketcher. You remove the highlighted element from the list in the Sketch object and add it to the end of the list. Here's the implementation of the handler function:

private: System::Void sendToBackContextMenuItem_Click(System::Object^  sender,
                                                             System::EventArgs^  e)
{
  if(highlightedElement)
  {
    sketch -= highlightedElement;              // Delete the highlighted
                                               // element
    sketch->push_front(highlightedElement);    // Then add it back to the
                                               // beginning
    highlightedElement->highlighted = false;
    Invalidate(highlightedElement->bound);
    highlightedElement = nullptr;
    Update();
  }
}

You delete the highlighted element using the operator-=() function, and then add it back to the beginning of the list using the push_front() function that you will define in the Sketch class in a moment. You reset the highlighted member of the element to false and highlightedElement to nullptr before calling Invalidate() for the form to redraw the region occupied by the previously highlighted element.

You can implement push_front() as a public function in the Sketch class definition as follows:

void push_front(Element^ element)
{
  elements->push_front(element);
}

Implementing the Element Move Operation

You'll move an element by dragging it with the left mouse button down. This implies that a move operation needs a different set of functions from normal to be carried out by the mouse event handlers, so a move will have to be modal, as in the MFC version of Sketcher. You can identify possible modes with an enum class that you can add following the ElementType enum in Form1.h:

enum class Mode {Normal, Move};

This defines just two modes, but you could add more if you wanted to implement other modal operations. You can now add a private member of type Mode with the name mode to the Form1 class. Initialize the new variable to Mode::Normal in the Form1 constructor.

The only thing the Click event handler for the Move menu item has to do is set mode to Mode::Move:

private: System::Void moveContextMenuItem_Click(System::Object^  sender,

System::EventArgs^  e)
{
  mode = Mode::Move;
}

The mouse event handlers for the form must take care of moving an element. Change the code for the MouseDown handler so it sets drawing to true only when mode is Mode::Normal:

private: System::Void Form1_MouseDown(System::Object^  sender,

System::Windows::Forms::MouseEventArgs^  e)
{
  if(e->Button == System::Windows::Forms::MouseButtons::Left)
  {
    if(mode == Mode::Normal)
    {
      drawing = true;
    }
    firstPoint = e->Location;
  }
}

This sets drawing to true only when mode has the value Mode::Normal. If mode has a different value, only the point is stored for use in the MouseMove event handler. The MouseMove event handler will not create elements when drawing is false, so this functionality is switched off for all modes except Mode::Normal. The MouseUp event handler will restore mode to Mode::Normal when a move operation is complete.

Moving an Element

Because you now draw all elements relative to a given position, you can move an element just by changing the inherited position member to reflect the new position and to adjust the location of the bounding rectangle in a similar way. You can add a public Move() function to the Element base class to take care of this:

void Move(int dx, int dy)
{
  position.Offset(dx, dy);
  boundRect.X += dx;
  boundRect.Y += dy;
}

Calling the Offset() function for the Point object, position, adds dx and dy to the coordinates stored in the object. To adjust the location of boundRect, just add dx to the Rectangle object's X field and dy to its Y field.

The MouseMove event handler will expedite the moving process:

private: System::Void Form1_MouseMove(System::Object^  sender,
 System::Windows::Forms::MouseEventArgs^  e) {
   if(drawing)
  {
    if(tempElement)
      Invalidate(tempElement->Bound);  // The old element region
    switch(elementType)
    {
       // Code to create a temporary element as before...
    }
    Invalidate(tempElement->bound);    // The new element region
    Update();
   }
     else if(mode == Mode::Normal)
   {
       // Code to highlight the element under the cursor as before
    }
   else if(mode == Mode::Move &&
                         e->Button ==
 System::Windows::Forms::MouseButtons::Left)
   {  // Move the highlighted element
     if(highlightedElement)
     {
        Invalidate(highlightedElement->bound);     // Region before move
        highlightedElement->Move(e->X - firstPoint.X, e->Y - firstPoint.Y);
        firstPoint = e->Location;
        Invalidate(highlightedElement->bound);     // Region after move
        Update();
     }
   }
}

This event handler now provides three different functions: it creates an element when drawing is true, it does element highlighting when drawing is false and mode is Mode::Normal, and it moves the currently highlighted element when mode is Mode::Move and the left mouse button is down. This means that moving an element in this version of Sketcher will work a little differently from in the native version. To move an element, you select Move from the context menu, then drag the element to its new position with the left button down.

The new else if clause executes only when mode has the value Mode::Move and the left button is down. Without the button test, moving would occur if you moved the cursor after clicking the menu item. This would happen without a MouseDown event occurring, which would create some confusion, as firstPoint would not have been initialized. With the code as you have it here, the moving process is initiated when you click the menu item and release the left mouse button. You then press the left mouse button and drag the cursor to move the highlighted element.

If there is a highlighted element, you invalidate its bounding rectangle before moving the element the distance from firstPoint to the current cursor location. You then update firstPoint to the current cursor position, ready for the next move increment, and invalidate the new region the highlighted element occupies. You finally call Update() to redraw the invalid regions of the form.

The last part you must implement to complete the ability to move elements is in the MouseUp event handler:

private: System::Void Form1_MouseUp(System::Object^  sender,
 System::Windows::Forms::MouseEventArgs^  e)
{
  if(!drawing)
  {
    mode = Mode::Normal;
    return;
  }
  if(tempElement)
  {
    sketch += tempElement;
    tempElement = nullptr;
    Invalidate();
  }
  drawing = false;
}

Releasing the left mouse button ends a move operation. The drawing variable is false when an element is being moved, and the only thing the handler has to do when this is the case is reset mode to Mode::Normal to end the move operation.

With that done, you should now have a version of CLR Sketcher in which you can move and delete elements, as illustrated in Figure 17-8.

FIGURE 17-8

Figure 17.8. FIGURE 17-8

The Send to Back facility ensures that any element can always be highlighted to be moved or deleted; if an element will not highlight, just Send to Back any elements enclosing the element you want to move or delete. In Chapter 19, you will implement the capability to save a sketch in a file.

SUMMARY

In this chapter, you've seen how to apply STL collection classes to the problems of managing objects and managing pointers to objects. Collections are a real asset in programming for Windows, because the application data you store in a document often originates in an unstructured and unpredictable way, and you need to be able to traverse the data whenever a view needs to be updated.

You have also seen how to create document data and manage it in a pointer list in the document, and, in the context of the Sketcher application, how the views and the document communicate with each other.

You have improved the view capability in Sketcher in several ways. You've added scrolling to the views using the MFC class CScrollView, and you've introduced a pop-up at the cursor for moving and deleting elements. You have also implemented an element-highlighting feature to provide the user with feedback during the moving or deleting of elements.

CLR Sketcher has most of the features of MFC Sketcher, including drawing operations and a context menu, but you implemented some things a little differently because the characteristics of the CLR are not the same as the characteristics of MFC. The coding effort was considerably less than for MFC Sketcher because of the automatic code generation provided by the Forms Designer capability. The downside of using Forms Designer is that you can't control how the code is organized, and the Form1 class becomes rather unwieldy because of the sheer volume of code in the definition. However, the Class View is a great help in navigating around the class definition.

WHAT YOU LEARNED IN THIS CHAPTER

TOPIC

CONCEPT

Windows coordinate systems

When you draw in a device context using the MFC, coordinates are in logical units that depend on the mapping mode set. Points in a window that are supplied along with Windows mouse messages are in client coordinates. The two coordinate systems are usually not the same.

Screen coordinates

Coordinates that define the position of the cursor are in screen coordinates that are measured in pixels relative to the upper-left corner of the screen.

Converting between coordinate systems

Functions to convert between client coordinates and logical coordinates in an MFC application are available in the CDC class.

WM_PAINT messages

Windows requests that a view be redrawn by sending a WM_PAINT message to your MFC application. This causes the OnDraw() member of the affected view to be called.

The OnDraw() function

You should always do any permanent drawing of a document in the OnDraw() member of the view class in your MFC application. This ensures that the window is drawn properly when that is required by Windows.

Drawing efficiently

You can make your OnDraw() implementation more efficient by calling the RectVisible() member of the CDC class to check whether an entity needs to be drawn.

Updating multiple views

To get multiple views updated when you change the document contents, you can call the UpdateAllViews() member of the document object. This causes the OnUpdate() member of each view to be called.

Updating efficiently

You can pass information to the UpdateAllViews() function to indicate which area in the view needs to be redrawn. This makes redrawing the views faster.

Context menus

You can display a context menu at the cursor position in an MFC application in response to a right mouse click. This menu is created as a normal pop-up.

Menus and toolbars in Windows Forms applications

You create menu strips, context menus, and toolbars in a Windows Forms application by dragging the appropriate component from the Toolbox window to the form in the Forms Designer window. You cause the context menu to be displayed on the form in response to a right click by setting the ContextMenuStrip property for the form.

Creating event handlers in Windows Forms applications

You create event handlers for a component in a Windows Forms application through the Properties window for the component. You can change the name of the event handler function that is created by changing the value of its (name) property.

Controlling what is displayed in a menu

Implementing the DropDownOpening event handler for a menu item enables you to modify the drop-down before it displays.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.189.157