Chapter 4. Buttons and Other Controls

In the Windows Presentation Foundation, the term control is somewhat more specialized than in earlier Windows programming interfaces. In Windows Forms, for example, everything that appears on the screen is considered a control of some sort. In the WPF, the term is reserved for elements that are user interactive, which means that they generally provide some kind of feedback to the user when they are prodded with the mouse or triggered by a keystroke. The TextBlock, Image, and Shape elements discussed in Chapter 3 all receive keyboard, mouse, and stylus input, but they choose to ignore it. Controls actively monitor and process user input.

The Control class descends directly from FrameworkElement:

Object

       DispatcherObject (abstract)

                DependencyObject

                           Visual (abstract)

                                     UIElement

                                            FrameworkElement

                                                      Control

Window derives from Control by way of ContentControl, so you've already seen some of the properties that Control adds to FrameworkElement. Properties defined by Control include Background, Foreground, BorderBrush, BorderThickness, and font-related properties, such as FontWeight and FontStretch. (Although TextBlock also has a bunch of font properties, TextBlock does not derive from Control. TextBlock defines those properties itself.)

From Control descend more than 50 other classes, providing programmers with favorites such as buttons, list boxes, scroll bars, edit fields, menus, and toolbars. The classes implementing these controls can all be found in the System.Windows.Controls and System.Windows.Controls.Primitives namespaces, along with other classes that do not derive from Control.

The archetypal control is the button, represented in the WPF by the Button class. The Button class has a property named Content and an event named Click that is triggered when the user presses the button with the mouse or keyboard.

The following program creates a Button object and installs a handler for the Click event to display a message box in response to button clicks.

ClickTheButton.cs

[View full width]//----------------------------------------------- // ClickTheButton.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.ClickTheButton { public class ClickTheButton : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new ClickTheButton()); } public ClickTheButton() { Title = "Click the Button"; Button btn = new Button(); btn.Content = "_Click me, please!"; btn.Click += ButtonOnClick; Content = btn; } void ButtonOnClick(object sender, RoutedEventArgs args) { MessageBox.Show("The button has been clicked and all is well.", Title); } } }


The Content property of the Button object is assigned a text string, and the Button object itself is set to the Content property of the Window. Don't worry about the exact order of these statements: You can set button properties and attach the event handler either before or after the button is made part of the window.

That fact that Button has a Content property just like Window is no coincidence. Both of these classes (and a few more) derive from ContentControl, and that's where the Content property is defined. Here's a partial class hierarchy starting with Control:

Control

       ContentControl

                 ButtonBase (abstract)

                                   Button

                       Window

That both Window and ButtonBase have the same Content property has a profound implication: All the varieties of objects that you can use as the content of a window can also be used as the content of a button. A button can display a bitmap, a Shape object, and formatted text. In fact, you can even set the Content property of one Button object to another Button object.

By now, you may not be surprised to see the button occupying the entire interior of the window's client area, and to see the button change its size accordingly as you change the size of the window.

The button initially does not have input focus, which means that it doesn't receive keystrokes typed from the keyboard. If you run the program and press the space bar, nothing will happen. However, even if the button does not have input focus, you can trigger the button by pressing Alt+C. If you watch closely, you'll even see the first letter of "Click me, please!" become underlined when you press the Alt key. Such a keyboard shortcut is provided when you precede a letter in the text string that you set to the Content property with an underline character. This feature is used more often for menu items than for buttons, but when used with buttons and other controls, it can give dialog boxes a more extensive keyboard interface.

You can give the button input focus by pressing the Tab key or clicking the button with the left mouse button. You'll notice a dotted line within the button border that indicates it has input focus. Thereafter, you can trigger the button by pressing the space bar or the Enter key.

You can programmatically give the button input focus by including the following statement in the window class's constructor:

btn.Focus();

Even if the button does not have input focus, it can respond to presses of the Enter key if you make the button a "default" button:

btn.IsDefault = true;

Or, you can make it respond to the Escape key by virtue of this property:

btn.IsCancel = true;

These two properties are generally used for the OK and Cancel buttons in dialog boxes. You can set both properties for the same buttona technique commonly used in "About" dialogs that contain only a single OK button.

Look closely and you'll notice that the border around the button changes a bit when the mouse passes over the button. The button also changes its background color when you click it. This feedback is part of what distinguishes controls from other elements.

