Chapter 15. Toolbars and Status Bars


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.

CraftTheToolbar.cs

[View full width]

//------------------------------------------------ // CraftTheToolbar.cs (c) 2006 by Charles Petzold //------------------------------------------------ using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; namespace Petzold.CraftTheToolbar { public class CraftTheToolbar : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new CraftTheToolbar()); } public CraftTheToolbar() { Title = "Craft the Toolbar"; RoutedUICommand[] comm = { ApplicationCommands.New, ApplicationCommands.Open, ApplicationCommands.Save, ApplicationCommands.Print, ApplicationCommands.Cut, ApplicationCommands.Copy, ApplicationCommands.Paste, ApplicationCommands.Delete }; string[] strImages = { "NewDocumentHS.png", "openHS .png", "saveHS.png", "PrintHS.png", "CutHS.png", "CopyHS.png", "PasteHS.png", "DeleteHS.png" }; // Create DockPanel as content of window. DockPanel dock = new DockPanel(); dock.LastChildFill = false; Content = dock; // Create Toolbar docked at top of window. ToolBar toolbar = new ToolBar(); dock.Children.Add(toolbar); DockPanel.SetDock(toolbar, Dock.Top); // Create the Toolbar buttons. for (int i = 0; i < 8; i++) { if (i == 4) toolbar.Items.Add(new Separator()); // Create the Button. Button btn = new Button(); btn.Command = comm[i]; toolbar.Items.Add(btn); // Create an Image as content of the Button. Image img = new Image(); img.Source = new BitmapImage( new Uri("pack://application:,, /Images/" + strImages[i])); img.Stretch = Stretch.None; btn.Content = img; // Create a ToolTip based on the UICommand text. ToolTip tip = new ToolTip(); tip.Content = comm[i].Text; btn.ToolTip = tip; // Add the UICommand to the window command bindings. CommandBindings.Add( new CommandBinding(comm[i], ToolBarButtonOnClick)); } } // Do-nothing command handler for button. void ToolBarButtonOnClick(object sender, ExecutedRoutedEventArgs args) { RoutedUICommand comm = args.Command as RoutedUICommand; MessageBox.Show(comm.Name + " command not yet implemented", Title); } } }



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

    Control

          ItemsControl

                HeaderedItemsControl

                      ToolBar

    Panel (abstract)

          StackPanel

                ToolBarPanel

          ToolBarOverflowPanel

    ToolBarTray

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.

MoveTheToolbar.cs

[View full width]

//----------------------------------------------- // MoveTheToolbar.cs (c) 2006 by Charles Petzold //----------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.MoveTheToolbar { public class MoveTheToolbar : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new MoveTheToolbar()); } public MoveTheToolbar() { Title = "Move the Toolbar"; // Create DockPanel as content of window. DockPanel dock = new DockPanel(); Content = dock; // Create ToolBarTray at top and left of window. ToolBarTray trayTop = new ToolBarTray(); dock.Children.Add(trayTop); DockPanel.SetDock(trayTop, Dock.Top); ToolBarTray trayLeft = new ToolBarTray(); trayLeft.Orientation = Orientation .Vertical; dock.Children.Add(trayLeft); DockPanel.SetDock(trayLeft, Dock.Left); // Create TextBox to fill rest of client area. TextBox txtbox = new TextBox(); dock.Children.Add(txtbox); // Create six Toolbars... for (int i = 0; i < 6; i++) { ToolBar toolbar = new ToolBar(); toolbar.Header = "Toolbar " + (i + 1); if (i < 3) trayTop.ToolBars.Add(toolbar); else trayLeft.ToolBars.Add(toolbar); // ... with six buttons each. for (int j = 0; j < 6; j++) { Button btn = new Button(); btn.FontSize = 16; btn.Content = (char)('A' + j); toolbar.Items.Add(btn); } } } } }



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.

FormatRichText.cs

[View full width]

