Crosshairs and Supporting Text That Moves

Environment: [VC6, MFC,  SP4, NT4/2000]

Skill Level:
Beginner, Intermediate

 

Introduction

Not long ago, I was asked to implement a crosshair functionality in an OCX
control that displays graphical data. Recalling that I saw something on
CodeGuru, I found Paul Shaffers article ROP
Codes, Rubber Bands, Clip Regions & Coordinate Transforms
, which shows
how to draw Rubberbands and which explores GDI map modes and coordinate transforms.
I wanted a functionality as shown in the above picture that is: when users move
the cursor, the X and Y coordinate values should change as the cursor is
moved. Easy I thought, just extend the technique of drawing with an R2_NOTXORPEN
ROP2 code. It turns out that things were not quite that simple.

Problems with TextOut and R2_NOTXORPEN

Drawing the crosshair per-se turned out to be fairly easy. The problems
started appearing when I tried to erase text by simply writing it twice using
the TextOut function as in the code snippet below:


pdc->SetROP2(R2_NOTXORPEN);
pdc->SetMapMode(MM_TEXT);
pdc->SetTextAlign(TA_TOP | TA_LEFT);
pdc->SetBkMode(TRANSPARENT);
pdc->SetTextColor(RGB(255,255,255));
pdc->SetBkColor(0);
pdc->TextOut(10,10, _T("Test"));
// erasing does not work, it just gets overwritten
pdc->TextOut(10,10, _T("Test"));

It turns out that writing the text twice using TextOut() does not work. Then how do you
move text interactively?

Saving the background

My solution to the problem is to save the area of the DC to be
overwritten to a bitmap; only after saving is the text drawn. When it
comes time to erase the text, the area occupied by the text is restored by
copying it back from the previously saved bitmap. This technique will also work if you
need to drag text (or any other graphics) and show the text while it is being dragged.
The two functions below save and restore an area of screen to/from the
display. Note that the bitmaps need to be saved between messages, so I use
a CBitmap* member variable.


void CDrawTextCtrl::SaveAreaRectangleToBitmap(CDC* pdc, CRect& rc,
CBitmap** ppBitmap)
{
CDC dcMem; // memory dc

// initialize other memory dc
dcMem.CreateCompatibleDC(pdc);

// Create a compatible bitmap.
*ppBitmap = new CBitmap;
(*ppBitmap)->CreateBitmap(rc.Width(),
rc.Height(),
pdc->GetDeviceCaps(PLANES),
pdc->GetDeviceCaps(BITSPIXEL),
NULL) ;

// Select the bitmap into the memory DC.
CBitmap* pOldBitmap = dcMem.SelectObject(*ppBitmap);

// Blt the memory device context to the screen.
dcMem.BitBlt( 0, // dst X
0, // dst Y
rc.Width(), // dst width
rc.Height(), // dst height
pdc, // source DC
rc.left, // source starting X
rc.top, // source starting Y
SRCCOPY) ;

// cleanup
dcMem.SelectObject(pOldBitmap);
}

void CDrawTextCtrl::CopySavedAreaRectangleBack(CDC *pdc,
CBitmap* pBitmap, CRect& rcOld)
{
CDC dcMem; // memory dc

// at first, we need to copy the background from
// our saved bitmap back to the screen DC

// initialize other memory dc
dcMem.CreateCompatibleDC(pdc);

// Select the bitmap into the memory DC.
CBitmap* pOldBitmap = dcMem.SelectObject(pBitmap);

// Blt the bitmap to screen
pdc->BitBlt( rcOld.left,
rcOld.top,
rcOld.Width(),
rcOld.Height(),
&dcMem,
0,
0,
SRCCOPY);

// cleanup
dcMem.SelectObject(pOldBitmap);
}

Junk crosshair left behind when resizing, losing focus and other small
problems

A number of details gave problems while writing the code. At first
I did not erase the crosshair before copying the background to the bitmap.
This yielded junk lines when moving the mouse quickly. If
a users resized the control, usually an ‘old crosshair’ would be left and shown
with the new crosshair after resizing. The solution to this problem was to
erase the crosshair when starting to size the control. This was done by simply
forcing a redraw of the control and by deleting the saved
bitmaps. While trying to take screenshots of the completed
control, I switched between running applications and found another problem. Two crosshairs
were appearing after switching back to the test control container and moving the
mouse. Also, I noticed that the crosshair was still being drawn in the
ActiveX test control container even when another program had the focus. Playing
with the program, other focus and redrawing-related problems that needed
solving. Finally when moving to the bottom right of the control, labels
would run into each other, and in certain other boundary cases, the
crosshairs would be drawn through one of the labels. Below the code that
calculates the rectangle where the label for the X crosshair is drawn


// calculate the rectangle where the text will appear,
// leave 2 pixels for space. Check for enough space on right
// and make sure we don’t run through anything

if (point.x + sizeTextX.cx + 2 > m_rcCtl.right )
{
// not enough space, text will run into right side
// so paint to the left
rcX.SetRect(point.x – 2 – sizeTextX.cx,
m_rcCtl.bottom – sizeTextX.cy,
point.x – 2,
m_rcCtl.bottom);
}
else // enough space
{
rcX.SetRect(point.x + 2,
m_rcCtl.bottom – sizeTextX.cy,
point.x + sizeTextX.cx + 2,
m_rcCtl.bottom);
// does the horizontal crossline go through the X label, if so offset
if (point.y > rcX.top)
rcX.OffsetRect(0, -sizeTextX.cy);
}

Conclusion

This project is a perfectly good example why estimating software projects is
so difficult – I had initially thought I could provide the functionality in one
hour or so whereas the final debugged project took about eight times longer.
This article shows how to draw crosshairs, but more importantly how to draw text
and other GDI objects that need to be moved (and redrawn) interactively by the
user. Below the code that determines the rectangle where to place the label for
the X crosshair position.

Possible Improvements

  • Hit testing of the labels could be improved, in some cases the labels
    still run into each other.

 

Downloads


Download source – 16 Kb

 

More by Author

Must Read