Not very long ago, menus and toolbars were easy to distinguish. Menus consisted of a hierarchical collection of text items, while toolbars consisted of a row of bitmapped buttons. But once icons and controls began appearing on menus, and drop-down menus sprouted from toolbars, the differences became less obvious. Traditionally, toolbars are positioned near the top of the window right under the menu, but toolbars can actually appear on any side of the window. If they're at the bottom, they should appear above the status bar (if there is one). ToolBar is a descendant of HeaderedItemsControl, just like MenuItem and (as you'll see in the next chapter) TreeViewItem. This means that ToolBar has an Items collection, which consists of the items (buttons and so forth) displayed on the toolbar. ToolBar also has a Header property, but it's not cusomarily used on horizontal toolbars. It makes more sense on vertical toolbars as a title. There is no ToolBarItem class. You put the same elements and controls on the toolbar that you put on your windows and panels. Buttons are very popular, of course, generally displaying small bitmaps. The ToggleButton is commonly used to display on/off options. The ComboBox is very useful on toolbars, and a single-line TextBox is also possible. You can even put a MenuItem on the toolbar to have drop-down options, perhaps containing other controls. Use Separator to frame items into functional groups. Because toolbars tend to have more graphics and less text than windows and dialog boxes, it is considered quite rude not to use tooltips with toolbar items. Because toolbar items often duplicate menu items, it is common for them to share command bindings. Here is a rather nonfunctional program that creates a ToolBar and populates it with eight buttons. The program defines an array of eight static properties from the ApplicationCommands class (of type RoutedUICommand) and a corresponding array of eight file names of bitmaps located in the Images directory of the project. Each button's Command property is assigned one of these RoutedUICommand objects and gets a bitmapped image.
Clicking any of the buttons causes a message box to pop up from the ToolBarButtonOnClick event handler. This event handler becomes associated with each of the RoutedUICommand objects when the commands are added to the window's command bindings collection in the last statement of the for loop. Notice that the Text property of each RoutedUICommand plays a role in the creation of the ToolTip control associated with each button. A very nice byproduct of these command bindings is a keyboard interface. Typing Ctrl+N, Ctrl+O, Ctrl+S, Ctrl+P, Ctrl+X, Ctrl+C, Ctrl+V, or the Delete key will also bring up the message box. The images are from the library shipped with Microsoft Visual Studio 2005. They are 16 pixels square. In some cases, I had to use an image-editing program to change the resolution of the image to 96 dots per inch. In the context of the Windows Presentation Manager, that means that the images are really 1/6 inch square, about the height of 12-point type. On higher-resolution displays these images should remain about the same physical size, although they may not be quite as sharp. Here's an interesting experiment. Remove the statement that prevents the toolbar from filling the client area: dock.LastChildFill = false; Now add a RichTextBox control to the DockPanel. This code can go right before the for loop: RichTextBox txtbox = new RichTextBox(); dock.Children.Add(txtbox); Also for this experiment it is important to set the keyboard input focus to the RichTextBox at the very close of the window constructor: txtbox.Focus(); As you may know, RichTextBox processes Ctrl+X, Ctrl+C, and Ctrl+V to implement Cut, Copy, and Paste. (You can also see these three commands on a little context menu when you right-click the RichTextBox.) But the command bindings that RichTextBox implements interact with the window's command bindings in very desirable ways: If there is no text selected in the RichTextBox, the Cut and Copy buttons are disabled! If you select some text and you click one of these buttons, it performs the operation rather than displaying the message box. (However, if the buttons are disabled and you type Ctrl+X or Ctrl+C, the RichTextBox ignores the keystrokes and the program displays the message box.) Even without this help from RichTextBox, a program that implements a Paste button needs to enable and disable the button based on the contents of the clipboard. If a program chooses not to use the standard RoutedUICommand objects, it will need to set a timer to check the contents of the clipboard (perhaps every tenth second) and enable the button based on what it finds. Using the standard RoutedUICommand objects is usually easier because calls are made to the CanExecute handler associated with the command binding whenever the contents of the clipboard change. Some programs (such as Microsoft Internet Explorer) display some text along with toolbar buttons for those buttons whose meaning might not be so obvious. You can easily do this with a WPF toolbar by using a StackPanel on the button. First, comment out this statement from the if loop: btn.Content = img; Add the following code right after that statement: StackPanel stack = new StackPanel(); stack.Orientation = Orientation.Horizontal; btn.Content = stack; TextBlock txtblk = new TextBlock(); txtblk.Text = comm[i].Text; stack.Children.Add(img); stack.Children.Add(txtblk); Now each button displays text to the right of the image. You can move the text below the image simply by changing the Orientation of the StackPanel to Vertical. You can change the order of the Image and TextBlock simply by swapping the statements that add them to the StackPanel children collection. You may have noticed a little grip-like image at the far left of the toolbar. If you pass your mouse over the grip, you'll see the mouse cursor change to Cursors.SizeAll (the four-directional arrow cursor), but despite the visual cues, you won't be able to budge the toolbar. (I'll show you how to get that to work shortly.) If you set the Header property of the ToolBar object, that text string (or whatever) appears between the grip and the first item on the toolbar. If you make the window too narrow to fit the entire toolbar, a little button at the far right becomes enabled. Clicking that button causes the other buttons on the toolbar to appear as a little popup. What you are looking at here is an object of type ToolBarOverflowPanel. In addition, the ToolBar uses a class named ToolBarPanel to arrange the items on itself. It is unlikely you'll need to use ToolBarPanel or ToolBarOverflowPanel yourself. The following class hierarchy shows all ToolBar-related classes: FrameworkElement Although ToolBarPanel and ToolBarOverflowPanel work behind the scenes, the ToolBarTray class becomes important if you're implementing multiple toolbars or you want vertical toolbars. The MoveTheToolbar program demonstrates the use of the ToolBarTray. It creates two of them, one docked at the top of the client area and the other docked to the left. Within each ToolBarTray, the program creates three ToolBar controls with a header (just for identification) and six buttons containing letters as content.
Notice that the ToolBarTray docked at the left of the client area is given an Orientation of Vertical. Any ToolBar controls that are part of that tray display their items vertically. The program sets the Header property of each ToolBar to a number. The toolbars numbered 1, 2, and 3 are in the top ToolBarTray. Those numbered 4, 5, and 6 are in the left ToolBarTray. Within each tray you can move the toolbars so that they follow one another horizontally across the top or vertically on the left (this is the default arrangement) or you can move them into multiple rows (on the top) or multiple columns (on the left). You cannot move a ToolBar from one ToolBarTray to another. If you want to initialize the positions of the toolbars within the tray, or you want to save the arrangment preferred by the user, you use two integer properties of ToolBar named Band and BandIndex. For a horizontal ToolBar, the Band indicates the row the ToolBar occupies. The BandIndex is a position in that row, starting from the left. For a vertical ToolBar, the Band indicates the column the ToolBar occupies, and the BandIndex is a position in that column from the top. By default, all toolbars have a Band of 0 and a BandIndex numbered beginning at 0 and increasing based on the order in which they're added to the ToolBarTray. For the remainder of this chapter, I'd like to show toolbars in a more "real-life" application. The FormatRichText program that I'll be assembling has no menu, but it does have four toolbars and one status bar. The four toolbars are devoted to handling file I/O, the clipboard, character formatting, and paragraph formatting. As usual, the FormatRichText class that is the bulk of this program inherits from Window. But I wanted to keep the source code files reasonably small and to group related pieces of code, so I've split the FormatRichText class into six parts using the partial keyword. Each part of this class is in a different file. The first file is named FormatRichText.cs, but the file that contains the code related to opening and saving files is located in FormatRichText.File.cs. This project also requires a link to the ColorGridBox.cs file from Chapter 11. Here's the first file.
The constructor creates a DockPanel, ToolBarTray, and a RichTextBox. Notice that the RichTextBox is stored as a field to be accessible to the other parts of the FormatRichText class. The constructor then calls five methods (AddFileToolBar and so forth), each of which is in a separate file. The second and third arguments to these methods are the desired Band and BandIndex properties of the ToolBar control. The second file of the FormatRichText project is named FormatRichText.File.cs and is devoted to loading and saving files. A RichTextBox control is capable of loading and saving files in four different formats. These four formats correspond to four static read-only fields in the DataFormats class. They are DataFormats.Text, DataFormats.Rtf (Rich Text Format), DataFormats.Xaml, and DataFormats.XamlPackage (which is actually a ZIP file containing other files that contribute to a complete document). These formats are listed in an array defined as a field near the top of this file. Notice that DataFormats.Text is repeated at the end to make a total of five. These correspond to the five sections of the strFilter, which is required by the OpenFileDialog and SaveFileDialog classes to show the different file types and extensions in the dialog boxes.
The AddFileToolBar method creates a ToolBar object and then creates three Button objects corresponding to the standard commands ApplicationCommands.New, ApplicationCommands.Open, and ApplicationCommands.Save. (You'll notice that the FormatRichText program responds to Ctrl+N, Ctrl+O, and Ctrl+S, the standard keyboard accelerators for these commands.) To keep this program simple, I've omitted some amenities. The program doesn't retain a file name, so it can't implement Save by simply saving the file under that name. Nor does it warn you about files that you've modified but haven't yet saved. These features are missing here but they are implemented in the NotepadClone program in Chapter 18. The OnOpen and OnSave methods are fairly similar to each other. They both display a dialog box and, if the user presses the Open or Save button, the method obtains a FlowDocument object from the RichTextBox, and then creates a TextRange object corresponding to the entire content of the document. The TextRange class has Load and Save methods; the first argument is a Stream, the second is a field from DataFormats. The methods index the formats array using the FilterIndex property of the dialog box. This property indicates the part of strFilter that the user had selected prior to pressing the Open or Save button. (Notice that 1 is subtracted from FilterIndex because the property is 1-based rather than 0-based.) The next file handles the commands normally found on the Edit menu. Experimentation with the CraftTheToolbar program at the beginning of this chapter revealed that command bindings added to the window become connected to command bindings for the child with input focusthat is, the RichTextBox. The code in this file creates the buttons and command bindings, and adds the command bindings to the window's collection, but most of them are unimplemented. The RichTextBox itself handles most of the clipboard logic.
The only button that didn't seem to work right was Delete, so I added CanExecute and Executed handlers for that command. The RichTextBox keeps the Undo and Redo buttons enabled all the time, which is not quite right, but when I tried to implement CanExecute handlers for those commands by calling the CanUndo and CanRedo methods of RichTextBox, I discovered that those methods always return true as well! It's with the next file that things start to get interesting. This is the ToolBar that handles character formatting, which includes the font family, font size, bold and italic, foreground color, and background color. The controls in this toolbar must display the font family and other character formatting associated with the currently selected text or (if there's no selection) the text insertion point. You obtain the currently selected text in the RichTextBox with the Selection property. This property is of type TextSelection, which is basically a TextRange object. TextRange defines two methods named GetPropertyValue and ApplyPropertyValue. The first argument to both methods is a dependency property involved with formatting. For example, here's how to get the FontFamily associated with the currently selected text of a RichTextBox named txtbox: txtbox.Selection.GetPropertyValue(FlowDocument.FontFamilyProperty); Notice that you're obtaining a property of the FlowDocument, which is what the RichTextBox stores. What happens when the current selection encompasses multiple font families (or other character or paragraph properties) is not well documented. You should check for a return value of null and also that the type of the return value is what you need. To set a new FontFamily (named fontfam, for example) to the current selection (or insertion point), you call: txtbox.Selection.ApplyPropertyValue(FlowDocument.FontFamilyProperty, fontfam); To keep the controls in the toolbar updated, you must attach a handler for the SelectionChanged event of the RichTextBox. The character-formatting ToolBar uses ComboBox controls for the font family and font size. The ComboBox is a combination of a TextBox and a ListBox, and you can control whether it's more like one or the other. ComboBox derives from Selector, just like ListBox. You fill the ComboBox with items using either the Items collection or the ItemsSource property. ComboBox inherits SelectedIndex, SelectedItem, and SelectedValue properties from Selector. Unlike ListBox, the ComboBox does not have a multiple-selection mode. In its normal resting state, the ComboBox displays just one line of text, which is settable and accessible through the Text property. A button at the far right of the ComboBox causes the actual list of items to be displayed. The part of the ComboBox displaying the list of items is known as the "drop-down." ComboBox defines read-write properties MaxDropDownHeight and IsDropDownOpen, and two events DropDownOpen and DropDownClosed. ComboBox defines an important property named IsEditable that has a default value of false. In this non-editable mode, the top part of the ComboBox displays the selected item (if there is one). If the user clicks on that display, the drop-down is unfurled or retracted. The user cannot type anything into that field, but pressing a letter might cause an item to be selected that begins with that letter. A program can obtain a text representation of the selected item or set the selected item through the Text property, but it's probably safer to use SelectedItem. If a program attempts to set the Text property to something that doesn't correspond to an item in the ComboBox, no item is selected. If a program sets IsEditable to true, the top part of the ComboBox changes to a TextBox, and the user can type something into that field. However, there is no event to indicate when this text is changing. The ComboBox has a third mode that has limited use: If a program sets IsEditable to true, it can also set IsReadOnly to true. In that case, the user cannot change the item in the EditBox, but the item can be selected for copying to the clipboard. IsReadOnly has no effect when IsEditable is false. Combo boxes can be tricky in actual use, even whenlike the ComboBox for the font familythe IsEditable property keeps its default setting of false. (It makes no sense for a user to type a font family that does not exist in the list.) It is tempting to just attach a handler for the SelectionChanged event of the ComboBox, and to conclude processing with a call to shift input focus back to the RichTextBox, and that is how I've implemented the ComboBox for the FontFamily. If the user just clicks the arrow on the ComboBox, and then clicks a font family, that works fine. It also works fine if the user clicks the arrow on the ComboBox and then uses the keyboard to scroll through the list, pressing Enter to select an item or Escape to abandon the whole process. However, there are some flaws with this simple approach: Suppose a user clicks the text part of the ComboBox and then clicks it again so that the list retracts. The ComboBox should still have input focus. The user can now press the up and down arrow keys to scroll through the list. Should the selected text in the RichTextBox change to reflect the selected font family? Probably, and I think the ComboBox should retain input focus during this process. However, suppose the user next presses Escape. The selected text in the RichTextBox should revert back to the font family it had before the ComboBox was invoked, and keyboard input focus should then shift from the ComboBox back to the RichTextBox. Keyboard input focus should also shift back to the RichTextBox when the user presses the Enter key, but in that case the current selection in the ComboBox should be applied to the selected text in the RichTextBox. Implementing this logicthe sadistic teacher saysis an exercise left to the reader. The ComboBox for the font size is even worse. Traditionally, such a ComboBox must list a bunch of common font sizes, but it should also allow the user to type something else. The ComboBox must have its IsEditable property set to true. But how do you know when the user has finished typing something? A user should be able to leave the ComboBox by pressing Tab (in which case keyboard focus moves to the next control in the ToolBar), Enter (in which case focus goes to the RichTextBox), or Escape (in which case focus goes to the RichTextBox but the ComboBox value is restored to what it was before editing). The user can also get out of the ComboBox by clicking the mouse somewhere else. All of these involve a loss of input focus, so the following code installs an event handler for the LostKeyboardFocus event of the ComboBox. It is this event handler that contains the Double.TryParse code to convert the text entered by the user into a number. If that conversion fails, the text is restored to its original value. Saving that original value required a handler for the GotKeyboardFocus event. The job also required a handler for PreviewKeyDown to process the Enter and Escape keys.
The remainder of this file is straightforward by comparison. The AddCharToolBar method also creates two ToggleButton objects for Bold and Italic, and a menu for background and foreground colors. Yes, the toolbar contains an entire menu, but it consists of just two items. Each of these two MenuItem objects has its Header set to a bitmap to represent the background or foreground color, and has an Items collection that contains just a ColorGridBox control. Following the character-formatting toolbar, the paragraph-formatting toolbar should be a snap, since it contains only four images for alignment: Left, Right, Center, and Justified. However, I couldn't find adequate images, so I had to create them right in the code. The CreateButton method puts a 16-unit-square Canvas on a ToggleButton and draws 5 lines that represent the various types of alignment.
That concludes the toolbar logic. The FormatRichText program also includes a status bar. In this program, the StatusBar is an ItemsControl (much like Menu) and the StatusBarItem is a ContentControl (just like MenuItem), as this partial class hierarchy shows: Control Status bars are customarily docked at the bottom of the client area. In practice, status bars usually only contain text and the occasional ProgressBar when a large file needs to be loaded or saved (or some other lengthy job takes place). Internally, a StatusBar uses a DockPanel for layout, so if your status bar contains multiple items, you can call DockPanel. SetDock to position them. The last item fills the remaining interior space of the status bar, so you can use HorizontalAlignment to position it. For status bars containing only one item, use HorizontalAlignment to position the item. The StatusBar in this program merely displays the current date and time.
FormatRichText is well on its way to mimicking much of the functionality of the Windows WordPad program, but I'm not going to pursue that. Instead, Chapter 18 features a clone of Windows Notepad, admittedly a much easier goal, but one that pays off when it is adapted in Chapter 20 to become a valuable programming tool called XamlCruncher. |