*
MSDN*
Results by Bing
|Developer Centers|Library|Downloads|Code Center|Subscriptions|MSDN Worldwide
Search for


Advanced Search
MSDN Home > MSJ > March 1997
March 1997


Code for this article: C++0397.exe (93KB)
Paul DiLascia is a freelance software consultant specializing in training and software development in C++ and Windows. He is the author of Windows ++: Writing Reusable Code in C++ (Addison-Wesley, 1992).

Q I'm writing a computer-based tutorial that teaches people how to use and troubleshoot the scientific equipment my company produces. I want the user interface to show a picture of the instrument (a gas chromatograph) and let the user click on various parts of it to find out what that part does. I scanned a color photo of the gas chromatograph as a bitmap image and I can display it fine, but I can't figure out how to implement non-rectangular "hot spots." Is there some way to do this? I've seen other apps that seem to do it, but I can't figure out how. Also, I want to display tool tips in the main window as the user moves the mouse over one of the hot spots, like what happens with toolbar buttons. I'm using Visual C++® with MFC 4.0.
Bruce Metger

A There's nothing built into Windows or MFC to do what you want, but it's not too hard to implement. I wrote a program called VIRGIL that displays the painting "Virgil reading the Aeneid to Augustus and Octavia," by the 18th-century artist Taillasson. If you move the mouse over one of the figures in the painting, the name of the figure appears in the status line, and if you leave the mouse there for a moment, a tooltip appears with the character's name (see Figure 1). VIRGIL uses the CDib class from my article, "More Fun with MFC: DIBs, Palettes, Subclassing, and a Gamut of Reusable Goodies" (in MSJ January 1997 and this issue), to display the image, and it uses CPalMsgHandler to handle palette messages to realize virgil.bmp's palette correctly. VIRGIL provides an example of CPalMsgHandler used in a non-doc/view app: there are no doc/view classes in VIRGIL. CMainFrame draws the bitmap directly on its client area.

Figure 1 VIRGIL with a tooltip
Figure 1 VIRGIL with a tooltip

As for implementing the non-rectangular hot spots, the idea is fairly simple. In addition to the image itself, VIRGIL maintains another, hidden bitmap, which I call the "mask" (see Figure 2). The mask is a sort of paint-by-numbers picture where each person (mouse-detectable region) is painted a different color. To figure out who the mouse is pointing to at any time, all I have to do is look at what color the corresponding pixel in the mask is, and then look this color up in a table that identifies which character or object it is.
Figure 2 The mask
Figure 2 The mask

I used the Windows 95® Paint program to create the mask. I started with a copy of the original image, then edited it until I had what you see in Figure 2. I made Virgil red, Augustus dark green, and Octavia's Maiden blue. The colors you use aren't important, but each region must have a unique color. You may want to avoid light grey and dark grey—RGB(192,192,192) and RGB(128, 128,128)—because, under certain circumstances, some Win32 API functions will remap these colors to current screen colors. (If you use my CDib class, this shouldn't be a problem.) Once I created the mask bitmap, I added it along with the original image as BITMAPs in my resource (RC) file.

    IDB_BITMAP     BITMAP  MOVEABLE PURE      
                            "res\\virgil.bmp"
    IDB_BITMAPMASK BITMAP  MOVEABLE PURE   
                            "res\\mask.bmp"
As for implementing the code, there are two basic tasks. First is figuring out the name of the character where the mouse is; second is using it in tooltips. Since tooltips are a separate issue, I started by implementing the status bar indicator you see in Figures 1 and 2. Status bar indicators are easy in MFC. All you have to do is give your indicator an ID and then implement an ON_COMMAND_UPDATE_UI handler for it.

 BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
 •
 •
 •
 ON_UPDATE_COMMAND_UI(ID_INDICATOR_COLOR, OnIndicatorColor)
 END_MESSAGE_MAP()
 
 void CMainFrame::OnIndicatorWho(CCmdUI* pCmdUI) 
 {
    CPoint pt;
    GetCursorPos(&pt);               // current mouse pos
    ScreenToClient(&pt);             // in client coords
    pCmdUI->SetText(GetWhoItIs(pt)); // find who and set text
 }
MFC packages the status line indicator into a special CCmdUI object and calls your handler during idle processing, so all you have to do to change the indicator text is call CCmdUI::SetText. GetWhoItIs is the VIRGIL function that does the work to figure out which character, if any, is under a given point in the bitmap; it returns the name of the character as a CString. Figure 3 shows the code (what's not shown here is in my article in this issue and in the downloadable source code). First GetWhoItIs creates a memory device context compatible with the screen, then it selects the mask bitmap into it. It does this so it can call CDC::GetPixel to get the color of the pixel under the point.
In theory, you don't have to create a DC because you can look at the bitmap bits directly. In practice, however, it's a little tricky and error-prone because the bitmap pixels are arranged in a backwards order (from bottom to top) and you have to take into account the possibility of extra padding at the end of each row, which Windows sometimes uses to make each new row of pixels start on a word boundary. It's a lot simpler to just select the bitmap into a device context and call GetPixel. If your app is super performance-conscious, you might consider getting the color directly from the bitmap bits.
Once CMainFrame::GetWhoItIs has the color of the pixel in the mask bitmap that corresponds to where the mouse is, it does a simple linear search through a static table to find the entry with the color that matches. The table is what links colors in the mask with people in the painting. There's no way I can think of to generate it automatically; you just have to type the correct RGB values into the table by hand. As a debugging aid (and so you can see what's going on behind the scenes), I gave VIRGIL a View Mask command to let you view the mask instead of the image. When the mask is displayed, I show the x, y position and RGB color value as part of the status indicator/tooltip text. This makes it easy to see what you're doing—what the RGB values of your colors are—as you enter the data for the table. Once your program is ready to ship, you can disable View Mask.
Since all I have to do in VIRGIL is display the name of the character or object, I made GetWhoItIs return a CString. In your tutorial application, you might return a pointer to a structure that contains more information about what part of your gas chromatograph it is. All you really need is an ID from which you can then get the information elsewhere. Also, once you have GetWhoItIs (or GetWhatItIs) working, you can use it in OnLButtonDown or OnLButtonDblClk handlers to do something when the user clicks an object in the image—like go to that part of the tutorial. I didn't bother to implement mouse clicking for VIRGIL since that's the easy part.
Once I got the status bar indicator working, proving that VIRGIL could successfully identify objects in the painting, I set out to implement the tooltip feature you asked for. While I used a few tricks to make the tooltips work right for VIRGIL's non-rectangular regions, I want to emphasize that tooltips are a totally separate issue.
Dealing with tooltips can be a real pain in C, but MFC makes it easy. You don't have to create a tooltip window or CToolTipCtrl object or anything like that. All you have to do is call CWnd::EnableToolTips to enable tooltips, override the virtual function CWnd::OnToolHitTest, and write a WM_NOTIFY handler for TTN_NEEDTEXT. MFC maintains a single, thread-global tooltip control for each thread in your app. It's the same tooltip control MFC uses to display toolbar button tips, if you use the CBRS_TOOLTIPS style and remember to add the tooltip text at the end of each menu prompt string, separated from the prompt by a newline as in "Open the file\nOpen".
But toolbar buttons aren't the only thing MFC uses the tooltip control for. If you enable tooltips for one of your windows (by calling CWnd::EnableToolTips), MFC calls that window's OnToolHitTest function whenever it may be time to display a new tip—that is, whenever the user moves the mouse someplace new.

 int CWnd::OnToolHitTest(CPoint pt, TOOLINFO* pTI) const
MFC calls your function with the current mouse location as a CPoint and a pointer to a TOOLINFO struct where you can supply more information to the tooltip control. If the mouse is "somewhere," you can return any integer value (nHit) you want that identifies the "somewhere" to your own app. Any value except -1, which means "nowhere"—that is, the mouse is not on a tool for which there is a tip.
For VIRGIL, CMainFrame::OnToolHitTest returns -1 if the point is not over one of the characters (GetWhoItIs returns an empty string). If the mouse is over one of the characters, CMainFrame returns the x, y coordinates of the point as an integer, with y in the high-order word and x in the low one. Why did I do it this way? The obvious thing would be to assign IDs like Virgil=1, Augustus=2, and so on and return those. But if I do that, MFC won't redisplay the tooltip if the user moves the mouse elsewhere inside the same person. If OnToolHitTest returns the same nHit value as it did the last time around, MFC won't display the tip. You can observe this behavior in any MFC program with toolbar buttons: move the mouse into one of the buttons and wait until the tooltip appears. Wait a few seconds more, until it goes away. Now, if you move the mouse around inside the button and stop, the tooltip won't reappear no matter how long you wait. This is either a bug or a feature, depending on your outlook. In any case, for VIRGIL I wanted the tooltip to reappear when the user moves the mouse within one of the characters, so I returned the point coded as an integer, which is obviously unique for every point in the painting. Once again, it doesn't matter what value you return, as long a it's not -1.
In addition to returning the hit code (nHit), you also have to fill out the TOOLINFO struct. Here's how VIRGIL does it in CMainFrame::OnToolHitTest:

 int nHit = MAKELONG(pt.x, pt.y);
 pTI->hwnd = m_hWnd;
 pTI->uId  = nHit;
 pTI->rect = CRect(CPoint(pt.x-1,pt.y-1),CSize(2,2));
 pTI->uFlags |= TTF_NOTBUTTON;
 pTI->lpszText = LPSTR_TEXTCALLBACK;
Most of this is obvious—like setting hwnd and uId—but some of it is less so. I set the rect member to a 2-pixel-wide, 2-pixel-high rectangle centered around the mouse location. The tooltip control uses this rectangle as the bounding rectangle of the "tool," which I want to be tiny, so moving the mouse anywhere will constitute moving outside the tool. I set TTF_NOTBUTTON in uFlags because the tooltip is not associated with a button. This is a special MFC flag defined in afxwin.h; MFC uses it to do help for tooltips. There's another MFC-extended flag for tooltips, TTF_ALWAYSTIP. You can use it if you want MFC to display the tip even when your window is not active.
You may have noticed that so far I haven't told MFC or the tooltip or the TOOLINFO what the actual text of the tip is. That's what LPSTR_TEXTCALLBACK is for. This special value tells the tooltip control (the internal, thread-global one that MFC uses) to call my window back to get the text. It does this by sending my window a WM_NOTIFY message with notification code TTN_NEEDTEXT. My CMainFrame handler for this is trivial—now that I've already implemented GetWhoItIs!

 void CMainFrame::OnGetTooltipText(NMHDR* pNMHDR, LRESULT* plRes)
 {
    TOOLTIPTEXT& ttt = *((TOOLTIPTEXT*)pNMHDR);
    strncpy(ttt.szText,
            GetWhoItIs(CPoint (pNMHDR->idFrom)),
            sizeof(ttt.szText));
 }
That is, just find out who it is and copy the text into the TOOLTIPTEXT structure. Notice that the point is coded as the idFrom field in NMHDR—because that's what I set as the uId field in the TOOLINFO structure when MFC called my OnToolHitTest.
So there you have it. Non-rectangular hot regions in a bitmap. Tooltips anywhere in your window.
Before I say goodbye, I should point out a different style of tooltipping. The default implementation for CWnd::OnToolHitTest figures out which child window is under the mouse, and returns that child's ID as the nHit value. It also sets uId to the child HWND and uFlags to TTF_IDISHWND (ID is HWND) in the TOOLINFO. What does this mean and why should you care? MFC's default implementation provides an even easier way to get tooltips in one particular situation. If you have a window with child controls/windows and you want to implement tooltips for each child window, you don't have to write OnToolHitTest; all you have to do is enable tooltipping and implement a TTN_NEEDTEXT handler that gets the text for whatever window the mouse is over. In this case, because uFlags has TTF_IDISHWND (ID is HWND) set, the idFrom field in NMHDR is the HWND of the child window, not its ID.

Q How can I display a 24-bit color bitmap in a dialog box using MFC? I am trying to write a fancy About dialog that displays a photo of the developers who created our product. I selected the Picture control (static bitmap) in Developer Studio, and gave the name of my bitmap file as the resource name. The bitmap displays fine in Developer Studio, but when I run my program, all the colors aren't there and it looks really junky. I've seen other apps that have good-looking bitmaps in their About dialogs. How do they do it?

Thomas Angels

A They do it by drawing the bitmap manually, which requires writing a lot of code—unless you use the CDib class referred to in my articles, "More Fun with MFC: DIBs, Palettes, Subclasses and a Gamut of Reusable Goodies" (MSJ January 1997 and this issue). CDib is a general-purpose class for loading and displaying Device Independent Bitmaps (DIBs), the kinds that have lots of colors. My article also explains why your bitmap is missing its colors: because you haven't realized its palette. Unfortunately, static bitmap controls aren't smart enough to do it for you. So the first thing you have to do is get the January 1997 issue out right now and read my article.

Figure 4 Bitmap in VIRGIL's About dialog
Figure 4 Bitmap in VIRGIL's About dialog

Read it? Good. Now I'll show you how to put CDib to work in a dialog box. I added a bitmap to the About box in the VIRGIL program from the preceding question. Figure 4 shows the dialog box and Figure 3 shows the code. The first thing I did was derive a new class, CDibCtl. This control holds a CDib as a data member.

 class CDibCtl : public CStatic {
    CDib m_dib;
    •
    •
    •
 };
The constructor calls CDib::Load to load the bitmap from the resource file.

 CDibCtl::CDibCtl(UINT nID)
 {
    m_dib.Load(AfxGetResourceHandle(), nID);
 }
For ordinary apps like VIRGIL, AfxGetResourceHandle is the same as AfxGetInstanceHandle, but for DLLs it's the handle of the DLL, not the EXE, so you should get into the habit of using AfxGetResourceHandle when you load resources.
The only other function of significance in CDibCtl is OnPaint.

 void CDibCtl::OnPaint()
 {
    CPaintDC dc(this);
    m_dib.Draw(dc);
 }
Pretty trivial. All it does is call CDib::Draw. You can see how useful it is to have a class like CDib, instead of implementing DIB drawing as open code. This is where encapsulation really starts to pay off.
If you read my article, you know that drawing the DIB is only half the story. You also have to realize the DIB's palette. This is best done in the dialog, not the static control. Why? Because it's the dialog that receives focus when switching from one app to another, not the static control within the dialog. So I added a CPalMsgHandler to my CAboutDialog and hooked it up to the DIB's palette in my OnInitDialog handler.

 BOOL CAboutDlg::OnInitDialog()
 {
    BOOL bRet = CDialog::OnInitDialog();
    m_ctlDib.SubclassDlgItem(IDB_BITMAP1, this);
    m_palMsgHandler.Install(this, m_ctlDib.GetPalette());
    return bRet;
 }
I didn't have to add a CPalMsgHandler to the main frame class (CMainFrame) because I already had one there to handle palette messages for drawing the image.
That's all there is to it. Now the bitmap appears in its true colors, as in Figure 4. And if, while the About dialog is displayed, the user decides to visit another app that changes the palette, the dialog will realize its palette correctly in the background. When the user returns to the dialog, it will rerealize the bitmap's palette in the foreground. All this magic happens courtesy of CPalMsgHandler. Without it, you'd have to write many many lines of palette-handling code and duplicate it in CAboutDialog as well as CMainFrame. With CPalMsghandler, all you do is instantiate the class and hook it up. Amazing.

Have a question about programming in C or C++? Send it to askpd@pobox.com

From the March 1997 issue of Microsoft Systems Journal. Get it at your local newsstand, or better yet, subscribe.

© 1997 Microsoft Corporation. All rights reserved. Legal Notices.

© 2014 Microsoft Corporation. All rights reserved. Contact Us |Terms of Use |Trademarks |Privacy & Cookies
Microsoft