ButtonBase defines a property named ClickMode that you can set to a member of the ClickMode enumeration to govern how the button responds to mouse clicks. The default is ClickMode.Release, which means that the Click event is not fired until the mouse button is released. You can alternatively set the property to ClickMode.Press or ClickMode.Hover; the latter fires the event when the mouse cursor is simply passed over the button.

I know I've been ignoring an elephant in the room, which is the enormousness of the button as it fills the window's client area. As you discovered in the previous chapter, FrameworkElement defines a property named Margin, which you can use to give an element a little space around its exterior:

btn.Margin = new Thickness(96);

Insert that statement, recompile, and now the button is surrounded by an inch of space on all sides. The button is still pretty big, of course, but let's take advantage of that by altering the button internals somewhat. The content of the buttonthe text stringis currently hovering in the center. We can instead put it down in the lower-left corner with the following two statements:

btn.HorizontalContentAlignment = HorizontalAlignment.Left; btn.VerticalContentAlignment = VerticalAlignment.Bottom;

In the previous chapter, I discussed the HorizontalAlignment and VerticalAlignment properties. Look closely and you'll see that the properties in these two lines of code have the word Content in the middle, and because of this, they refer to what's going on inside the button. By now you know that HorizontalAlignment is an enumeration with four members: Center, Left, Right, and Stretch, and that VerticalAlignment is also an enumeration with the four fields: Center, Top, Bottom, and Stretch. (The use of the Stretch members in this context have the same effect as Left and Top.)

It is also possible to add a little padding to the inside of the button. You use the same Thickness structure as the Margin property:

btn.Padding = new Thickness(96);

Now there's about an inch of space between the interior of the button and the button's content.

The Margin property (defined by FrameworkElement) affects the exterior of the button; the Padding property (defined by Control) affects the interior of the button.

Just as the HorizontalContentAlignment and VerticalContentAlignment properties defined by Control affect the positioning of the button's content within the button, the HorizontalAlignment and VerticalAlignment properties affect the positioning of the button within its container (the client area). For a button, these two properties are set to HorizontalAlignment.Stretch and VerticalAlignment.Stretch by default, which is why the button is stretched to fill the client area of the window. Let's set both values to Center:

btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center;

Now the button's size is affected solely by the content of the button, including the padding. Although it's not immediately obvious, the Margin is still respected as well. This is more evident if you make the margin different on the four sides:

btn.Margin = new Thickness(96, 192, 192, 96);

If you set the HorizontalAlignment and VerticalAlignment to something other than Center, you also see how the Margin property is observed:

btn.HorizontalAlignment = HorizontalAlignment.Left; btn.VerticalAlignment = VerticalAlignment.Top;

With a margin of 0, the button would sit in the corner of the client area. With a larger margin, the button is displaced from the corner. The margin plays such an important role in positioning the button that if you make the window smaller than what the margin requires, the button itself will start to disappear!

Now let's remove all the experimental code except for two statements that set HorizontalAlignment and VerticalAlignment to Center:

btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center;

The button should now be properly sized to its content and sitting in the center of the window. The button will accommodate itself to the size of its content, even if the size of that content changes after the button has been displayed. You can also put in line breaks. And if you really need to, you can give the button an explicit size:

btn.Width = 96; btn.Height = 96;

Of course, the less explicitly you set the sizes of controls, the happier you'll be. Remember that WPF will try to size the content to fit the individual user's screen resolution. Setting sizes of controls to specific values countermands this.

If you eliminate all the code you've added, there's still another approach to making the button a proper size. This is by setting the SizeToContent property of the window:

SizeToContent = SizeToContent.WidthAndHeight;

Now the window is sized to fit a normal-sized button. Any Margin set on the Button will be honored.