//----------------------------------------------- // FormatRichText.cs (c) 2006 by Charles Petzold //----------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.FormatRichText { public partial class FormatRichText : Window { RichTextBox txtbox; [STAThread] public static void Main() { Application app = new Application(); app.Run(new FormatRichText()); } public FormatRichText() { Title = "Format Rich Text"; // Create DockPanel as content of window. DockPanel dock = new DockPanel(); Content = dock; // Create ToolBarTray docked at top of client area. ToolBarTray tray = new ToolBarTray(); dock.Children.Add(tray); DockPanel.SetDock(tray, Dock.Top); // Create RichTextBox. txtbox = new RichTextBox(); txtbox.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; // Call methods in other files. AddFileToolBar(tray, 0, 0); AddEditToolBar(tray, 1, 0); AddCharToolBar(tray, 2, 0); AddParaToolBar(tray, 2, 1); AddStatusBar(dock); // Fill rest of client area with RichTextBox and give it focus. dock.Children.Add(txtbox); txtbox.Focus(); } } }



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.

FormatRichText.File.cs

[View full width]

//---------------------------------------------------- // FormatRichText.File.cs (c) 2006 by Charles Petzold //---------------------------------------------------- using Microsoft.Win32; using System; using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; namespace Petzold.FormatRichText { public partial class FormatRichText : Window { string[] formats = { DataFormats.Xaml, DataFormats .XamlPackage, DataFormats.Rtf, DataFormats.Text, DataFormats.Text }; string strFilter = "XAML Document Files (*.xaml)|*.xaml|" + "XAML Package Files (*.zip)|*.zip|" + "Rich Text Format Files (*.rtf)|*.rtf|" + "Text Files (*.txt)|*.txt|" + "All files (*.*)|*.*"; void AddFileToolBar(ToolBarTray tray, int band, int index) { // Create the ToolBar. ToolBar toolbar = new ToolBar(); toolbar.Band = band; toolbar.BandIndex = index; tray.ToolBars.Add(toolbar); RoutedUICommand[] comm = { ApplicationCommands.New, ApplicationCommands.Open, ApplicationCommands.Save }; string[] strImages = { "NewDocumentHS.png", "openHS .png", "saveHS.png" }; // Create buttons for the ToolBar. for (int i = 0; i < 3; i++) { Button btn = new Button(); btn.Command = comm[i]; toolbar.Items.Add(btn); Image img = new Image(); img.Source = new BitmapImage( new Uri("pack:/ /application:,,/Images/" + strImages[i])); img.Stretch = Stretch.None; btn.Content = img; ToolTip tip = new ToolTip(); tip.Content = comm[i].Text; btn.ToolTip = tip; } // Add the command bindings. CommandBindings.Add( new CommandBinding (ApplicationCommands.New, OnNew)); CommandBindings.Add( new CommandBinding (ApplicationCommands.Open, OnOpen)); CommandBindings.Add( new CommandBinding (ApplicationCommands.Save, OnSave)); } // New: Set content to an empty string. void OnNew(object sender, ExecutedRoutedEventArgs args) { FlowDocument flow = txtbox.Document; TextRange range = new TextRange(flow .ContentStart, flow .ContentEnd); range.Text = ""; } // Open: Display dialog box and load file. void OnOpen(object sender, ExecutedRoutedEventArgs args) { OpenFileDialog dlg = new OpenFileDialog(); dlg.CheckFileExists = true; dlg.Filter = strFilter; if ((bool)dlg.ShowDialog(this)) { FlowDocument flow = txtbox.Document; TextRange range = new TextRange (flow.ContentStart, flow.ContentEnd); FileStream strm = null; try { strm = new FileStream(dlg .FileName, FileMode.Open); range.Load(strm, formats[dlg .FilterIndex - 1]); } catch (Exception exc) { MessageBox.Show(exc.Message, Title); } finally { if (strm != null) strm.Close(); } } } // Save: Display dialog box and save file. void OnSave(object sender, ExecutedRoutedEventArgs args) { SaveFileDialog dlg = new SaveFileDialog(); dlg.Filter = strFilter; if ((bool)dlg.ShowDialog(this)) { FlowDocument flow = txtbox.Document; TextRange range = new TextRange (flow.ContentStart, flow.ContentEnd); FileStream strm = null; try { strm = new FileStream(dlg .FileName, FileMode.Create); range.Save(strm, formats[dlg .FilterIndex - 1]); } catch (Exception exc) { MessageBox.Show(exc.Message, Title); } finally { if (strm != null) strm.Close(); } } } } }



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.

