In addition to the toolbars, the SDI and MDI applications have a status bar at the bottom of the frame that provides textual descriptions of the toolbar buttons and menu entries. The status bar also has default areas that display whether the Caps, Num, and Scroll Lock keys are on.
Today, you will learn
One of the driving intentions behind the development of Graphical User Interfaces (GUI) such as Windows was the goal of making computers easier to use and learn. In the effort to accomplish this goal, GUI designers stated that all applications should use a standard set of menus and that the menus should be organized in a standardized manner. When Microsoft designed the Windows operating system, it followed this same philosophy, using a standard set of menus organized in a standard order on most of its applications.
A funny thing happened once Windows became widely used. The application designers found that new users still had a difficult time learning new applications and that advanced users found the menus cumbersome. As a result, the application designers invented toolbars as one solution to both problems.
A toolbar is a small band attached to the window frame or a dialog window that is floating independent of the application frame. This band (or dialog) has a number of small buttons containing graphic images that can be used in place of the menus. The application designers place the most commonly used functions for their applications on these toolbars and do their best to design graphical images that illustrate the functions the buttons serve.
Once advanced users learned what each of the toolbar buttons do, the toolbars were a hit. However, novice users still had problems learning what the toolbar does. As a result, the application designers went back to the drawing board to come up with ways to help the new user learn how use the toolbar buttons.
One of the solutions was to use the information bar that many of them had begun placing at the bottom of application windows to provide detailed descriptions of both menu entries and toolbar buttons. One of the other solutions was to provide a little pop-up window with a short description of the button that appears whenever the mouse is positioned over the button for more than a couple of seconds. The first of these solutions became known as the status bar, and the second became known as tooltips. Both solutions are in common practice with most Windows applications today.
If you want to design and use your own toolbars and status bars in your applications, you might think that Visual C++ provides plenty of support for your efforts and even makes it easy to implement. After all, Microsoft's own application developers have been in the forefront of developing these elements, and most, if not all, of Microsoft's Windows applications are developed using its own Visual C++. Well, you are correct in making that assumption, and today, you'll learn how to create your own custom toolbars and status bars for your applications.
For learning how to create your own toolbar, you will modify the application that you created on Day 10, "Creating Single Document Interface Applications," the SDI drawing application, to add a toolbar for selecting the color to use in drawing.
NOTE: Although the sample application you are working with today is an extension to the application you built on Day 10, all file and class names have been changed from Day10 to Toolbar. If you are making the changes in the Day 10 project, then when the following text specifies that you make changes to the CToolbarDoc class, you should make the changes to the CDay10Doc class. Likewise, when you are asked to edit the Toolbar.rc file, you can edit the Day10.rc file.
If all you want to do is add a few additional toolbar buttons to the default toolbar that the AppWizard creates when you start a new SDI or MDI application, you can pull up the toolbar in the Visual C++ designer through the Resource View in the workspace pane and begin adding new buttons. Just as in the Menu Designer, the end of the toolbar always has a blank entry, waiting for you to turn it into another toolbar button, as shown in Figure 12.1. All you have to do is select this blank button, drag it to the right if you want a separator between it and the button beside it, or drag it to a different position if you want it moved. After you have the button in the desired location, you paint an icon on the button that illustrates the function that the button will trigger. Finally, double-click the button in the toolbar view to open the button's properties dialog and give the button the same ID as the menu that it will trigger. The moment that you compile and run your application, you will have a new toolbar button that performs a menu selection that you chose. If you want to get rid of a toolbar button, just grab it on the toolbar view, and drag it off the toolbar.
FIGURE 12.1. The toolbar designer.
To insert a new toolbar, right-click on the Toolbar folder and select Insert Toolbar from the pop-up menu. This creates an empty toolbar with a single blank button. As you start drawing an icon on each of the blank buttons in the toolbar, another blank button is added on the end.
For use in your drawing application, fill eight buttons with the eight colors available in the drawing application.
Once you draw icons on each of the buttons in your toolbar, double-click on the first button in the toolbar view. This should open the toolbar button properties dialog. In the ID field, enter (or select from the drop-down list) the ID of the menu that this toolbar button should trigger. In the Prompt field, enter the description that should appear in the status bar for this toolbar button. (If you entered a prompt for the menu, then this field is automatically populated with the menu description.) At the end of the status bar description, add \n and a short description to appear in the tooltips for the toolbar button.
NOTE: In C/C++, the \n string is a shorthand notation for "begin a new line." In the prompt for toolbar buttons and menu entries, this string is used to separate the status bar descriptions of the menu entries and the tooltips pop-up prompt that appears when the mouse is held over a toolbar button for a few seconds. The first line of the prompt is used for the status bar description, and the second line is used for the tooltips description. The tooltips description is only used with the toolbars, so there's no reason to add this for menu entries that will have no toolbar equivalents.
For example, for the black button on the toolbar that you are creating for your drawing application, enter an ID of ID_COLOR_BLACK and a prompt of Black drawing color\nBlack, as shown in Figure 12.2.
FIGURE 12.2. The toolbar button properties dialog.
Once you finish designing your toolbar and have icons on all of your buttons with the properties set for each button, you will change the toolbar ID. In the workspace pane, right-click the new toolbar that you just added and open its properties dialog. Change the toolbar ID to a descriptive name.
As an example, for the color toolbar that you created for your drawing application, change the toolbar ID to IDR_TBCOLOR.
In the previous SDI and MDI applications, you didn't add any functionality that required you to touch the frame window. Well, because the toolbar is attached to the frame, you'll have to begin adding and modifying code in that module. If you open the CMainFrame class to the OnCreate function, you'll see where it's creating the existing toolbar and then later in this function where the toolbar is being attached to the frame.
Before you can add your toolbar to the application frame, you need to add a variable to the CMainFrame class to hold the new toolbar. This variable of type CToolBar should be protected in accessibility.
To add your color toolbar to your draw application, right-click the CMainFrame class in the Class View tab of the workspace pane. Select Add Member Variable from the pop-up menu, and specify the variable type as CToolBar, the name as m_wndColorBar, and the access as protected.
After you add a variable for your toolbar, you need to add some code in the OnCreate function in the CMainFrame class to add the toolbar and attach it to the frame. Make the modifications in Listing 12.1 to add the color toolbar to your drawing application.
1: int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) 2: { 3: if (CFrameWnd::OnCreate(lpCreateStruct) == -1) 4: return -1; 5: 6: if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, ÂWS_CHILD | WS_VISIBLE | CBRS_TOP 7: | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | ÂCBRS_SIZE_DYNAMIC) || 8: !m_wndToolBar.LoadToolBar(IDR_MAINFRAME)) 9: { 10: TRACE0("Failed to create toolbar\n"); 11: return -1; // fail to create 12: } 13: 14: ///////////////////////
15: // MY CODE STARTS HERE
16: /////////////////////// 17: 18: // Add the color toolbar 19: int iTBCtlID; 20: int i; 21: 22: // Create the Color Toolbar 23: if (!m_wndColorBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | 24: WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | 25: CBRS_FLYBY | CBRS_SIZE_DYNAMIC) || 26: !m_wndColorBar.LoadToolBar(IDR_TBCOLOR)) 27: { 28: TRACE0("Failed to create toolbar\n"); 29: return -1; // fail to create 30: } 31: // Find the Black button on the toolbar 32: iTBCtlID = m_wndColorBar.CommandToIndex(ID_COLOR_BLACK); 33: if (iTBCtlID >= 0) 34: { 35: // Loop through the buttons, setting them to act as radio Âbuttons 36: for (i= iTBCtlID; i < (iTBCtlID + 8); i++) 37: m_wndColorBar.SetButtonStyle(i, TBBS_CHECKGROUP); 38: } 39: 40: /////////////////////// 41: // MY CODE ENDS HERE 42: /////////////////////// 43: 44: if (!m_wndStatusBar.Create(this) || 45: !m_wndStatusBar.SetIndicators(indicators, 46: sizeof(indicators)/sizeof(UINT))) 47: { 48: TRACE0("Failed to create status bar\n"); 49: return -1; // fail to create 50: } 51: 52: // TODO: Delete these three lines if you don't want the toolbar to 53: // be dockable 54: m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY); 55: 56: /////////////////////// 57: // MY CODE STARTS HERE 58: /////////////////////// 59: 60: // Enable docking for the Color Toolbar 61: m_wndColorBar.EnableDocking(CBRS_ALIGN_ANY); 62: 63: /////////////////////// 64: // MY CODE ENDS HERE 65: /////////////////////// 66: 67: EnableDocking(CBRS_ALIGN_ANY); 68: DockControlBar(&m_wndToolBar); 69: 70: /////////////////////// 71: // MY CODE STARTS HERE 72: /////////////////////// 73: 74: // Dock the Color Toolbar 75: DockControlBar(&m_wndColorBar); 76: 77: /////////////////////// 78: // MY CODE ENDS HERE 79: /////////////////////// 80: 81: return 0; 82: }
The first part of the code you added,
if (!m_wndColorBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) || !m_wndColorBar.LoadToolBar(IDR_TBCOLOR))
contains two separate functions that are necessary in creating a toolbar. The first function, CreateEx, creates the toolbar itself, whereas the second, LoadToolBar, loads the toolbar that you designed in the toolbar designer. The second function, LoadToolBar, requires a single argument, the ID for the toolbar that you want to create.
The CreateEx function has several arguments that you can pass with the function. The first argument, and the only required argument, is a pointer to the parent window. In this case (which is normally the case), this argument is a pointer to the frame window to which the toolbar will be attached.
The second argument is the style of controls on the toolbar that is to be created. Several toolbar control styles are available for use, some of which have been introduced with the last two versions of Internet Explorer. Table 12.1 lists the available styles.
Style | Description |
TBSTYLE_ALTDRAG | Allows the user to move the toolbar by dragging it while holding down the Alt key. |
TBSTYLE_CUSTOMERASE | Generates a NM_CUSTOMDRAW message when erasing the toolbar and button background, allowing the programmer to choose when and whether to control the background erasing process. |
TBSTYLE_FLAT | Creates a flat toolbar. Button text appears under the bitmap image. |
TBSTYLE_LIST | Button text appears to the right of the bitmap image. |
TBSTYLE_REGISTERDROP | For use in dragging and dropping objects onto toolbar buttons. |
TBSTYLE_TOOLTIPS | Creates a tooltip control that can be used to display descriptive text for the buttons. |
TBSTYLE_TRANSPARENT | Creates a transparent toolbar. |
TBSTYLE_WRAPABLE | Creates a toolbar that can have multiple rows of buttons. |
The third argument is the style of the toolbar itself. This argument is normally a combination of window and control bar styles. Normally, only two or three window styles are used, and the rest of the toolbar styles are control bar styles. The list of the normally used toolbar styles appears in Table 12.2.
Style | Description |
WS_CHILD | The toolbar is created as a child window. |
WS_VISIBLE | The toolbar will be visible when created. |
CBRS_ALIGN_TOP | Allows the toolbar to be docked to the top of the view area of the frame window. |
CBRS_ALIGN_BOTTOM | Allows the toolbar to be docked to the bottom of the view area of the frame window. |
CBRS_ALIGN_LEFT | Allows the toolbar to be docked to the left side of the view area of the frame window. |
CBRS_ALIGN_RIGHT | Allows the toolbar to be docked to the right side of the view area of the frame window. |
CBRS_ALIGN_ANY | Allows the toolbar to be docked to any side of the view area of the frame window. |
CBRS_BORDER_TOP | Places a border on the top edge of the toolbar when the top of the toolbar is not docked. |
CBRS_BORDER_BOTTOM | Places a border on the bottom edge of the toolbar when the top of the toolbar is not docked. |
CBRS_BORDER_LEFT | Places a border on the left edge of the toolbar when the top of the toolbar is not docked. |
CBRS_BORDER_RIGHT | Places a border on the right edge of the toolbar when the top of the toolbar is not docked. |
CBRS_FLOAT_MULTI | Allows multiple toolbars to be floated in a single miniframe window. |
CBRS_TOOLTIPS | Causes tooltips to be displayed for the toolbar buttons. |
CBRS_FLYBY | Causes status bar message text to be updated for the toolbar buttons at the same time as the tooltips. |
CBRS_GRIPPER | Causes a gripper to be drawn on the toolbar. |
The fourth argument, which you did not provide in your code, is the size of the toolbar borders. This argument is passed as a standard CRect rectangle class to provide the length and height desired for the toolbar. The default value is 0 for all of the rectangle dimensions, thus resulting in a toolbar with no borders.
The fifth and final argument, which you also did not provide in your code, is the toolbar's child window ID. This defaults to AFX_IDW_TOOLBAR, but you can specify any defined ID that you need or want to use for the toolbar.
After you create the toolbar, there is a curious bit of code:
// Find the Black button on the toolbar iTBCtlID = m_wndColorBar.CommandToIndex(ID_COLOR_BLACK); if (iTBCtlID >= 0) { // Loop through the buttons, setting them to act as radio buttons for (i= iTBCtlID; i < (iTBCtlID + 8); i++) m_wndColorBar.SetButtonStyle(i, TBBS_CHECKGROUP); }
The first line in this code snippet uses the CommandToIndex toolbar function to locate the control number of the ID_COLOR_BLACK button. If you design your toolbar in the order of colors that you used on the menu, this should be the first control, with a index of 0. It's best to use the CommandToIndex function to locate the index of any toolbar button that you need to alter, just in case it's not where you expect it to be. This function returns the index of the toolbar control specified, and you use this as a starting point to specify the button style of each of the color buttons.
In the loop, where you are looping through each of the eight color buttons on the toolbar, you use the SetButtonStyle function to control the behavior of the toolbar buttons. The first argument to this function is the index of the button that you are changing. The second argument is the style of button that you want for the toolbar button specified. In this case, you are specifying that each of the buttons be TBBS_CHECKGROUP buttons, which makes them behave like radio buttons, where only one of the buttons in the group can be selected at any time. The list of the available button styles is in Table 12.3.
Style | Description |
TBSTYLE_AUTOSIZE | The button's width will be calculated based on the text on the button. |
TBSTYLE_BUTTON | Creates a standard push button. |
TBSTYLE_CHECK | Creates a button that acts like a check box, toggling between the pressed and unpressed state. |
TBSTYLE_CHECKGROUP | Creates a button that acts like a radio button, remaining in the pressed state until another button in the group is pressed. This is actually the combination of the TBSTYLE_CHECK and TBSTYLE_GROUP button styles. |
TBSTYLE_DROPDOWN | Creates a drop-down list button. |
TBSTYLE_GROUP | Creates a button that remains pressed until another button in the group is pressed. |
TBSTYLE_NOPREFIX | The button text will not have an accelerator prefix associated with it. |
TBSTYLE_SEP | Creates a separator, making a small gap between the buttons on either side. |
The last thing that you do in the code that you add to the OnCreate function in the CMainFrame class is the following:
// Enable docking for the Color Toolbar m_wndColorBar.EnableDocking(CBRS_ALIGN_ANY); EnableDocking(CBRS_ALIGN_ANY); // (AppWizard generated line) // Dock the Color Toolbar DockControlBar(&m_wndColorBar);
In the first of these lines, you called the EnableDocking toolbar function. This function enables the toolbar for docking with the frame window. The value passed to this toolbar function must match the value passed in the following EnableDocking function that is called for the frame window. The available values for these functions are listed in Table 12.4. These functions enable the borders of the toolbar, and the frame window, for docking. If these functions are not called, then you will not be able to dock the toolbar with the frame window. If a specific side is specified in these functions for use in docking, and the sides do not match, you will not be able to dock the toolbar with the frame.
Style | Description |
CBRS_ALIGN_TOP | Allows the toolbar to be docked to the top of the view area of the frame window. |
CBRS_ALIGN_BOTTOM | Allows the toolbar to be docked to the bottom of the view area of the frame window. |
CBRS_ALIGN_LEFT | Allows the toolbar to be docked to the left side of the view area of the frame window. |
CBRS_ALIGN_RIGHT | Allows the toolbar to be docked to the right side of the view area of the frame window. |
CBRS_ALIGN_ANY | Allows the toolbar to be docked to any side of the view area of the frame window. |
CBRS_FLOAT_MULTI | Allows multiple toolbars to be floated in a single miniframe window. |
0 | The toolbar will not be able to dock with the frame. |
The final function that you added was a frame window function, DockControlBar, which is passed the address of the toolbar variable. This function physically docks the toolbar to the frame window. Because all of this code appears in the OnCreate function for the frame window, the toolbar is docked before the user sees either the window or the toolbar.
Now, after adding all of this code to the OnCreate function of the CMainFrame class, if you compile and run your application, you'll find a working color toolbar that you can use to select the drawing color, as shown in Figure 12.3.
FIGURE 12.3. The color toolbar on the drawing program.
Now that you have your color toolbar on the frame of your drawing application, it would be nice to be able to show and hide it just as you can the default toolbar and status bar through the View menu. This is simple enough functionality to add, but it doesn't necessarily work the way you might expect it to.
The first thing you need to do is add a menu entry to toggle the visibility of the color bar. Do this through the Menu Designer, adding a new menu entry on the View menu. Specify the menu properties as shown in Table 12.5.
Property | Setting |
ID | ID_VIEW_COLORBAR |
Caption | &Color Bar |
Prompt | Show or hide the colorbar\nToggle ColorBar |
To determine whether the toolbar is visible or hidden, you can get the current style of the toolbar and mask out for the WS_VISIBLE style flag. If the flag is in the current toolbar style, then the toolbar is visible. By placing this evaluation into the SetCheck function in the UPDATE_COMMAND_UI event message handler, you can check and uncheck the color bar menu entry as needed.
To add this functionality to your drawing program, add an event handler for the UPDATE_COMMAND_UI event message on the ID_VIEW_COLOR menu. Be sure to add this event-handler function into the CMainFrame class. (You're still making all of your coding changes so far in the frame class.) Edit the event-handler function, adding the code in Listing 12.2.
1: void CMainFrame::OnUpdateViewColorbar(CCmdUI* pCmdUI) 2: { 3: // TODO: Add your command update UI handler code here 4: /////////////////////// 5: // MY CODE STARTS HERE 6: /////////////////////// 7: 8: // Check the state of the color toolbar 9: pCmdUI->SetCheck(((m_wndColorBar.GetStyle() & WS_VISIBLE) != 0)); 10:
11: ///////////////////////
12: // MY CODE ENDS HERE 13: /////////////////////// 14: }
Because the CToolBar class is derived from the CWnd class (via the CControlBar class), you might think that you could call the ShowWindow function on the toolbar itself to show and hide the toolbar. Well, you can, but the background for the toolbar will not be hidden along with the toolbar. All the user would notice is the toolbar buttons appearing and disappearing. (Of course, this might be the effect you are after, but your users might not like it.)
Instead, you use a frame window function, ShowControlBar, to show and hide the toolbar. This function takes three arguments. The first argument is the address for the toolbar variable. The second argument is a boolean, specifying whether to show the toolbar. (TRUE shows the toolbar; FALSE hides the toolbar.) Finally, the third argument specifies whether to delay showing the toolbar. (TRUE delays showing the toolbar; FALSE shows the toolbar immediately.)
Once a toolbar is toggled on or off, you need to call another frame window function, RecalcLayout. This function causes the frame to reposition all of the toolbars, status bars, and anything else that is within the frame area. This is the function that causes the color toolbar to move up and down if you toggle the default toolbar on and off.
To add this functionality to your drawing program, add an event handler for the COMMAND event message on the ID_VIEW_COLOR menu. Be sure to add this event-handler function into the CMainFrame class. (You're still making all of your coding changes so far in the frame class.) Edit the event-handler function, adding the code in Listing 12.3.
1: void CMainFrame::OnViewColorbar() 2: { 3: // TODO: Add your command handler code here 4: 5: /////////////////////// 6: // MY CODE STARTS HERE 7: /////////////////////// 8: BOOL bVisible; 9: 10: // Check the state of the color toolbar 11: bVisible = ((m_wndColorBar.GetStyle() & WS_VISIBLE) != 0); 12: 13: // Toggle the color bar 14: ShowControlBar(&m_wndColorBar, !bVisible, FALSE); 15: // Reshuffle the frame layout 16: RecalcLayout(); 17: 18: /////////////////////// 19: // MY CODE ENDS HERE 20: /////////////////////// 21: }
At this point, after compiling and running your application, you should be able to toggle your color toolbar on and off using the View menu.
It's commonplace now to use applications that have more than just buttons on toolbars. Look at the Visual C++ Developer Studio, for example. You've got combo boxes that enable you to navigate through your code by selecting the class, ID, and function to edit right on the toolbar. So how do you add a combo box to a toolbar? It's not available in the toolbar designer; all you have there are buttons that you can paint icons on. You can't add a combo box to any toolbar by using any of the Visual C++ wizards. You have to write a little C++ code to do it.
To learn how to add a combo box to a toolbar, you'll add a combo box to the color toolbar you just created. The combo box will be used to select the width of the pen the user will use to draw images. (If you haven't added the support for different drawing widths from the exercise at the end of Day 10, you might want to go back and add that now.)
To add a combo box to your toolbar, the first thing that you need to do is what Visual C++ was designed to prevent you from having to do. You need to edit the resource file yourself. You cannot do this through the Visual C++ Developer Studio. If you try to open the resource file in the Developer Studio, you will be popped into the Resource View tab of the workspace pane, editing the resource file through the various resource editors and designers. No, you'll have to edit this file in another editor, such as Notepad.
Close Visual C++, the only way to guarantee that you don't write over your changes. Open Notepad and navigate to your project directory. Open the resource file, which is named after the project with a .rc filename extension. Once you open this file in Notepad, scroll down until you find the toolbar definitions. (You can search for the word "toolbar.") Once you've found the toolbar definitions, go to the end of the Color toolbar definition and add two separator lines at the bottom of the toolbar definition.
For instance, to make these changes to your drawing application, you need to navigate to the Toolbar project directory and then open the Toolbar.rc file. (If you are adding these toolbars to the MDI drawing application, you need to look for the Day11.rc file.) Search for the toolbar section, and then add two SEPARATOR lines just before the end of the IDR_TBCOLOR section, as shown in Listing 12.4. Once you add these two lines, save the file, exit Notepad, and restart Visual C++, reloading the project.
1: ////////////////////////////////////////////////////////////////////// 2: // 3: // Toolbar 4: // 5: 6: IDR_MAINFRAME TOOLBAR DISCARDABLE 16, 15 7: BEGIN 8: BUTTON ID_FILE_NEW 9: BUTTON ID_FILE_OPEN 10: BUTTON ID_FILE_SAVE 11: SEPARATOR 12: BUTTON ID_EDIT_CUT 13: BUTTON ID_EDIT_COPY 14: BUTTON ID_EDIT_PASTE 15: SEPARATOR 16: BUTTON ID_FILE_PRINT 17: BUTTON ID_APP_ABOUT 18: END 19: 20: IDR_TBCOLOR TOOLBAR DISCARDABLE 16, 15 21: BEGIN 22: BUTTON ID_COLOR_BLACK 23: BUTTON ID_COLOR_BLUE 24: BUTTON ID_COLOR_GREEN 25: BUTTON ID_COLOR_CYAN 26: BUTTON ID_COLOR_RED 27: BUTTON ID_COLOR_MAGENTA 28: BUTTON ID_COLOR_YELLOW 29: BUTTON ID_COLOR_WHITE 30: SEPARATOR 31: SEPARATOR 32: END
You added these two SEPARATOR lines in the toolbar definition so that the second separator can act as a place holder for the combo box that you are going to add to the toolbar. There are two reasons that you had to make this edit by hand and not use the Visual C++ toolbar designer. The first reason is that the toolbar designer would not allow you to add more than one separator to the end of the toolbar. The second reason is that, if you don't add anything else on the end of your toolbar after the separator, the toolbar designer decides that the separator is a mistake and removes it for you. In other words, the Visual C++ toolbar designer does not allow you to add the place holder for the combo box to your toolbar.
Next, you need to add the text strings that you will load into your combo box. To add these strings, you need to open the string table in the Resource View of the workspace pane. Here you find all of the strings that you entered as prompts in various properties dialogs. This table has a number of IDs, the values of those IDs, and textual strings that are associated with those IDs, as shown in Figure 12.4. You'll need to add the strings to be placed into your toolbar combo box in the string table; each line in the drop-down list must have a unique ID and entry in the strings table.
FIGURE 12.4. The string table editor.
For instance, to add the strings for the combo box that you will be adding to the color toolbar, insert a new string, either by selecting Insert|New String from the menu or by right-clicking the string table and selecting New String from the pop-up menu.
In the String properties dialog, specify a string ID for the string and then enter the string to appear in the drop-down list. Close the properties dialog to add the string. For the strings in the Width combo box that you are going to add to the color toolbar, add the strings in Table 12.6.
ID | Caption |
IDS_WIDTH_VTHIN | Very Thin |
IDS_WIDTH_THIN | Thin |
IDS_WIDTH_MEDIUM | Medium |
IDS_WIDTH_THICK | Thick |
IDS_WIDTH_VTHICK | Very Thick |
Before you can add the combo box to the color toolbar, you need to create a combo box variable that you can use for the combo box. Because you are not able to add this combo box through any of the designers, you need to add it as a variable to the CMainFrame class.
To add the combo box variable to the main frame class for the color toolbar, select the Class View tab in the workspace pane. Right-click the CMainFrame class and select Add Member Variable from the pop-up menu. Specify the variable type as CComboBox, the name as m_ctlWidth, and the access as protected.
Once you add the combo box variable to the main frame class, you need to perform a series of actions, all once the toolbar has been created:
To organize this so that it doesn't get too messy, it might be advisable to move the creation of the color toolbar to its own function that can be called from the OnCreate function of the main frame class. To create this function, right-click the CMainFrame class in the workspace pane and select Add Member Function from the pop-up menu. Specify the function type as BOOL, the function description as CreateColorBar, and the access as public. Edit the new function, adding the code in Listing 12.5.
1: BOOL CMainFrame::CreateColorBar() 2: { 3: int iTBCtlID; 4: int i; 5: 6: if (!m_wndColorBar.CreateEx(this, TBSTYLE_FLAT, ÂWS_CHILD | WS_VISIBLE | CBRS_TOP 7: | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | ÂCBRS_SIZE_DYNAMIC) || 8: !m_wndColorBar.LoadToolBar(IDR_TBCOLOR)) 9: { 10: TRACE0("Failed to create toolbar\n"); 11: return FALSE; // fail to create 12: } 13: iTBCtlID = m_wndColorBar.CommandToIndex(ID_COLOR_BLACK); 14: if (iTBCtlID >= 0) 15: { 16: for (i= iTBCtlID; i < (iTBCtlID + 8); i++) 17: m_wndColorBar.SetButtonStyle(i, TBBS_CHECKGROUP); 18: } 19: // Add the Combo 20: int nWidth = 100; 21: int nHeight = 125; 22: 23: // Configure the combo place holder 24: m_wndColorBar.SetButtonInfo(9, IDC_CBWIDTH, TBBS_SEPARATOR, ÂnWidth); 25: 26: // Get the colorbar height 27: CRect rect; 28: m_wndColorBar.GetItemRect(9, &rect); 29: rect.bottom = rect.top + nHeight; 30: 31: // Create the combo box 32: m_ctlWidth.Create(WS_CHILD | WS_VISIBLE | WS_VSCROLL | 33: CBS_DROPDOWNLIST, rect, &m_wndColorBar, IDC_CBWIDTH); 34: 35: // Fill the combo box 36: CString szStyle; 37: if (szStyle.LoadString(IDS_WIDTH_VTHIN)) 38: m_ctlWidth.AddString((LPCTSTR)szStyle); 39: if (szStyle.LoadString(IDS_WIDTH_THIN)) 40: m_ctlWidth.AddString((LPCTSTR)szStyle);
41: if (szStyle.LoadString(IDS_WIDTH_MEDIUM))
42: m_ctlWidth.AddString((LPCTSTR)szStyle); 43: if (szStyle.LoadString(IDS_WIDTH_THICK)) 44: m_ctlWidth.AddString((LPCTSTR)szStyle); 45: if (szStyle.LoadString(IDS_WIDTH_VTHICK)) 46: m_ctlWidth.AddString((LPCTSTR)szStyle); 47: 48: return TRUE; 49: }
On line 24 in Listing 12.5, you specify that the combo box should be created using the object ID IDC_CBWIDTH. This object ID is used to identify the combo box when the combo box sends an event message to the application or when you need to specify what list entry is displayed in the edit field. However, this object ID doesn't exist in your application. Before you can compile the application, you'll need to add this ID to the project resource IDs, just as you did on Day 4, "Working with Timers." To add this ID to your project, select the Resource view in the workspace pane. Select the top of the resource tree and right-click the mouse to trigger the context menu. Select Resource Symbols from the pop-up menu and add the object ID IDC_CBWIDTH. Make sure that you add the new object ID with a unique numerical value so that it won't conflict with any other objects in use in your application.
After creating the toolbar and configuring all of the toolbar buttons, the first thing you need to do is to configure the separator that is acting as the place holder for the combo box you are about to create. You do this with the SetButtonInfo toolbar function, as follows:
m_wndColorBar.SetButtonInfo(9, IDC_CBWIDTH, TBBS_SEPARATOR, nWidth);
This function takes four arguments. The first argument is the current index of the control in the toolbar--in this case, the tenth control in the toolbar (eight color buttons and two separators). The second argument is the new ID of the toolbar control. This is the ID that will be placed in the event message queue when a control event occurs. The third argument is the type of toolbar control this control should be. The fourth and final argument is somewhat deceptive. If you look at the function documentation, the fourth argument is the new index of the control in the toolbar. This is the position to which the control will be moved. However, if the control is a separator, this argument specifies the width of the control and doesn't move it anywhere. Because this toolbar control is a separator, this argument has the effect of setting it to be as wide as the combo box that you are going to create.
Now that you have configured the toolbar separator as the place holder for the combo box, you need to get the position of the combo box place holder on the toolbar so that you can use it to set the position of the combo box:
m_wndColorBar.GetItemRect(9, &rect); rect.bottom = rect.top + nHeight;
In the first line, you called the toolbar function GetItemRect to get the position and size of the placeholder for the combo box. In the next line, you added the height of the drop-down list to the height that the combo box will eventually be.
Now that you've got a place holder sized correctly, and you have the position and size for the combo box, it's time to create the combo box. You do this with the Create combo box function, as follows:
m_ctlWidth.Create(WS_CHILD | WS_VISIBLE | WS_VSCROLL | CBS_DROPDOWNLIST, rect, &m_wndColorBar, IDC_CBWIDTH);
The first argument to the combo box Create function is the combo box style. Normally, several style flags are combined to create a combination style value. Table 12.7 lists the flags that you can use in this value.
Style | Description |
WS_CHILD | Designates this as a child window (required). |
WS_VISIBLE | Makes the combo box visible. |
WS_DISABLED | Disables the combo box. |
WS_VSCROLL | Adds vertical scrolling to the drop-down list. |
WS_HSCROLL | Adds horizontal scrolling to the drop-down list. |
WS_GROUP | Groups controls. |
WS_TABSTOP | Includes the combo box in the tabbing order. |
CBS_AUTOHSCROLL | Automatically scrolls the text in the edit control to the right when the user types a character at the end of the line. This allows the user to enter text wider than the edit control into the combo box. |
CBS_DROPDOWN | Similar to CBS_SIMPLE, but the list is not displayed unless the user selects the icon next to the edit control. |
CBS_DROPDOWNLIST | Similar to CBS_DROPDOWN, but the edit control is replaced with a static-text item displaying the currently selected item in the list. |
CBS_HASSTRINGS | The owner of the list box is responsible for drawing the list box contents. The list box items consist of strings. |
CBS_OEMCONVERT | Text entered in the edit control is converted from ANSI to the OEM character set and then back to ANSI. |
CBS_OWNERDRAWFIXED | The owner of the list box is responsible for drawing the list box contents. The contents of the list are fixed in height. |
CBS_OWNERDRAWVARIABLE | The owner of the list box is responsible for drawing the list box contents. The contents of the list are variable in height. |
CBS_SIMPLE | The list box is displayed at all times. |
CBS_SORT | Automatically sorts the strings in the list box. |
CBS_DISABLENOSCROLL | List shows a disabled scrollbar when there are not enough items in the list to require scrolling. |
CBS_NOINTEGRALHEIGHT | Specifies that the combo box is exactly the size specified. |
The second argument is the rectangle that the combo box is to occupy. This argument is the position within the parent window--in this case, the toolbar--that the combo box will stay in. It will move with the parent window (the toolbar), staying in this position the entire time.
The third argument is a pointer to the parent window. This is the address of the color toolbar variable.
The fourth argument is the object ID for the combo box.
The final action that you have to do in creating the combo box on the color toolbar is populate the drop-down list with the available items that the user can select from. You do this with the combination of two functions:
if (szStyle.LoadString(IDS_WIDTH_VTHIN)) m_ctlWidth.AddString((LPCTSTR)szStyle);
The first function is a CString function, LoadString. This function takes a string ID and loads the string matching the ID from the string table. The second function is a combo box function, AddString, which adds the string passed in as an argument to the drop-down list. By calling this function combination for each of the elements that should be in the drop-down list, you can populate the combo box from the application string table.
After moving all of the code to create the color toolbar to a separate function, you can update the OnCreate function so that it calls the CreateColorBar function where it used to create the color toolbar, as in Listing 12.6.
1: int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) 2: { 3: if (CFrameWnd::OnCreate(lpCreateStruct) == -1) 4: return -1; 5: 6: if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, ÂWS_CHILD | WS_VISIBLE | CBRS_TOP 7: | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | ÂCBRS_SIZE_DYNAMIC) || 8: !m_wndToolBar.LoadToolBar(IDR_MAINFRAME)) 9: { 10: TRACE0("Failed to create toolbar\n"); 11: return -1; // fail to create 12: } 13: 14: /////////////////////// 15: // MY CODE STARTS HERE 16: /////////////////////// 17: 18: // Add the color toolbar 19: if (!CreateColorBar()) 20: { 21: TRACE0("Failed to create color toolbar\n"); 22: return -1; // fail to create 23: } 24: 25: /////////////////////// 26: // MY CODE ENDS HERE 27: /////////////////////// 28: 29: if (!m_wndStatusBar.Create(this) || 30: !m_wndStatusBar.SetIndicators(indicators, 31: sizeof(indicators)/sizeof(UINT))) 32: { 33: TRACE0("Failed to create status bar\n"); 34: return -1; // fail to create 35: } 36: 37: // TODO: Delete these three lines if you don't want the toolbar to 38: // be dockable 39: m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY); 40: 41: /////////////////////// 42: // MY CODE STARTS HERE 43: /////////////////////// 44: 45: // Enable docking for the Color Toolbar 46: m_wndColorBar.EnableDocking(CBRS_ALIGN_ANY); 47: 48: /////////////////////// 49: // MY CODE ENDS HERE 50: /////////////////////// 51:
52: EnableDocking(CBRS_ALIGN_ANY);
53: DockControlBar(&m_wndToolBar); 54: 55: /////////////////////// 56: // MY CODE STARTS HERE 57: /////////////////////// 58: 59: // Dock the Color Toolbar 60: DockControlBar(&m_wndColorBar); 61: 62: /////////////////////// 63: // MY CODE ENDS HERE 64: /////////////////////// 65: 66: return 0; 67: }
Now when you compile and run your application, you should have a combo box on the end of your color toolbar, as in Figure 12.5. However, the combo box doesn't do anything yet.
FIGURE 12.5. The color toolbar with a width combo box.
Adding an event handler for the combo box is fairly simple, although it does have to be done by hand (because the Class Wizard doesn't even know that the combo box exists). You have to add an ON_CBN_SELCHANGE entry into the message map and then add the actual message-handler function into the CMainFrame class.
To start with, add the message-handler function by selecting the CMainFrame class in the workspace pane and selecting New Member Function from the pop-up menu. Enter the function type as afx_msg void, the function definition as OnSelChangeWidth, and the access as protected. Edit the new function as in Listing 12.7.
1: void CMainFrame::OnSelChangeWidth() 2: { 3: // Get the new combo selection 4: int nIndex = m_ctlWidth.GetCurSel(); 5: if (nIndex == CB_ERR) 6: return; 7: 8: // Get the active document 9: CToolbarDoc* pDoc = (CToolbarDoc*)GetActiveDocument(); 10: // Do we have a valid document? 11: if (pDoc) 12: // Set the new drawing width 13: pDoc->SetWidth(nIndex); 14: 15: }
In this function, you first get the current selection from the combo box. Remember that the entries were added in order, and the CBS_SORT flag was not specified in the combo box creation, so the selection index numbers should correspond to the widths in the document. As a result, you can get a pointer to the current document instance, using the GetActiveDocument function, and then pass the new width to the document using its SetWidth function.
For the combo box selection changes to call this message-handler function, you need to add the appropriate entry to the CMainFrame message map. Scroll to the top of the CMainFrame source code until you find the message map section. Add line 12 in Listing 12.8 to the message map.
1://///////////////////////////////////////////////////////////////////// 2: // CMainFrame 3: 4: IMPLEMENT_DYNCREATE(CMainFrame, CFrameWnd) 5: 6: BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) 7: //{{AFX_MSG_MAP(CMainFrame) 8: ON_WM_CREATE() 9: ON_COMMAND(ID_VIEW_COLORBAR, OnViewColorbar) 10: ON_UPDATE_COMMAND_UI(ID_VIEW_COLORBAR, OnUpdateViewColorbar) 11: //}}AFX_MSG_MAP 12: ON_CBN_SELCHANGE(IDC_CBWIDTH, OnSelChangeWidth) 13: END_MESSAGE_MAP()
This message map entry
ON_CBN_SELCHANGE(IDC_CBWIDTH, OnSelChangeWidth)
specifies that on combo box selection change events with the object ID of the color toolbar combo box, the OnSelChangeWidth function should be called. Now if you compile and run your application, you should be able to change the drawing width with the combo box on the color toolbar.
The one remaining problem with the combo box is that it needs to be updated if the user selects a new value from the menu instead of the combo box. One of the most efficient methods of doing this is to set the current selection in the combo box when any of the menu selections are triggered. This requires a function in the main frame class that can be called from the document class to accomplish this action. All the function in the main frame needs to do is to set the current selection in the combo box.
To implement this function in the main frame, add a new member function to the CMainFrame class, specifying the function type as void, the definition as UpdateWidthCB(int nIndex), and the access as public. Once you add this function, edit the function as in Listing 12.9.
Listing 12.9. The CMAINFRAME.UPDATEWIDTHCB function.
1: void CMainFrame::UpdateWidthCB(int nIndex) 2: { 3: // Set the new selection in the combo box 4: m_wndColorBar.m_ctlWidth.SetCurSel(nIndex); 5: }
This function uses a single combo box function, SetCurSel, which sets the current selection in the combo box drop-down list to the entry specified with the index number. The edit control of the combo box is updated with the new selected list entry. If an index number that doesn't exist in the drop-down list is supplied to the combo box, then the function returns an error.
On the document side, you need to call this function in the main frame whenever the appropriate menu event-handling functions are called. Because this could occur in sev- eral functions, it makes the most sense to enclose the necessary functionality in a single function. This function needs to get a pointer to the view associated with the document and then, through the view, get a pointer to the frame, which can then be used to call the UpdateWidthCB function that you just added to the main frame class.
To add this function to your application, select the CToolbarDoc class in the workspace pane, and select Add Member Function from the pop-up menu. Specify void as the function type, UpdateColorbar(int nIndex) as the function definition, and private as the function access. Edit the function as in Listing 12.10.
1: void CToolbarDoc::UpdateColorbar(int nIndex) 2: { 3: // Get the position of the first view 4: POSITION pos = GetFirstViewPosition(); 5: // Did we get a valid position? 6: if (pos != NULL) 7: { 8: // Get a pointer to the view in that position 9: CView* pView = GetNextView(pos); 10: // Do we have a valid pointer to the view? 11: if (pView) 12: { 13: // Get a pointer to the frame through the view 14: CMainFrame* pFrame = (CMainFrame*)pView-ÂGetTopLevelFrame(); 15: // Did we get a pointer to the frame? 16: if (pFrame) 17: // Update the combo box on the color toolbar 18: // through the frame 19: pFrame->UpdateWidthCB(nIndex); 20: } 21: } 22: }
This function traces through the path that you have to follow to get to the application frame from the document class. The first thing that you did was get the position of the first view associated with the document, using the GetFirstViewPosition function. A document may have multiple views open at the same time, and this function returns the position of the first of those views.
The next function, GetNextView, returns a pointer to the view specified by the position. This function also updates the position variable to point to the next view in the list of views associated with the current document.
Once you have a pointer to the view, you can call the window function, GetTopLevelFrame, which returns a pointer to the application frame window. You have to call this function through the view because the document is not descended from the CWnd class, although the view is.
Once you have a pointer to the frame window, you can use this pointer to call the function you created earlier to update the combo box on the toolbar. Now if you call this new function from the Width menu command event handlers, as in Listing 12.11, the combo box that you placed on the color toolbar is automatically updated to reflect the currently selected drawing width, regardless of whether the width was selected from the combo box or the pull-down menu.
1: void CToolbarDoc::OnWidthVthin() 2: { 3: // TODO: Add your command handler code here 4: // Set the new width 5: m_nWidth = 0; 6: // Update the combo box on the color toolbar 7: UpdateColorbar(0); 8: }
Earlier today, you learned how to specify status bar messages and tooltips for both toolbar buttons and menus. What if you want to use the status bar to provide the user with more substantial information? What if, as in the Visual C++ Developer Studio, you want to provide information about what the user is doing, where he is in the document he is editing, or the mode that the application is in? This information goes beyond the Caps, Num, and Scroll lock keys that Visual C++ automatically reports on the status bar.
It's actually easy to add additional panes to the status bar, as well as take away the panes that are already there. To learn just how easy a change this is, you will add a new pane to the status bar in your drawing application that will display the color currently in use.
Before you add a new status bar pane, you need to add a new entry to the application string table for use in the status bar pane. This string table entry will perform two functions for the status bar pane. The first thing it will do is provide the object ID for the status bar pane. You will use this ID for updating the pane as you need to update the text in the pane. The second function this string table entry will perform is size the pane. To size the pane correctly, you need to provide a caption for the string table entry that is at least as wide as the widest string that you will place in the status bar pane.
Add a new string to your application string table, using the same steps you used earlier when adding the text for the combo box you placed on the color toolbar. Specify the string ID as ID_INDICATOR_COLOR and the caption as MAGENTA (the widest string that you will put into the status bar pane).
A small section in the first part of the main frame source code defines the status bar layout. This small table contains the object IDs of the status bar panes as table elements, in the order in which they are to appear from left to right on the status bar.
To add the color pane to the status bar, add the ID of the color pane to the status bar indicator table definition, just after the message map in the source-code file for the main frame. Place the color pane ID in the table definition in the position that you want it to be on the status bar, as in line 18 of Listing 12.12.
1://///////////////////////////////////////////////////////////////////// 2: // CMainFrame 3: 4: IMPLEMENT_DYNCREATE(CMainFrame, CFrameWnd) 5: 6: BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) 7: //{{AFX_MSG_MAP(CMainFrame) 8: ON_WM_CREATE()
9: ON_COMMAND(ID_VIEW_COLORBAR, OnViewColorbar)
10: ON_UPDATE_COMMAND_UI(ID_VIEW_COLORBAR, OnUpdateViewColorbar) 11: //}}AFX_MSG_MAP 12: ON_CBN_SELCHANGE(IDC_CBWIDTH, OnSelChangeWidth) 13: END_MESSAGE_MAP() 14: 15: static UINT indicators[] = 16: { 17: ID_SEPARATOR, // status line indicator 18: ID_INDICATOR_COLOR, 19: ID_INDICATOR_CAPS, 20: ID_INDICATOR_NUM, 21: ID_INDICATOR_SCRL, 22: }; 23: 24://///////////////////////////////////////////////////////////////////// 25: // CMainFrame construction/destruction
If you want to drop any of the lock key indicators from the status bar, just remove them from the indicators table definition. If you examine the OnCreate function, where the status bar is created (just after the toolbars are created), you'll see where this table is used to create the status bar with the following code:
if (!m_wndStatusBar.Create(this) || !m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT)))
Once the status bar is created, the SetIndicators function is called on the status bar to add the panes as they are defined in the indicators table. The strings associated with the IDs in the indicators table are used to initialize the panes and set their size. If you compile and run your application at this point, you see the new color pane on the status bar with the caption from the string table displayed within.
Once you've added the pane to the status bar, you can let the UPDATE_COMMAND_UI event do all the updating of the pane. All you need to do is add an event handler for this event on the object ID of the pane and use this event to set the pane text. Because the status bar is always visible, the UPDATE_COMMAND_UI event for the panes on the status bar is triggered every time that the application is idle. This means that it is triggered after the application is finished processing just about every keystroke and mouse movement. In almost a week, on Day 18, "Doing Multiple Tasks at One Time--Multitasking," you will learn more about how often and when any tasks that are performed when the application is idle are triggered.
In the event handler, you need to create a string containing the name of the current color (or whatever other text you want to display in the status bar pane). Next, you have to make sure that the pane is enabled. Finally, you need to set the text of the pane to the string that you have created.
To implement this in your application, you need to create an UPDATE_COMMAND_UI event handler. Once again, the Class Wizard does not know about the status bar pane, so you have to create the message handler and add it to the message map yourself. To create the message handler, add a new member function to the document class (CToolbarDoc) with a type of afx_msg void, a definition of OnUpdateIndicatorColor (CCmdUI *pCmdUI), and an access of protected. Edit the newly created function, adding the code in Listing 12.13.
1: void CToolbarDoc::OnUpdateIndicatorColor(CCmdUI *pCmdUI) 2: { 3: CString strColor; 4: 5: // What is the current color? 6: switch (m_nColor) 7: { 8: case 0: // Black 9: strColor = "BLACK"; 10: break; 11: case 1: // Blue 12: strColor = "BLUE"; 13: break; 14: case 2: // Green 15: strColor = "GREEN"; 16: break; 17: case 3: // Cyan 18: strColor = "CYAN"; 19: break; 20: case 4: // Red 21: strColor = "RED"; 22: break; 23: case 5: // Magenta 24: strColor = "MAGENTA"; 25: break; 26: case 6: // Yellow 27: strColor = "YELLOW"; 28: break; 29: case 7: // White 30: strColor = "WHITE"; 31: break; 32: } 33: // Enable the status bar pane 34: pCmdUI->Enable(TRUE); 35: // Set the text of the status bar pane 36: // to the current color 37: pCmdUI->SetText(strColor); 38: }
In this function, you followed three steps exactly: You created a string with the current color name, made sure that the pane was enabled, and set the pane text to the string that you had created.
Now, to make sure that your new message handler is called when it is supposed to be, you need to add an ON_UPDATE_COMMAND_UI entry to the message map at the top of the document source code file, as specified in Listing 12.14.
1:///////////////////////////////////////////////////////////////////////
2: // CToolbarDoc 3: 4: IMPLEMENT_DYNCREATE(CToolbarDoc, CDocument) 5: 6: BEGIN_MESSAGE_MAP(CToolbarDoc, CDocument) 7: ON_UPDATE_COMMAND_UI(ID_INDICATOR_COLOR, OnUpdateIndicatorColor) 8: //{{AFX_MSG_MAP(CToolbarDoc) 9: ON_UPDATE_COMMAND_UI(ID_WIDTH_VTHIN, OnUpdateWidthVthin) 10: . 11: . 12: ON_COMMAND(ID_WIDTH_VTHIN, OnWidthVthin) 13: //}}AFX_MSG_MAP 14: END_MESSAGE_MAP()
After adding the message handler and message map entry, you should now be able to compile and run your application and see the color status bar pane automatically updated to reflect the current drawing color, as shown in Figure 12.6.