By default, the font that appears in a button is Tahoma with an em size of 11. (That's 8? points.) You can change the em size easily, as well as the font family:

btn.FontSize = 48; btn.FontFamily = new FontFamily("Times New Roman");

You can alternatively set the FontSize and FontFamily of the Window and the button will inherit those values. (But they won't take precedence over values set specifically for the Button.) And, of course, you can change the colors:

btn.Background = Brushes.AliceBlue; btn.Foreground = Brushes.DarkSalmon; btn.BorderBrush = Brushes.Magenta;

The BorderThickness property has no effect on buttons. Changing the button background is not altogether satisfactory because the button no longer provides feedback when it's being clicked.

On the other hand, you might want to give the user additional feedback, perhaps when the mouse enters the airspace above the button. Here's a program that uses a TextBlock to display formatted text in the button, but changes the color in response to MouseEnter and MouseLeave events:

FormatTheButton.cs

[View full width]//------------------------------------------------ // FormatTheButton.cs (c) 2006 by Charles Petzold //------------------------------------------------ using System; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; namespace Petzold.FormatTheButton { public class FormatTheButton : Window { Run runButton; [STAThread] public static void Main() { Application app = new Application(); app.Run(new FormatTheButton()); } public FormatTheButton() { Title = "Format the Button"; // Create the Button and set as window content. Button btn = new Button(); btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; btn.MouseEnter += ButtonOnMouseEnter; btn.MouseLeave += ButtonOnMouseLeave; Content = btn; // Create the TextBlock and set as button content. TextBlock txtblk = new TextBlock(); txtblk.FontSize = 24; txtblk.TextAlignment = TextAlignment .Center; btn.Content = txtblk; // Add formatted text to the TextBlock. txtblk.Inlines.Add(new Italic(new Run ("Click"))); txtblk.Inlines.Add(" the "); txtblk.Inlines.Add(runButton = new Run ("button")); txtblk.Inlines.Add(new LineBreak()); txtblk.Inlines.Add("to launch the "); txtblk.Inlines.Add(new Bold(new Run ("rocket"))); } void ButtonOnMouseEnter(object sender, MouseEventArgs args) { runButton.Foreground = Brushes.Red; } void ButtonOnMouseLeave(object sender, MouseEventArgs args) { runButton.Foreground = SystemColors .ControlTextBrush; } } }


Notice that the Run object containing the word button is stored as a field named runButton. When the mouse cursor enters the button, that Run object's Foreground property is colored red. When the cursor leaves the button, the color is restored to the default control text color obtained from SystemColors.ControlTextBrush.

The next program displays a button with an image instead of text, and rather than load the image over the Internet or rely on an image in a file, the image has actually been embedded into the executable file as a resource. This word resource is used in a couple different ways in the Windows Presentation Foundation. The type of resource I'll show you here can be differentiated as an assembly resource. The resource is a file (generally a binary file) that is made part of the project in Visual Studio and becomes part of an executable or a dynamic-link library.

If you have a bitmap that you'd like to make part of a Visual Studio project, right-click the project name in the Solution Explorer and select Add Existing Item. (Or, select Add Existing Item from the Project menu in Visual Studio.) In the dialog box, navigate to the item and click the Add button. The file should then show up in the list of project files. Right-click the item and select Properties. Make sure that the Build Action is set for Resource.

In your program, you can gain access to that file using the Uri constructor, where filename is the name of the file:

Uri uri = new Uri("pack://application:,,/filename");

Notice the two commas and the forward slash in front of the file name.

If you have a bunch of image files in your project, you may want to keep them in a separate directory of the project. You would first right-click the project name in the Solution Explorer and select Add New Folder. (Or, select New Folder from the Project menu in Visual Studio.) Give the folder a name like Images. Then you can right-click that folder name to add the image files to that folder. When you need to load the images into your program, use the following syntax:

Uri uri = new Uri("pack://application:,,/Images/filename");

Notice that there's a forward slash in front of the directory name.

Here's a project that includes a file named munch.png of the famous screaming man from Edvard Munch's etching The Scream. Perhaps you could use this image on a button you really don't want the user to press.

ImageTheButton.cs

[View full width]//----------------------------------------------- // ImageTheButton.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.ImageTheButton { public class ImageTheButton : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new ImageTheButton()); } public ImageTheButton() { Title = "Image the Button"; Uri uri = new Uri("pack://application: ,,/munch.png"); BitmapImage bitmap = new BitmapImage(uri); Image img = new Image(); img.Source = bitmap; img.Stretch = Stretch.None; Button btn = new Button(); btn.Content = img; btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; Content = btn; } } }


If you have an image file that has some transparency, you might want to try using that one in this program: You'll see the normal Button background through the transparent parts.

Now, wouldn't it be nice to have both an image and text in a button? Yes, it certainly would. But we're stuck with the same problem we had in putting multiple objects in a window: The Content property can be set to only one object. I'm sure this is annoying, but it won't be long before a solution will be at hand.

Installing a handler for the button's Click event is certainly the most traditional way for a program to be notified when the user clicks a button. However, it could be that a particular operation in your program can be triggered through several sourcesperhaps a menu item, a toolbar item, and a button. Perhaps you even implement a scripting language that can trigger this same operation. It might make more sense to route all these controls through a single point.

This is the idea behind a property named Command, which is defined by the ButtonBase class and some other classes, including MenuItem. For common commands, you set the Command property of the Button object equal to a static property of the classes ApplicationCommands, ComponentCommands, MediaCommands, NavigationCommands, or EditingCommands. The first four of these classes are located in the System.Windows.Input namespace; the last is in System.Windows.Documents. The static properties of these classes are all of type RoutedUICommand. You can also create your own objects of type RoutedUICommand (as Chapter 14 demonstrates).

For example, if you wanted a particular button to perform a clipboard Paste command, you would set the Command property like so:

btn.Command = ApplicationCommands.Paste;

It's not required, but you may also want to set the button text to the standard text for that command:

btn.Content = ApplicationCommands.Paste.Text;

The other step is to associate this particular Paste command with event handlers. This association is called a binding, and it's one of several types of bindings you'll find in the Windows Presentation Foundation. The UIElement class defines (and the Control and Window classes inherit) a property named CommandBindings, which is a collection of CommandBinding objects. You'll probably use the CommandBindings collection of the window, so the statement looks something like this:

CommandBindings.Add(new CommandBinding(ApplicationCommands.Paste, PasteOnExecute, PasteCanExecute));

PasteOnExecute and PasteCanExecute are two event handlers that are located in your program. (You can name them whatever you want.) In the PasteOnExecute handler, you carry out the Paste command. Sometimes, however, your program can't carry out a Paste command because the clipboard doesn't contain data in the format you want. That's the purpose of the PasteCanExecute handler. This handler checks the clipboard for data of the proper format. If the data isn't available, the handler can set a flag and the Button will automatically be disabled.

If this same program also has a menu item for Paste, you just need to assign the Command property of the menu item to ApplicationCommands.Paste, and the same binding will apply.

Here's a simple program that demonstrates command bindings:

CommandTheButton.cs

[View full width]//------------------------------------------------- // CommandTheButton.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.CommandTheButton { public class CommandTheButton : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new CommandTheButton()); } public CommandTheButton() { Title = "Command the Button"; // Create the Button and set as window content. Button btn = new Button(); btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; btn.Command = ApplicationCommands.Paste; btn.Content = ApplicationCommands .Paste.Text; Content = btn; // Bind the command to the event handlers. CommandBindings.Add(new CommandBinding (ApplicationCommands.Paste, PasteOnExecute, PasteCanExecute)); } void PasteOnExecute(object sender, ExecutedRoutedEventArgs args) { Title = Clipboard.GetText(); } void PasteCanExecute(object sender, CanExecuteRoutedEventArgs args) { args.CanExecute = Clipboard .ContainsText(); } protected override void OnMouseDown (MouseButtonEventArgs args) { base.OnMouseDown(args); Title = "Command the Button"; } } }


The constructor of the window assigns ApplicationCommands.Paste to the Command property of the Button object. The constructor concludes by creating a CommandBinding object that associates ApplicationCommands.Paste with the event handlers PasteOnExecute and PasteCanExecute. This CommandBinding object is made a part of the window's CommandBindings collection.

The two event handlers PasteOnExecute and PasteCanExecute use static methods from the Clipboard class (located in System.Windows). The static GetText and ContainsText methods both exist in overloaded versions that allow you to be specific about the type of text you wantincluding comma-separated format, HTML, RTF, Unicode, or XAMLbut this program doesn't use those text types. The text it gets from the clipboard is just used to set the window's Title property. Since you may want to experiment with this program a bit, the last method in the program lets you click anywhere in the window to restore the Title text.

Before running the program, you might want to copy some text to the clipboard from Notepad or another text-based application (even Visual Studio) just so the program has something to work with. You'll discover that the button responds not only to the normal keyboard and mouse input, but also to Ctrl+V, the standard keyboard shortcut for Paste. That's one of the benefits of using a command binding rather than just handling the Click event.

Now copy a bitmap into the clipboard. No need to run Paint or PhotoShop! Just press the Print Screen key to copy the screen image into the clipboard. Whenever there's a change in the clipboard, any bound can-execute event handlers (such as PasteCanExecute) are called so that the handler has the opportunity to properly set the CanExecute property of the event arguments. The button is immediately disabled. Now copy some text into the clipboard. The button is enabled again. Behind the scenes, the command binding changes the IsEnabled property that Button inherits from UIElement.

While the most common buttons carry out commands either through Click event handlers or command bindings, other types of buttons indicate options. A CheckBox control consists of a square box generally followed by some text. The box can be checked or unchecked to indicate a particular program option. For example, a font-selection dialog might have CheckBox controls for Bold, Italic, and Underline.

RadioButton controls are generally used to display a group of mutually exclusive items. (The term comes from the buttons commonly found on car radios for selecting preset stations.)

Both CheckBox and RadioButton are considered types of "toggle" buttons, as is clear from a complete view of the classes that descend from ButtonBase:

Control

       ContentControl

                  ButtonBase (abstract)

                             Button

                             GridViewColumnHeader

                             RepeatButton

                             ToggleButton

                                         CheckBox

                                         RadioButton

ToggleButton is not abstract, so you can also create an object of type ToggleButton, which looks just like a regular button except that it toggles on and off. The ToggleButton in the following program changes the ResizeMode property of the window.

ToggleTheButton.cs

[View full width]//------------------------------------------------ // ToggleTheButton.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; namespace Petzold.ToggleTheButton { public class ToggleTheButton : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new ToggleTheButton()); } public ToggleTheButton() { Title = "Toggle the Button"; ToggleButton btn = new ToggleButton(); btn.Content = "Can _Resize"; btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; btn.IsChecked = (ResizeMode == ResizeMode.CanResize); btn.Checked += ButtonOnChecked; btn.Unchecked += ButtonOnChecked; Content = btn; } void ButtonOnChecked(object sender, RoutedEventArgs args) { ToggleButton btn = sender as ToggleButton; ResizeMode = (bool)btn.IsChecked ? ResizeMode .CanResize : ResizeMode.NoResize; } } }


ToggleTheButton uses the button to switch the ResizeMode property of the window between ResizeMode.CanResize and ResizeMode.NoResize. The button automatically toggles itself on and off as the user clicks it. The IsChecked property indicates the current setting. Notice that the program initializes IsChecked to true if ResizeMode is initially ResizeMode.CanResize and false otherwise. Two events named Checked and Unchecked are triggered as the button is checked and unchecked. You can attach the same event handler to both events and simply use the IsChecked property to distinguish them, which is exactly what ToggleTheButton does.

Notice that the IsChecked property must be cast to a bool before being used in the C# conditional operator. The IsChecked property is actually a nullable bool, and can take on values of true, false, and null. If you set the IsThreeState property of ToggleButton to true, you can set IsChecked to null to display a third, "indeterminate" state of the button. For example, an Italic button in a word processing program might use this indeterminate state if the currently selected text has a mix of both italic and non-italic characters. (However, if the currently selected text is in a font that doesn't allow italics, the program would disable the Italic button by setting its IsEnabled property to false.)

The CheckBox class adds almost nothing to ToggleButton. You can create a CheckBox in this program just by replacing the button creation code with this:

CheckBox btn = new CheckBox();

You don't need to change the event handler. The program works the same except that the button looks different. You could also change ToggleTheButton's button to a RadioButton, but the program wouldn't work properly. You cannot uncheck a radio button by manually clicking it.

Because a ToggleButton or a CheckBox essentially represents the value of a Boolean, it makes sense that a particular toggle button be associated with a Boolean property of some object. This is called data binding, and it is such an important part of the Windows Presentation Foundation that Chapter 23 is devoted to the topic.

A ToggleButton used with data binding doesn't need to have its IsChecked property initialized, or have handlers installed for the Checked and Unchecked events. What's required instead is a call to the SetBinding method, which the button inherits from the FrameworkElement class. For a ToggleButton or CheckBox object, the call is typically something like this:

btn.SetBinding(ToggleButton.IsCheckedProperty, "SomeProperty");

where the second argument is a string with the name of a property you want associated with the IsChecked state of the button. You specify the object that "SomeProperty" belongs to with the DataContext property of Button.

The first argument to the SetBinding method probably looks even stranger than the second argument. It refers not to the IsChecked property of a ToggleButton object, but instead to something called IsCheckedProperty that appears to be a static member of the ToggleButton class.

ToggleButton does indeed define a static field named IsCheckedProperty that is an object of type DependencyProperty. Chapter 8 is devoted in its entirety to dependency properties. For now, think of it just as a convenient way to refer to a particular property defined by ToggleButton.

So, to what Boolean property do we want to bind the ToggleButton? One convenient Boolean is the Topmost property of the window. As you'll recall, if Topmost is set to true, the window always appears in the foreground of other windows. The SetBinding call looks like this:

btn.SetBinding(ToggleButton.IsCheckedProperty, "Topmost");

The data binding won't try to guess which object in the program is the one whose Topmost property you're referring to, so you need to specify that object by setting the DataContext property of the button to the window object:

btn.DataContext = this;

Here's the complete program:

BindTheButton.cs

[View full width]//---------------------------------------------- // BindTheButton.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; namespace Petzold.BindTheButton { public class BindTheButton : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new BindTheButton()); } public BindTheButton() { Title = "Bind theButton"; ToggleButton btn = new ToggleButton(); btn.Content = "Make _Topmost"; btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; btn.SetBinding(ToggleButton .IsCheckedProperty, "Topmost"); btn.DataContext = this; Content = btn; ToolTip tip = new ToolTip(); tip.Content = "Toggle the button on to make " + "the window topmost on the desktop"; btn.ToolTip = tip; } } }


When you run this program, the button will be automatically initialized to the current state of the Topmost property of the window. As you click the button on and off, that property will be changed.

The System.Windows.Data namespace includes a class named Binding that you can alternatively use to create and set a data binding. This code can replace the statements that call SetBinding and set the DataContext property in the BindTheButton program:

Binding bind = new Binding("Topmost"); bind.Source = this; btn.SetBinding(ToggleButton.IsCheckedProperty, bind);

The Binding class gives you more options in defining the data binding, as you'll discover in Chapter 23.

The BindTheButton program also includes a ToolTip object that is set to the ToolTip property of the Button. The text appears when the mouse cursor pauses over the button. The ToolTip property is defined by FrameworkElement, so you can attach a tooltip to other elements as well as controls. Like ButtonBase and Windows, ToolTip derives from ContentControl, so you can use formatted text in the ToolTip, and even pictures if you really want to mess with the heads of your users.

If you'd like to see a ToolTip with an image, go back to the ImageTheButton program and insert the following four lines at the end of the constructor:

btn.Content = "Don't Click Me!"; ToolTip tip = new ToolTip(); tip.Content = img; btn.ToolTip = tip;

The Button gets some text to display, but the ToolTip gets The Scream.

Another simple control that derives from ContentControl is Label. Traditionally the Label control has been the standard way to display short text strings in forms and dialog boxes. For example, here's a label that might appear in a File Open dialog:

Label lbl = new Label(); lbl.Content = "File _name:";

With versatile elements such as TextBlock, the Label control would seem to be obsolete, but it's really not, and the key to its longevity is the ability to use underscores in text that you assign to the Content property. In a File Open dialog box, pressing Alt+N shifts the input focus not to the labelbecause labels are commonly non-focusablebut to the control after the label, which is probably a TextBox control that lets the user enter and edit text. Thus, labels continue to maintain their role as assistants in keyboard navigation.

Speaking of the TextBox control, the Windows Presentation Foundation supports the two we've come to expect:

Control

       TextBoxBase (abstract)

                  TextBox

                  RichTextBox

Although the TextBox has content, it is not considered a ContentControl because the content is always text. Instead it has a Text property that lets you set an initial string or obtain the string that the user typed.

Because TextBox derives from Control, you can specify the background and foreground brushes used in the control, as well as the font. However, at any time, all the text that appears in a particular TextBox will always be the same color and use the same font. The RichTextBox control lets the program (and the user) apply various character and paragraph formatting to different parts of the text. The difference between TextBox and RichTextBox is the same as the difference between Windows Notepad and Windows WordPad.

By default, a TextBox control allows entry of a single line of text. Here is the first dialog box to appear in this book. It contains a single TextBox control.

UriDialog.cs

[View full width]//------------------------------------------ // UriDialog.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.NavigateTheWeb { class UriDialog : Window { TextBox txtbox; public UriDialog() { Title = "Enter a URI"; ShowInTaskbar = false; SizeToContent = SizeToContent .WidthAndHeight; WindowStyle = WindowStyle.ToolWindow; WindowStartupLocation = WindowStartupLocation.CenterOwner; txtbox = new TextBox(); txtbox.Margin = new Thickness(48); Content = txtbox; txtbox.Focus(); } public string Text { set { txtbox.Text = value; txtbox.SelectionStart = txtbox .Text.Length; } get { return txtbox.Text; } } protected override void OnKeyDown (KeyEventArgs args) { if (args.Key == Key.Enter) Close(); } } }


This UriDialog class inherits from Window but it doesn't include a Main method (that method will be in the class for the main window of this program) and it sets several properties at the top of the constructor. Dialog boxes shouldn't appear in the taskbar; they usually are sized to their content, and they often get a WindowStyle of ToolWindow. (The ToolWindow style allows a dialog to be resizable but not to have minimize or maximize buttons.) The WindowStartupLocation property specifies that the dialog is to be positioned in the center of its owner (a window we haven't met yet).

The Title says "Enter a URI" and a TextBox sits in the middle of the client area, ready for the user to type something in. The class also has a public property named Text. Dialog box classes almost always contain public properties. That's how a program interacts with the dialog. This Text property simply provides public access to the Text property of the TextBox control. Notice also that the set accessor uses the SelectionStart property of the TextBox to position the cursor at the end of whatever text is set. The user can close the dialog by clicking the Close icon or pressing the Enter key, a feature that the OnKeyDown override provides.

Here's the program that uses the UriDialog. This program sets the Content property of its window to an object of type Frame. Frame also inherits from ContentControl, but this program sets the Source property of Frame rather than the Content property.

NavigateTheWeb.cs

[View full width]//----------------------------------------------- // NavigateTheWeb.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.NavigateTheWeb { public class NavigateTheWeb : Window { Frame frm; [STAThread] public static void Main() { Application app = new Application(); app.Run(new NavigateTheWeb()); } public NavigateTheWeb() { Title = "Navigate the Web"; frm = new Frame(); Content = frm; Loaded += OnWindowLoaded; } void OnWindowLoaded(object sender, RoutedEventArgs args) { UriDialog dlg = new UriDialog(); dlg.Owner = this; dlg.Text = "http://"; dlg.ShowDialog(); try { frm.Source = new Uri(dlg.Text); } catch (Exception exc) { MessageBox.Show(exc.Message, Title); } } } }


The program sets an event handler for the Loaded event. By the time OnWindowLoaded is called, the main window is already displayed on the screen. That's when the window creates an object of type UriDialog, assigns its Owner, and assigns the Text property the string "http://". The call to ShowDialog displays the modal dialog and doesn't return until the user closes the dialog. With any luck, the user will type a valid URI of a Web site. Notice that the dialog box expands in width if the URI is too long to fit in the text box. The TextBox expands to fit the text, and the Window follows.

When ShowDialog returns, the program sets the Source property of the Frame control and the result is an instant Web browser! Of course, you don't have a row of navigation buttons, but the Backspace key functions as the browser's Back button, and you have a few other options (including printing) by right-clicking the Web page.

The UriDialog box is able to close itself when the user presses the Enter key because by default the TextBox isn't interested in the Enter key. If you set

txtbox.AcceptsReturn = true;

the single-line edit control becomes a multiline edit control and now handles carriage returns. Here's a program that contains a TextBox control that fills the window's client area. Perhaps it's the beginning of a Notepad clone. But rather than use a File menu for loading and saving files, this program uses a file with a fixed name and location. The program could be used for taking random notes and preserving them between Windows sessions.

EditSomeText.cs

[View full width]//--------------------------------------------- // EditSomeText.cs (c) 2006 by Charles Petzold //--------------------------------------------- using System; using System.ComponentModel; // for CancelEventArgs using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.EditSomeText { class EditSomeText : Window { static string strFileName = Path.Combine( Environment.GetFolderPath( Environment.SpecialFolder .LocalApplicationData), "Petzold\\EditSomeText\ \EditSomeText.txt"); TextBox txtbox; [STAThread] public static void Main() { Application app = new Application(); app.Run(new EditSomeText()); } public EditSomeText() { Title = "Edit Some Text"; // Create the text box. txtbox = new TextBox(); txtbox.AcceptsReturn = true; txtbox.TextWrapping = TextWrapping.Wrap; txtbox.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; txtbox.KeyDown += TextBoxOnKeyDown; Content = txtbox; // Load the text file. try { txtbox.Text = File.ReadAllText (strFileName); } catch { } // Set the text box caret and input focus. txtbox.CaretIndex = txtbox.Text.Length; txtbox.Focus(); } protected override void OnClosing (CancelEventArgs args) { try { Directory.CreateDirectory(Path .GetDirectoryName(strFileName)); File.WriteAllText(strFileName, txtbox.Text); } catch (Exception exc) { MessageBoxResult result = MessageBox.Show("File could not be saved: " + exc.Message + "\nClose program anyway?", Title, MessageBoxButton.YesNo, MessageBoxImage.Exclamation); args.Cancel = (result == MessageBoxResult.No); } } void TextBoxOnKeyDown(object sender, KeyEventArgs args) { if (args.Key == Key.F5) { txtbox.SelectedText = DateTime.Now .ToString(); txtbox.CaretIndex = txtbox .SelectionStart + txtbox.SelectionLength; } } } }


The program defines a location and name for the file in the area known as local isolated storage. The static File.ReadAllText method attempts to load the file in the window's constructor, and the static File.WriteAllText method saves the file. The program also installs an event handler for the KeyDown event so that it can insert the date and time into the file when the user presses F5.

The RichTextBox is much more complex than the TextBox because it stores text that could have a variety of different character and paragraph formatting. If you're familiar with previous Windows controls similar to RichTextBox, you might naturally assume that the control stores text in the Rich Text Format (RTF), a file format that dates from the mid-1980s and which was intended to provide an interchange format among word processing programs. The WPF version of RichTextBox certainly supports RTF (and plain text as well) but it also supports an XML-based file format that is a subset of the Extensible Application Markup Language (XAML) supported by WPF.

The following program uses the standard File Open and File Save dialog boxes that can be found in the Microsoft.Win32 namespace. The program doesn't have a menu, so the dialog boxes are invoked when you press Ctrl+O and Ctrl+S. This feature presented a problem: The RichTextBox likes to see all the keystrokes. If you attach a handler to the control's KeyDown event, you won't get anything.

The solution is a technique I'll discuss more in Chapter 9. Keyboard, mouse, and stylus events actually occur first in the window, in the form of preview events. So, to examine keystrokes before they get to the RichTextBox, the following program overrides the window's OnPreviewKeyDown method. When the override encounters a Ctrl+O or Ctrl+S, it displays the proper dialog box and sets the Handled property of the event arguments to true, indicating that the particular event has been taken care of, and the keystroke should not go to the RichTextBox.

Because there are no menus or toolbars in this program, text formatting is limited to the built-in commands Ctrl+I, Ctrl+U, and Ctrl+B for italics, underline, and bold.

EditSomeRichText.cs

[View full width]//------------------------------------------------- // EditSomeRichText.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; namespace Petzold.EditSomeRichText { public class EditSomeRichText : Window { RichTextBox txtbox; string strFilter = "Document Files(*.xaml)|*.xaml|All files (*.*)|*.*"; [STAThread] public static void Main() { Application app = new Application(); app.Run(new EditSomeRichText()); } public EditSomeRichText() { Title = "Edit Some Rich Text"; txtbox = new RichTextBox(); txtbox.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; Content = txtbox; txtbox.Focus(); } protected override void OnPreviewTextInput (TextCompositionEventArgs args) { if (args.ControlText.Length > 0 && args.ControlText[0] == '\x0F') { 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); Stream strm = null; try { strm = new FileStream(dlg .FileName, FileMode.Open); range.Load(strm, DataFormats.Xaml); } catch (Exception exc) { MessageBox.Show(exc .Message, Title); } finally { if (strm != null) strm.Close(); } } args.Handled = true; } if (args.ControlText.Length > 0 && args.ControlText[0] == '\x13') { SaveFileDialog dlg = new SaveFileDialog(); dlg.Filter = strFilter; if ((bool)dlg.ShowDialog(this)) { FlowDocument flow = txtbox .Document; TextRange range = new TextRange(flow.ContentStart, flow.ContentEnd); Stream strm = null; try { strm = new FileStream(dlg .FileName, FileMode.Create); range.Save(strm, DataFormats.Xaml); } catch (Exception exc) { MessageBox.Show(exc .Message, Title); } finally { if (strm != null) strm.Close(); } } args.Handled = true; } base.OnPreviewTextInput(args); } } }


Formatted text gets into and out of the RichTextBox through the Document property. This Document property is of type FlowDocument, and the following code creates a TextRange object that encompasses the entire document:

TextRange range = new TextRange(flow.ContentStart, flow.ContentEnd);

The TextRange class defines two methods named Load and Save. The first argument is a Stream; the second is a string describing the desired data format. For this second argument it's easiest to get those strings from static members of the DataFormats class. With the RichTextBox, you can use DataFormats.Text, DataFormats.Rtf, DataFormats.Xaml, and DataFormats.XamlPackage, which is actually a ZIP file that contains binary resources the document might require. This program is hard-coded to use DataFormats.Xaml. (The program can't load any arbitrary XAML file, however.) You may want to look at these files to get a glimpse of the format of documents under the Windows Presentation Foundation.

I'm sure there are many other marvelous and useful programs we could write with a window that hosts just a single object, but I can't think of any more. And that is why we really need to figure out how to put multiple controls in a window.


Previous Page Next Page

новости в казахстане