FormatRichText.Edit.cs

[View full width]

//---------------------------------------------------- // FormatRichText.Edit.cs (c) 2006 by Charles Petzold //---------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; namespace Petzold.FormatRichText { public partial class FormatRichText : Window { void AddEditToolBar(ToolBarTray tray, int band, int index) { // Create Toolbar. ToolBar toolbar = new ToolBar(); toolbar.Band = band; toolbar.BandIndex = index; tray.ToolBars.Add(toolbar); RoutedUICommand[] comm = { ApplicationCommands.Cut, ApplicationCommands.Copy, ApplicationCommands.Paste, ApplicationCommands.Delete, ApplicationCommands.Undo, ApplicationCommands.Redo }; string[] strImages = { "CutHS.png", "CopyHS.png", "PasteHS.png", "DeleteHS.png", "Edit_UndoHS.png", "Edit_RedoHS.png" }; for (int i = 0; i < 6; i++) { if (i == 4) toolbar.Items.Add(new Separator()); Button btn = new Button(); btn.Command = comm[i]; toolbar.Items.Add(btn); Image img = new Image(); img.Source = new BitmapImage( new Uri("pack:/ /application:,,/Images/" + strImages[i])); img.Stretch = Stretch.None; btn.Content = img; ToolTip tip = new ToolTip(); tip.Content = comm[i].Text; btn.ToolTip = tip; } CommandBindings.Add(new CommandBinding (ApplicationCommands.Cut)); CommandBindings.Add(new CommandBinding (ApplicationCommands.Copy)); CommandBindings.Add(new CommandBinding (ApplicationCommands.Paste)); CommandBindings.Add(new CommandBinding( ApplicationCommands .Delete, OnDelete, CanDelete)); CommandBindings.Add(new CommandBinding (ApplicationCommands.Undo)); CommandBindings.Add(new CommandBinding (ApplicationCommands.Redo)); } void CanDelete(object sender, CanExecuteRoutedEventArgs args) { args.CanExecute = !txtbox.Selection .IsEmpty; } void OnDelete(object sender, ExecutedRoutedEventArgs args) { txtbox.Selection.Text = ""; } } }



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.

FormatRichText.Char.cs

[View full width]

