WPF makes it relatively easy for the average software developer to create rich and exciting user interfaces that integrate print-quality text, 2D and 3D graphics, animation, and multimedia content. However, for the foreseeable future, most business applications will still require a user interface constructed predominantly from the everyday Windows-style controls that users have become familiar with over the past 15 years.
For those readers coming from a Windows Forms background, it will help to be able to know which WPF control
is equivalent to each of the familiar Windows Forms controls. The .NET Framework documentation summarizes the equivalent controls at http://msdn2.microsoft.com/en-us/library/ms750559.aspx
.
WPF provides a rich set of highly functional controls in the System.Windows.Controls
namespace that meet the everyday needs of the business application developer. The recipes in this chapter focus on how to use these standard controls and describe how to do the following:
Display text (recipes 3-1 and 3-2)
Display static images (recipe 3-3)
Handle text input by a user (recipes 3-4, 3-5, and 3-6)
Validate and spell check text input by a user (recipes 3-7 and 3-8)
Handle and generate button clicks and set a default button for a form (recipes 3-9, 3-10, and 3-11)
Provide quick keyboard access to text buttons and boxes (recipes 3-12 and 3-13)
Get user input from a slider (recipe 3-14)
Display a context menu on a control (recipe 3-15)
Display and control the display properties of tool tips (recipes 3-16, 3-17, and 3-18)
Display and allow the user to select items from a set of radio buttons (recipe 3-19)
Display and allow the user to select items from a set of check boxes (recipe 3-20)
Display and allow the user to select items from a hierarchical tree (recipe 3-21)
Display and allow the user to select items from a list (recipe 3-22)
Change the content of a list dynamically (recipe 3-23)
Display and allow the user to select items from a combo box (recipe 3-24)
Display any control rotated from its default orientation (recipe 3-25)
This chapter focuses on how to use the standard capabilities of the WPF controls found in the System.Windows.Controls
namespace. All of these controls are highly customizable in terms of behavior and appearance; these advanced features will be discussed in Chapter 5 and Chapter 6.
You need to display text surrounded by braces (curly brackets) as the content of a control, as in {surrounded by braces}. Curly braces are used to denote markup extensions in XAML and are not normally valid in control content.
Place the content within the XAML element (where braces are permitted), or if you must use a content attribute, prefix the text with a pair of braces ({}).
In XAML, braces identify the use of markup extension syntax within XAML control attributes. WPF will attempt to process any such string with a markup extension handler. On the occasions you need to surround the text within a control's content with braces, you must signal to WPF that the content is not markup extension syntax. You do this by prefixing the text with a pair of braces.
The following XAML demonstrates a Button
, RadioButton
, TextBox
, and TextBlock
(all from the System.Windows.Controls
namespace) that display content surrounded by braces (see Figure 3-1):
<Window x:Class="Recipe_03_01.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 3_01" Height="200" Width="250"> <StackPanel> <Button Content="{}{A Button}" Margin="10" Name="button1" Width="75" /> <RadioButton Content="{}{A RadioButton}" HorizontalAlignment="Center" Margin="10" /> <TextBox Text="{}{A TextBox}" HorizontalAlignment="Center" Margin="10" /> <TextBlock HorizontalAlignment="Center" Margin="10" Text="{}{A TextBlock}" /> </StackPanel> </Window>
The TextBlock
control provides a lightweight and easy-to-use way to include small amounts (typically up to one paragraph) of flow content in your UI. The properties of the TextBlock
provide extensive control over the formatting of the contained text. Table 3-1 lists some of the more commonly used properties of the TextBlock
control.
Table 3-1. Commonly Used Properties of the TextBlock
Control
Property | Description |
---|---|
| The name of the font family to apply to the text, for example, |
| The size of the text expressed as a number and an optional unit identifier. By default, the unit is assumed to be |
| The style to apply to the text; available styles include |
| The weight to apply to the text; some of the available weights are |
| The alignment of the text within the |
| One or more comma-separated decoration styles to apply to the text. Possible values are |
| The wrapping behavior of text within the |
The following XAML demonstrates how to create the formatted TextBlock
control shown in Figure 3-2:
<Window x:Class="Recipe_03_02.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 3_02" Height="100" Width="300"> <Grid> <TextBlock FontFamily="Tahoma, Arial" FontSize="20" FontStyle="Italic" FontWeight="Light" HorizontalAlignment="Center" TextAlignment="Right" TextDecorations="Underline, Strikethrough" TextWrapping="Wrap" VerticalAlignment="Center"> The quick brown fox jumped over the lazy brown dog. </TextBlock> </Grid> </Window>
Use the Sytem.Windows.Controls.Image
control, and specify the path to the image you want to display in the Image.Source
property.
The Image
control provides an easy way to display static images and supports the following image types:
.bmp
.gif
.ico
.jpg
.png
.wdp
.tiff
To control the size of the image, you can use the Width
or Height
property of the Image
control. You specify the size of the image as a number with an optional unit identifier. By default, the unit is assumed to be px
(pixels) but can also be in
(inches), cm
(centimeters), or pt
(points).
You should only ever set one, and not both, of Width
or Heigth
. WPF will determine the appropriate size for the unspecified property in order to keep the aspect ratio of the image unchanged. If you specify both properties, the image will likely appear stretched or squashed.
As a standard control, the Image
class inherits many useful features from its parent classes, making it straightforward to control things such as the position, opacity, and tool tip associated with your image.
The following XAML demonstrates how to size and place images on a form using the Image
control. It also demonstrates how easy it is to associate a tool tip (discussed in recipe 3-16) and an opacity setting with an image (see Figure 3-3).
<Window x:Class="Recipe_03_03.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 3_03" Height="300" Width="400"> <StackPanel Orientation="Horizontal"> <Image Margin="10" ToolTip="Bottom Image" Width="100" Source="ApressLogo.gif" /> <Image Margin="-30" Opacity=".7" Source="ApressLogo.gif" ToolTip="Middle Image" Width="150" /> <Image Source="ApressLogo.gif" ToolTip="Top Image" VerticalAlignment="Top" Width="150" /> </StackPanel> </Window>
The easiest control to use for getting text input from a user is the TextBox
control. The TextBox
control supports only simple text formatting, so if you need more complex formatting, you should use the System.Windows.Controls.RichTextBox
control (discussed in recipe 3-5).
Despite its limitations, the TextBox
is a highly functional control that supports features such as multiline text entry, word wrap, scrolling, text selection, text alignment, cut/copy/paste, and drag and drop. Table 3-2 summarizes some of the more commonly used members of the TextBox
control.
Table 3-2. Commonly Used Members of the TextBox
Control
Member | Summary |
---|---|
Properties | |
| Controls whether the |
| Controls whether the user can insert Tab characters in the |
| Gets or sets the current insertion position index of the |
IsReadOnly | Controls whether the |
| Gets the total number of lines in the |
| Controls the maximum number of characters that the user can type into the |
| Gets or sets the currently selected |
| Gets or sets the content of the |
| Controls the alignment of text in the |
| Controls the word wrapping behavior of text in the |
Methods | |
| Appends text to the existing content of the |
| Clears all the contents of the |
| Copies the currently selected |
| Cuts the currently selected |
| Pastes the current content of the clipboard over the currently selected |
| Selects a specified range of text in the |
Member | Summary |
| Selects the entire content of the |
| Undoes the most recent undoable action on the |
Event | |
| The event fired when the text in a |
The TextBox
is not a lightweight control, containing anywhere from 10 to 30 individual visual elements depending on its configuration. This can become a performance problem if you try to display a large number of TextBox
controls. If you only need to display and not edit text, you should use a System.Windows. Controls.TextBlock
control (see recipe 3-2).
The following XAML demonstrates how to use a TextBox
control that supports multiline input with word wrap and vertical scrolling. The buttons provide simple demonstrations of how to apply some of the functionality listed in Table 3-2.
<Window x:Class="Recipe_03_04.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 3_04" Height="300" Width="300"> <StackPanel> <TextBox AcceptsReturn="True" Height="100" IsReadOnly="True" Name="textBox1" TextAlignment="Left" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto"> Default starting text. </TextBox> <WrapPanel Margin="10"> <Button Margin="5" Name="textButton" Width="75" Click="TextButton_Click">Set Text</Button> <Button Margin="5" Name="selectAllButton" Width="75" Click="SelectAllButton_Click">Select All</Button> <Button Margin="5" Name="clearButton" Width="75" Click="ClearButton_Click">Clear</Button> <Button Margin="5" Name="prependButton" Width="75" Click="PrependButton_Click">Prepend</Button> <Button Margin="5" Name="insertButton" Width="75" Click="InsertButton_Click">Insert</Button>
<Button Margin="5" Name="appendButton" Width="75" Click="AppendButton_Click">Append</Button> <Button Margin="5" Name="cutButton" Width="75" Click="CutButton_Click">Cut</Button> <Button Margin="5" Name="pasteButton" Width="75" Click="PasteButton_Click">Paste</Button> <Button Margin="5" Name="undoButton" Width="75" Click="UndoButton_Click">Undo</Button> <WrapPanel> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <RadioButton Checked="EditableChecked" Grid.Column="0" HorizontalAlignment="Center" IsChecked="True" Margin="5" Name="editableRadioButton" > Editable</RadioButton> <RadioButton Checked="EditableChecked" Grid.Column="1" HorizontalAlignment="Center" Margin="5" Name="readonlyRadioButton"> Read Only</RadioButton> </Grid> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <RadioButton Checked="AlignmentChecked" Grid.Column="0" HorizontalAlignment="Center" IsChecked="True" Margin="5" Name="leftAlignRadioButton"> Left</RadioButton> <RadioButton Checked="AlignmentChecked" Grid.Column="1" HorizontalAlignment="Center" Margin="5" Name="centerAlignRadioButton"> Center</RadioButton> <RadioButton Checked="AlignmentChecked" Grid.Column="2" HorizontalAlignment="Center" Margin="5" Name="rightAlignRadioButton"> Right</RadioButton> </Grid> </StackPanel< </Window>
The following code-behind handles the events fired by the buttons and radio buttons:
using System.Windows; using System.Windows.Controls; namespace Recipe_03_04 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); } // Handles the checking of the Text Alignment RadioButtons. private void AlignmentChecked(object sender, RoutedEventArgs e) { RadioButton button = e.OriginalSource as RadioButton; if (e.OriginalSource == leftAlignRadioButton) { textBox1.TextAlignment = TextAlignment.Left; } else if (e.OriginalSource == centerAlignRadioButton) { textBox1.TextAlignment = TextAlignment.Center; } else if (e.OriginalSource == rightAlignRadioButton) { textBox1.TextAlignment = TextAlignment.Right; } textBox1.Focus(); } // Handles the click of the Append button. Adds text to the end // of the TextBox content. private void AppendButton_Click(object sender, RoutedEventArgs e) { textBox1.AppendText(" *** appended text ***"); }
// Handles the click of the Clear button. Clears all content from // the TextBox. private void ClearButton_Click(object sender, RoutedEventArgs e) { textBox1.Clear(); } // Handles the click of the Cut button. Cuts the currently // selected text and places it in the clipboard. private void CutButton_Click(object sender, RoutedEventArgs e) { if (textBox1.SelectionLength == 0) { MessageBox.Show("Select text to cut first.", Title); } else { MessageBox.Show("Cut: " + textBox1.SelectedText, Title); textBox1.Cut(); } } // Handles the checking of the Editable / ReadOnly RadioButtons. private void EditableChecked(object sender, RoutedEventArgs e) { RadioButton button = e.OriginalSource as RadioButton; if (e.OriginalSource == editableRadioButton) { textBox1.IsReadOnly = false; } else if (e.OriginalSource == readonlyRadioButton) { textBox1.IsReadOnly = true; } textBox1.Focus(); } // Handles the click of the Insert button. Inserts text into // the TextBox at the current cursor location.
private void InsertButton_Click(object sender, RoutedEventArgs e) { textBox1.Text = textBox1.Text.Insert( textBox1.CaretIndex, " *** inserted text *** "); } // Handles the click of the Paste button. Pastes the current // content of the clipboard into the TextBox at the current // cursor location. private void PasteButton_Click(object sender, RoutedEventArgs e) { textBox1.Paste(); } // Handles the click of the Prepend button. Adds text to the start // of the TextBox content. private void PrependButton_Click(object sender, RoutedEventArgs e) { textBox1.Text = textBox1.Text.Insert(0, "*** Prepended text *** "); } // Handles the click of the Select All button. Selects all the // content in the TextBox. private void SelectAllButton_Click(object sender, RoutedEventArgs e) { textBox1.SelectAll(); // Set the focus on the TextBox to make the selection visible. textBox1.Focus(); } // Handles the click of the Set Text Button. Sets the content // of the TextBox to a default value. private void TextButton_Click(object sender, RoutedEventArgs e) { textBox1.Text = "Replace default text with initial text value"; } // Handles the click of the Undo Button. Undoes the last undoable // event. private void UndoButton_Click(object sender, RoutedEventArgs e) { textBox1.Undo(); } } }
You need to allow the user to edit large amounts of text and give them fine-grained control over the formatting of text they enter.
The RichTextBox
is a sophisticated and highly functional control designed to allow you to display and edit System.Windows.Documents.FlowDocument
objects. The combination of the RichTextBox
and FlowDocument
objects provides the user with access to advanced document-editing capabilities that you do not get in a System.Windows.Controls.TextBox
control. These features include mixed text formatting, hyphenation, tables, lists, paragraphs, and embedded images.
To populate the content of a RichTextBox
statically, you include a FlowDocument
element as the content of the RichTextBox XAML declaration. Within the FlowDocument
element, you can define richly formatted content using elements of the flow document content model. Key structural elements of this content model include Figure
, Hyperlink
, List
, ListItem
, Paragraph
, Section
, and Table
.
To populate the RichTextBox
in code, you must work with a FlowDocument
object directly. You can either create a new FlowDocument
object or obtain one currently in a RichTextBox
through the RichTextBox.Document
property.
You manipulate the content of the FlowDocument
by selecting portions of its content using a System.Windows.Documents.TextSelection
object. The TextSelection
object contains two properties, Start
and End
, which identify the beginning and end positions of the FlowDocument
content you want to manipulate. Once you have a suitable TextSelection
object, you can manipulate its content using the TextSelection
members.
Chapter 7 contains more recipes using the RichTextBox
control and FlowDocument
object. For detailed information about flow content, see the .NET Framework documentation at http://msdn2.microsoft.com/en-us/library/ms753113.aspx
.
To simplify the manipulation of FlowDocument
objects, the RichTextBox
supports standard commands defined by the ApplicationCommands
and EditingCommands
classes from the System.Windows.Input
namespace. The RichTextBox
also supports standard key combinations to execute basic text-formatting operations such as applying bold, italic, and underline formats to text as well as cutting, copying, and pasting selected content. Table 3-3 summarizes some of the more commonly used members of the RichTextBox
control.
Table 3-3. Commonly Used Members of the RichTextBox
Control
Member | Summary |
---|---|
Properties | |
| Controls whether the user can insert Tab characters in the |
| Gets or sets the current insertion position index of the |
| Gets or sets the |
| Determines whether the |
IsReadOnly | Controls whether the |
| Gets a |
| Determines whether the |
Methods | |
| Appends text to the existing content of the |
| Copies the currently selected |
| Cuts the currently selected |
| Pastes the current content of the clipboard over the currently selected |
| Selects the entire content of the |
| Undoes the most recent undoable action on the |
Event | |
| The event fired when the text in a |
The following code provides a simple example of a RichTextBox
used to edit a FlowDocument
. The XAML defines a static FlowDocument
that contains a variety of structural and formatting elements. The user interface provides a set of buttons to manipulate the RichTextBox
content. The buttons rely on the application and editing command support provided by the RichTextBox
control and use a style (discussed further in Chapter 6) to make the RichTextBox
the target of the button's command.
<Window x:Class="Recipe_03_05.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 3_05" Height="350" Width="500"> <DockPanel> <StackPanel DockPanel.Dock="Top" Orientation="Horizontal"> <StackPanel.Resources> <Style TargetType="{x:Type Button}"> <Setter Property="CommandTarget" Value="{Binding ElementName=rtbTextBox1}" /> </Style> </StackPanel.Resources> <Button Content="Clear" Name="btnClear" Click="btnClear_Click" /> <Separator Margin="5"/> <Button Content="Cu_t" Command="ApplicationCommands.Cut" /> <Button Content="_Copy" Command="ApplicationCommands.Copy" /> <Button Content="_Paste" Command="ApplicationCommands.Paste" /> <Separator Margin="5"/> <Button Content="_Undo" Command="ApplicationCommands.Undo" /> <Button Content="_Redo" Command="ApplicationCommands.Redo" /> <Separator Margin="5"/> <Button Content="_Bold" Command="EditingCommands.ToggleBold" /> <Button Content="_Italic" Command="EditingCommands.ToggleItalic" /> <Button Content="Underline" Command="EditingCommands.ToggleUnderline" /> <Separator Margin="5"/> <Button Content="_Right" Command="EditingCommands.AlignRight" /> <Button Content="C_enter" Command="EditingCommands.AlignCenter" /> <Button Content="_Left" Command="EditingCommands.AlignLeft" /> </StackPanel> <RichTextBox DockPanel.Dock="Bottom" Name="rtbTextBox1" HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible"> <FlowDocument> <Paragraph FontSize="12"> Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. </Paragraph> <Paragraph FontSize="15"> Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure. </Paragraph> <Paragraph FontSize="18">A List</Paragraph>
<List> <ListItem> <Paragraph> <Bold>Bold List Item</Bold> </Paragraph> </ListItem> <ListItem> <Paragraph> <Italic>Italic List Item</Italic> </Paragraph> </ListItem> <ListItem> <Paragraph> <Underline>Underlined List Item</Underline> </Paragraph> </ListItem> </List> </FlowDocument> </RichTextBox> </DockPanel> </Window>
The following code-behind contains the event handler that handles the Clear button provided on the user interface defined earlier:
The following code-behind contains the event handler that handles the Clear button provided on the user interface defined earlier:
using System.Windows; namespace Recipe_03_05 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); } // Handles Clear Button click event. private void btnClear_Click(object sender, RoutedEventArgs e) { // Select all the text in the FlowDocument and cut it. rtbTextBox1.SelectAll(); rtbTextBox1.Cut(); } } }
Figure 3-4 shows what the RichTextBox
looks like when the example is first run.
You need to load the content of a System.Windows.Controls.RichTextBox
from a file or save the current content to a file.
Load the content of the System.Windows.Documents.FlowDocument
object contained in the RichTextBox.Document
from a file using the XamlReader
class. Save the content of the FlowDocument
object using the XamlWriter
class. Both the XamlReader
and XamlWriter
classes are members of the System.Windows.Markup
namespace.
The XamlReader
and XamlWriter
classes make it easy to serialize XAML to and from disk. Because you can represent a FlowDocument
(the content model used within a RichTextBox
) as XAML, the XamlReader
and XamlWriter
classes provide an excellent way to store and retrieve the content of a RichTextBox
.
To load a FlowDocument
stored in a XAML file into a RichTextBox
, pass a System.IO.FileStream
object representing the file to the static XamlReader.Load
method. You must cast the returned System.Object
to a FlowDocument
and handle any formatting or casting errors that occur in the process. Once you have the FlowDocument
, assign it to the Document
property of the RichTextBox
.
To write the FlowDocument
content of a RichTextBox
to a file, pass the FlowDocument
from the RichTextBox.Document
property and a FileStream
object representing the destination file to the XamlWriter.Save
method.
The following XAML displays a RichTextBox
containing some default text along with the buttons necessary to create a new empty FlowDocument
, open a FlowDocument
, and save a FlowDocument:
<Window x:Class="Recipe_03_06.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 3_06" Height="300" Width="300"> <DockPanel> <StackPanel DockPanel.Dock="Top" Orientation="Horizontal"> <Button Content="_New" Name="btnNew" Click="btnNew_Click" /> <Button Content="_Open" Name="btnOpen" Click="btnOpen_Click" /> <Button Content="_Save" Name="btnSave" Click="btnSave_Click" /> </StackPanel> <RichTextBox DockPanel.Dock="Bottom" Name="rtbTextBox1" HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible"> <FlowDocument> <Paragraph> Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. </Paragraph> <Paragraph> Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure. </Paragraph> </FlowDocument> </RichTextBox> </DockPanel> </Window>
The following code-behind shows the event handlers for the New, Open, and Save buttons. The btnOpen_Click
event handler uses the Microsoft.Win32.OpenFileDialog
to provide a standard Windows dialog box to allow the user to select the file to open. Similarly, the btnSave_Click
event handler uses the Microsoft.Win32.SaveFileDialog
.
using Microsoft.Win32; using System; using System.IO; using System.Windows; using System.Windows.Documents; using System.Windows.Markup;
namespace Recipe_03_06 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { private String currentFileName = String.Empty; public Window1() { InitializeComponent(); } // Handles the Open Button Click event private void btnOpen_Click(object sender, RoutedEventArgs e) { // Use a standard OpenFileDialog to allow the user to // select the file to open. OpenFileDialog dialog = new OpenFileDialog(); dialog.FileName = currentFileName; dialog.Filter = "XAML Files (*.xaml)|*.xaml"; // Display the OpenFileDialog and read if the user // provides a file name. if (dialog.ShowDialog() == true) { // Remember the new file name. currentFileName = dialog.FileName; { using (FileStream stream = File.Open(currentFileName, FileMode.Open)) { // TODO: Need logic to handle incorrect file format errors. FlowDocument doc = XamlReader.Load(stream) as FlowDocument; if (doc == null) { MessageBox.Show("Could not load document.", Title); } else { rtbTextBox1.Document = doc; } } }
} } // Handles the New Button Click event private void btnNew_Click(object sender, RoutedEventArgs e) { // Create a totally new FlowDocument for the RichTextBox. rtbTextBox1.Document = new FlowDocument(); currentFileName = String.Empty; } // Handles the Save Button Click event private void btnSave_Click(object sender, RoutedEventArgs e) { // Use a standard SaveFileDialog to allow the user to // select the file to save. SaveFileDialog dialog = new SaveFileDialog(); dialog.FileName = currentFileName; dialog.Filter = "XAML Files (*.xaml)|*.xaml"; // Display the SaveFileDialog and save if the user // provides a file name. if (dialog.ShowDialog() == true) { // Remember the new file name. currentFileName = dialog.FileName; using (FileStream stream = File.Open(currentFileName, FileMode.Create)) { XamlWriter.Save(rtbTextBox1.Document, stream); } } } } }
You need to allow the user to enter secret information (such as a password) and mask the characters entered so they cannot be read from the screen.
The PasswordBox
control works like a simplified System.Windows.Controls
.TextBox control. But for each character the user types, the PasswordBox
control displays a placeholder symbol instead of the character entered. You can define the placeholder character by setting the PasswordChar
property of the PasswordBox
control; an asterisk (*) is the default.
In an extra effort to improve the security of the data entered by the user, the PasswordBox
control stores its content internally in a System.Security.SecureString
. This stores the string encrypted in memory, ensuring that if the memory is paged or written to disk as part of a memory dump, then the password remains secure. However, you can get the password from the PasswordBox
via the Password
property as a System.String
, meaning you have to be very careful with the data if you want to maintain the same level of protection throughout your code.
The following example demonstrates how to use a PasswordBox
to allow the user to enter a password, which it then displays when the user clicks the OK button (which is not very secure but demonstrates how to use the Password
property). The example uses an exclamation mark as the password character.
<Window x:Class="Recipe_03_07.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 3_07" Height="100" Width="300"> <StackPanel Orientation="Horizontal"> <TextBlock Margin="5" VerticalAlignment="Center"> Enter Password: </TextBlock> <PasswordBox Name="passwordBox" PasswordChar="!" VerticalAlignment="Center" Width="150" /> <Button Content="OK" IsDefault="True" Margin="5" Name="button1" VerticalAlignment="Center" Click="button1_Click" /> </StackPanel> </Window>
The following code-behind handles the button click event:
using System.Windows; namespace Recipe_03_07 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); } // Handles the Button Click event private void button1_Click(object sender, RoutedEventArgs e) { MessageBox.Show("Password entered: " + passwordBox.Password, Title); } } }
Figure 3-5 shows how the PasswordBox
masks the user input with a selectable substitute character.
You need to do a spell check as the user enters text into a System.Windows.Controls.TextBox
or System.Windows.Controls.RichTextBox
control.
Enable spell checking on the TextBox
or RichTextBox
control by setting the attached property SpellCheck.IsEnabled
to True
.
The System.Windows.Controls.SpellCheck
class provides real-time spell-checking functionality for text-editing controls. When enabled on a TextBox
or RichTextBox
control, as the user types text into the control, any unrecognized words are underlined in red. If the user right-clicks a highlighted word, they get a context menu, and they can select to replace the word from a set of suggested alternatives. The user can also choose to ignore the highlighted word, which will clear the highlighting of that word while the text entry control exists.
The SpellCheck
control provides multilingual support. A TextBox
control will use the language defined by the xml:lang
attribute. For RichTextBox
, the SpellCheck
control determines which dictionary to use based on the current keyboard input language.
As of this writing, the SpellCheck
control supports only US English (xml:lang="en-US")
, UK English (xml:lang="en-GB")
, French (xml:lang="fr-FR")
, and German (xml:lang="de-DE")
. You must have the appropriate language pack installed to enable multilingual dictionary support. Unfortunately, there is currently no way to create new dictionaries or to customize the content of the dictionaries provided.
The following XAML declares a TextBox
with spell checking enabled:
<Window x:Class="Recipe_03_08.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 3_08" Height="100" Width="300"> <StackPanel> <TextBlock FontSize="14" FontWeight="Bold" Text="A spell-checking TextBox:"/> <TextBox AcceptsReturn="True" AcceptsTab="True" FontSize="14" Margin="5" SpellCheck.IsEnabled="True" TextWrapping="Wrap"> The qick red focks jumped over the lasy brown dog. </TextBox> </StackPanel> </Window>
Figure 3-6 shows the spell-check-enabled TextBox
from the previous example. It also shows the context menu displayed to allow the user to select suggested alternatives to the word identified as being misspelled.
When the user clicks a Button
control, WPF raises the Button
control's Click
event, which in turn invokes the method that is configured to handle the event. In XAML, the easiest way to register a method to handle a Click
event is to specify the name of the handler method as the value of the Click
attribute of the Button
element.
Depending on what you are doing and the number of buttons you need to accommodate, you can create individual Click
handler methods for each Button
, or you can create a single method that determines which Button
generated the Click
event and takes the appropriate action.
To use the second approach, you can use the OriginalSource
property of the System.Windows.RoutedEventArgs
object that is passed to the Click
event handler to determine which Button
raised the event. The OriginalSource
property contains a System.Object
reference to the control that raised the event. You can compare the Object
reference to see whether it is the same as a particular Button
, or you can cast the Object
reference to a Button
and inspect its properties to determine which action to take.
Chapter 12 contains recipes that provide more detail on the various mouse and keyboard input events and how to handle them appropriately.
The following example presents three Button
controls. When the user clicks a Button
, the example displays a System.Windows.MessageBox
containing the name of the Button
selected. This example casts the Object
in the RoutedEventArgs.OriginalSource
property to a Button
to access the Name
of the Button
. Recipe 3-4 contains an example that directly compares a known Button
control with the Object
in the OriginalSource
property.
<Window x:Class="Recipe_03_09.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 3_09" Height="200" Width="200"> <StackPanel> <Button Click="SharedButtonClickHandler" Height="23" Margin="10" Name="button1" Width="75">Button One</Button> <Button Click="SharedButtonClickHandler" Height="23" Margin="10" Name="button2" Width="75">Button Two</Button> <Button Click="SharedButtonClickHandler" Height="23" Margin="10" Name="button3" Width="75">Button Three</Button> </StackPanel> </Window>
Here is the code-behind containing the shared Click
event handler:
using System.Windows; using System.Windows.Controls; namespace Recipe_03_09 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); }
private void SharedButtonClickHandler(object sender, RoutedEventArgs e) { Button source = e.OriginalSource as Button; if (source != null) { MessageBox.Show("You pressed " + source.Name, Title); } } } }
A standard System.Windows.Controls.Button
control raises click events when the user clicks, releases, or hovers over the button (controlled by the Button.ClickMode
property). The RepeatButton
control provides an easy way to repeatedly raise Click
events for the entire duration during which the user "holds down" a button.
Two properties control the timing of the Click
events raised by the RepeatButton
. The Delay
property defines (in milliseconds) the delay from when the user first activates the RepeatButton
to when it raises the first Click
event. The default Delay
value is the same as that of the keyboard delay obtained via the System.Windows.SystemParameters.KeyboardDelay
property. The Interval
property defines (in milliseconds) the time between subsequent Click
events (as long as the user is still holding down the RepeatButton
). The default Interval
value is the same as that of the keyboard speed obtained via the System.Windows.SystemParameters.KeyboardSpeed
property.
The following example uses two RepeatButton
controls to move a slider. The first RepeatButton
must be clicked and held to move the slider to the left. The second RepeatButton
moves the slider to the right while the mouse hovers over the xbutton.
<Window x:Class="Recipe_03_10.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 3_10" Height="100" Width="200"> <StackPanel> <Slider Name="slider" Maximum="100" Minimum="0" Value="50" /> <StackPanel Orientation="Horizontal"> <RepeatButton Click="SliderLeft_Click" Content="Click Me" Height="23" Margin="10" Width="70" ToolTip="Click to move slider left" /> <RepeatButton Click="SliderRight_Click" ClickMode="Hover" Content="Touch Me" Height="23" Margin="10" ToolTip="Hover to move slider right" Width="70" /> </StackPanel> </StackPanel> </Window>
The following code-behind shows how to handle the Click
events raised by the RepeatButton
controls:
using System.Windows; namespace Recipe_03_10 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); } // Handles the SliderLeft Click event. private void SliderLeft_Click(object sender, RoutedEventArgs e) { // Reduce the value of the slider by one for each click. slider.Value -= 1; } // Handles the SliderRight Click event. private void SliderRight_Click(object sender, RoutedEventArgs e) { // Increase the value of the slider by one for each click. slider.Value += 1; } } }
You need to identify one default button from a group of buttons that is "clicked" when the user presses Enter.
It is standard behavior in a data entry form that the currently selected button is "clicked" when the user presses the Enter key. But when the user is active in a nonbutton control such as a text entry control or a list, users expect forms to have a default button that is "clicked" when they press Enter. Usually, this will be the OK, Next, Send, or Submit button.
By default, the currently selected Button
(as you tab through them, for example) has a colored border identifying that it is the Button
that will be clicked if you press Enter. By setting the IsDefault
property of a Button
control to True
, you identify that Button
as the default. Whenever the user is focused on a nonbutton control, the default Button
will have a colored border to indicate that it will be "clicked" if the user presses Enter.
If you configure two default Button
controls, when the user presses Enter, focus jumps to the first default Button
in the tab order (making it active), but it is not clicked automatically.
The following example contains a System.Windows.Controls.TextBox
and three Button
controls. The button3 Button
is configured as the default. As the default, btnThree
is selected when the application first loads and is "clicked" when you press Enter while editing the TextBox
.
<Window x:Class="Recipe_03_11.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 3_11" Height="100" Width="300"> <DockPanel> <TextBox DockPanel.Dock="Top" Margin="5"> Button three is the default button. </TextBox> <StackPanel HorizontalAlignment="Center" DockPanel.Dock="Bottom" Orientation="Horizontal"> <Button Click="SharedButtonClickHandler" Content="Button One" Margin="5" Name="btnOne" Width="75" /> <Button Click="SharedButtonClickHandler" Content="Button Two" Margin="5" Name="btnTwo" Width="75" />
<Button Click="SharedButtonClickHandler" Content="Button Three" IsDefault="True" Margin="5" Name="btnThree" /> </StackPanel> </DockPanel> </Window>
Here is the code-behind containing the shared Click
event handler:
using System.Windows; using System.Windows.Controls; namespace Recipe_03_11 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); } // Handles the click event for all buttons. private void SharedButtonClickHandler(object sender, RoutedEventArgs e) { Button source = e.OriginalSource as Button; if (source != null) { MessageBox.Show("You pressed " + source.Name, Title); } } } }
Figure 3-7 shows how a default button is identified when the user is focused on a text entry control.
You need to provide a keyboard shortcut so that users can jump to specific System.Windows.Controls.TextBox
controls.
Create a label for the TextBox
using the System.Windows.Controls.Label
control. Define access keys within the content of the Label
control by preceding the desired access character with an underscore (_)
. Then use the Target
property of the Label
control to identify the TextBox
that is the intended target when the user presses the access key.
The TextBox
is one of the most common controls users want to access quickly from the keyboard (another being buttons, which are discussed in recipe 3-13). However, the TextBox
itself has no constant text in which you can embed an access key. Using a Label
control, you can specify an access key in the content of the Label
and then identify a TextBox
target for the access key.
To define an access key in a Label
, precede the letter you want to be the access key with an underscore (_)
. By default, the first underscore identifies the access key, so if you need to use underscores in the Label
prior to the desired access key, use double underscores (__)
so that WPF doesn't treat it as an access key and instead displays a normal underscore.
When a user presses the access key for the Label
(while pressing Alt), instead of jumping to the Label
, the user's focus is redirected to the targeted TextBox
.
The following XAML displays a window containing three Label
and TextBox
control pairs. Each Label
defines a numeric quick access key (1, 2, or 3) that, when pressed in conjunction with the Alt key, takes you to the TextBox
associated with the Label
. In Windows Vista, the default configuration displays only the underscore when you press the Alt key.
<Window x:Class="Recipe_03_12.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 3_12" Height="200" Width="250"> <StackPanel> <StackPanel Margin="10" Orientation="Horizontal"> <Label Target="{Binding ElementName=textBox1}">Label _1</Label> <TextBox Name="textBox1" Width="150"></TextBox> </StackPanel>
<StackPanel Margin="10" Orientation="Horizonal"> <Label Target="{Binding ElementName=textBox2}">Label_2</Label> <TextBox Name="textBox2" Width="150"></TextBox> </StackPanel> <StackPanel Margin="10" Orientation="Horizontal"> <Label Targe="{Binding ElementName=textBox3}">Label_3</Label> <TextBox Name="textBox3" Width="150"></TextBox> </StackPanel> <StackPanel> </Window>
You need to provide a keyboard shortcut so that users can quickly "click" a System.Windows.Controls.Button
control without using the mouse.
In the Content
property of the Button
, precede the letter you want to be the access key with an underscore (_).
The first underscore character in the Content
property of a Button
control identifies the access key for that Button
. If the user presses one of the available access keys while holding the Alt key, WPF raises the Click
event on the Button
as if the user had clicked the Button
with the mouse.
Because the first underscore identifies the access key, if you need to use underscores in the content prior to the desired access key, use double underscores (__) so that WPF doesn't treat it as an access key and instead displays a normal underscore.
Even though the access key underscore is not displayed onscreen as a normal part of the Button
content, it is still there. You must take this into consideration if you need to work with the Button
content programmatically.
The following XAML demonstrates how to use underscores to define access keys for the three Button
controls:
<Window x:Class="Recipe_03_13.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 3_13" Height="100" Width="300"> <StackPanel HorizontalAlignment="Center" Orientation="Horizontal"> <Button Click="SharedButtonClickHandler" Height="23" Margin="5" Name="button1" Width="75">Button _One</Button> <Button Click="SharedButtonClickHandler" Height="23" Margin="5" Name="button2" Width="75">Button _Two</Button> <Button Click="SharedButtonClickHandler" Height="23" Margin="5" Name="button3" Width="75">Button T_hree</Button> </StackPanel> </Window>
The following code-behind provides the Click
event handler for the preceding XAML:
using System; using System.Windows; using System.Windows.Controls; namespace Recipe_03_13 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); } private void SharedButtonClickHandler(object sender, RoutedEventArgs e) { Button source = e.OriginalSource as Button; if (source != null) { string message = String.Format("{0} was pressed.", source.Content); MessageBox.Show(message, Title); } } } }
Use the System.Windows.Controls.Slider
control. You can obtain the current value of the Slider
from its Value
property or handle the ValueChanged
event to respond dynamically as the Slider
value changes.
A Slider
control allows a user to choose one value from a range of values by moving a thumb along a track. You set the values for the extremes of the track using the Minimum
and Maximum
properties of the Slider
control and get or set the current position of the thumb using the Value
property.
Other properties of the Slider
control allow you to control the frequency and location of tick marks that divide the length of the track and how the thumb moves along the track in response to user interaction. Table 3-4 summarizes some of the most commonly used properties of the Slider
control.
Table 3-4. Commonly Used Properties of the Slider
Control
Property | Description |
---|---|
| Determines whether the |
| The size of the change in the |
| The maximum value the |
| The minimum value the |
| The size of the change in the |
| The interval between ticks along the |
| The location of the tick marks relative to the |
| A set of specific tick marks to show along the length of the |
| The current value of the |
The following XAML displays two Slider
controls. The top Slider
allows the user to move the thumb freely between the Minimum
and Maximum
values but specifies a LargerChange
value of 10 for when the user clicks the track. The bottom Slider
uses a TickFrequency
value of 25 and forces the thumb to align to a tick using the IsSnapToTickEnabled
property. This results in a slider that always moves in increments of 25.
<Window x:Class="Recipe_03_14.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 3_14" Height="200" Width="300"> <StackPanel> <TextBlock Margin="5" Text="0" FontSize="20" HorizontalAlignment="Center" Name="txtSliderValue" /> <Slider LargeChange="10" Margin="5" Maximum="1000" Minimum="0" Name="slider1" TickPlacement="TopLeft" Ticks="100, 200, 400, 800" Value="0" ValueChanged="slider_ValueChanged" /> <Button Name="btnGetSliderValue1" Width="100" Click="GetSliderValue_Click">Get Slider 1 Value</Button> <Slider IsSnapToTickEnabled="True" Margin="5" Maximum="1000" Minimum="0" Name="slider2" TickFrequency="25" TickPlacement="BottomRight" Value="1000" ValueChanged="slider_ValueChanged" /> <Button Name="btnGetSliderValue2" Width="100" Click="GetSliderValue_Click">Get Slider 2 Value</Button> </StackPanel> </Window>
The following code-behind demonstrates how to obtain the current Value
of a Slider
in response to a button click and also shows how to handle the ValueChanged
event to respond dynamically to the movement of the Slider
thumb:
using System.Windows; using System.Windows.Controls; namespace Recipe_03_14 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); } // Handles all GetSliderValue Button Clicks private void GetSliderValue_Click(object sender, RoutedEventArgs e) { Button button = e.OriginalSource as Button; string message = "Unknown slider."; if (button == btnGetSliderValue1) { message = "Slider1 value = " + slider1.Value; } else if (button == btnGetSliderValue2) { message = "Slider2 value = " + slider2.Value; } MessageBox.Show(message, Title); } // Handles all Slider ValueChangedEvents. private void slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) { Slider slider = e.OriginalSource as Slider; if (slider != null) { txtSliderValue.Text = slider.Value.ToString(); } } } }
Figure 3-8 shows the sample code where the number at the top of the window shows the Value property of the top slider.
Set the ContextMenu
property of the control, and configure the context menu using a hierarchy of System.Windows.Controls.MenuItem
objects to define each menu option.
You attach a context menu to an element by setting that element's ContextMenu
property. With the ContextMenu
property defined, right-clicking the control will bring up the context menu.
You define the structure and content of the context menu by creating a hierarchy of MenuItem
objects. The Header
property of the MenuItem
defines the name displayed for the menu option. MenuItem
supports the basic formatting of the Header
, including things such as the font family, size, style, and weight. You can also define access keys by prefixing the appropriate character in the Header
with an underscore (_).
To define the functionality of the ContextMenu
items, you can either define Click
event handlers on each MenuItem
or define Command
bindings. If the control the ContextMenu
is attached to is aware of the bound Command
, then you do not need to implement special logic because WPF passes the Command
to the parent control when the user clicks the MenuItem
.
Chapter 4 contains more examples of using commands and command bindings.
The ContextMenu
also supports properties that make it easy to create rich visual effects such as HasDropShadow
to turn on a shadow behind the ContextMenu
and Opacity
to control the opacity of the ContextMenu
.
The following XAML demonstrates how to define a ContextMenu
for a TextBox
that lets the user clear, select, cut, copy, and paste content from the TextBox
. A submenu allows the user to change the format of the TextBox
content. The example uses a shared Click
event handler to handle format changes, individual Click
event handlers for the Clear and Select All menu items, and standard Command
bindings for the Cut, Copy, and Paste commands, which the TextBox
knows how to handle.
<Window x:Class="Recipe_03_15.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 3_15" Height="100" Width="300"> <Grid> <TextBox FontSize="16" Height="23" Name="txtTextBox" > <TextBox.ContextMenu> <ContextMenu HasDropShadow="True" Opacity=".8"> <MenuItem Command="Cut" Header="Cu_t" /> <MenuItem Command="Copy" Header="_Copy" /> <MenuItem Command="Paste" Header="_Paste" /> <Separator/> <MenuItem Click="SelectAll_Click" Header="_Select All" /> <MenuItem Click="Clear_Click" Header="_Clear" /> <Separator/> <MenuItem Header="Format"> <MenuItem Click="TextStyle_Click" Header="_Normal" Name="miNormal"></MenuItem> <MenuItem Click="TextStyle_Click" FontWeight="Bold" Header="_Bold" Name="miBold"></MenuItem> <MenuItem Click="TextStyle_Click" FontStyle="Italic" Header="_Italic" Name="miItalic"></MenuItem> </MenuItem> </ContextMenu> </TextBox.ContextMenu> A TextBox control with ContextMenu.</TextBox> </Grid> </Window>
The following code-behind contains the shared Click
event handler to handle format changes and individual Click
event handlers for the Clear and Select All context menu items:
using System.Windows; namespace Recipe_03_15 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); } // Handles Clear Button Click. private void Clear_Click(object sender, RoutedEventArgs e) { txtTextBox.Clear(); } // Handles the Select All Button Click. private void SelectAll_Click(object sender, RoutedEventArgs e) { txtTextBox.SelectAll(); } // Handles all the Button Click events that format the TextBox. private void TextStyle_Click(object sender, RoutedEventArgs e) { if (sender == miNormal) { txtTextBox.FontWeight = FontWeights.Normal; txtTextBox.FontStyle = FontStyles.Normal; } else if (sender == miBold) { txtTextBox.FontWeight = FontWeights.Bold; } else if (sender == miItalic) { txtTextBox.FontStyle = FontStyles.Italic; } } } }
Figure 3-9 shows the two levels of ContextMenu
open over the TextBox
as defined in this recipe's sample code.
Assign a System.Windows.Controls.ToolTip
control to the ToolTip
property of the control on which you want to display the tool tip.
The System.Windows.FrameworkElement
class implements the ToolTip
property, providing a simple mechanism through which to display a tool tip on any FrameworkElement
-derived control.
The ToolTip
property is of type System.Object
, so you can assign it any object, and the property will attempt to render the object as a tool tip for display. This provides a great deal of flexibility in how you define tool tip content. For simple textual tool tips, you can specify the text to display as the value of the control's ToolTip
attribute. When creating richer, more complex tool tips, you should use property element syntax to specify structured ToolTip
content.
The content of a ToolTip
can contain interactive controls such as buttons, but they never get focus; you can't click or otherwise interact with them.
The following XAML demonstrates both how to use the ToolTip
attribute to specify the tool tip of a Button
and how to use a ToolTip
object defined using property element syntax to create a tool tip that contains larger and more structured content comprising a label and a list of values:
<Window x:Class="Recipe_03_16.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 3_16" Height="150" Width="300"> <StackPanel Name="stackPanel1"> <Button Height="23" Margin="10" Name="button1" ToolTip="A simple textual ToolTip" Width="175"> Button with Simple ToolTip </Button> <Button Content="Button with a Richer ToolTip" Height="23" Margin="10" Name="button2" Width="175"> <Button.ToolTip> <StackPanel Name="stackPanel2" Width="200"> <Label Name="label1" HorizontalAlignment="Left"> List of Things: </Label> <ListBox Name="listBox1" Margin="10" > <ListBoxItem>Thing 1</ListBoxItem> <ListBoxItem>Thing 2</ListBoxItem> <ListBoxItem>Thing 3</ListBoxItem> </ListBox> </StackPanel> </Button.ToolTip> </Button> </StackPanel> </Window>
Figure 3-10 shows the previous example with the ToolTip
visible for the lower Button
.
On the control you want to associate the tool tip with, set the ShowOnDisabled
attached property of the System.Windows.Controls.ToolTipService
class to True
.
Usually, disabled controls (those with IsEnabled
set to False
) do not display tool tips when the user hovers over them with the mouse pointer. The ToolTipService
class provides a set of global services that you can use to control the behavior of ToolTip
objects. The ToolTipService
property that enables the display of tool tips on disabled controls is named ShowOnDisabled
. By assigning True
to this attached property on a control, you override the default ToolTip
behavior and force WPF to display the ToolTip
even though the control is disabled.
The following XAML demonstrates how to use the ToolTipService.ShowOnDisabled
attached property to enable the display of a ToolTip
for a disabled Button
control. The code contains two Button
controls: the first shows the default behavior of a ToolTip
on a disabled control, and the second shows the effect of setting the ToolTipService.ShowOnDisabled
property to True
.
<Window x:Class="Recipe_03_17.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 3_17" Height="150" Width="300"> <StackPanel> <Button Content="Disabled Button without ToolTipService" Height="23" IsEnabled="False" Margin="10" Name="button1" Width="200"> <Button.ToolTip> ToolTip on a disabled control </Button.ToolTip> </Button> <Button Content="Disabled Button with ToolTipService" Height="23" IsEnabled="False" Margin="10" Name="button2" ToolTipService.ShowOnDisabled="True" Width="200"> <Button.ToolTip> ToolTip on a disabled control </Button.ToolTip> </Button> </StackPanel> </Window>
You need to control how long your application displays a System.Windows.Controls.ToolTip
or where the ToolTip
is located relative to the associated control.
Apply the attached properties of the System.Windows.Controls.ToolTipService
class to the control with which the ToolTip
is associated. The ToolTipService.ShowDuration
property controls the display duration of the ToolTip
. The ToolTipService.Placement
property along with the ToolTipService.HorizontalOffset
and ToolTipService.VerticalOffset
properties control the position of the ToolTip
.
The ToolTipService
class provides a set of attached properties that you can use to control the behavior of ToolTip
objects. The ToolTipService.ShowDuration
property takes a System.Int32
value that specifies the number of milliseconds for which to display the ToolTip
. The default value is 5000.
The ToolTipService.Placement
property takes a value from the System.Windows.Controls.Primitives.PlacementMode
enumeration, which determines where WPF places the ToolTip
when it is displayed. The PlacementMode
enumeration contains 12 values, offering a wide choice of how you place your ToolTip
. Many of the PlacementMode
options are relative to the placement target of the ToolTip
. By default, the placement target is the control on which the ToolTip
is defined but can be overridden using the ToolTipService.PlacementTarget
property. Table 3-5 summarizes some of the more commonly used PlacementMode
values.
Table 3-5. Property Values for the Placement of ToolTip
Controls
Value | Description |
---|---|
| The top of the |
| The |
| The right of the |
| The top of the |
| The upper-left corner of the |
| The left of the |
The ToolTipService.HorizontalOffset
and ToolTipService.VerticalOffset
properties allow you to specify System.Double
values to fine-tune the position of the ToolTip
depending on the value of the ToolTipService.Placement
property. By default, the offset values are assumed to be in px
(pixels) but can also be in
(inches), cm
(centimeters), or pt
(points).
WPF provides fine-grained control over the placement of ToolTip
controls using the PlacementTarget
, PlacementRectangle
, HorizontalOffset
, and VerticalOffset
properties provided by the ToolTipService
. For a thorough description of ToolTip
placement logic, see the MSDN article at http://msdn2.microsoft.com/en-us/library/bb613596.aspx
.
The following XAML declares three buttons, each with ToolTip
controls using different properties of the ToolTipService
class to control position and display duration:
<Window x:Class="Recipe_03_18.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 3_18" Height="200" Width="300"> <StackPanel> <Button Height="40" Margin="5" ToolTipService.Placement="Mouse" ToolTipService.ShowDuration="1000"> <Button.ToolTip> <ToolTip> ToolTip displayed for 1 second... </ToolTip> </Button.ToolTip> Button with ToolTip under Mouse </Button> <Button Height="40" Margin="5" ToolTipService.Placement="Center"> <Button.ToolTip> <ToolTip> ToolTip displayed for 5 seconds... </ToolTip> </Button.ToolTip> Button with Centered ToolTip </Button> <Button Height="40" Margin="5" ToolTipService.HorizontalOffset="5cm" ToolTipService.Placement="Relative" ToolTipService.ShowDuration="10000" ToolTipService.VerticalOffset="50px"> <Button.ToolTip> <ToolTip > ToolTip displayed for 10 seconds... </ToolTip>
</Button.ToolTip> Button with offset ToolTip </Button> </StackPanel> </Window>
Create a set of System.Windows.Controls.RadioButton
controls. To know which particular RadioButton
is selected, either test its IsSelected
property when the user exits the form containing the RadioButton
or handle the RadioButton.Checked
event.
All RadioButton
controls in a single parent container form a single group by default. To create multiple independent groups in a single parent container or to create groups that span multiple containers, set the GroupName
property on each RadioButton
to define its group membership.
Typically, you consider the state of a set of radio controls when the user submits a form via the click of a button. Unfortunately, there is no straightforward way to identify which RadioButton
in a group is checked. Instead, as part of the form-processing logic, you must individually check the IsSelected
property of each RadioButton
. When you have large numbers or a dynamic set of RadioButton
controls, it is easiest to loop through all the controls in a container and filter out those that are not RadioButton
controls and those that do not belong to the correct group.
Alternatively, if you need to take an action as the user clicks the RadioButton
, assign an event handler to its Checked
event.
The following example demonstrated how to create two groups of RadioButton
controls. Using the GroupName
property, the example creates two groups within a single System.Windows.Controls.StackPanel
(on the left) and also creates a group (Group1) that spans the left and right StackPanel
containers.
<Window x:Class="Recipe_03_19.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 3_19" SizeToContent="Height" Width="300">
<Grid Name="grid"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Border Grid.Column="0" BorderBrush="Gray" BorderThickness="1" /> <Border Grid.Column="1" BorderBrush="Gray" BorderThickness="1" /> <StackPanel Grid.Column="0" HorizontalAlignment="Center" Margin="5" Name="spLeftContainer"> <TextBlock FontSize="16" Text="Radio Group 1" /> <RadioButton Content="Radio Button 1A" GroupName="Group1" IsChecked="True" Margin="5" Name="rbnOneA" /> <RadioButton Content="Radio Button 1B" GroupName="Group1" Margin="5" Name="rbnOneB" /> <RadioButton Content="Radio Button 1C" GroupName="Group1" Margin="5" Name="rbnOneC" /> <Separator/> <TextBlock FontSize="16" Text="Radio Group 2" /> <RadioButton Checked="RadioButton_Checked" GroupName="Group2" Content="Radio Button 2A" IsChecked="True" Margin="5" Name="rbnTwoA" /> <RadioButton Checked="RadioButton_Checked" GroupName="Group2" Content="Radio Button 2B" Margin="5" Name="rbnTwoB"/> <RadioButton Checked="RadioButton_Checked" GroupName="Group2" Content="Radio Button 2C" Margin="5" Name="rbnTwoC"/> </StackPanel> <StackPanel Grid.Column="1" HorizontalAlignment="Center" Margin="5" Name="spRightContainer"> <TextBlock FontSize="16" Text="Radio Group 1" /> <RadioButton Content="Radio Button 1D" GroupName="Group1" Margin="5" Name="rbnOneD" /> <RadioButton Content="Radio Button 1E" GroupName="Group1" Margin="5" Name="rbnOneE" /> </StackPanel> <Button Content="Show Group1 Selection" Grid.ColumnSpan="2" Grid.Row="1" HorizontalAlignment="Center" Margin="10" MaxHeight="25" Click="Button_Click" /> </Grid> </Window>
The following code-behind demonstrates how to handle the RadioButton.Checked
event and also how to loop through the children of a container to determine which RadioButton
from a particular group is checked:
using System; using System.Linq; using System.Windows; using System.Windows.Controls; namespace Recipe_03_19 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); } // Handles the Submit Button Click event. private void Button_Click(object sender, RoutedEventArgs e) { RadioButton radioButton = null; // Try the first (left) container to see if one of // the radio buttons in Group1 is checked. radioButton = GetCheckedRadioButton( spLeftContainer.Children, "Group1"); // If no RadioButton in the first container is checked, try // the second (right) container. if (radioButton == null) { radioButton = GetCheckedRadioButton( spRightContainer.Children, "Group1"); } // We must have at least one RadioButton checked to display. MessageBox.Show(radioButton.Content + " checked.", Title); } // A method that loops through a UIElementCollection and identifies // a checked RadioButton with a specified group name. private RadioButton GetCheckedRadioButton( UIElementCollection children, String groupName) {
return children.OfType<RadioButton>(). FirstOrDefault( rb => rb.IsChecked == true && rb.GroupName == groupName); } // Handles the RadioButton Checked event for all buttons in Group2. private void RadioButton_Checked(object sender, RoutedEventArgs e) { // Don't handle events until the Window is fully initialized. if (!this.IsInitialized) return; RadioButton radioButton = e.OriginalSource as RadioButton; if (radioButton != null) { MessageBox.Show(radioButton.Content + " checked.", Title); } } } }
Figure 3-11 shows the two groups of RadioButton
controls created in the previous example.
Create a set of System.Windows.Controls.CheckBox
controls. To know which particular CheckBox
is selected, test the IsChecked
property of each CheckBox
when the user exits the form containing the set of CheckBox
controls. To respond as soon as the user clicks a CheckBox
to change its state, handle the Checked
, Unchecked
, and Indeterminate
events of the CheckBox
control.
To determine the state of a CheckBox
, test the value of its IsChecked
property. The IsChecked
property is of type bool?
, meaning it can be True
, False
, or Null
. A value of True
means the CheckBox
is checked, False
means it is unchecked, and Null
means it is in an indeterminate state. By default, the user cannot switch a CheckBox
into an indeterminate state, but by setting CheckBox.IsThreeState
to True
, the user can click the CheckBox
to toggle through the three states.
To respond as soon as the user clicks a CheckBox
, you could handle the CheckBox.Click
event and determine what state the CheckBox
is in, but it is easier to handle the Checked
, Unchecked
, and Indeterminate
events that the CheckBox
raises as it is entering the appropriate state (that is, as the CheckBox
gets unchecked, it raises the Unchecked
event).
The following example demonstrates two approaches to determining the state of a set of CheckBox
controls. The XAML defines a simple window containing four CheckBox
controls (two enabled with tristate support). Each time the user changes the state of a CheckBox
, the example shows a message box describing the change. The user can also click a button, which will populate a list with the names of the CheckBox
controls that are currently checked.
<Window x:Class="Recipe_03_20.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 3_20" Height="250" Width="300"> <StackPanel Name="panel"> <CheckBox Checked="CheckBox_Checked" Content="First CheckBox" IsChecked="True" Margin="2" Name="checkbox1" Unchecked="CheckBox_Unchecked"/> <CheckBox Checked="CheckBox_Checked" Content="Second CheckBox" IsChecked="False" Margin="2" Name="checkbox2" Unchecked="CheckBox_Unchecked"/> <CheckBox Checked="CheckBox_Checked" Content="Third CheckBox (Tri-State Enabled)" Indeterminate="CheckBox_Indeterminate" IsChecked="True" IsThreeState="True" Margin="2" Name="checkbox3" Unchecked="CheckBox_Unchecked"/> <CheckBox Checked="CheckBox_Checked" Content="Fourth CheckBox (Tri-State Enabled)" Indeterminate="CheckBox_Indeterminate" IsChecked="False" IsThreeState="True" Margin="2" Name="checkbox4" Unchecked="CheckBox_Unchecked"/>
<Button Content="Get Selected" Margin="5" MaxWidth="100" Click="Button_Click" /> <TextBlock FontWeight="Bold" Text="Selected CheckBoxes:" /> <ListBox Margin="5" MinHeight="2cm" Name="listbox" /> </StackPanel> </Window>
The following code-behind provides the logic for processing the Button.Cick
event to populate the list with the names of the checked CheckBox
controls and the event handlers for displaying message boxes as the state of a CheckBox
changes:
using System.Linq; using System.Windows; using System.Windows.Controls; namespace Recipe_03_20 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); } // Handles Button Click event to populate the ListBox with // the names of the currently checked CheckBox controls. private void Button_Click(object sender, RoutedEventArgs e) { // Clear the content of the ListBox. listbox.Items.Clear(); // Process each CheckBox control in the main StackPanel. foreach (CheckBox checkbox in panel.Children.OfType<CheckBox>() .Where( cb => cb.IsChecked == true)) { listbox.Items.Add(checkbox.Name); } } // Handles all the CheckBox Checked events to display a message // when a CheckBox changes to a checked state. private void CheckBox_Checked(object sender, RoutedEventArgs e) { // Don't handle these events during initialization. if (!IsInitialized) return;
CheckBox checkbox = e.OriginalSource as CheckBox; if (checkbox != null) { MessageBox.Show(checkbox.Name + " is checked.", Title); } } // Handles all the CheckBox Indeterminate events to display a message // when a CheckBox changes to an indeterminate state. private void CheckBox_Indeterminate(object sender, RoutedEventArgs e) { // Don't handle these events during initialization. if (!IsInitialized) return; CheckBox checkbox = e.OriginalSource as CheckBox; if (checkbox != null) { MessageBox.Show(checkbox.Name + " is indeterminate.", Title); } } // Handles all the CheckBox Unchecked events to display a message // when a CheckBox changes to an unchecked state. private void CheckBox_Unchecked(object sender, RoutedEventArgs e) { // Don't handle these events during initialization. if (!IsInitialized) return; CheckBox checkbox = e.OriginalSource as CheckBox; if (checkbox != null) { MessageBox.Show(checkbox.Name + " is unchecked.", Title); } } } }
Figure 3-12 shows the previous example after the user has toggled the fourth CheckBox
into an indeterminate state and has clicked the Get Selected Button
.
You need to present a hierarchical set of data as a tree with collapsible branches and allow a user to select an item from the tree.
Use the System.Windows.Controls.TreeView
control to present an expandable tree of items. Use the SelectedItem
property of the TreeView
control to determine the currently selected item. To respond to the selection of items in the ListBox
dynamically, handle the TreeView.SelectionChanged
event or the Selected
event raised by the System.Windows.Controls.TreeViewItem
objects that wrap each element contained in the TreeView
controls.
The easiest way to define the content of a static TreeView
is to include the content elements directly within the XAML TreeView
element. You can include controls directly within the TreeView
element XAML or, for greater control over the formatting of the contained items, wrap them in a TreeViewItem
element.
A more flexible way to define the content of a TreeView
is to data bind it to a collection. Chapter 5 describes how to use data binding.
To determine the current TreeView
selection, you can get the SelectedItem
property of the TreeView
at an appropriate time (that is, when the user submits or closes the form containing the TreeView
), or you can handle selection dynamically by handling events raised by the TreeView
and the individual TreeViewItem
objects.
On a change in selection, the TreeView
raises a SelectedItemChanged
. The individual TreeViewItem
objects raise a Selected
event that contains details that allow you to identify that the TreeViewItem
has changed. When a TreeViewItem
raises the Selected
event, the hierarchy of parent TreeViewItem
objects that contain the item each raise a Selected
event in turn.
The following example demonstrates how to use a TreeView
control to display hierarchical information. As the user selects an item, the example shows a MessageBox
for each TreeViewItem.Selected
event raised. The user can also click the Button
to see a message box identifying the header of the currently selected TreeViewItem
. The example uses a Style
with an EventSetter
element to assign a common Selected
event handler to all instances of TreeViewItem
.
<Window x:Class="Recipe_03_21.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 3_21" Height="200" Width="300"> <DockPanel LastChildFill="True"> <DockPanel.Resources> <Style TargetType="{x:Type TreeViewItem}"> <EventSetter Event="Selected" Handler="TreeViewItem_Selected" /> </Style> </DockPanel.Resources> <Button Click="Button_Click" DockPanel.Dock="Bottom" Content="Show Selected" MaxHeight="23" MaxWidth="100" /> <TreeView FontSize="16" Name="tvTree"> <TreeViewItem Header="Birds" IsExpanded="True"> <TreeViewItem Header="Flighted"> <TreeViewItem Header="Falcon" /> <TreeViewItem Header="Starling" /> </TreeViewItem> <TreeViewItem Header="Flightless" IsExpanded="True"> <TreeViewItem Header="Emu" /> <TreeViewItem Header="Kiwi" /> </TreeViewItem> </TreeViewItem> <TreeViewItem Header="Reptiles"> <TreeViewItem Header="Lizards"> <TreeViewItem Header="Blue Tonge" /> <TreeViewItem Header="Frilled" /> <TreeViewItem Header="Iguana" /> </TreeViewItem>
<TreeViewItem Header="Snakes"> <TreeViewItem Header="Anaconda" /> <TreeViewItem Header="Cobra" /> <TreeViewItem Header="Rattlesnake" /> </TreeViewItem> </TreeViewItem> </TreeView> </DockPanel> </Window>
The following code contains the event-handling logic for the TreeView
example:
using System; using System.Windows; using System.Windows.Controls; namespace Recipe_03_21 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); } // Handles the Selected event for all TreeViewItems. private void TreeViewItem_Selected(object sender, RoutedEventArgs e) { String message = String.Empty; // As the Selected event is fired by successive // parent TreeViewItem controls of the actually // selected TreeViewItem, the sender will change, // but the e.OriginalSource will continue to // refer to the TreeViewItem that was actually // clicked. TreeViewItem item = sender as TreeViewItem; if (item == e.OriginalSource) { // Event raised by clicked item. message = String.Format("Item selected: {0} ({1} child items)", item.Header, item.Items.Count); }
else { // Event raised by a parent of clicked item. message = String.Format("Parent of selected: {0} ({1} child items)", item.Header, item.Items.Count); } MessageBox.Show(message, Title); } // Handles the Button Click event. private void Button_Click(object sender, RoutedEventArgs e) { TreeViewItem item = tvTree.SelectedItem as TreeViewItem; if (item != null) { MessageBox.Show("Item selected: " + item.Header, Title); } else { MessageBox.Show("No item selected", Title); } } } }
Figure 3-13 shows the example in its initial state with the Birds and Flightless items expanded by default.
Use the System.Windows.Controls.ListBox
control to present a list of items. Use the SelectedItem
property of the ListBox
control to determine the currently selected item (use SelectedItems
for multiselect lists). To respond to the selection of items in the ListBox
dynamically, handle the ListBox.SelectionChanged
event or the Selected
event raised by the System.Windows.Controls.ListBoxItem
objects that wrap each element contained in the ListBox
controls.
The ListBox
control makes it incredibly easy to present a set of things to the user as a list. In fact, you can put anything that derives from System.Object
into a ListBox
. Any list item derived from System.Windows.UIElement
will render according to its OnRender
implementation, whereas other items will be rendered using ToString
.
The easiest way to define the content of a static list is to include the content elements directly within the XAML ListBox
element. You can include controls directly within the ListBox
element XAML or, for greater control over the formatting of the contained items, wrap them in a ListBoxItem
element.
To determine the current ListBox
selection, you can process the SelectedItem
or SelectedItems
properties of the ListBox
at an appropriate time (that is, when the user submits or closes the form containing the ListBox
), or you can handle selection dynamically by handling events raised by the ListBox
and the individual ListBoxItem
objects. On a change in selection, the ListBox
raises a SelectionChanged
event that passes details of all the items currently selected along with those that were just unselected. The individual ListBoxItem
objects also raise a Selected
event that contains details that allow you to identify the ListBoxItem
that has changed.
Recipe 3-23 describes how to add and remove items from a ListBox
programmatically. Also, the flexibility of the ListBox
control means that it is a useful base from which to create rich custom controls that look and behave differently than the generic ListBox
(see Chapter 4 and Chapter 6 for more details).
The following XAML defines in single-select ListBox
containing a variety of other controls, including a nested ListBox
. Some items are contained directly within the ListBox
, while others are wrapped in ListBoxItem
elements.
<Window x:Class="Recipe_03_22.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 3_22" Height="300" Width="300"> <StackPanel> <ListBox SelectionChanged="OuterListBox_SelectionChanged" Name="outerListBox"> <ListBoxItem Content="List Box Item 1" FontFamily="Tahoma" FontSize="14" HorizontalContentAlignment="Left" /> <ListBoxItem Content="List Box Item 2" FontFamily="Algerian" FontSize="16" HorizontalContentAlignment="Center" /> <ListBoxItem Content="List Box Item 3" FontSize="20" FontFamily="FreeStyle Script" HorizontalContentAlignment="Right" /> <Button Content="Button directly in a list" Margin="5" /> <ListBoxItem HorizontalContentAlignment="Center" Margin="5"> <Button Content="Button wrapped in ListBoxItem" /> </ListBoxItem> <ListBox Height="50" Margin="5"> <ListBoxItem Content="Inner List Item 1" Selected="InnerListBoxItem_Selected" /> <ListBoxItem Content="Inner List Item 2" Selected="InnerListBoxItem_Selected" /> <ListBoxItem Content="Inner List Item 3" Selected="InnerListBoxItem_Selected" /> <ListBoxItem Content="Inner List Item 4" Selected="InnerListBoxItem_Selected" /> </ListBox> <StackPanel Margin="5" Orientation="Horizontal"> <Label Content="Enter some text:" /> <TextBox MinWidth="150" /> </StackPanel> </ListBox> <TextBlock Text="No item currently selected." Margin="10" HorizontalAlignment="Center" Name="txtSelectedItem" /> </StackPanel> </Window>
The following code-behind contains the logic used to handle the event raised by the outer ListBox
and the items contained in the inner ListBox
. When the user selects an item in the outer ListBox
, the code handles the ListBox.SelectionChanged
event and displays the ToString
output of the selected item at the bottom of the form. When the user selects an item in the inner ListBox
, the code handles the ListBoxItem.Selected
event and displays the Content
property of the selected item in a MessageBox
.
using System.Windows; using System.Windows.Controls; namespace Recipe_03_22 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); } // Handles ListBoxItem Selected events for the ListBoxItems in the // inner ListBox. private void InnerListBoxItem_Selected(object sender, RoutedEventArgs e) { ListBoxItem item = e.OriginalSource as ListBoxItem; if (item != null) { MessageBox.Show(item.Content + " was selected.", Title); } } // Handles ListBox SelectionChanged events for the outer ListBox. private void OuterListBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { object item = outerListBox.SelectedItem; if (item == null) { txtSelectedItem.Text = "No item currently selected."; } else { txtSelectedItem.Text = item.ToString(); } } } }
Figure 3-14 shows the ListBox
from the example. The first button is the currently selected item.
You need to add items to, and remove items from, a System.Windows.Controls.ListBox
control at runtime.
To add an item, create a new System.Windows.Controls.ListBoxItem
object, configure it, and add it to the ListBox
using the ListBox.Items.Add
method. To remove an item, use the ListBox.Items.Remove
method.
The content of a ListBox
is contained in a System.Windows.Controls.ItemCollection
collection, which is accessed via the ListBox.Items
property. By modifying the content of the ItemCollection
, you control the visible content of the ListBox
.
You can add any object to the ItemCollection
. Any list item derived from System.Windows.UIElement
will render according to its OnRender
implementation, whereas other items will be rendered using ToString
. Wrapping the object in a ListBoxItem
gives you greater control over the format and layout of the item when it is displayed in the ListBox
.
Recipe 3-22 describes the basic structure of a ListBox
and ListBoxItem
in more detail.
The following XAML defines a simple ListBox
control in Extended selection mode containing some statically defined items. The example contains System.Windows.Controls.TextBox
and System.Windows.Controls.Button
controls that allow the user to add and remove items from the list.
<Window x:Class="Recipe_03_23.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 3_23" Height="300" Width="300"> <StackPanel> <ListBox FontSize="16" Height="150" Margin="5" Name="listBox1" SelectionMode="Extended"> <ListBoxItem>List Item 1</ListBoxItem> <ListBoxItem>List Item 2</ListBoxItem> <ListBoxItem>List Item 3</ListBoxItem> </ListBox> <StackPanel HorizontalAlignment="Center" Orientation="Horizontal"> <Label Content="_New item text:" VerticalAlignment="Center" Target="{Binding ElementName=textBox}" /> <TextBox Margin="5" Name="textBox" MinWidth="120" /> </StackPanel> <StackPanel HorizontalAlignment="Center" Orientation="Horizontal"> <utton Click="btnAddListItem_Click" Content="Add Item" IsDefault="True" Margin="5" Name="btnAddListItem" /> <Button Click="btnDeleteListItem_Click" Content="Delete Items" Margin="5" Name="btnDeleteListItem" /> <Button Click="btnSelectAll_Click" Content="Select All" Margin="5" Name="btnSelectAll" /> </StackPanel> </StackPanel> </Window>
The following code-behind handles the Button
click events that add and remove items from the list. When the user clicks the Add Item Button
, the event handler gets the content of the TextBox
, wraps it in a ListBoxItem
, formats it, and adds it to the ListBox
. When the user clicks the Delete Items Button
, the event handler loops through the set of currently selected items and removes them from the ListBox
.
using System.Windows; using System.Windows.Controls; using System.Windows.Media; namespace Recipe_03_23 { /// <summary> /// Interaction logic for Window1.xaml /// </summary>
public partial class Window1 : Window { public Window1() { InitializeComponent(); } // Handles the Add Item Button Click event. private void btnAddListItem_Click(object sender, RoutedEventArgs e) { // Ensure there is text to add. if (textBox.Text.Length == 0) { MessageBox.Show("Enter text to add to the list.", Title); } else { // Wrap the text in a ListBoxItem and configure. ListBoxItem item = new ListBoxItem(); item.Content = textBox.Text; item.IsSelected = true; item.HorizontalAlignment = HorizontalAlignment.Center; item.FontWeight = FontWeights.Bold; item.FontFamily = new FontFamily("Tahoma"); // Add the ListBoxItem to the ListBox listBox1.Items.Add(item); // Clear the content of the textBox and give it focus. textBox.Clear(); textBox.Focus(); } } // Handles the Delete Item Button Click event. private void btnDeleteListItem_Click(object sender, RoutedEventArgs e) { // Ensure there is at least one item selected. if (listBox1.SelectedItems.Count == 0) { MessageBox.Show("Select list items to delete.", Title); }
else { // Iterate through the selected items and remove each one. // Cannot use foreach because we are changing the underlying // data. while (listBox1.SelectedItems.Count > 0) { listBox1.Items.Remove(listBox1.SelectedItems[0]); } } } // Handles the Select All Button Click event. private void btnSelectAll_Click(object sender, RoutedEventArgs e) { listBox1.SelectAll(); } } }
You need to present a list of items as an expandable combo box and allow the user to select an item from the list.
Use the System.Windows.Controls.ComboBox
control to present the expandable list of items. Use the SelectedItem
property of the ComboBox
control to identify the currently selected item. To respond to the selection of items in the ComboBox
dynamically, handle the ComboBox.SelectionChanged
event or the Selected
event raised by the System.Windows.Controls.ComboBoxItem
objects that wrap each element contained in the ComboBox
control.
The ComboBox
control inherits from the System.Windows.Controls.ItemsControl
the same as the System.Windows.Controls.ListBox
, and they are similar in their use (recipes 3-22 and 3-23 demonstrate how to use the ListBox
control). The key difference between the ListBox
and the ComboBox
is the way they are rendered, which results in the ComboBox
allowing the user to select only one item at a time. The other key difference is that by setting the ComboBox.IsEditable
property to True
, you allow the user to type a value into the ComboBox
instead of being able to choose only one of the values in the drop-down list.
As with a ListBox
, you can put anything that derives from System.Object
into a ComboBox
. Any list item derived from System.Windows.UIElement
will render according to its OnRender
implementation, whereas other items will be rendered using ToString
.
To define static content for a ComboBox
, include the content elements directly within the XAML ComboBox
element. You can include controls directly within the ComboBox
element XAML or, for greater control over the formatting of the contained items, wrap them in a ComboBoxItem
element.
To determine the current ComboBox
selection, you can query the SelectedItem
property of the ComboBox
at an appropriate time (that is, when the user submits or closes the form containing the ComboBox
). If the SelectedItem
property is Null
and IsEditable
is True
, you can determine whether the user has typed a value into the ComboBox
via the ComboBox.Text
property.
To handle ComboBox
selection more dynamically, you can handle the events raised by the ComboBox
and the individual ComboBoxItem
objects. On a change in selection, the ComboBox
raises a SelectionChanged
event that passes details of the item currently selected. Alternatively, when newly selected, a ComboBoxItem
raises a Selected
event, which contains details that allow you to identify the individual ComboBoxItem
that raised the event.
The following XAML defines a ComboBox
containing five ComboBoxItem
items and sets the third item to be selected using the IsSelected
property. The Get Selected button causes the example to determine which ComboBoxItem
is currently selected. As the user changes their selection, the example handles both ComboBoxItem.Selected
and ComboBox.SelectionChanged
events, displaying messages about which item is now selected.
<Window x:Class="Recipe_03_24.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 3_24" Height="100" Width="300"> <StackPanel> <ComboBox Name="comboBox" IsEditable="True" Margin="5" SelectionChanged="ComboBox_SelectionChanged"> <ComboBoxItem Content="ComboBox Item 1" Selected="ComboBoxItem_Selected" /> <ComboBoxItem Content="ComboBox Item 2" Selected="ComboBoxItem_Selected" /> <ComboBoxItem Content="ComboBox Item 3" Selected="ComboBoxItem_Selected" IsSelected="True"/> <ComboBoxItem Content="ComboBox Item 4" Selected="ComboBoxItem_Selected" /> <ComboBoxItem Content="ComboBox Item 5" Selected="ComboBoxItem_Selected" /> </ComboBox> <Button Content="Get Selected" Margin="5" Width="100" Click="Button_Click" /> </StackPanel> </Window>
The following code-behind contains the event handlers used by the preceding XAML:
using System; using System.Windows; using System.Windows.Controls; namespace Recipe_03_24 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); } // Gets the currently selected ComboBoxItem when the user // clicks the Button. If the SelectedItem of the ComboBox // is null, the code checks to see if the user has entered // text into the ComboBox instead. private void Button_Click(object sender, RoutedEventArgs e) { ComboBoxItem item = comboBox.SelectedItem as ComboBoxItem; if (item != null) { MessageBox.Show("Current item: " + item.Content, Title); } else if (!String.IsNullOrEmpty(comboBox.Text)) { MessageBox.Show("Text entered: " + comboBox.Text, Title); } } // Handles ComboBox SelectionChanged events. private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { // Do not handle events until Window is fully initialized. if (!IsInitialized) return; ComboBoxItem item = comboBox.SelectedItem as ComboBoxItem;
if (item != null) { MessageBox.Show("Selected item: " + item.Content, Title); } } // Handles ComboBoxItem Selected events. private void ComboBoxItem_Selected(object sender, RoutedEventArgs e) { // Do not handle events until Window is fully initialized. if (!IsInitialized) return; ComboBoxItem item = e.OriginalSource as ComboBoxItem; if (item != null) { MessageBox.Show(item.Content + " was selected.", Title); } } } }
WPF makes many things trivial that are incredibly complex to do in Windows Forms programming. One of those things is the ability to rotate controls to any orientation yet still have them appear and function as normal. Admittedly, it is not every day you need to display a rotated control, but when you do, you will appreciate how easy it is in WPF. Most frequently, the ability to rotate controls becomes important when you start to customize the appearance of standard controls using templates (as discussed in Chapter 6) or when you create custom controls (as discussed in Chapter 4).
Both the LayoutTransform
and RenderTransform
have a RotateTransform
property, in which you specify the angle in degrees you want your control rotated by. Positive values rotate the control clockwise, and negative values rotate the control counterclockwise. The rotation occurs around the point specified by the CenterX
and CenterY
properties. These properties refer to the coordinate space of the control that is being transformed, with (0,0) being the upper-left corner. Alternatively, you can use the RenderTransformOrigin
property on the control you are rotating; this allows you to specify a point a relative distance from the origin using values between 0 and 1 that WPF automatically converts to specific values.
The difference between the LayoutTransform
and RenderTransform
is the order in which WPF executes the transformation. WPF executes the LayoutTransform
as part of the layout processing, so the rotated position of the control affects the layout of controls around it. The RenderTransform
, on the other hand, is executed after layout is determined, which means the rotated control does not affect the positioning of other controls and can therefore end up appearing partially over or under other controls.
The following XAML demonstrates a variety of rotated controls. The bottom left shows the difference in behavior of a LayoutTransform
compared to a RenderTransform
(shown in the bottom-right corner). See Figure 3-15.
<Window x:Class="Recipe_03_25.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WPF Recipes 3_25" Height="350" Width="400"> <Grid ShowGridLines="True"> <Grid.RowDefinitions> <RowDefinition MinHeight="140" /> <RowDefinition MinHeight="170" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <TextBox Grid.Row="0" Grid.Column="0" Height="23" HorizontalAlignment="Center" Text="An upside down TextBox." Width="140"> <TextBox.LayoutTransform> <RotateTransform Angle="180"/> </TextBox.LayoutTransform> </TextBox> <Button Content="A rotated Button" Grid.Row="0" Grid.Column="1" Height="23" Width="100"> <Button.LayoutTransform> <RotateTransform Angle="-120"/> </Button.LayoutTransform> </Button> <StackPanel Grid.Row="1" Grid.Column="0" > <TextBlock HorizontalAlignment="Center" Margin="5"> Layout Tranform </TextBlock> <Button Margin="5" Width="100">Top Button</Button>
<Button Content="Middle Button" Margin="5" Width="100"> <Button.LayoutTransform> <RotateTransform Angle="30" /> </Button.LayoutTransform> </Button> <Button Margin="5" Width="100">Bottom Button</Button> </StackPanel> <StackPanel Grid.Row="1" Grid.Column="1" > <TextBlock HorizontalAlignment="Center" Margin="5"> Render Tranform </TextBlock> <Button Margin="5" Width="100">Top Button</Button> <Button Content="Middle Button" Margin="5" RenderTransformOrigin="0.5, 0.5" Width="100"> <Button.RenderTransform> <RotateTransform Angle="30" /> </Button.RenderTransform> </Button> <Button Margin="5" Width="100">Bottom Button</Button> </StackPanel> </Grid> </Window>
Figure 3-15 shows a variety of rotated controls.