Status Bars
It has become common, even expected, for Windows applications to include status bars that display context-sensitive help for toolbar buttons and menu items. SDK-style Windows applications customarily display descriptive help text for menu items by trapping WM_MENUSELECT messages and updating the status bar. MFC provides an easier way. When a CStatusBar is connected to a frame window, it automatically displays a string of help text when a menu item is highlighted. If the application includes a toolbar, and if the toolbar style includes a CBRS_FLYBY flag, the status bar also displays flyby text for toolbar buttons. The best part is that all you're responsible for besides creating and initializing the status bar (something that requires just a few lines of code) is providing the help text in the form of string resources in your application's RC file. The framework does the rest.
Status bars can do much more than just display help text, of course. A status bar can be divided into one or more areas that are variously referred to as panes, panels, or indicators. The text of each pane can be set individually, so one pane can display the current line number or page number in a document while another displays menu and toolbar help and still others display the current Caps Lock and Num Lock states. Some status bars even contain progress controls that report percentage-complete figures for potentially lengthy operations such as document saving and loading.
Creating and Initializing a Status Bar
In MFC, a status bar is an instance of CStatusBar. An application that uses a status bar typically declares a CStatusBar object as a member of the frame window class. Then the frame window's OnCreate handler creates the status bar with a statement like this one:
m_wndStatusBar.Create (this);
The lone argument passed to Create identifies the status bar's parent window. Passing a this pointer referring to a frame window makes the status bar a child of the frame window. A status bar created in this way doesn't need to be destroyed before the application terminates because it's destroyed automatically when its parent is destroyed. CStatusBar::Create also accepts parameters specifying the status bar's style and child window ID, but the default values MFC provides for these parameters do quite nicely for most applications.
After it's created, a status bar is initialized by calling CStatusBar::SetIndicators. SetIndicators specifies the number of panes the status bar will contain and optionally assigns string resources to individual panes. The statements
UINT nIndicator = ID_SEPARATOR;
m_wndStatusBar.Create (this);
m_wndStatusBar.SetIndicators (&nIndicator, 1);
create a simple status bar containing just one pane. ID_SEPARATOR is a generic ID that says no string resource is associated with this pane. You can create a simple "binary" pane that indicates whether a particular feature of your application is on or off by specifying a string resource ID instead of ID_SEPARATOR and connecting the pane to an update handler that uses CCmdUI::Enable to enable and disable the pane. An enabled pane displays the string resource assigned to it, but a disabled pane is blank. The status bar created by the following code sample includes a pane that displays the text string "INS" when the application is in insert mode and nothing when it's in overstrike mode. This example assumes that insert mode is on when m_bInsert is nonzero and off when m_bInsert is 0:
// In the RC file
STRINGTABLE
BEGIN
ID_INDICATOR_INS "INS"
END
// In CMainFrame's message map
ON_UPDATE_COMMAND_UI (ID_INDICATOR_INS, OnUpdateIndicator)
// In CMainFrame::OnCreate
static UINT nIndicators[] = {
ID_SEPARATOR,
ID_INDICATOR_INS
};
m_wndStatusBar.Create (this);
m_wndStatusBar.SetIndicators (nIndicators, 2);
// Elsewhere in CMainFrame
void CMainFrame::OnUpdateIndicator (CCmdUI* pCmdUI)
{
pCmdUI->Enable (m_bInsert);
}
In this example, the frame window handles the UI update commands. In a real application, it might be more appropriate to make OnUpdateIndicator a member of the document or the view class. ID_INDICATOR_INS is a symbolic constant defined elsewhere in the application; MFC doesn't define it for you.
MFC defines four special indicator IDs for status bar panes that display keyboard states and maps them to a common update handler in the CFrameWnd class:
ID_INDICATOR_CAPS, which corresponds to the Caps Lock key
ID_INDICATOR_NUM, which corresponds to the Num Lock key
ID_INDICATOR_SCRL, which corresponds to the Scroll Lock key
ID_INDICATOR_KANA, which corresponds to the Kana key on Japanese keyboards
A status bar pane assigned the ID value ID_INDICATOR_CAPS displays the word "CAP" when Caps Lock is on. Similarly, an ID_INDICATOR_NUM pane displays "NUM" when Num Lock is on, an ID_INDICATOR_SCRL pane displays "SCRL" when Scroll Lock is on, and an ID_INDICATOR_KANA pane displays "KANA" when Kana mode is enabled on Japanese keyboards. The framework (in reality, CFrameWnd::OnUpdateKeyIndicator) keeps these indicators in sync with the keyboard. Consequently, you can create a status bar with Caps Lock, Num Lock, and Scroll Lock indicators simply by adding the magic ID values to the array passed to SetIndicators:
static UINT nIndicators[] = {
ID_SEPARATOR,
ID_INDICATOR_CAPS,
ID_INDICATOR_NUM,
ID_INDICATOR_SCRL
};
m_wndStatusBar.Create (this);
m_wndStatusBar.SetIndicators (nIndicators, 4);
The resulting status bar is shown in Figure 12-6. The blank pane indicates that Scroll Lock is inactive. CStatusBar automatically positions all panes after the first at the far right end of the status bar and stretches the leftmost pane to fill the remaining space. It sizes the other panes so that they're just wide enough to display the text strings assigned to them. Panes other than the first are also drawn "indented" so that they're visible even when they're blank.
Figure 12-6. Status bar with Caps Lock, Num Lock, and Scroll Lock indicators.
Providing Context-Sensitive Help for Menu Items
When you assign the first (leftmost) pane in a status bar the value ID_SEPARATOR, you enable a special feature of MFC that is elegant in both design and simplicity. When the user highlights a menu item, the framework checks to see whether the application's EXE file contains a string resource whose ID equals the menu item ID. If the search turns up a match, the string resource is loaded and displayed in the status bar pane. As a result, you can provide context-sensitive help for your application's menus by providing string resources whose IDs match the menu item IDs. If a menu item and a toolbar button share the same ID, the same string resource doubles as help text for the menu item and as flyby text for the toolbar.
As it does for toolbar buttons, the framework provides default help strings for ID_FILE_NEW, ID_FILE_OPEN, and other common command IDs. It also provides default help strings for commands found in the system menu. (For a complete list of predefined IDs and the help text and ToolTip text associated with them, look in the MFC source code file Prompts.rc.) Simply include the header file Afxres.h in your application's RC file, and the framework's predefined string resources will be included, too. If you use AppWizard to create the application, Afxres.h is included for you. Rather than add string resources for other menu items by hand, you can double-click a menu item in the menu editor and enter a string in the Menu Item Properties window's Prompt box.
You can override the help text for predefined menu item IDs by defining your own string resources with identical ID values. For a nice touch, include an
AFX_IDS_IDLEMESSAGE "Ready"
statement in your application's string table, and the framework will display the word "Ready" in the status bar when no menu is pulled down or no item is selected. As usual, this is done for you if you use AppWizard to add a status bar to your application.
Creating Custom Status Bar Panes
Now you know how to display help text in a status bar, add Caps Lock, Num Lock, and Scroll Lock indicators, and create simple on/off indicators by combining string resources and update handlers. But what about more complex status bars like the ones featured in Microsoft Word, Microsoft Excel, Microsoft PowerPoint, and other Windows applications? How, for example, would you create a status bar pane that displays the time of day or the current page number?
For starters, you can add panes to a status bar and size them any way you want using CStatusBar's SetPaneInfo function. SetPaneInfo accepts four parameters: the 0-based index of the pane whose attributes you want to modify and the pane's ID, style, and width, in that order. The pane style specifies whether the pane will be drawn indented, protruding, or flush with the face of the status bar. It also determines whether the pane is currently enabled or disabled and identifies variable-width panes that expand and contract with the status bar. The style is a combination of one or more of the following values:
Style
Description
SBPS_NOBORDERS
Draws the pane flush with the surface of the status bar.
SBPS_POPOUT
Draws the pane so that it protrudes from the status bar.
SBPS_NORMAL
Draws the pane so that it is indented into the status bar.
SBPS_DISABLED
Disables the pane. Disabled panes don't display text.
SBPS_STRETCH
Stretches the pane to fill unused space when the status bar is resized. Only one pane per status bar can have this style.
SBPS_OWNERDRAW
Creates an owner-draw pane.
The following code creates a status bar with three custom panes. The first pane is 64 pixels wide and is drawn flush with the surface of the status bar. The second is also 64 pixels wide, but it protrudes from the status bar. The third is a variable-width pane whose right edge follows the right edge of the status bar. It's drawn with an indented border.
static UINT nIndicators[] = {
ID_SEPARATOR,
ID_SEPARATOR,
ID_SEPARATOR
};
m_wndStatusBar.Create (this);
m_wndStatusBar.SetIndicators (nIndicators, 3);
m_wndStatusBar.SetPaneInfo (0, ID_SEPARATOR, SBPS_NOBORDERS, 64);
m_wndStatusBar.SetPaneInfo (1, ID_SEPARATOR, SBPS_POPOUT, 64);
m_wndStatusBar.SetPaneInfo (2, ID_SEPARATOR, SBPS_NORMAL ¦
SBPS_STRETCH, 0);
In a real application, you'll probably want to avoid hard pixel counts and, instead, base pane widths on a scalable screen metric such as the average width of a character in the status bar font. You can get a CFont pointer for the default status bar font by calling the GetFont function a CStatusBar inherits from CWnd.
Once a custom pane is created, it's your job to tell the status bar what to display inside the pane. You can add text to a pane in two ways. You can call CStatusBar::SetPaneText to set the text directly, or you can assign the pane an update handler and let the update handler set the text with CCmdUI::SetText. Which method you use depends on how you want the pane to be updated. The following code fragment sets a timer to fire every 200 milliseconds and uses SetPaneText to update an hours:minutes:seconds display in pane 2. (Windows timers are discussed in Chapter 14.) In this case, the ID assigned to the pane in the call to SetIndicators or SetPaneInfo is irrelevant because SetPaneText identifies panes by index.
// In CMainFrame::OnCreate
SetTimer (ID_TIMER, 200, NULL);
void CMainFrame::OnTimer (UINT nTimerID)
{
CTime time = CTime::GetCurrentTime ();
int nSecond = time.GetSecond ();
int nMinute = time.GetMinute ();
int nHour = time.GetHour () % 12;
CString string;
string.Format (_T ("%0.2d:%0.2d:%0.2d"), nHour, nMinute, nSecond);
m_wndStatusBar.SetPaneText (2, string);
}
An alternative approach is to assign the pane a unique ID such as ID_INDICATOR_TIME and connect it to an update handler with a message-map entry. Now the time-of-day display in the status bar will be continually updated by the framework.
// In the message map
ON_UPDATE_COMMAND_UI (ID_INDICATOR_TIME, OnUpdateTime)
void CMainFrame::OnUpdateTime (CCmdUI* pCmdUI)
{
CTime time = CTime::GetCurrentTime ();
int nSecond = time.GetSecond ();
int nMinute = time.GetMinute ();
int nHour = time.GetHour () % 12;
CString string;
string.Format (_T ("%0.2d:%0.2d:%0.2d"), nHour, nMinute, nSecond);
pCmdUI->SetText (string);
}
The best way to define ID_INDICATOR_TIME is to add a string resource with that ID to your application. Assign the string a dummy value such as "MMMMM," and MFC will use the width of the string to size the status bar pane. Incidentally, you can include a leading tab character ("\t") in text written to a status bar to center the text in the pane or two leading tab characters ("\t\t") to right-align the text.
Status Bar Support in AppWizard
You can use AppWizard to add a status bar to an MFC application by checking the Initial Status Bar box in AppWizard's Step 4 dialog box, as shown in Figure 12-7. AppWizard responds by adding a CStatusBar member variable to the main frame window class and hooking it up with an OnCreate handler that creates a four-pane status bar: an ID_SEPARATOR pane in which help text appears and indicator panes for the Caps Lock, Num Lock, and Scroll Lock keys.
Figure 12-7. Using AppWizard to add a status bar.
One of the first questions new MFC programmers ask about AppWizard-generated status bars is, "How do I get rid of the keyboard indicator panes?" The answer is simple. Begin by finding the following statements in the CPP file for the AppWizard-generated main frame window class:
static UINT indicators[] =
{
ID_SEPARATOR, // status line indicator
ID_INDICATOR_CAPS,
ID_INDICATOR_NUM,
ID_INDICATOR_SCRL,
};
Then remove the final three entries so that the array looks like this:
static UINT indicators[] =
{
ID_SEPARATOR // status line indicator
};
That's all there is to it. Rebuild the application and the indicator panes will be no more.