December 1998
Download Dec98CQA.exe (76KB)
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.
|
Once again it's time to follow up on some questions and reader comments from previous columns.
FormSwap
|
Figure FormSwap2 |
In other words, all you have to do is delete the old view, create a new one, recalculate the layout, and initialize the form. The only tricky part is that you have to set up a create context to initialize the new view.
If you don't do this, the new view won't have the correct document and frame pointers. Since MFC doesn't automatically send WM_INITIAL_UPDATE to the view when you use a create context, you have to send it manually.
In truth, for FormSwap2 I could pass NULL as the CCreateContext to CCSplitterWnd::CreateView because CreateView loads create context information from the current active pane by default. In FormSwap2, after you delete the right pane, the active pane becomes the left pane, which already has all the correct context information. But in the general case, there might not be another pane with valid context information, so just in case you try to use my code in some other view-swapping situation, set the context information explicitly. The views themselves are implemented as four classes (CView1 through CView4), each derived from CFormView. I encapsulated the mapping between forms and classes in another class, CFormData (see Figure 3), with a global instance FormData. To get the runtime class for the view associated with the nth form, you can write:
FormData also stores the index of the current selected form. When CMainFrame gets a notification from the left pane (list view) that the user has selected a new item, the frame calls a special function, CMainFrame::ViewForm, to view the new form. This function uses FormData to know which view class to use.
As I already mentioned, CMySplitterWnd::ChangeView is the function that actually changes the view. Whether you use a table (as I have) or some other mechanism to map form index to view class, CFormData hides the implementation. And by making FormData global, you can access it anywhere. CLeftView uses FormData to initialize itself.
|
Figure 4 Testing FormSwap2 |
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:
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
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.
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.
CMyView::GetHighlight returns TRUE if the point lies inside the ellipsebut only if the view has focus.
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.
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:
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
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:
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. |
|
From the December 1998 issue of Microsoft Systems Journal.