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.


December 1998

Microsoft Systems Journal Homepage

C++ Q&A

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
In the September 1998 issue, Richard Lynders asked how to change the form in a splitter window. His app comprised a list view in the left pane and a form view in the right pane. Selecting an item in the list displays a new form on the right (see Figure 1).

Figure 1 FormSwap2
Figure FormSwap2


I described a solution in which the CFormView-derived class cleverly recreates itself using a different dialog for the form. But as readers Joseph Chambers and Jörg Frick pointed out, my design isn't so clever after all because it uses a single view class for all forms, so the view's message map must contain handlers for all controls in all possible forms. This is fine if you only have a few forms, but Richard's app has over 100 different forms. If each form had only five controls, that's 500 message map entries—yikes! What you really want is a separate view class for each form, so the message maps are more manageable.
Well, at MSJ we always try to please. So this month I redesigned FormSwap to implement form switching in a different way. FormSwap2 has a new CMySplitterWnd (see Figure 2) with a ChangeView function that changes the view class entirely. The implementation is surprisingly straightforward.

 void CMySplitterWnd::ChangeView(int row, 
     int col, CRuntimeClass* pViewClass)
 {
     .
     . 
     .
     // ASSERT checks CCreateContext cc;
     // set up create context—see below
     .
     .
     .
     DeleteView(row, col);
     CreateView(row, col, pViewClass, CSize(0,0), &cc);
     RecalcLayout();
 
     // initialize the view
     CWnd* pWnd = GetPane(row, col);
     if (pWnd)
     pWnd->SendMessage(WM_INITIALUPDATE);
 }
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.
 // create context to preserve doc, etc.

 //
 CView* pView = STATIC_DOWNCAST(CView, 
     GetPane(row, col));

 CFrameWnd* pFrame = pView->GetParentFrame();
 ASSERT(pFrame);
 
 CCreateContext cc;
 memset(&cc, sizeof(cc), 0);
 cc.m_pNewViewClass = pViewClass;
 cc.m_pCurrentDoc = pView->GetDocument();
 cc.m_pNewDocTemplate = cc.m_pCurrentDoc ?
     cc.m_pCurrentDoc->GetDocTemplate() : NULL;
 cc.m_pCurrentFrame = pFrame;
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:
 CRuntimeClass* pViewClass =   	FormData.GetViewClass(n);
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.
 void CMainFrame::ViewForm(UINT iWhich)
 {
     if (iWhich != FormData.GetCurrentForm()) {
 
         CMySplitterWnd& sw = m_wndSplitter;
         sw.ChangeView(ROWFORMVIEW, COLFORMVIEW, 
             FormData.GetViewClass(iWhich));
         FormData.SetCurrentForm(iWhich);
     }
 }
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.
 void CLeftView::OnInitialUpdate()
 {
     CListCtrl& lc = GetListCtrl();
 •
 •
 •
     UINT nForms = FormData.GetMaxNumForms();
     lc.DeleteAllItems();
     for (UINT i=0; i<nForms; i++)
         lc.InsertItem(i, FormData.GetDisplayName(i));
 }
Figure 4 Testing FormSwap2
Figure 4 Testing FormSwap2

Figure 5 TRACE Output
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 control—to 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 correct—SetCapture 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 window—simple.
 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 Flyby—or 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 want—I 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 ellipse—but 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.


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

From the December 1998 issue of Microsoft Systems Journal.