
Beginning Visual C++ 2005 (2006) [eng]-1
.pdf
Creating the Document and Improving the View
m_pSelected accordingly. The test 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 check if 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 |
|
|
CSketcherDoc* pDoc=GetDocument(); |
// Get a pointer to the document |
CElement* pElement = 0; |
// Store an element pointer |
CRect aRect(0,0,0,0); |
// Store a rectangle |
POSITION aPos = pDoc->GetListHeadPosition(); // Get first element position |
|
m_pSelected = 0; |
|
while(aPos) |
// Iterate through the list |
{ |
|
pElement = pDoc->GetNext(aPos); |
|
aRect = pElement->GetBoundRect(); |
|
aDC.LPtoDP(aRect); |
// Convert to device coordinates |
aRect.NormalizeRect(); |
// Renormalize the rectangle |
if(aRect.PtInRect(point)) |
// Is the current element under the cursor? |
{ |
|
m_pSelected = pElement; |
|
break; |
|
} |
|
} |
|
aDC.SetROP2(R2_NOTXORPEN); |
// Set the drawing mode |
if((nFlags&MK_LBUTTON) && (this==GetCapture())) |
|
{ |
|
aDC.DPtoLP(&point); |
// convert point to Logical |
m_SecondPoint = point; |
// Save the current cursor position |
if(m_pTempElement) |
|
{ |
|
if(CURVE == GetDocument()->GetElementType()) |
// Is it a curve? |
{ // We are drawing a curve
// so add a segment to the existing curve static_cast<CCurve*>(m_pTempElement)->AddSegment(m_SecondPoint);
m_pTempElement->Draw(&aDC); |
// Now draw it |
return; |
// We are done |
} |
|
aDC.SetROP2(R2_NOTXORPEN); |
// Set drawing mode |
// Redraw the old element so it disappears from the view |
|
m_pTempElement->Draw(&aDC); |
|
delete m_pTempElement; |
// Delete the old element |
m_pTempElement = 0; |
// Reset the pointer to 0 |
} |
|
// Create a temporary element of the type and color that // is recorded in the document object, and draw it m_pTempElement = CreateElement();// Create a new element
799

Chapter 15
m_pTempElement->Draw(&aDC); |
// Draw the element |
}
}
The new code looks like a lot, but it is very simple. It tests whether the cursor lies within the bounding rectangle for each element in turn, and stores the address of the first element where this is the case in m_pSelected. If the cursor does not lie within any of the bounding rectangles, m_pSelected element contains 0. Note how you convert each bounding rectangle to device coordinates and renormalize before testing it. Without this the rectangle would be in the wrong place because the mapping mode is
MM_LOENGLISH.
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. If there are no elements under the cursor, the second context pop-up appears, allowing 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 of the view window. Messages for these are not passed to the document view window in your application, so the pop-up is not displayed.
Note that the context menu to select elements and colors isn’t quite right — they set the right type or color in the class but the check marks in the pop-up are not set properly. The document class handles the messages from the menu, but the UPDATE_COMMAND_UI messages don’t apply to the context menu — they only work with the IDR_SketcherTYPE menu. Read on to see how you can fix that.
Checking the Context Menu Items
Checking the items in the no element menu has to be done in the OnContextMenu() function in the CSketcherView class before the context menu is displayed. The CMenu class has a function designed to do exactly what you want. Its prototype is:
UINT CheckMenuItem(UINT nIDCheckItem, UINT nCheck);
This function checks or unchecks any item in the context menu. The first parameter selects which entry in the context pop-up is to be checked or unchecked; the second parameter is a combination of two flags, one of which determines how the first parameter specifies which item is to be checked, and the other specifies whether the menu item is to be checked or unchecked. Because each flag is a single bit in a UINT value, you combine the two using the bitwise OR.
The flag to determine how the item is identified can be one of two possible values:
MF_BYPOSITION |
The first parameter is an index where 0 specifies the first item, 1 the |
|
second, and so on. |
MF_BYCOMMAND |
The first parameter is a menu ID. |
|
|
800

