October 1998
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."
|
|
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 flagsbut 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.
When the user first moves the mouse inside the window, OnMouseMove checks to see if the mouse is in the ellipse: |
|
|
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. |
|
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: |
|
|
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 thingnamely, 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 |
|
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 |
|
I'd write |
|
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.
|
|
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: |
|
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 |
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: |
|
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 |
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: |
|
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.
|
|
From the October 1998 issue of Microsoft Systems Journal.