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

 



Comments

  • Not really the case

    Posted by FateLinegod on 10/13/2005 11:40am

    This 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.

    • Fate Farted

      Posted by FateLinegod on 10/13/2005 11:42am

      OMG FART LINEGOD! SHUT UP TEE-HEE!

      Reply
    Reply
  • Works well for me

    Posted by Legacy on 10/06/2003 12:00am

    Originally posted by: phil cunningham

    it works great and solves something that's given me headaches for months.

    Many thanks for the code.

    Phil

    Reply
  • How about overlapped text?

    Posted by Legacy on 06/03/2003 12:00am

    Originally 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:00am

    Originally 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.
    To 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.

    Reply
  • XOR is a better technique

    Posted by Legacy on 07/27/2001 12:00am

    Originally 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.

    Reply
  • different method

    Posted by Legacy on 07/17/2001 12:00am

    Originally posted by: Steven XXX

    i had done what u are doing, but my method is different.
    instead 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.

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • IBM Worklight is a mobile application development platform that lets you extend your business to mobile devices. It is designed to provide an open, comprehensive platform to build, run and manage HTML5, hybrid and native mobile apps.

  • New IT trends to support worker mobility — such as VDI and BYOD — are quickly gaining interest and adoption. But just as with any new trend, there are concerns and pitfalls to avoid.  Download this paper to learn the most important considerations to keep in mind for your VDI project.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds