This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.


October 1998

Microsoft Systems Journal Homepage

C++ Q&A

Download Oct98CQA.exe (39KB)

Paul DiLascia is the author of Windows ++: Writing Reusable Code in C++ (Addison-Wesley, 1992) and a freelance consultant and writer-at-large. He can be reached at askpd@pobox.com or http://pobox.com/~askpd.

Q I'm developing an ActiveX® control that works somewhat like the Outlook bar. I need to create a mouseover feature for my control. I handle WM_MOUSEMOVE to determine when the user has moved the mouse over the item. It works fine, but sometimes if the user moves the mouse too fast, I don't get a WM_MOUSEMOVE because the mouse has left the window. How can I know when the user has moved the mouse outside my window?
Jakob Praher

A Yes, it's true, mouse motion is not continuous! Nothing in computers or life ever is, not even the movement of electrons in your CPU. As the user moves the mouse across the screen, Windows® sends WM_MOUSEMOVE messages for various points along the way, to the windows under those points. So even if your window gets WM_MOUSEMOVE messages, there's no way to know when the user moves the mouse outside your window, because you never know which WM_MOUSEMOVE was the last one. It's a little like that joke where the woman asks a man on the bus which stop to get off for the museum and he says, "Oh, that's easy. Get off one stop before I do."
      Because of this discontinuity of mousemove messages, you can't even use WM_MOUSEMOVE (alone) to implement the mouseover feature for a region that lies entirely within your window. To see why, suppose the mouse is over the item and it's highlighted, and then the user very quickly moves the mouse away, completely outside your window. Your window may not get another WM_MOUSEMOVE message; if the user is fast, the next WM_MOUSEMOVE message will go to some other window, leaving your item in its highlighted state. This is not very good, and definitely not the way to get a check when bonus time rolls around again.
      But you know there must be some way to do it, because there are plenty of apps and ActiveX widgets with mouseover that work fine no matter how fast the user moves the mouse—unless she moves it so fast as to detach it from the computer before the electrons can make their quantum way down the mouse's tail (which, needless to say, isn't very likely). So how do these other apps implement mouseover?
      When I first considered this question, I thought: easy, just capture the mouse. No, that doesn't mean set a trap with a piece of cheese. It means calling ::SetCapture when you go into mouseover mode. When you call SetCapture, Windows sends all mouse messages to your window, regardless of the mouse's location.
      But I soon realized I hadn't adjusted my thinking cap for Win32®. Mouse capture is one of those few things that changed significantly in the jump from 16 to 32 bits. As the updated documentation for SetCapture says, "This function cannot be used to capture mouse input meant for another process." (See, sometimes the docs are actually helpful!) In other words, it won't work for finding out when the mouse leaves your window, because as soon as it does, some other process is likely to get the message. In general, apps can't just go grabbing the mouse the way they could in Windows 3.1. (If they could, a bad app could block all other apps by capturing the mouse and never releasing it.)
      So how can you find out when the mouse leaves your window? Well it turns out there's a new function called TrackMouseEvent that notifies you when the mouse hovers over a window for a certain amount of time or—and this is just the ticket for mouseover—when the mouse leaves a window. First, you fill out a small structure, then you call the function:


 TRACKMOUSEEVENT tme;
 tme.cbSize = sizeof(tme);
 tme.dwFlags = TME_LEAVE;
 tme.hwndTrack = m_hWnd;
 TrackMouseEvent(&tme);