Creating the Document and Improving the View
Use MF_BYCOMMAND so you won’t have to worry about the sequence in which the menu items appear in the pop-up, or even in which sub-menu they appear.
The possible flag values to check or uncheck an item are MF_CHECKED and MF_UNCHECKED, respectively.
The code for checking or unchecking a menu item is essentially the same for all the menu items in the second context pop-up. See how you can set the check for the menu item Black correctly. The first argument to the CheckMenuItem() function will be the menu ID, ID_COLOR_BLACK. The second argument is MF_BYCOMMAND combined with either MF_CHECKED or MF_UNCHECKED, depending on the current color selected. You can obtain the current color from the document using the GetElementColor() function, with the following statement:
COLORREF Color = GetDocument()->GetElementColor();
You can use the Color variable to select the appropriate flag using the conditional operator, and then combine the result with the MF_BYCOMMAND flag to obtain the second argument to the CheckMenuItem() function, so the statement to set the check for the item is:
menu.CheckMenuItem(ID_COLOR_BLACK,
(BLACK==Color?MF_CHECKED:MF_UNCHECKED)|MF_BYCOMMAND);
You don’t need to specify the sub-menu here because the menu item is uniquely defined in the menu by its ID. You just need to change the ID and the color value in this statement to obtain the statement to set the flags for each of the other color menu items.
Checking the element menu items is essentially the same. To check the Line menu item you can write:
unsigned int ElementType = GetDocument()->GetElementType(); menu.CheckMenuItem(ID_ELEMENT_LINE,
(LINE==ElementType?MF_CHECKED:MF_UNCHECKED)|MF_BYCOMMAND);
The complete code for the OnContextMenu() handler is:
void CSketcherView::OnContextMenu(CWnd* pWnd, CPoint point)
{
CMenu menu; menu.LoadMenu(IDR_CURSOR_MENU);
// Set check marks if it’s the no element menu if(m_pSelected == 0)
{
// Check color menu items
COLORREF Color = GetDocument()->GetElementColor(); menu.CheckMenuItem(ID_COLOR_BLACK,
(BLACK==Color?MF_CHECKED:MF_UNCHECKED)|MF_BYCOMMAND); menu.CheckMenuItem(ID_COLOR_RED,
(RED==Color?MF_CHECKED:MF_UNCHECKED)|MF_BYCOMMAND); menu.CheckMenuItem(ID_COLOR_GREEN,
(GREEN==Color?MF_CHECKED:MF_UNCHECKED)|MF_BYCOMMAND); menu.CheckMenuItem(ID_COLOR_BLUE,
(BLUE==Color?MF_CHECKED:MF_UNCHECKED)|MF_BYCOMMAND);
// Check element menu items
801

Chapter 15
unsigned int ElementType = GetDocument()->GetElementType(); menu.CheckMenuItem(ID_ELEMENT_LINE,
(LINE==ElementType?MF_CHECKED:MF_UNCHECKED)|MF_BYCOMMAND); menu.CheckMenuItem(ID_ELEMENT_RECTANGLE,
(RECTANGLE==ElementType?MF_CHECKED:MF_UNCHECKED)|MF_BYCOMMAND);
menu.CheckMenuItem(ID_ELEMENT_CIRCLE, (CIRCLE==ElementType?MF_CHECKED:MF_UNCHECKED)|MF_BYCOMMAND);
menu.CheckMenuItem(ID_ELEMENT_CURVE, (CURVE==ElementType?MF_CHECKED:MF_UNCHECKED)|MF_BYCOMMAND);
}
CMenu* pPopup = menu.GetSubMenu(m_pSelected == 0 ? 1 : 0); ASSERT(pPopup != NULL);
pPopup->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, this);
}
With this change, the context menu items should be checked correctly when you build and run Sketcher again.
Highlighting Elements
Ideally, the user will want to know which element is under the cursor before they right-click 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. 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 do this in the Draw() member function for an element. All you need to do is pass an 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.
Highlights all work in the same way, so 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:
COLORREF m_Color; CRect m_EnclosingRect; int m_Pen;
//Color of an element
//Rectangle enclosing an element
//Pen width
public:
virtual ~CElement(void); // Virtual draw operation
virtual void Draw(CDC* pDC,CElement* pElement=0) {}
CRect GetBoundRect(); |
// Get the bounding rectangle for an element |
protected: |
|
CElement(void); |
// Here to prevent it being called |
};
802

