 |
Figure 5 TRACE Output
|
To test FormSwap2, I implemented a couple of new menu commands (see Figure 4). File | Test Doc gets handled by CMyDoc::OnDocTest. The handler generates a TRACE diagnostic to show that it actually received control. The handler also displays a list of views attached to the document. Figure 5 shows the TRACE output from FormSwap2 as you cycle through each form. The document remains attached to the proper views at all times: CLeftView and whichever CViewX is mapped to the current form. The second test command, File | Test View, has its handler in each different view class. The handlers generate TRACE diagnostics to show they received controlto demonstrate that the correct view's message map is in fact hooked up.
I'm glad readers found the drawback in my original implementation. It just goes to show how in MFC there's always more than one way to skin a cat, and the method you choose depends on what you're trying to accomplish. My first solution is more appropriate if you have a small number of forms and controls and you don't want to bother writing a bunch of view classes. (I borrowed the solution from another app I wrote where this was the case.) The second solution (FormSwap2) is better if you have lots of forms and controls because it lets you break up the message maps into manageable chunks and files that different programmers could be working on at the same time.
Before leaving FormSwap, let me explain something else a few readers asked about. Also in the September 1998 issue, Ben Mundy asked how to make his frame the correct size for his form-based app. I described a complex algorithm for computing the correct size of the parent frame based on the size of its child form. Some readers asked why I didn't use this much simpler solution:
CMyFormView::OnInitialUpdate()
{
CFormView::OnInitialUpdate();
GetParentFrame()->RecalcLayout();
ResizeParentToFit();
}
Indeed, this will work fine to solve Ben's question as stated. But ResizeParentToFit doesn't work if the form view is embedded in a splitter window, as is the case for FormSwap. In my haste to reuse code by answering both questions with the same app, I made life more difficult for myself and forgot to mention ResizeParentToFit. The algorithm in my September column is necessary only if you have a splitter window;
in a simple, non-splitter, form-based app, use ResizeParentToFit.
Flyby
OK, on to the Flyby program from the October 1998 issue. This program illustrates how to implement a mouseover feature by drawing an ellipse that turns red when you move the mouse over it. The basic idea is to use WM_
MOUSEMOVE to know when the mouse has moved in or out of the ellipse. The only problem is that if the user moves the mouse quickly outside your window, you won't get a WM_
MOUSEMOVE. To implement mouseover, you need to know when the mouse has left your window entirely.
I told you to use TrackMouseEvent if you're only writing for Windows NT®, but if you need to run in Windows® 9x as well, I showed you how to set a timer to check whether the mouse has moved outside the window. Why this hack? Because TrackMouseEvent isn't available in Windows 9x, and SetCapture and ReleaseCapture don't work either because, as the official documentation states: "Mouse capture is also affected by the Windows 95 and Windows NT localized input queues.... If the mouse is captured while the mouse button is up, the window receives mouse input only as long as the mouse is over that window or another window created by the same thread."
Well, contrary to what the documentation states, even if the user moves the mouse completely outside your window, your program will still get WM_MOUSEMOVE messages if you've captured the mouse. This is true in both Windows 9x and Windows NT. Several readers sent email pointing this out and suggested that SetCapture is better than my set-a-timer hack. While the premise of this reasoning is correctSetCapture does in fact capture out-of-window movement (kind of like an out-of-body experience)the conclusion that SetCapture is a better way to implement mouseover doesn't follow. SetCapture is still unacceptable for another reason: once you capture the mouse, your app no longer receives keyboard input. For example, if your program has captured the mouse and the user types Alt+F hoping to get the File menu or Alt+F4, nothing happens. Oh well.
SetCapture is mostly intended for implementing the down/up action for custom-draw buttons. The user presses the mouse button, your app gets WM_LBUTTONDOWN, you draw your custom button in the down state, and then you capture the mouse until
the user releases the mouse button. At which point you redraw your button in the up state. SetCapture isn't very useful as a general means of finding out when the mouse has left your window. So it seems the set-a-timer thing is the best you can do.
But wait! The friendly Redmondtonians recently informed me that while TrackMouseEvent is not supported for Windows 9x, there's a _TrackMouseEvent in comctl32.dll. This function calls the "real" TrackMouseEvent if there is one (Windows NT); otherwise it duplicates the functionality itself (Windows 9x). Evidently Microsoft Internet Explorer 4.0 software needs TrackMouseEvent, no doubt to implement the very mouseover feature I'm discussing.
As soon as I found out about _TrackMouseEvent, I rewrote Flyby to use it. At first, the implementation seemed straightforward. When the user moves the mouse into your view, the first thing you do is call _TrackMouseEvent.
void CMyView::OnMouseMove(UINT nFlags, CPoint pt)
{
if (!m_bTrackLeave) {
// First time mouse entered my window:
// request leave notification
TRACKMOUSEEVENT tme;
tme.cbSize = sizeof(tme);
tme.hwndTrack = m_hWnd;
tme.dwFlags = TME_LEAVE;
_TrackMouseEvent(&tme);
m_bTrackLeave = TRUE;
}
•
•
•
}
When the user moves the mouse outside my view, Windows sends it a WM_MOUSELEAVE message. When the view gets the message, it turns off the highlight and repaints the windowsimple.
LPARAM CMyView::OnMouseLeave(
WPARAM wp, LPARAM lp)
{
m_bTrackLeave = FALSE;
if (m_bHilite) {
m_bHilite = FALSE;
Invalidate(FALSE);
UpdateWindow();
}
return 0;
}
Like I said, it seems straightforward. But there's a major fly in the ointment of Flybyor a fly in _TrackMouseEvent, depending on your perspective. If you press Alt+F to invoke the File menu while the mouse is in the middle of the ellipse, Windows sends your view a WM_TRACKLEAVE message, even though the mouse hasn't left your window! (A typical Windows conundrum: when is leaving a window not leaving it?) In this situation, the previous code will unhighlight the ellipse, which is probably not what you wantI mean, the mouse is still inside it, right? Even in Internet Explorer 4.0, if you move the mouse over a hyperlink to highlight it, then press Alt+F to get a menu, the link remains highlighted.
The way to fix things is to modify OnMouseLeave so it only unhighlights the ellipse if the mouse is actually outside it.
CPoint pt = Mouse;
ScreenToClient(&pt);
BOOL bHilite = GetHighlight(pt);
if (!bHilite) { // if mouse really outside
Invalidate(FALSE);
UpdateWindow();
}
CMyView::GetHighlight returns TRUE if the point lies inside the ellipsebut only if the view has focus.
BOOL CMyView::GetHighlight(CPoint pt)
{
return m_hotRegion.PtInRegion(pt) &&
CWnd::GetFocus()==this;
}
You want to make sure that the view has focus because you probably don't want to highlight the ellipse/item if your app doesn't have focus. (If you do, you can remove the GetFocus check.) Adding the GetHighlight check fixes the menu bug: if you now press Alt+F while the mouse is inside the ellipse, it remains highlighted.
But now there's another problem. If you cancel the menu by clicking the mouse somewhere outside the view, you don't get another WM_MOUSELEAVE message, so the ellipse remains highlighted. Sigh. Is there any way to make this code work?
Don't despair, there's just one more bit of voodoo to nail it down. When the user cancels a popup menu, Windows sends a WM_EXITMENULOOP message. When that happens, you can once again check the mouse position and highlight or unhighlight your hot spot. There is, however, one little problem: Windows sends WM_EXITMENULOOP to the top-level parent window, not your view. If you want a quick solution, modify your CMainFrame class to handle WM_EXITMENULOOP by passing the message along to the active view.
CMainFrame::OnExitMenuLoop(...)
{
GetActiveView()->
SendMessage(WM_EXITMENULOOP, ...);
}
However, I chose to implement mouseover as a feature in the view, not the frame. Why should the frame know anything about it? In order to completely encapsulate the mouseover implementation inside the view, what's needed is a way to trap the parent frame's WM_EXITMENULOOP message. This is exactly what my CSubclassWnd class does (from the June 1997 column). CSubclassWnd lets you catch messages sent to another window. It works by installing its own message proc ahead of the MFC one, and then routes messages to a virtual WindowProc function. CSubclassWnd maintains a map to associate each hooked HWND with its CSubclassWnd object, just like MFC's window handle map. CMyView in Flyby uses CSubclassWnd to catch the frame's WM_EXITMENULOOP message. Figure 6 shows the final implementation. Whew.
By this point, you probably wish you'd stuck with my original set-a-timer hack, but setting a timer to periodically check whether the mouse has gone bye-bye is really pretty grody. _TrackMouseEvent is much cleaner. And, just in case you haven't been reading MSJ religiously every month (shame on you), here's the URL where you can find out how to redistribute comctl32.dll with your app: http://msdn.microsoft.com/developer/downloads/files/40comupd.htm.
Incidentally, _TrackMouseLeave also lets you request a message when the mouse has hovered over your window for a specified length of time. Just set TRACKMOUSELEAVE::
dwFlags to TME_HOVER. You can use TME_HOVER to implement tooltip-like features where something happens when the mouse has lingered over your window for a specified period of time. And if you don't like that little underbar in front of the name, you can always put the following lines somewhere in your app:
#ifndef TrackMouseLeave
#define TrackMouseLeave _TrackMouseLeave
#endif
I kind of like the underbar to remind me it's not really part of Windows, at least not in Windows 9x.
Last But Not Least
OK, so much for Flyby. Now for a couple of minor corrections. In my July 1998 column about Cancel dialogs, I described a CThreadJob class for implementing worker threads. My class used PostMessage to post progress messages from the worker thread to the main thread. I used PostMessage instead of SendMessage because I wanted the code that handles the message to run in the main thread, not the worker thread. In fact, as reader John Tasler points out, using either PostMessage or SendMessage will cause the code to execute in the main thread. Win32® automatically synchronizes HWND access through the thread that created the HWND.
The real reason it's better to use PostMessage is so the worker thread doesn't have to wait while the main thread processes the progress message. My code was correct, but I gave the wrong reason. Still, I wanted to correct any potential source of confusion. Multithreading programming is hard enough as it is!
John also gave me a really neat multithreading tip I'd like to share. He takes advantage of the HWND synchronization in Win32 to use non-thread-safe third-party libraries in a multithreaded COM object. Here's how it works. For every function in the API, you define a message and a struct to hold the function's arguments.
#define WM_CALLFOO WM_USER + 0x100
#define WM_CALLBAR WM_USER + 0x101
// etc...__
struct FOOFNARGS {
int p1;
BOOL p2;
// etc...
};
Next, you create an invisible window whose only job is to process WM_CALLXXX messages. The WM_CALLFOO handler for this window would look something like this:
LPARAM C3rdPartyAPIWnd::OnCallFoo(WPARAM wp, LPARAM lp)
{
FOOFNARGS& args = *((FOOFNARGS*)lp);
return Foo(args.p1, args.p2);
}
Any thread that needs to call Foo will instead send the hidden window a WM_CALLFOO message with a filled-in FOOFNARGS as LPARAM. You can even #define macros to make the invocation look like a function call. Since only the message handlers of the hidden window ever call the third-party API, and since SendMessage synchronizes access through the thread that created the window, all calls are guaranteed to come from the same thread, which is what the API is expecting. Pretty cool, John!
Finally, in last month's issue, I said you should return TRUE from the ON_COMMAND_EX handlers to have MFC keep routing the message. Obviously, I meant FALSE. TRUE means "I handled the message;" FALSE means "keep routing." Sorry, I've been spending too much time lately in the antimatter universe.
|