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.

Comments
Not really the case
Posted by FateLinegod on 10/13/2005 11:40amThis is so stupid and non-sexy LOL. I could develop a GUI action script protocol that could easily solve this algorithm without using GUI ROP2 CODE. You code Guru Comi's are responsible for action x control statements and Marcel deletion.
-
ReplyFate Farted
Posted by FateLinegod on 10/13/2005 11:42amOMG FART LINEGOD! SHUT UP TEE-HEE!
ReplyWorks well for me
Posted by Legacy on 10/06/2003 12:00amOriginally posted by: phil cunningham
it works great and solves something that's given me headaches for months.
Many thanks for the code.
Phil
ReplyHow about overlapped text?
Posted by Legacy on 06/03/2003 12:00amOriginally posted by: hero3blade
I don't think it's applicable to two texts which have overlapped region.
Reply
pdc->SetROP2(R2_NOTXORPEN) shouldn't be used with TextOut
Posted by Legacy on 03/24/2003 12:00amOriginally posted by: Echo
That's why your first code snippet does not work. Actually SetROP2() is used with drawing functions like LineTo(), Rectangle(), NOT with text functions.
ReplyTo erase text, I use GetTextExtent() to get the size of the text, and InvalidateRect() to make window call OnPaint() to update the area. It's an easier way.
But if the situation is complicated, such as the background of the text isn't painted by OnPaint(), Save & Copy back the bitmap is the only way.
XOR is a better technique
Posted by Legacy on 07/27/2001 12:00amOriginally posted by: Floppsy
Yes, I agree with Steven's comments. XOR is a better technique to draw a cross hair. This way, described by Mr. Brunzema seems a bit tedious, kludgy, and hard.
Replydifferent method
Posted by Legacy on 07/17/2001 12:00amOriginally posted by: Steven XXX
i had done what u are doing, but my method is different.
Replyinstead of backup the image, while moving the crossline, i actually draw the crossline using XOR function to the background image, and save the coord. to draw a new crossline, i just erase the old crossline using the XOR function again, and then draw the new crossline again using the same XOR function. although the color of the crossline might differ depending on the background, this method has an advantage of alway visible crossline.