September 1998
Download Sep98CQA.exe (23KB)
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 have written an application similar to Windows® Explorer. A derived tree view on the left and a display view on the right are contained inside a splitter window. I derived my display view from CFormView with the hope of displaying more than 100 form-based screens containing controls that are driven from options from the tree control. I thought I could use the CFormView and trick it into swapping between dialog templates, but I was unsuccessful.
I then tried to write my own class, CMultiFormView, and make it similar to the CFormView class, with one exception: instead of holding a single template resource (m_lpszTemplateName), I tried to make it an array. This didn't work either. Could you please give me some advice on how to implement a form-based view using MFC to change the form? Richard Lynders
Australia A 100 formswow! Fortunately, faking out the view is a lot simpler than designing so many forms. You started on the right track; it is possible to trick CFormView into using a different form. You just have to know the right MFC witchcraft. Fortunately, I'm here to act as voodoo master. |
Figure 1 The FormSwap App |
|
|
UnsubclassWindow detaches the window from the CFormView object, unhooks it from MFC, and returns the dialog's HWND. Once the dialog is unhooked, you can safely destroy it. After calling UnsubclassWindow, your view's m_hWnd is NULL, exactly as if the window had never been created. So the next thing to do is recreate itthis time using a different dialog template. |
|
You need to save the ID, window rectangle, and style flags before destroying the window so you can recreate the form with exactly the same state. That's all there is to it. Now the view has a different form.
Of course, there are just two little tricks that bear discussion. First, the rectangle you pass to Create must be in the parent window's client coordinates, so before you unsubclass, you have to do a little coordinate conversion: |
|
The second trick is required to fake out MFC. When you recreate the view, MFC's CView::OnCreate handler processes the WM_CREATE message. CView::OnCreate will ASSERT if the document pointer, m_pDocument, is NULL. So in order to circumnavigate MFC's overzealous assertion, you have to remove the document before creating the view, then reattach it afterward. |
|
The final SetForm function, which you can find in the sample code, is pretty general. You can use it to change the form in any CFormView-derived class.
Q I have a form-based app written in MFC. When the app starts up, the window is larger than the form. Because most of the form is blank, this doesn't look very good. I want to make the window just big enough to hold the form, but I don't know how big the form is until it's loaded. If I change the window size after the app comes up, then you can see the old size briefly, which looks even worse. Currently, I'm using trial and error to hardwire the size, but now every time someone changes the form, I have to manually edit the window size to fit. Isn't there some way I can make the window size fit the form automatically? Ben Mundy
A This is a typical Windows chicken-and-egg problem, one of those things that seems like it should be easy but isn't. One way to find the size of a dialog is to load the dialog resource into memorythat is, into a DLGTEMPLATE structureand look at the cx/cy parameters. Unfortunately, these values are in dialog base units, not pixels. So you have to call GetDialogBaseUnits and do some coordinate conversions, which depend on whether your dialog uses the system font or its own font. Fortunately, there's an easy and surefire way to find out the size of a dialog, even if it is a bit kludgy: just create the dialog, and see how big it is! Of course, you have to create it invisibly if you don't want the user to see what you're doing. But the dialog used for a form-based app is invisible by default, so in this case making the dialog invisible doesn't require extra effort. To see how it works, I added the automatic sizing feature to the FormSwap program mentioned in the answer to the previous question. The core of the autosize feature is a static function, GetDialogSize, which gets the size of the form. Yes, even in the days of C++, I still write static functions from time to time. But GetDialogSize is implemented using a C++ class, CTempDlg: |
|
CTempDlg has just one function override: OnInitDialog. OnInitDialog saves the position of the dialog in a data member, then quits. (MFC does a bit of trickery to allow this; in straight C you can't call EndDialog from within WM_INITDIALOG.) GetDialogSize uses CTempDlg to get the dialog's size, which it returns as a CSize. |
|
Pretty simple. If you try this at home, just remember that the dialog should have no borders and should be invisible (dialogs used in form views always are).
If you want to get the size of a normal dialog, you'll have to do a bit more coding. To first load the dialog template into memory, turn off the WS_VISIBLE and WS_BORDER styles in the DLGTEMPLATE structure and call CDialog:: CreateIndirect instead of Create. If you're already loading the dialog into memory, you may as well just convert the cx and cy values using GetDialogBaseUnits. In passing, I should also mention another function, MapDialogRect, that converts dialog box units to screen pixels. However, this function requires an HDLG, which is an already created dialog. Once you have the size of the dialog, you've solved the main problem of sizing your framebut you aren't out of the woods yet. Now the problem is: given that you know how big the dialog is (that is, how big the client view area should be), how big must you create your main window to get a client area exactly that size? This sounds simple but it turns out to be a real pain in the keyboard. There's a virtual function, CWnd::CalcWindowRect, that's supposed to accomplish this feat. Given a certain-sized client rectangle, it calculates the corresponding frame rectangle. Unfortunately, no one in Redmond bothered to implement this function for CFrameWnd. So that leaves it to you. Implementing CalcWindowRect is an exercise in nit-picking, or should I say pixel-picking. Given a certain-sized client rectangle, you have to add the heights and widths of all the toolbars, splitter borders, menus, window frames, and what-have-you. In the case of FormSwap, there are toolbars, the splitter window, and the left-pane view that must be taken into account. To calculate the height of the toolbars, you can use the MFC function CWnd::RepositionBars. You feed it a rectangle and a magic code, reposQuerywhich says you only want to calculate the toolbars, not actually move themand presto, you have the numbers you need. If you want to understand RepositionBars better, read the documentation. Good luck. After calculating the heights of the toolbars, the next troublesome task is calculating the height and width of all the splitter window components: the border with which it surrounds each pane and the splitter bar itself. All these magic numbers are contained within data members in CSplitterWnd, but naturally the data is protected, which means you can't access it! Sigh. So what do you do? Simple: just derive a new class with public functions to export the protected data, and use it instead of CSplitterWnd in your main frame. |
|
If you're lazy (the best programmers are), you might be tempted to just hardwire the border size and other measurements instead of going to all this trouble. But be warned: the values m_cyBorder and its sisters are different under Windows 3.1, Windows 95, and Windows NT®. So if you want the exact values, you must get them from an instantiated CSplitterWnd object.
By now you've probably fallen asleep just thinking of all this tedious work, but once you've calculated how big your main frame should be, the next step is to actually modify the frame's size. Where do you do it? There are oh, so many places. You could override PreCreateWindow and set the size in CREATESTRUCT, implement a WM_GETMINMAXINFO handler, or alter the size in CFrameWnd::OnCreate. In this particular case, however, there's only one option because the magic RepositionBars function works only after the toolbars have been created. This means the winner is door number three: OnCreate. |
|
With this and all the other code in place, the window now appears as in Figure 1, just large enough to enclose the form, and not one pixel larger. Whew!
Before signing off, let me spend a few words on WM_GETMINMAXINFO. Windows sends this message just before creating a window, and again whenever the user attempts to move or size it. WM_GETMINMAXINFO is your opportunity to specify the minimum and maximum sizes allowed for your window. If, after going to all that trouble to calculate the window size, you want to prevent the user from decreasing the size of the window, you should handle WM_GETMINMAXINFO. Just follow the directions in your Win32® Owner's Manual. Don't forget that Windows sends the first WM_GETMINMAXINFO before it creates your window, so RepositionBars won't work. You either have to implement CalcWindowRect another way, without using RepositionBars, or ignore the first WM_GETMINMAXINFO message. One way to do this would be to make the dialog size a data member, m_szDlg, which you initialize to zero and then set in OnCreate. Then your WM_GETMINMAXINFO handler would do nothing if m_szDlg is zero. Q I have an MFC doc/view app that was working fine until I recently added a splitter frame. I've got the splitter part working fine, but now when the app comes up, all the menu commands for commands handled by my document or view are grayed out. If I click the mouse in the view, they become enabled again. What's going on? Narwal Jadreep
A You got bitten by the command-routing gremlin, the one that comes out whenever you least expect it. I've written about command routing many times, but never in the context of splitter windows. So in the interest of seeing just how many questions I can answer with a single sample program, why not give it another whirl? The normal way to set up a splitter window is to override your frame window's OnCreateClient function. You call CSplitterWnd::CreateView to add your panes, and thenthis is what you forgot to doyou must call SetActiveView to set your frame's active view. |
|
You can make any pane the active view, but there can be only one. In this case, I chose the right pane, which is the form view (see Figure 1). Calling SetActiveView hooks your view up to the MFC command-routing system. Whenever the frame gets a command (WM_COMMAND or WM_
NOTIFY message), it routes it to the active view. The view, in turn, routes commands to its document. So until you call SetActiveView, MFC doesn't see your doc/view. If MFC can't find a handler for a particular menu command, it disables the menu item. (You can turn this behavior off by setting CFrameWnd::m_bAutoMenuEnable = FALSE.) The reason your menus came back to life once you clicked on the view is that CView contains a message handler for WM_
MOUSEACTIVATE that calls pFrame-SetActiveView(this). That is, when the user clicks on a view, the view makes itself active.
Now, it's nice that CView handles WM_MOUSEACTIVATE for you, but for many apps that's still not good enough. If you have an app with multiple splitter-pane views, why should only one of them (the active one) be allowed to handle commands? In FormSwap, there's a command, View | Next Form, that cycles through the forms. I implemented the handler for this in CRightView, which is the form view. Under MFC's default implementation, when I click on the left pane to select a different form from the list, the left pane becomes the active view and View | Next Form becomes disabled because there's no longer any handler for it. This is not what I want to happen; View | Next Form should be enabled all the time, whichever view is active. In fact, in FormSwap, there really is no notion of an active view since both views should always be active. Fortunately, it's easy to let all the views in a splitter window handle commands. All you have to do is override your frame's OnCmdMsg function, like so: |
|
Then, override the splitter window's OnCmdMsg: |
|
As I wrote in the June 1998 issue, OnCmdMsg determines which objects in your system receive command notifications. You never implement this function to actually do anything; you merely pass its arguments to other objects' OnCmdMsg functions. In this case, FormSwap passes the arguments to the splitter window, which in turn passes them to each pane/view. The advantage of doing it this way (instead of having the frame pass the args directly to each pane) is that the splitter window itself can also handle commands. For example, if you have a command that moves the splitter bar, it might make sense for the splitter window to handle it. The important things to remember with OnCmdMsg are to return TRUE if any object handles the message, and to pass the args to the base class if they don't. Ultimately, each object calls its base class OnCmdMsgthat is, the one inherited from CCmdTargetwhich routes the command to the appropriate handler in the object's message map. |
|
From the September 1998 issue of Microsoft Systems Journal.