Creating the Document and Improving the View
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 zero is to allow the use of the function with just one argument; the second will be supplied as 0 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:
class CLine : public CElement
{
public:
~CLine(void);
// Function to display a line
virtual void Draw(CDC* pDC, CElement* pElement=0);
// Constructor for a line object
CLine(CPoint Start, CPoint End, COLORREF aColor);
protected: |
|
CPoint m_StartPoint; |
// Start point of line |
CPoint m_EndPoint; |
// End point of line |
CLine(void); |
// Default constructor - should not be used |
};
The implementation for each of the Draw() functions for the classes derived from CElement all need to be extended in the same way. The function for the CLine class is:
void CLine::Draw(CDC* pDC, CElement* pElement)
{
//Create a pen for this object and
//initialize it to the object color and line width of 1 pixel CPen aPen;
COLORREF aColor = m_Color; |
// Initialize with element color |
if(this == pElement) |
// This element selected? |
aColor = SELECT_COLOR; |
// Set highlight color |
if(!aPen.CreatePen(PS_SOLID, m_Pen, aColor))
{
// 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 |
}
803

Chapter 15
This is a very simple change. You set the new local variable aColor to the current color stored in m_Color, and the if statement will reset the value of aColor to SELECT_COLOR when pElement is equal to this — which is the case when the current element and the selected element are the same. You also need to add the definition for SELECT_COLOR to the OurConstants.h file:
//Definitions of constants
#pragma once
//Element type definitions
//Each type value must be unique const unsigned int LINE = 101U; const unsigned int RECTANGLE = 102U; const unsigned int CIRCLE = 103U; const unsigned int CURVE = 104U;
///////////////////////////////////
//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);
///////////////////////////////////
Now you should add an #include directive for OurConstants.h to the CElements.cpp file to make the definition of SELECT_COLOR available. You have nearly implemented the highlighting. The derived classes of the CElement class are now able to draw themselves as selected — you just need a mechanism to cause an element to be selected. So where should you do this? You 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:
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.SetROP2(R2_NOTXORPEN); |
// |
Set the drawing mode |
if((nFlags&MK_LBUTTON) |
&& (this==GetCapture())) |
{ |
|
aDC.DPtoLP(&point); |
// convert point to Logical |
m_SecondPoint = point; |
// Save the current cursor position |
if(m_pTempElement)
{
if(CURVE == GetDocument()->GetElementType()) // Is it a curve?
{// We are drawing a curve
// so add a segment to the existing curve
static_cast<CCurve*>(m_pTempElement)->AddSegment(m_SecondPoint);
804

Creating the Document and Improving the View
m_pTempElement->Draw(&aDC); |
// Now draw it |
return; |
// We are done |
} |
|
aDC.SetROP2(R2_NOTXORPEN); |
// Set drawing mode |
// Redraw the old element so it disappears from the view |
|
|
m_pTempElement->Draw(&aDC); |
|
|
delete m_pTempElement; |
// Delete the old element |
|
m_pTempElement = 0; |
// Reset the pointer to |
0 |
}
//Create a temporary element of the type and color that
//is recorded in the document object, and draw it
m_pTempElement = CreateElement(); |
// |
Create a |
new element |
m_pTempElement->Draw(&aDC); |
// |
Draw the |
element |
}
else
{// We are not drawing an element so do highlighting...
CSketcherDoc* pDoc=GetDocument(); // Get a pointer to the document
CElement* pElement = 0; |
// Store an element pointer |
|
CRect aRect(0,0,0,0); |
// Store a rectangle |
|
POSITION aPos = pDoc->GetListHeadPosition(); |
// Get first element posn |
|
CElement* pOldSelection = m_pSelected; |
// Save old selected element |
|
m_pSelected = 0; |
|
|
while(aPos) |
// Iterate |
through the list |
{ |
|
|
pElement = pDoc->GetNext(aPos); aRect = pElement->GetBoundRect(); aDC.LPtoDP(aRect); aRect.NormalizeRect();
// Select the first element that appears under the cursor if(aRect.PtInRect(point))
{
m_pSelected = pElement;
break; |
|
|
} |
|
|
} |
|
|
if(m_pSelected == pOldSelection) |
// If new selection is same as old |
|
return; |
// |
we are done |
// Unhighlight old selection if there |
is one |
|
if(pOldSelection != 0) |
// |
Verify there is one |
{ |
|
|
aRect = pOldSelection->GetBoundRect();
aDC.LPtoDP(aRect); |
// Convert to device coords |
aRect.NormalizeRect(); |
// Normalize |
InvalidateRect(aRect, FALSE); |
// Invalidate area |
} |
|
// Highlight new selection if there is one if(m_pSelected != 0) // Verify there is one
{
aRect = m_pSelected->GetBoundRect();
805

Chapter 15
aDC.LPtoDP(aRect); |
// Convert to device coords |
aRect.NormalizeRect(); |
// Normalize |
InvalidateRect(aRect, FALSE); |
// Invalidate area |
} |
|
} |
|
} |
|
You only want to deal with highlighting elements when you are not in the process of creating a new element. All the highlighting code can thus be added in a new else clause for the main if. This involves moving the code you had previously to determine the element under the cursor to the new else clause and adding to it.
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 pOldSelection. You then search for an element under the cursor and if there is one, you store its address in m_pSelected.
If pOldSelection and m_pSelected are equal then either they both contain the address of the same element or they are both zero. If they are the same and non-zero, what was already highlighted should stay highlighted so there’s nothing to be done. If they are both zero, nothing was highlighted and nothing needs to be highlighted so there’s nothing to do in this case too. Either way you just return from the function. If they are different, you may have to do something with both.
If pOldSelection is not null then you must un-highlight the old element. The mechanism is the same as before(get the bounding rectangle in device coordinates and pass it to the InvalidateRect() function for the device context. You then check m_pSelected and if it is not null then you have to highlight the element whose address it contains. This again involves getting the bounding rectangle in device coordinates and pass it to the InvalidateRect() function.
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;
POSITION aPos = pDoc->GetListHeadPosition();
CElement* pElement = 0; |
// Store for an element pointer |
while(aPos) |
// Loop while aPos is not null |
{ |
|
pElement = pDoc->GetNext(aPos); |
// Get the current element pointer |
// If the element is visible... |
|
if(pDC->RectVisible(pElement->GetBoundRect())) pElement->Draw(pDC, m_pSelected);// ...draw it
}
}
806