//---------------------------------------------------- // FormatRichText.Char.cs (c) 2006 by Charles Petzold //---------------------------------------------------- using Petzold.SelectColorFromGrid; // for ColorGridBox using System; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; namespace Petzold.FormatRichText { public partial class FormatRichText : Window { ComboBox comboFamily, comboSize; ToggleButton btnBold, btnItalic; ColorGridBox clrboxBackground, clrboxForeground; void AddCharToolBar(ToolBarTray tray, int band, int index) { // Create ToolBar and add to ToolBarTray. ToolBar toolbar = new ToolBar(); toolbar.Band = band; toolbar.BandIndex = index; tray.ToolBars.Add(toolbar); // Create ComboBox for font families. comboFamily = new ComboBox(); comboFamily.Width = 144; comboFamily.ItemsSource = Fonts .SystemFontFamilies; comboFamily.SelectedItem = txtbox .FontFamily; comboFamily.SelectionChanged += FamilyComboOnSelection; toolbar.Items.Add(comboFamily); ToolTip tip = new ToolTip(); tip.Content = "Font Family"; comboFamily.ToolTip = tip; // Create ComboBox for font size. comboSize = new ComboBox(); comboSize.Width = 48; comboSize.IsEditable = true; comboSize.Text = (0.75 * txtbox .FontSize).ToString(); comboSize.ItemsSource = new double[] { 8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72 }; comboSize.SelectionChanged += SizeComboOnSelection; comboSize.GotKeyboardFocus += SizeComboOnGotFocus; comboSize.LostKeyboardFocus += SizeComboOnLostFocus; comboSize.PreviewKeyDown += SizeComboOnKeyDown; toolbar.Items.Add(comboSize); tip = new ToolTip(); tip.Content = "Font Size"; comboSize.ToolTip = tip; // Create Bold button. btnBold = new ToggleButton(); btnBold.Checked += BoldButtonOnChecked; btnBold.Unchecked += BoldButtonOnChecked; toolbar.Items.Add(btnBold); Image img = new Image(); img.Source = new BitmapImage( new Uri("pack://application:,, /Images/boldhs.png")); img.Stretch = Stretch.None; btnBold.Content = img; tip = new ToolTip(); tip.Content = "Bold"; btnBold.ToolTip = tip; // Create Italic button. btnItalic = new ToggleButton(); btnItalic.Checked += ItalicButtonOnChecked; btnItalic.Unchecked += ItalicButtonOnChecked; toolbar.Items.Add(btnItalic); img = new Image(); img.Source = new BitmapImage( new Uri("pack://application:,, /Images/ItalicHS.png")); img.Stretch = Stretch.None; btnItalic.Content = img; tip = new ToolTip(); tip.Content = "Italic"; btnItalic.ToolTip = tip; toolbar.Items.Add(new Separator()); // Create Background and Foreground color menu. Menu menu = new Menu(); toolbar.Items.Add(menu); // Create Background menu item. MenuItem item = new MenuItem(); menu.Items.Add(item); img = new Image(); img.Source = new BitmapImage( new Uri("pack://application:,, /Images/ColorHS.png")); img.Stretch = Stretch.None; item.Header = img; clrboxBackground = new ColorGridBox(); clrboxBackground.SelectionChanged += BackgroundOnSelectionChanged; item.Items.Add(clrboxBackground); tip = new ToolTip(); tip.Content = "Background Color"; item.ToolTip = tip; // Create Foreground menu item. item = new MenuItem(); menu.Items.Add(item); img = new Image(); img.Source = new BitmapImage( new Uri("pack://application:,, /Images/Color_fontHS.png")); img.Stretch = Stretch.None; item.Header = img; clrboxForeground = new ColorGridBox(); clrboxForeground.SelectionChanged += ForegroundOnSelectionChanged; item.Items.Add(clrboxForeground); tip = new ToolTip(); tip.Content = "Foreground Color"; item.ToolTip = tip; // Install handler for RichTextBox SelectionChanged event. txtbox.SelectionChanged += TextBoxOnSelectionChanged; } // Handler for RichTextBox SelectionChanged event. void TextBoxOnSelectionChanged(object sender, RoutedEventArgs args) { // Obtain FontFamily of currently selected text... object obj = txtbox.Selection .GetPropertyValue( FlowDocument.FontFamilyProperty); // ... and set it in the ComboBox. if (obj is FontFamily) comboFamily.SelectedItem = (FontFamily)obj; else comboFamily.SelectedIndex = -1; // Obtain FontSize of currently selected text... obj = txtbox.Selection.GetPropertyValue( FlowDocument.FontSizeProperty); // ... and set it in the ComboBox. if (obj is double) comboSize.Text = (0.75 * (double)obj).ToString(); else comboSize.SelectedIndex = -1; // Obtain FontWeight of currently selected text... obj = txtbox.Selection.GetPropertyValue( FlowDocument.FontWeightProperty); // .. and set the ToggleButton. if (obj is FontWeight) btnBold.IsChecked = (FontWeight)obj == FontWeights.Bold; // Obtain FontStyle of currently selected text... obj = txtbox.Selection.GetPropertyValue( FlowDocument.FontStyleProperty); // .. and set the ToggleButton. if (obj is FontStyle) btnItalic.IsChecked = (FontStyle)obj == FontStyles.Italic; // Obtain colors and set the ColorGridBox controls. obj = txtbox.Selection.GetPropertyValue( FlowDocument.BackgroundProperty); if (obj != null && obj is Brush) clrboxBackground.SelectedValue = (Brush)obj; obj = txtbox.Selection.GetPropertyValue( FlowDocument.ForegroundProperty); if (obj != null && obj is Brush) clrboxForeground.SelectedValue = (Brush)obj; } // Handler for FontFamily ComboBox SelectionChanged. void FamilyComboOnSelection(object sender, SelectionChangedEventArgs args) { // Obtain selected FontFamily. ComboBox combo = args.Source as ComboBox; FontFamily family = combo.SelectedItem as FontFamily; // Set it on selected text. if (family != null) txtbox.Selection.ApplyPropertyValue( FlowDocument .FontFamilyProperty, family); // Set focus back to TextBox. txtbox.Focus(); } // Handlers for FontSize ComboBox. string strOriginal; void SizeComboOnGotFocus(object sender, KeyboardFocusChangedEventArgs args) { strOriginal = (sender as ComboBox).Text; } void SizeComboOnLostFocus(object sender, KeyboardFocusChangedEventArgs args) { double size; if (Double.TryParse((sender as ComboBox).Text, out size)) txtbox.Selection.ApplyPropertyValue( FlowDocument .FontSizeProperty, size / 0.75); else (sender as ComboBox).Text = strOriginal; } void SizeComboOnKeyDown(object sender, KeyEventArgs args) { if (args.Key == Key.Escape) { (sender as ComboBox).Text = strOriginal; args.Handled = true; txtbox.Focus(); } else if (args.Key == Key.Enter) { args.Handled = true; txtbox.Focus(); } } void SizeComboOnSelection(object sender, SelectionChangedEventArgs args) { ComboBox combo = args.Source as ComboBox; if (combo.SelectedIndex != -1) { double size = (double) combo .SelectedValue; txtbox.Selection.ApplyPropertyValue( FlowDocument .FontSizeProperty, size / 0.75); txtbox.Focus(); } } // Handler for Bold button. void BoldButtonOnChecked(object sender, RoutedEventArgs args) { ToggleButton btn = args.Source as ToggleButton; txtbox.Selection.ApplyPropertyValue (FlowDocument.FontWeightProperty, (bool)btn.IsChecked ? FontWeights .Bold : FontWeights.Normal); } // Handler for Italic button. void ItalicButtonOnChecked(object sender, RoutedEventArgs args) { ToggleButton btn = args.Source as ToggleButton; txtbox.Selection.ApplyPropertyValue (FlowDocument.FontStyleProperty, (bool)btn.IsChecked ? FontStyles .Italic : FontStyles.Normal); } // Handler for Background color changed. void BackgroundOnSelectionChanged(object sender, SelectionChangedEventArgs args) { ColorGridBox clrbox = args.Source as ColorGridBox; txtbox.Selection.ApplyPropertyValue (FlowDocument.BackgroundProperty, clrbox.SelectedValue); } // Handler for Foreground color changed. void ForegroundOnSelectionChanged(object sender, SelectionChangedEventArgs args) { ColorGridBox clrbox = args.Source as ColorGridBox; txtbox.Selection.ApplyPropertyValue (FlowDocument.ForegroundProperty, clrbox.SelectedValue); } } }



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.

