Add member variables to the view class. These are only used to remember the state of the view, so they do not need to be stored in the document. It would not be wrong to add them to the document, but they are not needed there. If you were to retrieve data from a file, it would not make sense to set the state of these variables with values from the file. These variables only deal with the current actions of the user.
bool m_bCapture; //Initialize it as false
CPoint m_pntStart; //Initialize it as (0,0)
Add the size of the rectangle as a variable to the document.
Note: By making this a variable, it is possible to make the size of
the rectangle change in response to some type of user input.
Challenge: When the control key is down while pressing the right-mouse
button, resize the rectangle according to the distance that the mouse moves.
CSize m_szRect; //Initialize it as (20,20)
When the right mouse button is pressed, test if the current point is in first
rectangle. If so, then capture mouse and set the state variable,
m_bCapture
, to true. Save the current point in the variable
m_pntStart
.
void CRecsView::OnRButtonDown(UINT nFlags, CPoint point) { if (GetDocument()->m_pointIndex == 0) return; CRect rect(GetDocument()->m_points[0], GetDocument()->m_szRect); if (rect.PtInRect(point)) { m_bCaptured = true; m_pntStart = point; SetCapture(); } }
Add a helper method that invalidates the old rectangle, calculates the new
point, invalidates the new rectangle, and sets the starting point to the
current point. This will have the effect of erasing the old rectangle and
drawing the new rectangle.
Experiment: Try commenting out one or the other of the
InvalidateRect
calls. You will see that the old rectangle is
not erased, or that part of the new rectangle is not drawn. Also, try commenting
out the last statement. You will notice that the reactangle drags at a faster
and faster rate. Can you explain why these things happen?
void CRecsView::InvalidateOldAndNew(CPoint point) { CRect rect(GetDocument()->m_points[0], GetDocument()->m_szRect); InvalidateRect(&rect); GetDocument()->m_points[0] += point - m_pntStart; rect = CRect(GetDocument()->m_points[0], GetDocument()->m_szRect); InvalidateRect(&rect); m_pntStart = point; }
If the mouse is captured as it moves, call the helper method to invalidate the old rectangle and the new rectangle.
void CRecsView::OnMouseMove(UINT nFlags, CPoint point) { if (m_bCapture) { InvalidateOldAndNew(point); } }
On mouse up, call the helper method, release mouse, and clear
m_bCapture
void CRecsView::OnRButtonUp(UINT nFlags, CPoint point) { if (m_bCaptured) { InvalidateOldAndNew(point); m_bCaptured = false; ::ReleaseCapture(); } }
Example: Recs
If a mapping mode other than MM_TEXT
is being used, then care
must be taken to be sure that comparisons are made using coordinates in the
correct mode. The only methods that use logical coordinates are the methods
that are part of the device context. This means that the CPoint
parameter of the OnRButtonDown
method is in device coordinates,
not logical coordinates. It also means that InvalidateRect
expects
device coordinates, not logical coordinates. On the other hand, the
Rectangle
method of the device context expects logical coordinates.
Note: It is also preferable to store logical coordinates in the document,
especially if a scroll view is being used. Can you explain why?
For these reasons, the following changes must be made to the application
if MM_HIENGLISH
is used instead of MM_TEXT
.
Initialize the CSize
in the document as (200, -200).
Note: The x- and y-coordinates in MM_HIENGLISH
agree
with the traditional Cartesean coordinate system.
Override OnPrepareDC
and set the maping mode in it
void CRecsView::OnPrepareDC(CDC* pDC, CPrintInfo* pInfo) { // TODO: Add your specialized code here and/or call the base class pDC->SetMapMode(MM_HIENGLISH); }
Modify OnLButtonDown so that the point is saved in the document in logical coordinates. It is necessary to create a client DC and prepare it so that it is similar to the DC that is used when drawing.
void CRecsView::OnLButtonDown(UINT nFlags, CPoint point) { CRecsDoc *pDoc = GetDocument(); // don't go past the end of the 100 points allocated if (pDoc->m_pointIndex == 100) return; CClientDC dc(this); OnPrepareDC(&dc); CPoint pointLP(point); dc.DPtoLP(&pointLP); //store the click location pDoc->m_points[pDoc->m_pointIndex] = pointLP; pDoc->m_pointIndex++; Invalidate(); }
Modify OnRButtonDown
so that the starting point is saved in
logical coordinates and the rectangle is in device coordinates. If arithmetic
is being done between two points or between a point and a rectangle, then
it is important that they are all in the same coordinate system. The starting
point will be added to the top-left point of the rectangle when the rectangle
when it is dragged. Since the top left point is stored in the document, it
should be stored in logical coordinates. The rectangle should be translated
to device, since it is being tested against a device point.
Note: It would be possible to do the arithmetic for the point
in device coordinates, but then the final result would have to be translated
back to logical coordinates to be stored in the document. By changing to
logical in the first place, an additional translation is avoided. It would
also be possible to leave the rectangle in logical and use the logical point
to do the hit testing. See the Experiment below for a discussion of
this technique.
void CRecsView::OnRButtonDown(UINT nFlags, CPoint point)
{ // TODO: Add your message handler code here and/or call default if (GetDocument()->m_pointIndex == 0) return; CRect rect(GetDocument()->m_points[0], GetDocument()->m_szRect); CClientDC dc(this); OnPrepareDC(&dc); dc.LPtoDP(&rect); CPoint pointLP(point); dc.DPtoLP(&pointLP); if (rect.PtInRect(point)) { m_bCaptured = true; m_pntStart = pointLP; this->SetCapture(); } }
Experiment: It would seem that extra work is being done here. It is
necessary to change the point into logical, but why is it necessary to change
the rectangle to device? Why not test if the logical point is contained in
the logical rectangle, instead of testing if the device point is in the device
rectangle? This could be done be commenting out
dc.LPtoDP(&rect);
and changing the PtInRect
test to
rect.PtInRect(pointLP)
Try it! You will see that the code doesn't work: the rectangle cannot be
dragged. The problem is with the logical coordinate system. Since the
PtInRect
method is not part of a DC, it expects a coordinate
system that is organized like device coordinates: positive y moves down the
screen. Because of this, it expects a rectangle defined with the top-left
x-coordinate less than the bottom-right x-coordinate, and the top-left
y-coordinate less than the bottom-right y-coordinate. That is not the case
if the logical rectangle is used. The solution is to replace
dc.LPtoDP(&rect);
with a call to
rect.NormalizeRect();
which will define the same rectangle, but will adust the top-left and
bottom-right points to agree with the expected coordinate system.
Modify InvalidateOldAndNew
so that device coordinate rectangles
are sent to InvalidateRect
. It is necessary to create a client
DC and prepare it so that it is similar to the DC that is used when drawing.
The code is almost identical to the original method, except for the calls
to LPtoDP
and the call to DPtoLP
. The decision
about which to use is based upon how the reactangle or point will be used.
If it is being saved in the document, then be sure to translate to logical,
if it is being used in a method that is not part of a DC, then translate
to device.
void CRecsView::InvalidateOldAndNew(CPoint point) { CClientDC dc(this); OnPrepareDC(&dc); CRect rect(GetDocument()->m_points[0], GetDocument()->m_szRect); dc.LPtoDP(&rect); InvalidateRect(&rect); CPoint pointLP(point); dc.DPtoLP(&pointLP); GetDocument()->m_points[0] += pointLP - m_pntStart; rect = CRect(GetDocument()->m_points[0], GetDocument()->m_szRect); dc.LPtoDP(&rect); InvalidateRect(&rect); m_pntStart = pointLP; }
Example: RecsLP
Challenge: Can you modify the code so that any one of the rectangles can be dragged?