Windows UI: Our WinMgr Sample Makes Custom Window Sizing Simple |
||
Paul DiLascia | ||
This article assumes you�re familiar with the Win32 API and MFC | ||
Level of Difficulty 1 2 3 | ||
Download the code for this article: WinMgr.exe (89KB) | ||
Browse the code for this article at Code Center: WinMgr | ||
SUMMARY Programmers using Visual Basic have always had an advantage over C++ programmers when it comes to component and window sizing. There are many third-party Visual Basic-based solutions to this age-old problem, but unfortunately, there are few elegant alternatives for the C++ crowd, short of using a full-fledged windowing toolkit. This article explains how to circumvent the tedious task of hardcoded pixel arithmetic. It starts by introducing a window sizing rules system, and then moves on to show how CWinMgr, a data-driven class, can intelligently manage an MFC application's window sizing. | ||
![]() WinMgr is a small C++ class library that solves the window-sizing problem once and for all. WinMgr does splitters and min/max info, too. It works in your frame, view, dialog, ActiveX® control—any window that needs to be sized. With WinMgr, you never have to write a line of window-sizing code because WinMgr implements rule-based sizing. Instead of writing tedious error-prone procedural instructions to size your windows, you create a table called a window map that describes your layout. The rules are simple enough to implement and use, but general enough to support most layouts. Once you've created your window map, all you have to do is handle a few messages by calling ready-made functions. For example: In short, WinMgr helps you achieve total layout bliss. (As
for the rest of your life, you're on your own.)
The Power of DataSuppose I told you I had developed a really cool new personal finance program that pays bills, manages your investments, orders groceries, and can even alert you the next time the Tank the Armadillo Beanie Baby shows up on eBay—with just one catch: to enter your personal info, you have to modify the source code and recompile the program. You'd look at me like I was some kind of software imbecile and head politely for the exit. Computer scientists long ago invented something called a database to store similar pieces of information. Amazingly, it's now possible to add data to a program without rebuilding it. This is so obvious you probably never even thought of it that way. And yet when it comes to coding, so many programmers are stuck in a procedural mindset, where the only way to accomplish something is to call a function. Such code cries for a table, but I see this sort of thing
more often than I wish, sometimes even in magazines. Don't programmers have any
pride?One of the Really Big Ideas I preach is the power of data-driven code. I can't emphasize enough how much more simple, reliable, understandable, and easy-to-modify your code will become any time you can substitute data for instructions. Obviously, a database is overkill for most programming tasks, but a simple table often provides an effective, easy, and most of all reliable way to drive your program. Not to mention the code is smaller and easier to change. One table is worth a thousand lines of code. Well, maybe not a thousand, but lots. Rule-based LayoutHow can you apply the Power of Data to sizing child windows? Easy. By inventing rules to describe the layout of windows. The droopy window goes to the left of the gloopy window, but above the loopy one, and so on. The rules should be as simple as possible and yet rich enough to support the kinds of layouts found in most apps. No doubt there are many rule systems you could devise that meet these criteria; WinMgr is what I came up with. So far, it works for me.There are two basic ideas. First, WinMgr abstracts the problem away from windows and reduces it to the more mundane world of rectangles. After all, as far as size and position go, a window is just a rectangle. The second idea is to divide the universe into groups of rows and columns. For example, you can think of a typical main window as a row group with three rows: toolbar, view, and status bar. The rows-and-columns idea may seem strange at first, but you'll soon get used to it. Within a group, each rectangle is sized according to certain rules. For a row group, each row in the group has width as wide as it can be, and height determined according to one of the following basic rules. FIXED The row height is a fixed value—for example, four pixels. PCT The row height is a fixed percentage of the total—for example, 75 percent. TOFIT The row height is whatever is required to hold its contents. This is the only rule that "knows" about windows. WinMgr sends a message to get the desired height (more on this later). REST The row height is what's left after sizing the other rows in the group. There can be only one REST rectangle per group. So much for rows. For columns, the rules are identical, but apply to the column's width instead of height. The height of a column is the tallest it can be. Throughout WinMgr, there's a duality between row/height and column/width. Figure 1 summarizes the rules. Well, that all seems fine and dandy if your layout resembles a bunch of rows, as in the archetypal main frame with toolbar, view, and status bar—or columns in a bar chart. But what if your application has a more complex layout with many windows? How does WinMgr cope? The answer lies in the power of nesting. Any of the rows in a row group or columns in a column group can itself be another group—row or column. In that case, WinMgr applies the rules recursively, using the calculated size of the group as the total available for its children. By alternating groups of rows and columns, it's possible to describe a variety of layouts. The following example should make this clearer. DopeyEditTo understand how WinMgr's rules work in practice, let me now turn to DopeyEdit, a simple text editor based on CEditView. Figure 2 shows the main window. Instead of a single window, DopeyEdit has three panes: a left license pane that shows a couple of logos and a place for the license agreement, and two right panes. The top-right pane is the actual edit view (CEditView) where users can edit the file; the bottom-right pane is the dopey pane because all it does is display a dopey message in 24pt Broadway type.![]() Figure 2 The DopeyEdit Text Editor DopeyEdit's three-pane layout seems simple enough at first, but up close the implementation is more complex. The license pane comprises three windows: two static bitmaps and one static text window. All three panes are set inside a four-pixel margin within the main view's client area. This margin forms an empty space where there are no windows, where the background shows through. The panes are also separated from each other by margins—but these in-between margins are actually sizer bars, also known as splitter bars, that let users adjust the relative sizes of the panes. So the DopeyEdit view actually comprises seven windows whose sizes and positions must be managed. Figure 3 illustrates this layout. ![]() Figure 3 DopeyEdit Main View The entire view lives within the main frame alongside tool and status bars, which makes 10 windows in all that require sizing. On top of that, DopeyEdit has a sizeable dopey dialog with no purpose other than to be sizeable (see Figure 4). This dialog has nine controls. ![]() Figure 4 Dopey Dialog Frame, view, dialog—DopeyEdit has three windows that require child window management, and it uses WinMgr in all three of these cases. Let's start with the frame, since it's the simplest of the three. The Main FrameMFC already provides code to size control bars and views within a frame, so WinMgr isn't strictly necessary here, but I wanted to use it for two reasons: to see if it would work and as an educational example to get your feet wet. The main frame layout is simple: three windows—toolbar, view, and status bar—arranged as rows. To use WinMgr, the first step is always to create a table called the window map that describes your layout. Here's the map for DopeyEdit's main frame. WinMgr.h (downloadable from the link at the top of this
article) provides the macros to build the map. You can give your map any name
you like; I chose MyMainWinMap. The macros create a C array of WINRECTs, a
special class that holds a rectangle, and other info. In the previous example,
MyMainWinMap has a row group with three child rectangles to represent toolbar,
view, and status bar. The entire group is a REST group, meaning its height is
whatever is left after sizing all the other rows—which, since there are none, is
the height of the whole client area.Within the group, Control IDs (AFX_IDW_XXX) map each entry to its corresponding window. The macros themselves determine which rule to apply. The toolbar and status bar are TOFIT types, using whatever height the toolbars require; the view is a REST type, using whatever height remains. The width of the entire group and all rectangles in it is the maximum possible—that is, the width of the frame's client area. Figure 5 illustrates the situation. Note that while MFC's built-in layout code requires using the AFX_IDW_XXX IDs, WinMgr lets you use any IDs you like. I used the MFC IDs only to avoid typing when creating the toolbars. ![]() Figure 5 DopeyEdit Main Frame Layout (If you're confused about why the group is a REST type and so is the view, remember: there can only be one REST rect per group. The entire group itself lives at a different level from its children. It behaves as though it's the only entry in an implicit top-level group. To make this explicit, the first entry in your window map must be BEGINROWS or BEGINCOLS—that is, a group.) OK, you have your window map, now what? The window map is just a table. By itself, it doesn't do anything. To use it, you need a CWinMgr. The best place to put one is in your parent window class, in this case the frame. And whenever you instantiate a CWinMgr, you must give it a pointer
to your window map. CWinMgr is the heart of WinMgr, the class that does all the
magic. Most of the action happens in two functions: CalcLayout and
SetWindowPositions. As their names suggest, CalcLayout computes the sizes and
positions of all the rectangles; SetWindowPositions moves the windows. Normally,
you'd call these functions from your OnSize handler, but since MFC's
CFrameWnd::OnSize delegates the job to a special CFrameWnd virtual function
RecalcLayout, which MFC calls from other places too, that's a better place for
frames. Because this implementation never calls the base class
CFrameWnd::RecalcLayout, MFC's layout code is completely out of the picture. All
child window sizing is done by CWinMgr.If none of the rectangles in the window map were of the TOFIT variety, you'd be done. But both the toolbar and status bar are TOFIT types. How does CWinMgr know how big is big enough "to fit?" By sending a message, of course. When CWinMgr wants to know the TOFIT size, it sends a registered message WM_WINMGR, with WPARAM equal to the child window ID and LPARAM equal to a pointer to a special NMWINMGR structure. CWinMgr sends this message first to the parent window (frame) and then, if the parent doesn't respond, to the child itself (toolbar, status bar, or view). This lets you implement either Windows-style message handling where father knows best, or a more object-oriented style where each window takes care of itself. In this case, it's more expedient (requires less typing) to handle the message in the frame. Since WM_WINMGR is a registered message, you have to use ON_REGISTERED_MESSAGE. And here's the handler: CMainFrame calls MFC's CControlBar::CalcFixedLayout to get
the toolbar's desired "to fit" size, and returns it in nmw.sizeinfo.szDesired.
Ditto for status bar. In a way I'm cheating here—using MFC to calculate the
height of the toolbars but not the layout itself. But the point is not to
eliminate MFC; the point is to illustrate how CWinMgr works and show it can
handle the simple frame layout. If CalcFixedLayout were not available, you'd
simply calculate the heights of the tool and status bars yourself. For the
toolbar, it's the bitmap height plus a few pixels for margins; for the status
bar it's the font height plus few pixels for margins. The important thing is
there's no procedural sizing code, only code to report the TOFIT
size.Window map, RecalcLayout, and WM_WINMGR handler—with these pieces in place, WinMgr sizes the windows perfectly, every time. Sizing on Steroids: DopeyEdit ViewWell, that was a fun exercise in needless redundancy. It's always refreshing to reinvent the wheel in a new way, just to prove it rolls. Now let's tackle something more difficult, something beyond MFC's window-sizing capabilities. DopeyEdit may be dopey as a text editor, but it has a complex window layout. You've already seen the seven-window view (see Figure 3). Not to worry, WinMgr can cope. In fact, the only thing that's really different from the frame is the window map, shown in Figure 6. Let's examine it closely.The first entry is a column group with three columns: left pane (a TOFIT row group), sizer bar (a FIXED column four pixels wide), and right pane (a REST row group). This means that the left pane is as wide as is required to hold its contents; the sizer bar is four pixels wide; the right pane (top-right pane, horizontal sizer bar, and bottom-right pane) is as wide as whatever is left. The height of all these panes and windows is the height of the entire view's client area, minus a four-pixel margin all around, as specified using the macro RCMARGINS(4,4). Drilling down, the first column is actually a row group with three rows: two bitmaps and one text control. These three rows constitute the left pane. The first two are TOFIT rows, mapped by IDs to the child windows ID_WIN_LOGO1 and ID_WIN_LOGO2. The third row is a REST rect, mapped to ID_WIN_LICENSE. Finally, the third and last column in CDopeyView's three-column layout is again another row group, also with three rows: the edit window, a REST rect mapped to ID_WIN_EDIT; another sizer bar (FIXED, four pixels high); and the dopey pane, height TOFIT. The width of all these is the width of the entire group, which, since it's a REST group, is whatever is left over from the view's client area after the first group and sizer bar have been sized. Are you confused yet? As before, it's the macros—RCREST, RCFIXED, RCTOFIT, and company—that specify which rule CWinMgr will use for each rectangle. Together, they describe the total layout. As always, the most interesting rule is TOFIT. CDopeyView's window map has three TOFIT windows: the two bitmaps and the dopey pane. I implemented these windows as classes CLogoWnd and CDopeyWnd, each derived from CStatic. The question is, how do these TOFIT windows work? As I mentioned earlier, one parameter of the WM_WINMGR message is a pointer to a NMWINMGR struct. One of the data members in NMWINMGR is NMWINMGR::sizeinfo, a special SIZEINFO struct. CWinMgr passes the total available size in szAvail, and
initializes the other members with appropriate defaults. szMin and szMax are the
min/max sizes (more on this subject later); szDesired is the desired TOFIT size.
WinMgr initializes szDesired to whatever size the window currently is, so if you
do nothing, the TOFIT size is whatever size the window already is. It's up to
you to set szDesired to the desired TOFIT size. CDopeyWnd and CLogoWnd each do
it differently.CDopeyWnd (the dopey pane) uses DrawText with DT_CALCRECT to calculate the size required to display its message: "This is the dopey pane. It is oh so dopey!" CDopeyWnd uses szAvail to know how wide the text is; DrawText tells it how high. Because CDopeyWnd::OnWinMgr ignores its current height,
DopeyEdit has the peculiar property that if you use the sizer bar to size the
dopey pane, then size the whole window, the dopey pane snaps back to its ideal
TOFIT size. (Go ahead, try it.) This is either a bug or a feature, depending on
your outlook.CLogoWnd handles WM_WINMGR slightly differently. CLogoWnd::OnCreate calculates m_szMinimum as the bitmap size plus a
few extra pixels for the border. (See CLogoWnd::Create in LogoWnd.cpp in Figure 7 for
full details.) Normally, you'd set szDesired to the TOFIT size, but in this case
setting szMin has the same effect. Why? Because the window size and thus the
default TOFIT size are initially zero. The first time CWinMgr requests the TOFIT
size, it initializes szDesired to this size—(0,0). CLogoWnd doesn't set
szDesired, but when control returns from CLogoWnd::OnWinMgr back to CWinMgr,
CWinMgr enlarges szDesired so it's at least as big as the minimum size, szMin.
And when you call SetWindowPositions, the bitmap window is actually enlarged to
this size, where it stays forever until the user changes it with the sizer bar
(more on this later). Why do it this way? So the logo window can be larger than
the bitmap, but not smaller.Astute readers may have noticed there's one other TOFIT entry in CDopeyView's window map. It's not a window; it's a group. The entire left pane (two logos and license) is a TOFIT row group. How does CWinMgr know the TOFIT size for a group? By calculating, of course. The TOFIT size of a group is just the sum of the TOFIT sizes of all its children. If row #1 needs 100 pixels and row #2 needs 200 pixels, then the pair must need 300 pixels. It's amazing how well computers can add, and they never make a mistake. As always, even though the size is specified as a SIZE, only the height or width applies, depending on whether the rectangle is a row or column. A Couple of DetailsOK, where are we? So far I've shown you the window map for CDopeyView, and the WM_WINMGR handlers for CLogoWnd and CDopeyWnd. There're only two more puzzle pieces required to finish CDopeyView. First is the OnSize handler, which is trivial: Look familiar? It's the same code as CMainFrame::RecalcLayout. Like
I said before, CMainFrame::RecalcLayout is just a frame thing. For any other
kind of window, OnSize is the place to do the sizing.Last but not least, there's one more detail required to make CDopeyView work. Remember those margins I told you about—the four-pixel border around all the panes? That border creates empty space where no windows exist, where the
view's background shows through. Unless you do something, this area will never
get painted. It will appear as a hole in your view, a hole in the shape of a
picture frame. Oops. To paint the margins space, you have to handle
WM_ERASEBKGND. This paints the entire client area whatever color buttons
are, which is gray in the normal Windows color scheme. To avoid screen flicker,
CDopeyView sets WS_CLIPCHILDREN in PreCreateWindow. If you don't use
WS_CLIPCHILDREN in your view, you have to carefully paint exactly the margins
and only the margins—which is easier than you might think because CWinMgr has
the rectangles already calculated!Whew! That may seem like a lot, but really it's not much code. To summarize: the main difference between CMainFrame with its toolbar, status bar, and view, and CDopeyView with its three panes and seven windows, boils down to the window map. That, plus a few straightforward message handlers: WM_WINMGR handlers for CLogoWnd and CDopeyWnd to report their TOFIT sizes; and WM_ERASEBKGND for the view to paint its background. As with the frame, there's no sizing code, no pixel-counting procedures. The entire layout is described in data. Pièce de Résistance: DialogsBut wait, there's more! You can use CWinMgr to size dialogs, too! Dialog-sizing is always a pesky problem in Windows because there are oh so many controls and there's no API function to just do it. You might think the way to size a dialog is to adjust all the controls proportionally from their initial sizes, but this doesn't always give stellar results. Some controls, such as edit controls or sliders and scrollbars, should grow with the dialog; others, like buttons and static text, generally shouldn't. You probably don't want a giant OK button just because the user made the dialog big! What you want is rule-based sizing—just what WinMgr provides.Fortunately, WinMgr doesn't know a dialog from a view from a frame. It works the same for all windows. The routine should be familiar if not stale by now: window map, OnSize handler, OnWinMgr handlers for TOFIT types. You've already seen DopeyEdit's dopey dialog in Figure 4. Figure 8 shows the map that produces it and Figure 9 illustrates the map graphically. It looks complicated, but really it's just a larger example of the same stuff I've already described. Only a few ideas are new for dialogs. First are radio button groups. How do you handle them? In a way, the group is a container, and the radio buttons are like children, but in fact the group box and radio buttons are sibling windows. How does WinMgr handle them? Well, CWinMgr already has just the right notion: namely, groups. When I first built WinMgr, I assumed groups would never correspond to actual windows. They were just a device to bundle rows or columns together. But a dialog group box behaves just like a WinMgr row group, so why not give the WinMgr group a window ID and see what happens? To my surprise, it worked! Of course, the radio buttons came
out squished against the top of the group and overlapping it, a minor detail
quickly fixed by adding margins. Figure 10
shows how margins work to keep the buttons well inside a dialog group. As the
figure shows, the group needs some extra space at the top to keep the radio
buttons below the group box text. Ignore the negative value for now—RCSPACE(-10) describes a FIXED
rectangle 10 pixels high with no window.It's the same as Remember, not every rectangle (row or column) in the window
map needs to have a window associated with it. If you create an entry with no
window, WinMgr sizes it, but SetWindowPositions does nothing with it. The result
is that nothing is displayed; the background shows through, and once again you
need to handle WM_ERASEBKGND to pain the empty space.Another problem with dialogs is the TOFIT size. Generally, in a dialog you want some controls to size with the dialog, while others like OK/Cancel buttons should keep their original size, if possible. To handle this, you'd have to make the buttons TOFIT, save the original size in OnInitDialog, and return it in szDesired whenever CWinMgr sends GET_SIZEINFO. But that's too tedious. The whole point of WinMgr is to make life easy for lazy programmers. I implemented a new function, CWinMgr::InitToFitSizeFromCurrent. This doesn't add any new features or change the layout rules; it's purely a convenience. InitToFitSizeFromCurrent tells WinMgr: "Make the desired size of every TOFIT window whatever size it is now." The idea is that you'll call it once when the dialog is first created, and then you don't have to bother handling WM_WINMGR—at least not for those windows. Here's the code. Whether or not you use InitToFitSizeFromCurrent, it's
important to call CalcLayout/SetWindowPositions because the layout as specified
in the window map will never match exactly what's in the resource file, and you
don't want the controls to jump abruptly the first time the user sizes the
dialog. So make sure you calc/reposition once before the dialog appears. WinMgr
has a special class, CSizeableDlg, that does this automatically. If you derive
from CSizeableDlg, you don't have to do anything.![]() Figure 11 Large Dialog Figure 11 shows the dopey dialog sized big; Figure 12 shows it small. The small version isn't especially interactive; it only lets users press OK or Cancel. I didn't bother to specify minimum sizes for all the controls, only the OK and Cancel buttons. If the user really wants to size this dialog down to nothing, who am I to get in the way? To make sure the OK and Cancel buttons don't vanish, CDopeyDialog specifies their minimum size as half the width of the initial size.
![]() Figure 12 To make life easy, I encapsulated all the generic dialog code in a special class, CSizeableDlg, that handles OnSize and OnInitDialog. All you have to do is derive from CSizeableDlg, create your window map, and go. See Figure 13 for details. Managing Min/Max InfoYou may think WinMgr is pretty cool, but there's still more! One of the annoying little loose ends of writing a Windows-based app is WM_GETMINMAXINFO. Windows sends this message once at the beginning and then again any time the user sizes your main window. Windows passes a MINMAXINFO struct that lets you specify minimum and maximum sizes. In practice, few programmers bother with WM_GETMINMAXINFO
because it requires so much pixel-counting code. But now CWinMgr has all the
information it needs to handle WM_GETMINMAXINFO, so why not use it? WinMgr
already knows the min/max size for each window. Figuring out the total is just a
matter of adding, something computers are good at.I added another function, CWinMgr::GetMinMaxInfo. GetMinMaxInfo calculates the min/max size and reports the results in a MINMAXINFO struct, just the way Windows wants it. GetMinMaxInfo even adds the height/width of all the window
elements—caption, menu, borders and so on—if they're present. The table you saw
earlier in Figure 1 shows
how CWinMgr determines the min/max size for each type of rectangle. You can
always handle GET_SIZEINFO to specify a particular min/max size for an
individual window (whether it's a TOFIT type or some other); WinMgr will use
your size when doing its arithmetic.In the case of DopeyEdit, the minimum height of the frame is the sum of the minimum heights of its rows—toolbar, view, and status bar. CMainFrame::OnWinMgr sets the min size for the toolbar and status bar, but not for the view. So how does CWinMgr know the minimum size for the view? Remember: if the parent window doesn't handle GET_SIZEINFO, WinMgr tries the child. CDopeyView::OnWinMgr uses GetMinMaxInfo to report its own minimum size. There's a subtle point here. The view acts as parent of its
own children (the seven windows) and as a child of the frame.
CDopeyView::OnWinMgr thus processes GET_SIZEINFO requests from the CWinMgr in
the main frame (CMainFrame::m_winMgr) as well as its own CWinMgr,
CDopeyView::m_winMgr. When Windows sends WM_GETMINMAXINFO to the main frame,
here's what happens, in detail.
![]() Figure 14 Minimum Size A Mystery ExplainedNow that you know about min/max info, I can explain a mystery. A little while ago, I showed you how to use RCSPACE to create blank space between windows. In the example, the margin was -10. Why the negative value?In the course of using GetMinMaxInfo, I discovered an interesting ambiguity. When calculating the minimum size of a group, CWinMgr naturally takes into account any dead space. For example, the main CDopeyView has a four-pixel margin so the min view size is the sum of the min sizes of the child
windows, plus 2×4 = 8 pixels for the margins. Fine. But when I got to dialogs, I
discovered that sometimes you don't want to include the margins in the min size.
You want the blank space to appear if there's room, but if the user really wants
to size the dialog down to nothing, it's OK to lose the margins, too—or other
blank space between controls. So sometimes the margins count, sometimes they
don't. How does WinMgr know which you want?To support both possibilities, I introduced a hack: if you want to include margins in the minimum size, specify them as positive values; if you want to exclude margins from the min size, use negative values. Finally, I should mention one minor gotcha to look for with
WM_GETMINMAXINFO. Windows sends WM_GETMINMAXINFO before WM_CREATE, before your
window has even been created. In this case, m_hWnd is NULL. Your OnGetMinMaxInfo
handler has to work in this situation. CWinMgr handles it gracefully, reporting
zeroes everywhere.Sizer BarsYou're probably sick of window maps by now, which is a good thing because it's time to move on. If you were paying any attention at all, you noticed that DopeyEdit has two sizer bars, also called splitter windows, that let users adjust the sizes of the panes. How do those work?There are many ways to split a window. MFC's CSplitterWnd makes all the panes children of a special splitter window (CSplitterWnd) that does the splitting. Other apps like Outlook® and Visual Studio® put one window inside a slightly larger one whose edges the user can drag. Either way, the splitter does all the size calculations—procedurally. CSplitterWnd has the additional limitation that it works only with views. But why all this code, why all this complexity? Judging from the e-mail I get, window-splitting is quite a cause for wonderment, when really window-splitting should be no cause for fuss. It's a simple operation. How hard can it be to change the sizes of two rectangles? Particularly once you have a tool like WinMgr that already does sizing? In fact, CWinMgr makes sizer bars (as I prefer to call them) almost trivial. Adjusting the sizes of child windows is just a matter of modifying the appropriate rectangles, then calling SetWindowPositions. I wrote a class, CSizerBar, that does just that. Unlike other implementations, a CSizerBar exists as a sibling, not a parent, of the windows it splits (as you saw earlier in Figure 3). Using CSizerBar is easy. First, give the sizer bar an ID and add it to your window map. The sizer has a FIXED height/width of four pixels. Naturally, you
can make it any size you want, or even variable (which would be really weird).
The location of the entry in the window map determines which windows the sizer
bar splits: namely, the entries on either side of it in the map. Duh. The
entries can even be groups, as in DopeyEdit, where the horizontal sizer bar
(ID_WIN_SIZERBAR_DOPEY) splits the groups that hold the left and right
panes.Next, add the sizer bar to your view, and create it as you would any other window. CSizerBar::Create expects the usual boring info plus a
reference to your CWinMgr. CSizerBar needs this to access the rectangles of the
windows on either side of it.When it's time to size—that is, when the user drags the bar and then lets go—CSizerBar sends WM_WINMGR to its parent, with notification code = NMWINMGR::SIZEBAR_MOVED. CSizerBar passes a point, ptMoved, that specifies the amount moved. This point is always of the form (x,0) or (0,y) since sizer bars can move horizontally or vertically, but not diagonally. CSizerBar doesn't actually move the windows; it only notifies its parent. It's up to you to handle the notification. But I wrote a function that does just what you want. This code works for both sizer bars—however many you
have—because WPARAM is the ID of the sizer bar. CWinMgr::MoveRect searches your
window map for the rectangle whose ID matches the one passed, moves that
window's rectangle by the amount requested (ptMoved), and adjusts the sizes of
the rectangles on either side accordingly. CSizerBar ensures that ptMoved is a
"good" amount, meaning one that doesn't violate any constraints like trying to
move the sizer bar outside the window. (Never a good idea.) Since MoveRect
manipulates the map, not the windows, you must call SetWindowPositions just like
you do from OnSize.Create the window, add it to your map, handle SIZEBAR_MOVED with two lines of code. CSizerBar makes splitting windows a snap. The reason it's so easy is, once again, that CWinMgr solves the sizing problem abstractly and generally, using data instead of code. You can perform any size operation you want on the rectangles, then call SetWindowPositions. For example, to tile your windows up, down, or sideways, all you have to do is implement your algorithm on the rectangles, then call SetWindowPositions. The bulk of CSizerBar is a lot of boring Windows mechanics to manage the user interaction. For details, see the code at the link at the top of this article. Here's the executive summary.
Rule-based ReviewWhew! If you made it this far, congratulations, you have more patience than I. Before we peek inside WinMgr, let me take a quick breather and share some WinMgr tips.CWinMgr makes window sizing easy by reducing the entire layout to a table. (The Power of Data, remember?) But creating that table, while easier than writing code, is not totally brainless. I know, because even though I have a brain, I find my window maps often produce bizarre and unexpected results. If you try WinMgr at home, don't freak if your OK button comes out as big as the entire window (it happened to me). You have to approach the map with cool reason. The rules work as claimed, they just take a little getting used to. If at first you don't succeed.... Once you get the hang of it, you'll be the envy of all your programmer pals, able to leap tall layouts in a single line of code. The whole trick is conceiving your layout as groups of rows and columns, and deciding which windows should be FIXED, TOFIT, PCT, or REST types. Here are some tips to get you started.
WinMgr has several functions to help you implement
CalculateWeirdPositions. CWinGroupIterator iterates the entries in a group;
CWinMgr::FindRect gets the entry (WINRECT) associated with a particular window
ID; and WINRECT has functions such as GetRect and SetRect that let you modify
it.Under the Hood, BrieflySo far I've shown you WinMgr from the outside, the way it looks to someone writing an application that uses it. Now it's time to open the hood to see what's inside. Alas, most of it is rather straightforward and dull, so I'll only cover the highlights beginning with the window map itself.The window map is a C array of WINRECTs. Each WINRECT holds information about a single entry. WINRECT is a class with several methods, but the data part looks like this: So a WINRECT is just a rectangle with some type information, a
child window ID, and a numeric param.BEGIN_WINDOW_MAP and END_WINDOW_MAP define the array. The other macros (RCFIXED, RCTOFIT, RCPERCENT, RCREST) initialize each WINRECT. Figure 1 gives the full poop. If you turned yourself into a preprocessor and gobbled the code to create MyMainWinMap for DopeyEdit's main frame, the result would be: The WINRECT constructor takes three args: flags, window ID, and a
LONG param whose meaning depends on the type. For a FIXED rect, param is the
fixed size; for PCT types it's the percentage from zero to 100.When you create a CWinMgr, you pass the constructor a reference to your map. The map is a flat array, but CWinMgr prefers to navigate it as a hierarchy, so the first thing it does is call a static function, WINRECT::InitMap, to initialize the map. InitMap converts the flat table into a hierarchy of linked
lists, as shown in Figure 15. WINRECTs have no parent/child pointers
because finding the parent or child is easy. To find the parent of any WINRECT,
scan the prev pointers backwards to NULL. The preceding entry in the map is the
parent. Finding children is even easier: the first child of a group is always
the next WINRECT in the table. WINRECT::Parent and WINRECT::Children encapsulate
these functions.![]() Figure 15 InitMap Hierarchy You've already seen CWinMgr::SetWindowPositions. There's also a GetWindowPositions. Both functions work as expected. SetWindowPositions uses ::DeferWindowPos to move all the windows in one fell swoop. DeferWindowPos is a standard Windows gimmick. The only trick is you have to know in advance how many windows there are—information which CWinMgr obtains from a helper function CountWindows. Zzzzz. The real guts and glory of CWinMgr lie in a protected virtual function, CalcGroup. The public function CalcLayout calls CalcGroup to do the work—that is, to calculate the sizes and positions of all the rectangles based on the rules. The algorithm goes like this:
Well, that's about it. The other CWinMgr functions you've seen—GetMinMaxInfo, MoveRect, FindRect, and so on—are all pretty much what you'd expect. CSizerBar and CSizeableDlg are equally predictable. All in all, WinMgr is pretty boring code. It's what it does that makes your app zing!
|
||
For related articles see: http://support.microsoft.com/default.aspx?scid=kb;en-us;q71669 http://support.microsoft.com/default.aspx?scid=kb;en-us;q74797 For background information see: Working with Resource Files |
||
Paul DiLascia is a freelance writer, consultant, and Web/UI designer-at-large. He is the author of Windows++: Writing Reusable Windows Code in C++(Addison-Wesley, 1992). Paul can be reached at askpd@pobox.com or http://www.dilascia.com. |
'Development' 카테고리의 다른 글
[STL/string] 문자열 뒤집기 (0) | 2008.11.26 |
---|---|
[Windows]98에서 정상작동하는 TransparentBlt (0) | 2008.11.03 |
dll 관련 (0) | 2008.10.19 |
[Eclipse]subclipse(Eclipse용 Subversion) (0) | 2008.10.09 |
[Eclipse]astyle(Artistic Style) plugin (0) | 2008.10.09 |