FormatRichText.Para.cs

[View full width]

//---------------------------------------------------- // FormatRichText.Para.cs (c) 2006 by Charles Petzold //---------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; namespace Petzold.FormatRichText { public partial class FormatRichText : Window { ToggleButton[] btnAlignment = new ToggleButton[4]; void AddParaToolBar(ToolBarTray tray, int band, int index) { // Create ToolBar and add to tray. ToolBar toolbar = new ToolBar(); toolbar.Band = band; toolbar.BandIndex = index; tray.ToolBars.Add(toolbar); // Create ToolBar items. toolbar.Items.Add(btnAlignment[0] = CreateButton(TextAlignment.Left, "Align Left", 0, 4)); toolbar.Items.Add(btnAlignment[1] = CreateButton(TextAlignment.Center, "Center", 2, 2)); toolbar.Items.Add(btnAlignment[2] = CreateButton(TextAlignment.Right, "Align Right", 4, 0)); toolbar.Items.Add(btnAlignment[3] = CreateButton(TextAlignment.Justify , "Justify", 0, 0)); // Attach another event handler for SelectionChanged. txtbox.SelectionChanged += TextBoxOnSelectionChanged2; } ToggleButton CreateButton(TextAlignment align, string strToolTip, int offsetLeft, int offsetRight) { // Create ToggleButton. ToggleButton btn = new ToggleButton(); btn.Tag = align; btn.Click += ButtonOnClick; // Set Content as Canvas. Canvas canv = new Canvas(); canv.Width = 16; canv.Height = 16; btn.Content = canv; // Draw lines on the Canvas. for (int i = 0; i < 5; i++) { Polyline poly = new Polyline(); poly.Stroke = SystemColors .WindowTextBrush; poly.StrokeThickness = 1; if ((i & 1) == 0) poly.Points = new PointCollection(new Point[] { new Point(2, 2 + 3 * i), new Point(14, 2 + 3 * i) }); else poly.Points = new PointCollection(new Point[] { new Point(2 + offsetLeft, 2 + 3 * i), new Point(14 - offsetRight, 2 + 3 * i) }); canv.Children.Add(poly); } // Create a ToolTip. ToolTip tip = new ToolTip(); tip.Content = strToolTip; btn.ToolTip = tip; return btn; } // Handler for TextBox SelectionChanged event. void TextBoxOnSelectionChanged2(object sender, RoutedEventArgs args) { // Obtain the current TextAlignment. object obj = txtbox.Selection .GetPropertyValue( Paragraph .TextAlignmentProperty); // Set the buttons. if (obj != null && obj is TextAlignment) { TextAlignment align = (TextAlignment)obj; foreach (ToggleButton btn in btnAlignment) btn.IsChecked = (align == (TextAlignment)btn.Tag); } else { foreach (ToggleButton btn in btnAlignment) btn.IsChecked = false; } } // Handler for Button Click event. void ButtonOnClick(object sender, RoutedEventArgs args) { ToggleButton btn = args.Source as ToggleButton; foreach (ToggleButton btnAlign in btnAlignment) btnAlign.IsChecked = (btn == btnAlign); // Set the new TextAlignment. TextAlignment align = (TextAlignment) btn.Tag; txtbox.Selection.ApplyPropertyValue (Paragraph.TextAlignmentProperty, align); } } }



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

     ContentControl

           StatusBarItem

     ItemsControl

           StatusBar

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