Creating the Document and Improving the View
You only need to change 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 either. 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 is 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(0); |
// |
Redraw all the |
views |
m_pSelected = 0; |
// |
Reset selected |
element ptr |
}
}
The code to delete an element is only executed 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 zero 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
807

Chapter 15
public:
void DeleteElement(CElement* pElement); // |
Delete an element |
|
unsigned int GetElementType() |
// |
Get the element type |
{return m_Element; }
//Rest of the class as before...
};
It accepts a pointer to the element to be deleted as an argument and returns nothing. You can implement it in SketcherDoc.cpp as:
void CSketcherDoc::DeleteElement(CElement* pElement)
{
if(pElement)
{
//If the element pointer is valid,
//find the pointer in the list and delete it POSITION aPosition = m_ElementList.Find(pElement); m_ElementList.RemoveAt(aPosition);
delete pElement; |
// Delete the element from the heap |
}
}
You shouldn’t have any trouble understanding how this works. After making sure that you have a nonnull pointer, you find the POSITION value for the pointer in the list using the Find() member of the list object. You use this with the RemoveAt() member to delete the pointer from the list, then delete the element pointed to by the parameter pElement from the heap.
That’s all you need 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 account 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, you use the value TRUE for when move mode is on, and FALSE for when it’s off. Of course, you could also define it as the fundamental type, bool, and the values are true and false.
You’ll also have to keep track of the cursor during the move, so you can 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:
808