Share via


From the July 2001 issue of MSDN Magazine.

MSDN Magazine

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
SUMMARYProgrammers 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.

I f your Windows®-based app does anything at all, it's likely to contain several child windows. A typical Explorer-style app has at least two panes, left and right, plus toolbar and status bar. More complex apps have more windows. Microsoft Excel has 28 windows according to Spy, and that's with only one spreadsheet open. (Of course, not all those windows are visible.) Managing the sizes and positions of so many windows is tedious, if not daunting. Your main window gets WM_SIZE, and now you have to reposition them all. If the children have children, they too must process WM_SIZE, and so on until the last descendant is moved. It's easy to get bogged down writing code that's no more than a painful exercise in pixel arithmetic. The problem is even worse for dialogs and dialog-based apps. You want to let users size your dialog, but if you only set WS_THICKFRAME, the controls fall off the window like lemmings off a cliff as the frame shrinks. What a drag.
      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:

  void CMyView::OnSize(UINT nType, int cx, int cy)
  
{
m_winMgr.CalcLayout(cx,cy,this);
m_winMgr.SetWindowPositions(this);
}

 

      In short, WinMgr helps you achieve total layout bliss. (As for the rest of your life, you're on your own.)

The Power of Data

      Suppose 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.

  DoMumbleBletch(p,q,4,5,27);
  
DoMumbleBletch(p,q,3,6,34);
•••
// 15 lines later
DoMumbleBletch(p,q,88,91,201);

 

      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 Layout

      How 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.

DopeyEdit

      To 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
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
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
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 Frame

      MFC 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.

  // in mainfrm.cpp
  
BEGIN_WINDOW_MAP(MyMainWinMap)
BEGINROWS(WRCT_REST,0,0)
RCTOFIT(AFX_IDW_TOOLBAR)
RCREST(AFX_IDW_WIN_FIRST)
RCTOFIT(AFX_IDW_STATUS_BAR)
ENDGROUP()
END_WINDOW_MAP()

 

      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
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.

    // mainfrm.h
  
class CMainFrame : public CFrameWnd {
protected:
CWinMgr m_winMgr;
};

 

And whenever you instantiate a CWinMgr, you must give it a pointer to your window map.

    // mainfrm.cpp
  
CMainFrame::CMainFrame() : m_winMgr(MyMainWinMap)
{
}

 

      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.

  // override MFC
  
void CMainFrame::RecalcLayout(BOOL bNotify)
{
m_winMgr.CalcLayout(this);
m_winMgr.SetWindowPositions(this);
// Don't call CFrameWnd!
// CFrameWnd::RecalcLayout(bNotify);
}

 

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.

  // in CMainFrame message map
  
ON_REGISTERED_MESSAGE(WM_WINMGR,OnWinMgr)

 

And here's the handler:

  LRESULT CMainFrame::OnWinMgr(WPARAM wp, LPARAM lp)
  
{
NMWINMGR& nmw = *(NMWINMGR*)lp;
if (nmw.code==NMWINMGR::GET_SIZEINFO) {
if (wp==AFX_IDW_TOOLBAR) {
nmw.sizeinfo.szDesired =
m_wndToolBar.CalcFixedLayout(FALSE,TRUE);
return TRUE;
} else // similar for AFX_IDW_STATUS_BAR
•••
}
return 0;
}

 

      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 View

      Well, 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.

  struct SIZEINFO {
  
SIZE szAvail; // total avail
SIZE szDesired; // desired size
SIZE szMin; // min size
SIZE szMax; // max size
};

 

      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.

  // in CDopeyWnd::OnWinMgr
  
CString s = // GetWindowText;
CRect rcText(0,0,nmr.sizeinfo.szAvail.cx,0);
dc.DrawText(s,&rcText,DT_CALCRECT);
nmr.sizeinfo.szDesired = rcText.Size();

 

      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.

  // in CLogoWnd::OnWinMgr
  
NMWINMGR& nmw = *(NMWINMGR*)lp;
nmw.sizeinfo.szMin = m_szMinimum;
return TRUE;

 

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 Details

      OK, 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:

  void CDopeyView::OnSize(UINT nType, int cx, int cy)
  
{
m_winMgr.CalcLayout(cx,cy,this);
m_winMgr.SetWindowPositions(this);
}

 

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?

  BEGINCOLS(WRCT_REST,0,RCMARGINS(4,4))
  

 

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.

  BOOL CDopeyView::OnEraseBkgnd(CDC* pDC)
  
{
CBrush brush(GetSysColor(COLOR_3DFACE));
CRect rc;
GetClientRect(&rc);
pDC->FillRect(&rc,&brush);
return TRUE;
}

 

      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: Dialogs

      But 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?

  BEGINROWS(WRCT_TOFIT,
  
IDC_GROUP1, // <== ID of real group box
0)

 

      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.

  RCSPACE(-10)
  

 

Ignore the negative value for now—RCSPACE(-10) describes a FIXED rectangle 10 pixels high with no window.
      It's the same as

  RCFIXED(0,-10)
  

 

      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.

  BOOL CSizeableDlg::OnInitDialog()
  
{
BOOL bRet = CDialog::OnInitDialog();
m_winMgr.InitToFitSizeFromCurrent(this);
m_winMgr.CalcLayout(this);
m_winMgr.SetWindowPositions(this);
•••
return bRet;
}

 

      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 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.

  // in CDopeyDialog::OnInitDialog
  
CSize sz = m_winMgr.GetRect(IDOK).Size();
m_szMinButton = CSize(sz.cx/2,sz.cy);
// in CDopeyDialog::OnWinMgr
if (nmw.code==NMWINMGR::GET_SIZEINFO) {
if (wp==IDOK || wp==IDCANCEL) {
nmw.sizeinfo.szMin = m_szMinButton;
return TRUE;
}
}

 


Figure 12
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 Info

      You 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.

  void CMainFrame::OnGetMinMaxInfo(MINMAXINFO* lpMMI)
  
{
lpMMI->ptMinTrackSize =
ptDontLetItGetSmallerThanThis;
}

 

      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.

  void CMainFrame::OnGetMinMaxInfo(MINMAXINFO* lpMMI)
  
{
m_winMgr.GetMinMaxInfo(this, lpMMI);
}

 

      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.

  // in CDopeyView::OnWinMgr
  
if (nmw.code==NMWINMGR::GET_SIZEINFO &&
wp==GetDlgCtrlID()) {
// report my (view's) min size.
m_winMgr.GetMinMaxInfo(this, nmw.sizeinfo);
return TRUE;
}

 

      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.

  1. Windows sends WM_GETMINMAXINFO to the frame.
  2. CMainFrame::OnGetMinMaxInfo calls CWinMgr::GetMinMaxInfo through the frame's CWinMgr object (CMainFrame::m_winMgr).
  3. CWinMgr sends WM_WINMGR to the frame, with notification code = NMWINMGR::GET_SIZEINFO, to get each child window's size information. CMainFrame reports the size info for toolbar and status bar; but it returns zero for the view—not handled.
  4. CWinMgr passes the size request to the view. This time, CDopeyView::OnWinMgr handles the message. To get its own size information, CDopeyView calls CWinMgr::GetMinMaxInfo through its own CWinMgr object (CDopeyView::m_winMgr).
  5. CWinMgr::GetMinMaxInfo computes the view's min size. It requests size information from all the view's children. The two CLogoWnds and CDopeyWnd report their minimum sizes, which get added into the total.

      In effect, CDopeyView::OnWinMgr provides a link from the frame to its grandchildren. The end result is that users cannot size DopeyEdit smaller than what's needed to hold the two bitmaps in the left pane, as shown in Figure 14. This situation is quite common: the main frame houses a window which itself houses several controls. To make sure all the grandchildren's min/max sizes are used in computing the total min/max size, each child with children must call CWinMgr::GetMinMaxInfo in order to report its own total min/max info, all the way down to the deepest parent in the hierarchy.

Figure 14 Minimum Size
Figure 14 Minimum Size

A Mystery Explained

      Now 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

  BEGINCOLS(WRCT_REST,0,RCMARGINS(4,4))
  

 

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.

  // margins not required:
  
BEGINROWS(WRCT_REST,4,RCMARGINS(-4,-4))

 

      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 Bars

      You'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.

  RCFIXED(ID_WIN_SIZERBAR_LICENSE,4)
  

 

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.

  // in CDopeyView::OnCreate
  
VERIFY(m_wndSizerBarLicense.Create(WS_VISIBLE|WS_CHILD,
this, m_winMgr, ID_WIN_SIZERBAR_LICENSE));

 

      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.

  // in CDopeyView::OnWinMgr
  
if (nmw.code==NMWINMGR::SIZEBAR_MOVED) {
m_winMgr.MoveRect(wp, nmw.sizebar.ptMoved, this);
m_winMgr.SetWindowPositions(this);
}

 

      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.

  1. CSizerBar implements the usual drag-state loop: enter drag mode, capture the mouse, move the bar, release the mouse.
  2. CSizerBar handles WM_SETCURSOR to set the cursor to the standard left/right or up/down thingie. Unlike other splitter windows, CSizerBar is smart enough to tell if it's horizontal or vertical. It determines its own orientation using the amazing algorithm: if I'm taller than I am wide, then I must be vertical.
  3. CSizerBar calls CWinMgr::OnGetSizeInfo to get the min/max sizes of the windows on either side of itself in order to constrain dragging appropriately. The details are an exercise in precise pixel-counting.
  4. When the user sizes, CSizerBar::DrawBar draws a black bar directly on the parent window's DC using XOR, so drawing a second time erases it. This is a standard graphics trick. DrawBar calls a virtual function, OnDrawBar, which you can override to do funky painting—as long as your algorithm has the XOR effect.
  5. CSizerBar::OnChar handles VK_ESCAPE to cancel sizing.

Rule-based Review

      Whew! 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.

  • The first entry in your window map must be a row group or a column group. It should almost always be a REST group—to use the whole client area.
  • Most groups are either TOFIT or REST.
  • Usually your window map will alternate groups of rows and columns. One exception is that group boxes are always row groups, even when they are inside another row group.
  • Use RCFIXED for windows such as sizer bars whose height or width is fixed.
  • There can be only one REST rect in each group, so it's a good idea to identify it first. Often the REST rect is the biggest window in the group—for example, the view in a frame.
  • Use TOFIT for any window/rectangle whose size varies dynamically, but isn't a REST or PCT type. For every TOFIT type, you must handle WM_WINMGR in the parent or child, to report the desired (szDesired) or minimum (szMin) size (unless you use InitToFitSizeFromCurrent for a sizeable dialog). Otherwise, your window will retain its original Create size, which is usually zero.
  • Remember: even though you specify the desired TOFIT size as a SIZE/CSize, only the height (row) or width (column) applies. The other dimension is determined by the group. For min/max info, both dimensions apply.
  • Use RCMARGINS to create margins within a group. Use negative margins if you don't want the margins to count when calculating the group's min size.
  • Use RCSPACE to create blank space between windows.
  • If your map has margins or blank space, don't forget to paint your background (handle WM_ERASEBKGND)! Use WS_CLIPCHILDREN to avoid flicker.
  • A group should not have a window ID unless it represents an actual group box in a dialog. A group box in a dialog should always be represented by a group.

      If the rules require practice, at least they're powerful enough to support most layouts. The TOFIT rule provides an escape hatch to implement almost any kind of layout you want. If you want a window whose height is proportional to the phase of the moon or current NASDAQ index (a shrinking window), you can do it provided you can get the information.

  void CMyWnd::DoWeirdSizeOp()
  
{
CalculateWeirdPositions(m_winMgr);
m_winMgr.SetWindowPositions(this);
}

 

      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, Briefly

      So 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:

  class WINRECT {
  
protected:
WINRECT* next; // next at this level
WINRECT* prev; // prev at this level
CRect rc; // the rectangle
WORD flags; // type/group info
UINT nID; // window ID or 0
LONG param; // depends on type
};

 

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:

  WINRECT MyMainWinMap[] = {
  
WINRECT(WRCF_ROWGROUP|WRCT_REST,0,0),
WINRECT(WRCT_TOFIT,AFX_IDW_TOOLBAR,0),
WINRECT(WRCT_REST, AFX_IDW_WIN_FIRST,0),
WINRECT(WRCT_TOFIT,AFX_IDW_STATUS_BAR,0),
WINRECT(WRCF_ENDGROUP,-1,0),
WINRECT(WRCT_END,-1, 0)
};

 

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.

  CWinMgr::CWinMgr(WINRECT* pWinMap) : m_map(pWinMap)
  
{
WINRECT::InitMap(m_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
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:

  1. CalcGroup assumes whoever called it has set the rectangle (rc) for the group itself. This rectangle is adjusted downward if the group has margins. The adjusted rectangle becomes the total area (rcTotal) which CalcGroup has to work with.
  2. After determining rcTotal, CalcGroup allocates to each child rectangle its minimum size. This requires sending a GET_SIZEINFO to each window. By default, the min size is zero and the max is SIZEMAX (infinite).
  3. Next, CalcGroup adjusts each rectangle's size upward, if there's room, to its desired size. The desired size depends on the type. For FIXED rects, it's the fixed size; for PCT types, it's the percentage of rcTotal; for TOFIT, CalcGroup sends a message. Always keeping in mind that size really means height or width, depending whether the rectangle is a row or column. CalcGroup does the REST rect (if any) last. The reason CalcGroup allocates the sizes in a two-step process (minimum first, then desired) is to make sure each window gets at least its minimum size. Otherwise, the first window might gobble all the space.
  4. The two previous steps only set the sizes of the rectangles, not their positions. Next, CalcGroup calls PositionRects to move the rectangles so they're contiguous.
  5. At this point, all the group's children have been calculated, but the grandchildren have not. As a final step, CalcGroup calls itself recursively for each group within the group. This is where the power and beauty of recursion really work wonders: just apply the same algorithm to each subgroup and sub-sub-group and sub-to-the-nth group, until all the groups are done or there's no stack space. Amazing!

      The only other function worthy of description is CWinRect::OnGetSizeInfo. This somewhat mammoth function (see Figure 16) stuffs a SIZEINFO struct with all the size info CWinMgr could want about a WINRECT: min, max, and desired (TOFIT) size. If the WINRECT is a group, OnGetSizeInfo calls itself recursively to get the SIZEINFO for each child and aggregates the total. If the WINRECT is a primitive type (not a group), OnGetSizeInfo determines the size info based on the rules: for FIXED, the desired size is the fixed size; for TOFIT, OnGetSizeInfo sends a message, and so on. OnGetSizeInfo always sends GET_SIZEINFO for min/max info, since all windows can use GET_SIZEINFO to specify min/max info; but CWinMgr uses szDesired only for TOFIT types.
      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!

WinMgr Quick Guru Guide

If you're a Windows®-based programming pro and you can't wait to use WinMgr to achieve window-sizing bliss, here's the guru's how-to.
      To size child windows in your frame or view:

  1. Create your window map using BEGIN_WINDOW_MAP, END_WINDOW_MAP, and the other macros in Figure 1.

      BEGIN_WINDOW_MAP(MyWindowMap)
      
    BEGINROWS(...)
    •••
    END_WINDOW_MAP(MyWindowMap)

     

    The first entry in your map must be a group (BEGINROWS or BEGINCOLS). You must figure out how to divide your window into groups of rows and columns. The article gives you tips.

  2. Instantiate a CWinMgr in your parent window (frame, view, dialog, and so on). Initialize it with your map.

      class CMyView {
      
    protected:
    CWinMgr m_winMgr;
    };
    CMyView::CMyView() : m_winMgr(MyWindowMap)
    {
    }

     

  3. Handle WM_SIZE as follows:

      void CMyView::OnSize(UINT nType, int cx, int cy)
      
    {
    m_winMgr.CalcLayout(cx,cy,this);
    m_winMgr.SetWindowPositions(this);
    }

     

    For frames, do this in CFrameWnd::RecalcLayout. Don't call the base class!

  4. If your window map creates empty space between child windows (as a result of margins or RCSPACE), you must handle WM_ERASEBKGND to paint the background.
  5. For each TOFIT window, you must handle WM_WINMGR, code = NMWINMGR::GET_SIZEINFO in either the parent (frame, view, dialog) or child window.

      // CMyChildWnd::OnWinMgr
      
    NMWINMGR& nmr = *(NMWINMGR*)lp;
    if (nmr.code==NMWINMGR::GET_SIZEINFO && wp==GetDlgCtrlID()) {
    nmr.sizeinfo.szDesired =
    ComputeWeirdSize(nmr.sizeinfo.szAvail);
    return TRUE; // handled
    }

     

    CWinMgr passes the total available size in NMWINMGR::sizeinfo.szAvail.

      To handle WM_GETMINMAXINFO automagically:

  1. Implement OnGetMinMaxInfo in your main frame like so:

      void CMainFrame::OnGetMinMaxInfo(MINMAXINFO* lpMMI)
      
    {
    MyMainWinMgr.GetMinMaxInfo(this, lpMMI);
    }

     

  2. If your view and/or any other child windows in your main window map also has a window map, make sure it handles NMWINMGR::GET_SIZEINFO like this:

      // CMyView::OnWinMgr
      
    NMWINMGR& nmw = *(NMWINMGR*)lp;
    if (nmw.code==NMWINMGR::GET_SIZEINFO && wp==GetDlgCtrlID()) {
    m_winMgr.GetMinMaxInfo(this, nmw.sizeinfo);
    return TRUE;
    }

     

  3. For any window that has a min/max size, handle NMWINMGR::GET_SIZEINFO to report the size.

      // CMyChildWnd::OnWinMgr(WPARAM wp, LPARAM lp)
      
    NMWINMGR& nmw = *(NMWINMGR*)lp;
    if (nmw.code==NMWINMGR::GET_SIZEINFO && wp==GetDlgCtrlID()) {
    nmw.sizeinfo.szMin = m_szMyMinSize;
    return TRUE;
    }

     

      To size a dialog:

  1. If you derive your dialog from CSizeableDlg, all you have to do is create your window map and handle WM_WINMGR. CSizeableDlg handles OnSize and OnGetMinMaxInfo for you. If you don't use CSizeableDlg, write your OnSize handler as shown previously, and OnInitDialog like so:

      BOOL CMySizeableDlg::OnInitDialog()
      
    {
    BOOL bRet = CDialog::OnInitDialog();
    m_winMgr.InitToFitSizeFromCurrent(this);
    m_winMgr.CalcLayout(this);
    m_winMgr.SetWindowPositions(this);
    return bRet;
    }

     

      To create a sizer bar:

  1. Instantiate a CSizerBar in your parent window.

      class CMyView : public CView {
      
    protected:
    CWinMgr m_winMgr;
    CSizerBar m_wndSizerBar;
    };

     

  2. Add the sizer bar to your window map, between the rectangles you want to split. They can be windows or groups.

      // in window map
      
    RCREST(ID_WIN_EDIT)
    RCFIXED(ID_WIN_SIZERBAR_DOPEY,4)
    RCTOFIT(ID_WIN_DOPEY)

     

    The sizer bar should be a FIXED type. Four pixels is a good height and width.

  3. Create the sizer bar in your OnCreate handler, just like any other window.

      // CMyView::OnCreate
      
    VERIFY(m_wndSizerBar.Create(WS_VISIBLE|WS_CHILD,
    this, m_winMgr, ID_SIZERBAR));

     

    You must give the sizer bar a reference to your CWinMgr and an ID.

  4. Handle WM_WINMGR, code = NMWINMGR::SIZEBAR_MOVED as follows:

      // CMyView::OnWinMgr
      
    if (nmw.code==NMWINMGR::SIZEBAR_MOVED) {
    // wp is control ID of sizer bar
    m_winMgr.MoveRect(wp, nmw.sizebar.ptMoved, this);
    m_winMgr.SetWindowPositions(this);
    }

     

      By default, the sizer bar is a static (CStatic) control with no text or image, so it appears as a COLOR_3DFACE box, as if part of the background. If you want something else, derive from CSizerBar and override OnDrawBar. You must use an XOR algorithm so painting a second time erases the bar.
      Happy sizing!

For related articles see:
https://support.microsoft.com/default.aspx?scid=kb;en-us;q71669
https://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 https://www.dilascia.com.