Now when the user moves the mouse outside the window m_hWnd (that's your window, Sherlock), Windows posts your window a WM_MOUSELEAVE message. It's a typical Windows function with a weirdo struct and weirdo flags—but hey, what do you expect? It doesn't get any easier than this. When you get the WM_MOUSELEAVE message, you can check to see whether you need to unhighlight some item that was highlighted.
      So what's the catch? The catch is that TrackMouseEvent doesn't exist in Windows 95, only in Windows 98 and Windows NT®. That's right. So unless you're writing an app just for Windows 98 or Windows NT, you're out of luck. If you are, then by all means use TrackMouseEvent; but if you have to support Windows 95, then it's back to the drawing board. So many operating systems... Sigh.
      The only surefire way to find out exactly when the mouse leaves your window in Windows 95 is to install a WH_ MOUSE hook to spy on mouse messages and detect when the mouse has left your window. Since the message you're looking for (WM_MOUSEMOVE) is targeted for a different window and thus potentially a different process, you have to install a system hook, not just a local one. This means you have to put the hook function in a DLL.
      I don't mind writing several hundred lines of code if I have to, but I would do almost anything rather than implement a DLL. That's because I'm a good programmer, and good programmers are lazy. Besides, installing a global mouse hook is fraught with peril. So the best thing to do is swallow your pride and look for a kludge. In this particular situation, that means using a timer. You don't really need to know exactly when the mouse leaves your window; you'll settle for knowing within, say, 50 milliseconds. So you can set a timer to wake up every 50 milliseconds and take a look-see where the mouse has run off to.
Figure 1 Flyby Test App
Figure 1 Flyby Test App
      I wrote a program called Flyby that draws a blue ellipse with the mouseover feature (see Figure 1). The ellipse turns red when you move the mouse over it. Flyby uses the set-a-timer kludge and implements the mouseover feature for a nonrectangular region (see Figure 2). The main action takes place in three functions: OnMouseMove, TrackMouseLeave, and OnTimer.
      When the user first moves the mouse inside the window, OnMouseMove checks to see if the mouse is in the ellipse:

 void CMyView::OnMouseMove(UINT nFlags, CPoint pt)
 {
     BOOL bHighlighted = m_hotRegion.PtInRegion(pt);
 
     if (bHighlighted != m_bHighlighted) {
         m_bHighlighted = bHighlighted;
         Invalidate(FALSE);
         UpdateWindow();


         TrackMouseLeave(m_bHighlighted);
     }
 }
      I implemented the ellipse as an elliptic CRgn, stored in the data member m_hotRegion. This region is updated in OnSize whenever the user sizes the window. If the mouse has moved into or out of the ellipse since the last mousemove event, OnMouseMove updates the highlight status, repaints the window, and calls TrackMouseLeave to start mouse tracking.

 BOOL CMyView::TrackMouseLeave(BOOL bTrackLeave)
 {
     if (bTrackLeave != m_bTrackLeave) {
         if (bLeave)
             SetTimer(1, 50, NULL);
         else
             KillTimer(1);
         m_bTrackLeave = bTrackLeave;
     }
     return m_bTrackLeave;
 }
      Tracking the mouse means setting a timer; conversely, turning tracking off means killing this timer. When the timer fires, my view looks to see where the mouse is:

 void CMyView::OnTimer(UINT nIDEvent)
 {
     // Get mouse pos in client coords
     CPoint pt = Mouse; // see below
     ScreenToClient(&pt);


     if (!m_rcClient.PtInRect(pt))
         SendMessage(WM_MOUSEMOVE, 
             0, MAKELONG(pt.x, pt.y));
 }
If the timer detects that the mouse has left the window entirely, the view sends itself a WM_MOUSEMOVE message with coordinates outside the window. In this case, OnMouseMove will do the right thing—namely, unhighlight the ellipse if it isn't unhighlighted already, and turn tracking off. Sending WM_MOUSEMOVE here is somewhat unorthodox; it would be safer to send a separate WM_MYMOUSELEAVE message since you might have other stuff in your OnMouseMove handler that assumes the point passed is inside the client area, which it normally is. The last piece of the picture is my view's OnDraw function, which draws the ellipse using a red or blue brush, depending on whether the mouse is inside or outside the ellipse.
      I used one little trick from my book, Windows++: Writing Reusable Windows Code in C++ (Addison-Wesley, 1992), a CMouse class that lets me represent the mouse as a global C++ object. CMouse wraps mouse functions like ::GetCapture and ::GetCursorPos into a class, and it has a cast operator so you can write cool code, like

 CPoint pt = Mouse;
to get the current mouse position in screen coordinates. Whether you decide to use CMouse and Mouse instead of the more normal GetCursorPos is, of course, totally up to you. I merely throw it out as an extra freebie, and to get you thinking in new, object-oriented ways.
      Flyby is a simple program with just one item, the ellipse. If I were writing an application such as yours, with many items that could be highlighted, I'd implement each item using a class CHotItem or something similar, with a virtual HitTest(CPoint) function that returned TRUE if the point lies inside the item and FALSE otherwise. Then instead of writing

 BOOL bHighlighted = m_hotRegion.PtInRegion(pt);
 if (bHighlighted != m_bHighlighted) {
 •
 •
 •
I'd write

 CHotItem* pCurItem = FindItem(pt);
 if (pCurItem != m_pCurItem) {
 •
 •
 •
FindItem would search the list of items, calling HitTest(pt) for each one. You'd also need a CHotItem::Invalidate function to tell an item to invalidate itself or, alternatively, if all your items are rectangular, you can get by with a GetRect function that returns a CRect, the item's bounding rectangle. So many ways to implement, so little time.

Q In Microsoft® Internet Explorer and other applications I have seen, the toolbar buttons are black and white, but turn to color when you move the mouse over them. How can I implement this for my app?

A It's not that hard, if you know the right messages and a little MFC. The Windows common toolbar class, ToolbarWindow32, uses an image list to store the images for the buttons in your toolbar. You can change the image list by sending the toolbar a TB_SETIMAGELIST message, passing a handle to the image list (HIMAGELIST) as the LPARAM parameter. There's another message, TB_SETHOTIMAGELIST, to set the hot image list, which contains images used when the user moves the mouse over one of the buttons. So the toolbar itself implements the mouseover feature; all you have to do is set the image lists. There's even a TB_SETDISABLEDIMAGELIST to set the image list used for buttons in the disabled state (TBSTATE_ ENABLED off). In theory, all you have to do is send these messages with your desired image list. Practice is a little more tricky, because when you do things like change the image list, MFC is bound to get confused.
      The normal way to create a toolbar in MFC is to first create the toolbar window, then call LoadToolBar to load the toolbar:


 // Typical way to create a toolbar
 if (!m_wndToolBar.Create(this) ||
     !m_wndToolBar.LoadToolBar(IDR_MAINFRAME)) {
     TRACE0("Failed to create toolbar\n");
     return -1;      // fail to create
 }
LoadToolBar is an all-purpose function that loads a special MFC toolbar resource and corresponding image bitmap and sets the button sizes. But you don't really need LoadToolBar if you're setting the images yourself. In fact, you don't even need a toolbar resource, just the button bitmaps.
      For example, to set the normal-state bitmap, you'd code something like this:

 CImageList il;
 VERIFY(il.Create(ID_MYBITMAP, 22, 0, clrBkgnd));
 m_wndToolBar.SendMessage(TB_SETIMAGELIST, 0,
                          (LPARAM)il.Detach());
You'd do the same for the hot and disabled image lists. The magic number 22 is the width of one button in your image list. Zero is the number of images to grow by (since you won't add any). clrBkgnd is the background color of your bitmap, which the image list will interpret as transparent. Figure 3 shows two bitmaps with magenta, RGB(255,0,255), used as the background bitmap. I borrowed them from the MFCIE sample app that comes with Visual Studio® 6.0.
Figure 3  Normal and Hot Toolbar Bitmaps
Figure 3 Normal and Hot Toolbar Bitmaps

      I wrote a CHotToolBarSetup class that encapsulates the details of setting the image lists from bitmaps, and an HTBTest sample app that shows how to use it. Figure 4 shows HTBTest in action with one of the buttons highlighted in color. Figure 5 shows the source code. To use CHotToolBarSetup, you just create an instance and call its only function:

 CHotToolBarSetup htb;
 htb.SetupHotToolBar(m_wndToolBar,
     IDB_COLDTOOLBAR,   // ID of "normal" bitmap 
     IDB_HOTTOOLBAR,    // ID of "hot" bitmap
     0,                 // ID of disabled bitmap
     22,                // width of one button
     RGB(255, 0, 255)); // transparent color
      SetupHotToolBar does the grunge work of creating image lists, loading the bitmaps, and setting the bitmaps in the toolbar. CHotToolBarSetup also takes care of a few other minor but crucial details. For example, it sets the toolbar style to TBSTYLE_FLAT in case you forget. (Windows only supports hot bitmaps for flat-style toolbars.)
Figure 4 HTBTest in Action
Figure 4 HTBTest in Action

      SetupHotToolBar calls CToolBar::SetSizes to tell MFC how big the image and buttons are. By MFC convention, the button is 7 ¥ 7 pixels larger than the bitmap image to allow room for drawing shaded lines around the button in various pressed states. CHotToolBarSetup has a m_szButtonMargin data member that's initialized to CSize(7,7) in the constructor. You can change this before calling SetupHotToolBar if you want a different margin. SetupHotToolBar also accepts an argument for the disabled bitmap, in case you need disabled buttons. If you don't specify a disabled bitmap and you disable one of your buttons, you'll get an ugly image generated from your normal image list.
      Since I'm rolling my own toolbar here (as opposed to calling LoadToolBar), the last thing I have to do is add the buttons themselves and map them to command IDs. So far, all I've done is set an image list; there are no buttons. The best place to add the buttons is in CMainFrame::OnCreate, immediately after calling SetupHotToolBar:

 // Add toolbar buttons 
 m_wndToolBar.SetButtons(NULL, NBUTTONS);
 for (int i=0; i<NBUTTONS; i++) {
   m_wndToolBar.SetButtonInfo(i,
                Buttons[i].id,      // command id
                Buttons[i].style,   // buttons style
                Buttons[i].iImage); // index of image in 
                                    // bitmap
 }
      I haven't shown you the Buttons table, which stores the command ID, button style (TBSTYLE_BUTTON|TBSTYLE_DROPDOWN), and image index for each of several buttons. Normally, this information is stored in the toolbar resource and read by CToolBar::LoadToolBar.


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

From the October 1998 issue of Microsoft Systems Journal.