[View full width]

//--------- --------------------------------------------- // FormatRichText.Status.cs (c) 2006 by Charles Petzold //------------------------------------------------ ------ using System; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; using System.Windows.Threading; namespace Petzold.FormatRichText { public partial class FormatRichText : Window { StatusBarItem itemDateTime; void AddStatusBar(DockPanel dock) { // Create StatusBar docked at bottom of client area. StatusBar status = new StatusBar(); dock.Children.Add(status); DockPanel.SetDock(status, Dock.Bottom); // Create StatusBarItem. itemDateTime = new StatusBarItem(); itemDateTime.HorizontalAlignment = HorizontalAlignment.Right; status.Items.Add(itemDateTime); // Create timer to update StatusBarItem. DispatcherTimer tmr = new DispatcherTimer(); tmr.Interval = TimeSpan.FromSeconds(1); tmr.Tick += TimerOnTick; tmr.Start(); } void TimerOnTick(object sender, EventArgs args) { DateTime dt = DateTime.Now; itemDateTime.Content = dt .ToLongDateString() + " " + dt .ToLongTimeString(); } } }



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.




Applications = Code + Markup. A Guide to the Microsoft Windows Presentation Foundation
Applications = Code + Markup: A Guide to the Microsoft Windows Presentation Foundation (Pro - Developer)
ISBN: 0735619573
EAN: 2147483647
Year: 2006
Pages: 72

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net