WPF provides developers with unparalleled options in customizing and modifying the visual appearance of controls.
In the first instance, you can simply assign custom values to the appearance properties of the built-in WPF controls. For example, you could set the Background
property of a System.Windows.Controls.Button
control to silver and the FontWeight
property to bold.
If you wanted to reuse this Button
control in different places within your application, you could define an application-wide System.Windows.Style
to set these property values and then apply this Style
to all Button
objects automatically (see Chapter 6).
Alternatively, suppose you wanted every Button
to display an image surrounded by a border. The content model in WPF makes this easy. Simply declare a System.Windows.Controls.Border
and a System.Windows.Controls.Image
in the inline XAML for your button. If you wanted to reuse this type of button across your application, you could define a System.Windows.Controls.ControlTemplate
with an application-wide Style
(see Chapter 6).
These mechanisms for changing the appearance offer a great deal of power and flexibility to change individual controls and elements. However, when you want to create reusable groups of controls and functionality, you need to create a user or custom control. User controls are ideal for situations where you need to encapsulate a group of visual elements and behaviors into one component that can be reused in different parts of your application.
However, because user controls encapsulate much of their visual appearance, you cannot change their style and control template in different contexts. This is where custom controls come in. They separate their interaction logic from their visual implementation, allowing other developers to reuse them within different applications and to customize their appearance themselves.
Finally, you can also create custom-drawn controls and render them to the screen using custom drawing logic.
This chapter focuses on how to create user and custom controls and custom-drawn elements, and it demonstrates some examples of all these types of controls. The recipes in this chapter describe how to:
Create a user control (recipe 4-1)
Incorporate it into the content model in WPF (recipe 4-2)
Add properties, events, and commands to user controls (recipes 4-3, 4-4, 4-5, and 4-6)
Set design-mode behavior in a user control (recipe 4-7)
Create a lookless custom control (recipes 4-9 and 4-10)
Support UI automation in a custom control (recipe 4-11)
Create a custom-drawn element (recipe 4-12)
Create a numeric text box control (recipe 4-13)
Create scrollable, zoomable, and draggable canvas controls (recipes 4-13, 4-14, and 4-15)
You need to create a user control to reuse part of the UI in different contexts within your application, without duplicating appearance or behavior logic.
Create a class that derives from System.Windows.Controls.UserControl
or System.Windows.Controls.ContentControl
, and place the visual elements you need in your reusable component in the XAML for the user control. Put custom logic in the code-behind for the UserControl
to control custom behavior and functionality.
A control that derives from UserControl
is useful for creating a reusable component within an application but is less useful if it can be shared by other applications, software teams, or even companies. This is because a control that derives from UserControl
cannot have its appearance customized by applying custom styles and templates in the consumer. If this is needed, then you need to use a custom control, which is a control that derives from System.Windows.UIElement.FrameworkElement
or System.Windows.Controls.Control
.
User controls provide a simple development model that is similar to creating WPF elements in standard windows. They are ideal for composing reusable UI controls out of existing components or elements, provided you do not need to allow them to be extensively customized by consumers of your control. If you do want to provide full control over the visual appearance of your control, or allow it to be a container for other controls, then a custom control is more suitable. Custom controls are covered later in this chapter.
To create a user control, right-click your project in Visual Studio, click Add, and then click the User Control option in the submenu. This creates a new XAML file and a corresponding code-behind file. The root element of the new XAML file is a System.Windows.Controls.UserControl
class. Inside this XAML file, you can create the UI elements that compose your control.
The following example demonstrates how to create a FileInputControl
, a custom reusable user control to encapsulate the functionality of browsing to a file and displaying the file name. This user control is then used in a window, as shown in Figure 4-1.
The XAML for the FileInputControl
is as follows:
<UserControl x:Class="Recipe_04_01.FileInputControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <DockPanel> <Button DockPanel.Dock="Right" Margin="2,0,0,0" Click="BrowseButton_Click"> Browse... </Button> <TextBox x:Name="txtBox" IsReadOnly="True" /> </DockPanel> </UserControl>
The code-behind for the control is as follows:
using System.Windows.Controls; using Microsoft.Win32; namespace Recipe_04_01 { public partial class FileInputControl : UserControl { public FileInputControl() { InitializeComponent(); } private void BrowseButton_Click( object sender, System.Windows.RoutedEventArgs e) { OpenFileDialog dlg = new OpenFileDialog(); if(dlg.ShowDialog() == true) { this.FileName = dlg.FileName; } }
public string FileName { get { return txtBox.Text; } set { txtBox.Text = value; } } } }
The XAML for the window that consumes this user control is as follows:
<Window x:Class="Recipe_04_01.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Recipe_04_01="clr-namespace:Recipe_04_01;assembly=" Title="WPF Recipes 4_01" Height="72" Width="300"> <Grid> <Recipe_04_01:FileInputControl Margin="8" /> </Grid> </Window>
You need to specify the Content
property of your System.Windows.Controls.UserControl
so that when the consumer defines an instance of your UserControl
, the consumer can set the value of this property as the inline content.
Use the System.Windows.Markup.ContentPropertyAttribute
attribute to decorate your user control's class declaration, and specify the name of the property you want to designate as the Content
property.
Because UserControl
ultimately inherits from System.Windows.Controls.ContentControl
, the Content
property is the default property to receive the value of any inline XAML declarations. For example, a consumer of a FileInputControl
(see the following code) might declare the instance of the control with the following XAML:
<local:FileInputControl>c: eadme.txt</local:FileInputControl>
Without the ContentProperty
attribute on the user control, this XAML declaration would replace the control elements inside the FileInputControl
and simply display a string. The ContentProperty
attribute tells the user control to instead use another property to set whenever a value is passed as inline content.
An explicit setting of the Content
property would still replace the visual elements inside the control, for example, <local:FileInputControl Content="c:
eadme.txt" />
. If this is a real possibility and you need to prevent this case as well, then you should create a custom control rather than a user control and specify the visual elements of the control in a control template. In this case, you could use a template binding to bind TextBox.Text
to the Content
property.
The following example demonstrates how to set the Content
property of a UserControl
. It defines a UserControl
called FileInputControl
that can be used to browse to a file using the Microsoft.Win32.OpenFileDialog
and to display the file name in a System.Windows.Controls.TextBox
. In the code-behind, the FileInputControl
class is decorated with the ContentProperty
attribute and passed the name of the FileName
property in the parameter of its constructor. The user control is then used in a window called Window1
. In the XAML for this window, an initial file name is set by specifying the text as the inline content.
The XAML for the FileInputControl
is as follows:
<UserControl x:Class="Recipe_04_02.FileInputControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <DockPanel> <Button DockPanel.Dock="Right" Margin="2,0,0,0" Click="BrowseButton_Click"> Browse... </Button>
<TextBox x:Name="txtBox" IsReadOnly="True" /> </DockPanel> </UserControl>
The code-behind for the FileInputControl
is as follows:
using System.Windows.Controls; using System.Windows.Markup; using Microsoft.Win32; namespace Recipe_04_02 { /// <summary> /// ContentProperty attribute /// </summary> [ContentProperty("FileName")] public partial class FileInputControl : UserControl { public FileInputControl() { InitializeComponent(); } private void BrowseButton_Click( object sender, System.Windows.RoutedEventArgs e) { OpenFileDialog dlg = new OpenFileDialog(); if(dlg.ShowDialog() == true) { this.FileName = dlg.FileName; } } public string FileName { get { return txtBox.Text; } set { txtBox.Text = value; } } } }
The following XAML shows how to use the FileInputControl
in a window and declares a file name in the inline content of the declaration, which then automatically sets the value of the FileName
property:
<Window x:Class="Recipe_04_02.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Recipe_04_02="clr-namespace:Recipe_04_02;assembly=" Title="WPF Recipes 4_02" Height="72" Width="300"> <Grid> <Recipe_04_02:FileInputControl Margin="8"> c: eadme.txt </Recipe_04_02:FileInputControl> </Grid> </Window>
You need to allow internal aspects of the behavior and appearance of your System.Windows.Controls.UserControl
to be changed by the control consumer and to be accessible to WPF features such as data binding, styles, and animations.
Create a standard .NET property in the code-behind of your user control, and use it in the internal configuration of the control to determine aspects of behavior or appearance. Create a static System.Windows.DependencyProperty
field, with the word Property added to the end of your property name, and use it to back the standard .NET property. Register the dependency property in the static constructor of the user control.
By using a DependencyProperty
to hold the value of behavioral or appearance properties of your user control, you can use the full range of WPF features such as data binding, styling, and animations to interact with these values.
The following example demonstrates how to use DependencyProperties
to interact with a custom PageNumberControl
that displays a descriptive page number string, for example, "Page 2 of 8."
The user control exposes Count
and Total
dependency properties in the code-behind, which are then used in the control's XAML to construct the display string.
using System.Windows; using System.Windows.Controls; namespace Recipe_04_03 { /// <summary> /// Show the page number text in the format: /// <!-- Page <Current> of <Total> /// </summary> public partial class PageNumberControl : UserControl { public PageNumberControl() { InitializeComponent(); } public int Current { get { return (int) GetValue(CurrentProperty); } set { if(value <= Total && value >= 0) { SetValue(CurrentProperty, value); } } } public static readonly DependencyProperty CurrentProperty = DependencyProperty.Register("Current", typeof(int), typeof(PageNumberControl), new PropertyMetadata(0)); public int Total { get { return (int) GetValue(TotalProperty); }
set { if(value >= Current && value >= 0) { SetValue(TotalProperty, value); } } } public static readonly DependencyProperty TotalProperty = DependencyProperty.Register("Total", typeof(int), typeof(PageNumberControl), new PropertyMetadata(0)); } }
The XAML for the PageNumberControl
is as follows:
<UserControl x:Class="Recipe_04_03.PageNumberControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Name="rootControl" Height="100" Width="200"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="10"> <!-- Show the page number text in the format: --> <!-- Page <Current> of <Total> --> <TextBlock Text="Page "/> <TextBlock Text="{Binding ElementName=rootControl, Path=Current}" /> <TextBlock Text=" of "/> <TextBlock Text="{Binding ElementName=rootControl, Path=Total}" /> </StackPanel> </UserControl>
The following XAML shows how to use the PageNumberControl
in a window and contains buttons that, when clicked, change the Current
and Total
properties and automatically update the display. Figure 4-2 shows the resulting window.
<Window x:Class="Recipe_04_03.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Recipe_04_03="clr-namespace:Recipe_04_03;assembly=" Title="WPF Recipes 4_03" Height="120" Width="260"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="0.25*" /> <RowDefinition Height="0.75*" /> </Grid.RowDefinitions> <Recipe_04_03:PageNumberControl x:Name="pageNumberControl" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="4" Current="2" Total="5" /> <GroupBox Header="Test" Margin="4" Grid.Row="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> <StackPanel Orientation="Horizontal"> <Button Click="DecreaseCurrent_Click" Margin="4"> Current-- </Button> <Button Click="IncreaseCurrent_Click" Margin="4"> Current++ </Button> <Button Click="DecreaseTotal_Click" Margin="4"> Total-- </Button> <Button Click="IncreaseTotal_Click" Margin="4"> Total++ </Button>
</StackPanel> </GroupBox> </Grid> </Window>
The code in the window's code-behind handles the click events of the buttons and simply increments or decrements the PageNumberControl
's dependency properties:
using System.Windows; namespace Recipe_04_03 { public partial class Window1 : Window { public Window1() { InitializeComponent(); } private void DecreaseCurrent_Click(object sender, RoutedEventArgs e) { pageNumberControl.Current--; } private void IncreaseCurrent_Click(object sender, RoutedEventArgs e) { pageNumberControl.Current++; } private void DecreaseTotal_Click(object sender, RoutedEventArgs e) { pageNumberControl.Total--; } private void IncreaseTotal_Click(object sender, RoutedEventArgs e) { pageNumberControl.Total++; } } }
You need to notify the control consumer when something happens in your System.Windows.Controls.UserControl
and allow it to use this event with WPF features such as triggers, animations, and event bubbling and tunneling.
Create a static property of type System.Windows.RoutedEvent
in the code-behind of your user control, with the word Event added to the end of the name of the event you want to raise, and register it with the EventManager
:
public static RoutedEvent SearchChangedEvent = EventManager.RegisterRoutedEvent( "SearchChanged", RoutingStrategy.Bubble, typeof(SearchChangedEventHandler), typeof(SearchControl));
Then use the RaiseEvent
method of the base System.Windows.UIElement
class to notify the consumer of the user control:
SearchChangedEventArgs args = new SearchChangedEventArgs(txtSearch.Text); args.RoutedEvent = SearchChangedEvent; RaiseEvent(args);
By using a RoutedEvent
to wrap an ordinary .NET event, you can expose this event to the consumer of your user control and allow it to use the full range of WPF features such as triggers, animations, and event bubbling and tunneling.
The following example demonstrates how to use a RoutedEvent
to notify the control consumer when the search text is changed within a custom search user control. The SearchControl
defined next contains a System.Windows.Controls.TextBox
for entering a new search string, as well as a System.Windows.Controls.Button
to raise a SearchChanged
event. The SearchChanged
event is also raised when the Enter key is pressed within the search TextBox
. An instance of this SearchControl
is defined in a window, and an event handler is added to the SearchChanged
events, which displays the new search text in a System.Windows.MessageBox
.
The XAML for the SearchControl
user control is as follows:
<UserControl x:Class="Recipe_04_04.SearchControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Height="Auto" Width="Auto"> <UserControl.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="SearchImage.xaml"/> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </UserControl.Resources> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="48"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <TextBlock> Enter your search text: </TextBlock> <TextBox x:Name="txtSearch" KeyDown="txtSearch_KeyDown" Grid.Row="1"/> <Button Grid.Column="1" Grid.RowSpan="2" Margin="4,0,0,0" Click="SearchButton_Click"> <Image Source="{StaticResource SearchImage}"/> </Button> </Grid> </UserControl>
The code-behind declares the SearchChanged RoutedEvent
:
using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace Recipe_04_04 { /// <summary> /// A reusable Search UserControl that raises a /// RoutedEvent when a new search is requested. /// </summary> public partial class SearchControl : UserControl { public SearchControl() { InitializeComponent(); } public static RoutedEvent SearchChangedEvent = EventManager.RegisterRoutedEvent( "SearchChanged", RoutingStrategy.Bubble, typeof(SearchChangedEventHandler), typeof(SearchControl)); /// <summary> /// The SearchChanged event that can be handled /// by the consuming control. /// </summary> public event SearchChangedEventHandler SearchChanged { add { AddHandler(SearchChangedEvent, value); } remove { RemoveHandler(SearchChangedEvent, value); } } private void SearchButton_Click( object sender, RoutedEventArgs e) { // Raise the SearchChanged RoutedEvent when // the Search button is clicked
OnSearchChanged(); } private void txtSearch_KeyDown( object sender, KeyEventArgs e) { if(e.Key == Key.Enter) { // Raise the SearchChanged RoutedEvent when // the Enter key is pressed in the Search TextBox OnSearchChanged(); } } private void OnSearchChanged() { SearchChangedEventArgs args = new SearchChangedEventArgs(txtSearch.Text); args.RoutedEvent = SearchChangedEvent; RaiseEvent(args); } } public delegate void SearchChangedEventHandler( object sender, SearchChangedEventArgs e); public class SearchChangedEventArgs : RoutedEventArgs { private readonly string searchText; public SearchChangedEventArgs( string searchText) { this.searchText = searchText; } public string SearchText { get { return searchText; } } } }
The following XAML shows how to use the SearchControl
in a window and adds an event handler to the SearchChanged
event. Figure 4-3 shows the resulting window.
<Window x:Class="Recipe_04_04.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Recipe_04_04="clr-namespace:Recipe_04_04;assembly=" Title="WPF Recipes 4_04" Height="86" Width="240"> <Grid> <Recipe_04_04:SearchControl Margin="8" SearchChanged="SearchControl_SearchChanged"/> </Grid> </Window>
The code in the code-behind for the window handles the SearchControl
's SearchChanged RoutedEvent
and shows the new search text in a message box:
using System.Windows; using Recipe_04_04; namespace Recipe_04_04 { /// <summary> /// This window creates an instance of SearchControl /// and handles the SearchChanged event, showing the /// new search text in a message box /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); } private void SearchControl_SearchChanged( object sender, SearchChangedEventArgs e) { MessageBox.Show("New Search: " + e.SearchText); } } }
You need to support common application commands in your System.Windows.Controls.UserControl
, such as Undo, Redo, Open, Copy, Paste, and so on, so that your control can respond to a command without needing any external code.
Use the System.Windows.Input.CommandManager
to register an instance of the System.Windows.Input.CommandBinding
class for each member of System.Windows.Input.ApplicationCommands
you need to support in your user control. The CommandBinding
specifies the type of command you want to receive notification of, specifies an event handler to determine when the command can be executed, and specifies another event handler to be called when the command is executed. These event handlers are called the CanExecute
and Executed
event handlers.
There are many predefined commands in WPF to support common scenarios, grouped as static properties on five different classes, mostly in the System.Windows.Input
namespace, as shown in Table 4-1.
Table 4-1. Predefined Common Commands
Value | Description |
---|---|
| Common commands for an application, for example, |
| Common commands for user interface components, for example, |
| Common commands used for multimedia, for example, |
| A set of commands used for page navigation, for example, |
| A set of commands for editing documents, for example, |
Each command has a System.Windows.Input.InputGestureCollection
that specifies the possible mouse or keyboard combinations that trigger the command. These are defined by the command itself, which is why you are able to register to receive these automatically by registering a CommandBinding
for a particular command.
A CommandBinding
for a particular command registers the CanExecute
and Executed
handlers so that the execution and the validation of the execution of the command are routed to these event handlers.
The following example creates a UserControl
called FileInputControl
that can be used to browse to a file using Microsoft.Win32.OpenFileDialog
and display the file name in a System.Windows.Controls.TextBox
.
It registers a CommandBinding
for two application commands, Open and Find. When the user control has focus and the keyboard shortcuts for the Open and Find command (Ctrl+O and Ctrl+F, respectively) are used, the Executed
event handler for the respective command is invoked.
The Executed
event handler for the Find command launches the OpenFileDialog
, as if the user has clicked the Browse button. This command can always be executed, so the CanExecute
event handler simply sets the CanExecute
property of System.Windows.Input.CanExecuteRoutedEventArgs
to True
.
The Executed
event handler for the Open command launches the file that is currently displayed in the TextBox
. Therefore, the CanExecute
event handler for this command sets the CanExecuteRoutedEventArgs
to True
only if there is a valid FileName
.
The XAML for the FileInputControl
is as follows:
<UserControl x:Class="Recipe_04_05.FileInputControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <DockPanel> <Button DockPanel.Dock="Right" Margin="2,0,0,0" Click="BrowseButton_Click"> Browse... </Button> <TextBox x:Name="txtBox" /> </DockPanel> </UserControl>
The code-behind for the FileInputControl
is as follows:
using System.Diagnostics; using System.IO; using System.Windows.Controls; using System.Windows.Input;
using Microsoft.Win32; namespace Recipe_04_05 { public partial class FileInputControl : UserControl { public FileInputControl() { InitializeComponent(); // Register command bindings // ApplicationCommands.Find CommandManager.RegisterClassCommandBinding( typeof(FileInputControl), new CommandBinding( ApplicationCommands.Find, FindCommand_Executed, FindCommand_CanExecute)); // ApplicationCommands.Open CommandManager.RegisterClassCommandBinding( typeof(FileInputControl), new CommandBinding( ApplicationCommands.Open, OpenCommand_Executed, OpenCommand_CanExecute)); } #region Find Command private void FindCommand_CanExecute( object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = true; } private void FindCommand_Executed( object sender, ExecutedRoutedEventArgs e) { DoFindFile(); } #endregion
#region Open Command private void OpenCommand_CanExecute( object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = !string.IsNullOrEmpty(this.FileName) && File.Exists(this.FileName); } private void OpenCommand_Executed( object sender, ExecutedRoutedEventArgs e) { Process.Start(this.FileName); } #endregion private void BrowseButton_Click( object sender, System.Windows.RoutedEventArgs e) { DoFindFile(); } private void DoFindFile() { OpenFileDialog dlg = new OpenFileDialog(); if(dlg.ShowDialog() == true) { this.FileName = dlg.FileName; } } public string FileName { get { return txtBox.Text; }
set { txtBox.Text = value; } } } }
The following XAML shows how to use the FileInputControl
in a window.
If the TextBox
has the focus, then pressing the keyboard shortcut Ctrl+F will automatically open the OpenFileDialog
. If a file is selected and a valid file name appears in the TextBox
, then the shortcut Ctrl+O will launch it.
<Window x:Class="Recipe_04_05.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Recipe_04_05="clr-namespace:Recipe_04_05;assembly=" Title="WPF Recipes 4_05" Height="72" Width="300"> <Grid> <Recipe_04_05:FileInputControl Margin="8"/> </Grid> </Window>
You need to add custom commands to your System.Windows.Controls.UserControl
to enable consumers of your control to bind to and execute units of functionality and custom behavior.
Create a static System.Windows.Input.RoutedCommand
property in the code-behind of your user control. In the static constructor, initialize a class-level instance of this RoutedCommand
and give it a name, the type of your user control, and any input gestures you want to associate with it. A System.Windows.Input.InputGesture
associates keyboard and mouse inputs with your commands so that when a certain key combination is pressed, for example, Ctrl+W, the System.Windows.Input.CommandManager
will execute your command.
Create an instance of the System.Windows.Input.CommandBinding
class for your RoutedCommand
, and specify an event handler to determine when the command can be executed and another event handler to be called when the command is executed.
Consumers of your control can now define visual elements that data bind directly to your static command property.
Three types of command classes in WPF support data binding, and they can all be found in the System.Windows.Input
namespace (see Table 4-2).
Table 4-2. Three Types of WPF Commands
Value | Description |
---|---|
| The basic command interface in WPF. This exposes two methods, |
| Implements |
| Derives from |
Creating a RoutedCommand
or RoutedUICommand
allows you to expose custom command functionality and automatically plug in to the event tunneling and bubbling mechanisms in WPF that route a consumer of your command to your custom event handlers.
The following example creates a user control called PageNumberControl
that displays a descriptive page number string, for example, "Page 2 of 8."
The code-behind for the user control exposes a public RoutedCommand
property called IncreaseTotal
, which increases the total number of pages when executed. The static constructor initializes a CommandBinding
that binds this command to the CanExecute
and Executed
event handlers.
The control is then consumed in a window, which demonstrates how to bind a System.Windows.Controls.Button
to the custom command in XAML.
The XAML for the PageNumberControl
is as follows:
<UserControl x:Class="Recipe_04_06.PageNumberControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Name="rootControl" Height="100" Width="200"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="10"> <!-- Show the page number text in the format: --> <!-- Page <Current> of <Total> --> <TextBlock Text="Page "/> <TextBlock
Text="{Binding ElementName=rootControl, Path=Current}" /> <TextBlock Text=" of "/> <TextBlock Text="{Binding ElementName=rootControl, Path=Total}" /> </StackPanel> </UserControl>
The code-behind for the PageNumberControl
is as follows:
using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace Recipe_04_06 { /// <summary> /// Show the page number text in the format: /// <!-- Page <Current> of <Total> /// </summary> public partial class PageNumberControl : UserControl { private static RoutedCommand increaseTotalCommand; public static RoutedCommand IncreaseTotal { get { return increaseTotalCommand; } } static PageNumberControl() { // Create an input gesture so that the command // is executed when the Add (+) key is pressed InputGestureCollection myInputs = new InputGestureCollection(); myInputs.Add( new KeyGesture( Key.Add, ModifierKeys.Control));
// Create a RoutedCommand increaseTotalCommand = new RoutedCommand( "IncreaseTotal", typeof(PageNumberControl), myInputs); // Create a CommandBinding, specifying the // Execute and CanExecute handlers CommandBinding binding = new CommandBinding(); binding.Command = increaseTotalCommand; binding.Executed += new ExecutedRoutedEventHandler(binding_Executed); binding.CanExecute += new CanExecuteRoutedEventHandler(binding_CanExecute); // Register the CommandBinding CommandManager.RegisterClassCommandBinding( typeof(PageNumberControl), binding); } public PageNumberControl() { InitializeComponent(); } static void binding_CanExecute( object sender, CanExecuteRoutedEventArgs e) { // The command can execute as long as the // Total is less than the maximum integer value PageNumberControl control = (PageNumberControl) sender; e.CanExecute = control.Total < int.MaxValue; } private static void binding_Executed( object sender, ExecutedRoutedEventArgs e) { // Increment the value of Total when // the command is executed PageNumberControl control = (PageNumberControl) sender; control.Total++; }
public int Current { get { return (int) GetValue(CurrentProperty); } set { if(value <= Total && value >= 0) { SetValue(CurrentProperty, value); } } } public static readonly DependencyProperty CurrentProperty = DependencyProperty.Register("Current", typeof(int), typeof(PageNumberControl)); public int Total { get { return (int) GetValue(TotalProperty); } set { if(value >= Current && value >= 0) { SetValue(TotalProperty, value); } } } public static readonly DependencyProperty TotalProperty = DependencyProperty.Register("Total", typeof(int), typeof(PageNumberControl)); } }
The following XAML shows how to use the PageNumberControl
in a window, with a Button
control that data binds to the IncreaseTotal
command:
<Window x:Class="Recipe_04_06.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Recipe_04_06="clr-namespace:Recipe_04_06;assembly=" Title="WPF Recipes 4_06" Height="120" Width="260"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="0.25*" /> <RowDefinition Height="0.75*" /> </Grid.RowDefinitions> <Recipe_04_06:PageNumberControl x:Name="pageNumberControl" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="4" Current="2" Total="5" /> <GroupBox Header="Test" Margin="4" Grid.Row="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> <StackPanel Orientation="Horizontal"> <Button Command="Recipe_04_06:PageNumberControl.IncreaseTotal" CommandTarget= "{Binding ElementName=pageNumberControl}" Margin="4"> Total++ </Button> </StackPanel> </GroupBox> </Grid> </Window>
Figure 4-4 shows the resulting window.
You need to determine whether your System.Windows.Controls.UserControl
is running in design mode (for example, being displayed in the Visual Studio or Expression Blend designer) and set specific behavior.
Use the System.ComponentModel.DesignerProperties.GetIsInDesignMode
method in the constructor for your user control.
The static System.ComponentModel.DesignerProperties
exposes an IsInDesignMode
attached property that returns true if the control is currently running in design mode.
Setting specific behavior for your user control when it is in design mode can be useful for priming your user control with the kind of data or property values that would normally be set only at runtime. This enables your control to display itself realistically for designers, even when there is no actual data or property values available for it during design.
The following example demonstrates a simple user control called MyUserControl
that contains a button with some text as Content
. The constructor for the control calls the GetIsInDesignMode
method and changes the button's Text
property depending on whether it is currently being displayed in design mode.
The XAML for the control is as follows:
<UserControl x:Class="Recipe_04_07.MyUserControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Grid> <Button x:Name="btnMode"> Set Design Mode Behavior </Button> </Grid> </UserControl>
The code-behind for the control calls the GetIsInDesignMode
:
using System.Windows.Controls; namespace Recipe_04_07 { public partial class MyUserControl : UserControl { public MyUserControl() { InitializeComponent(); // Call the GetIsInDesignMode method if(System.ComponentModel.DesignerProperties.GetIsInDesignMode(this)) { btnMode.Content = "In Design Mode"; } else { btnMode.Content = "Runtime"; } } } }
Figure 4-5 shows the control when displayed by the Visual Studio designer.
Figure 4-6 shows the control when displayed at runtime.
You need to create a custom control that encapsulates functionality and behavior logic but that can have its visual appearance changed by consumers. For example, you need consumers to be able to change the style, template, or visual theme of your control for a particular context, application, or operating system theme.
Create a lookless custom control class that contains interaction and behavior logic but little or no assumptions about its visual implementation. Then declare the default visual elements for it in a control template within a default style.
When creating the code for a custom control, you need to ensure it is lookless and assumes as little as possible about the actual implementation of the visual elements in the control template, because it could well be different across different consumers. This means ensuring that the UI is decoupled from the interaction logic by using commands and bindings, avoiding event handlers, and referencing elements in the ControlTemplate
whenever possible.
The first step in creating a lookless custom control is choosing which control to inherit from. You should derive from the most basic option available to you, because it provides the minimum required functionality and gives the control consumer the maximum freedom. On the other hand, it also makes sense to leverage as much built-in support as possible by deriving from an existing WPF control if it possesses similar behavior and functionality to your custom control. For example, if your control will be clickable, then it might make sense to inherit from the Button
class. If your control is not only clickable but also has the notion of being in a selected or unselected state, then it might make sense to inherit from ToggleButton
.
Some of the main base classes you can choose from are listed in Table 4-3.
Table 4-3. Main Base Classes for Creating a Custom Control
Name | Description |
---|---|
| This is usually the most basic element from which you will derive. Use this when you need to draw your own element by overriding the |
|
|
| This inherits from |
| This has a property called |
| This wraps another control to decorate it with a particular visual effect or feature. For example, the |
After choosing an appropriate base class for your custom control, you can create the class and put the logic for the interaction, functionality, and behavior of your control in the custom control class.
However, don't define your visual elements in a XAML file for the class, like you would with a user control. Instead, put the default definition of visual elements in a System.Windows.ControlTemplate
, and declare this ControlTemplate
in a default System.Windows.Style
.
The next step is to specify that you will be providing this new style; otherwise, your control will continue to use the default template of its base class. You specify this by calling the OverrideMetadata
method of DefaultStyleKeyProperty
in the static constructor for your class.
Next, you need to place your style in the Generic.xaml
resource dictionary in the Themes
subfolder of your project. This ensures it is recognized as the default style for your control. You can also create other resource dictionaries in this subfolder, which enables you to target specific operating systems and give your custom controls a different visual appearance for each one.
When a custom control library contains several controls, it is often better the keep their styles separate instead of putting them all in the same Generic.xaml
resource dictionary. You can use resource dictionary merging to keep each style in a separate resource dictionary file and then merge them into the main Generic.xaml
one.
The custom style and template for your control must use the System.Type.TargetType
attribute to attach it to the custom control automatically.
In Visual Studio, when you add a new WPF custom control to an existing project, it does a number of the previous steps for you. It automatically creates a code file with the correct call to DefaultStyleKeyproperty.OverrideMetadata
. It creates the Themes
subfolder and Generic.xaml
resource dictionary if they don't already exist, and it defines a placeholder Style
and ControlTemplate
in there.
When creating your custom control class and default control template, you have to remember to make as few assumptions as possible about the actual implementation of the visual elements. This is in order to make the custom control as flexible as possible and to give control consumers as much freedom as possible when creating new styles and control templates.
You can enable this separation between the interaction logic and the visual implementation of your control in a number of ways.
First, when binding a property of a visual element in the default ControlTemplate
to a dependency property of the control, use the System.Windows.Data.RelativeSource
property instead of naming the element and referencing it via the ElementName
property.
Second, instead of declaring event handlers in the XAML for the template, for example, for the Click
event of a Button, either add the event handler programmatically in the control constructor or bind to commands. If you choose to use event handlers and bind them program-matically, override the OnApplyTemplate
method and locate the controls dynamically.
Furthermore, give names only to those elements that without which the control would not be able to function as intended. By convention, give these intrinsic elements the name PART_ElementName
so that they can be identified as part of the public interface for your control. For example, it is intrinsic to a ProgressBar
that it has a visual element representing the total value at completion and a visual element indicating the relative value of the current progress. The default ControlTemplate
for the System.Windows.Controls.ProgressBar
therefore defines two named elements, PART_Track
and PART_Indicator
. These happen to be Border
controls in the default template, but there is no reason why a control consumer could not provide a custom template that uses different controls to display these functional parts.
If your control requires named elements, as well as using the previously mentioned naming convention, apply the System.Windows.TemplatePart
attribute to your control class, which documents and signals this requirement to users of your control and to design tools such as Expression Blend.
The following code example demonstrates how to separate the interaction logic and the visual implementation using these methods.
The following example demonstrates how to create a lookless custom control to encapsulate the functionality of browsing to a file and displaying the file name. Figure 4-7 shows the control in use.
The FileInputControl
class derives from Control
and uses the TemplatePart
attribute to signal that it expects a Button
control called PART_Browse
. It overrides the OnApplyTemplate
method and calls GetTemplateChild
to find the button defined by its actual template. If this exists, it adds an event handler to the button's Click
event.
The code for the control is as follows:
using System.Windows; using System.Windows.Controls; using System.Windows.Markup; using Microsoft.Win32; namespace Recipe_04_08 { [TemplatePart(Name = "PART_Browse", Type = typeof(Button))] [ContentProperty("FileName")] public class FileInputControl : Control { static FileInputControl() { DefaultStyleKeyProperty.OverrideMetadata( typeof(FileInputControl), new FrameworkPropertyMetadata( typeof(FileInputControl))); } public override void OnApplyTemplate() { base.OnApplyTemplate(); Button browseButton = base.GetTemplateChild("PART_Browse") as Button; if(browseButton != null) browseButton.Click += new RoutedEventHandler(browseButton_Click); } void browseButton_Click(object sender, RoutedEventArgs e) { OpenFileDialog dlg = new OpenFileDialog(); if(dlg.ShowDialog() == true) { this.FileName = dlg.FileName; } }
public string FileName { get { return (string) GetValue(FileNameProperty); } set { SetValue(FileNameProperty, value); } } public static readonly DependencyProperty FileNameProperty = DependencyProperty.Register ( "FileName", typeof(string), typeof(FileInputControl)); } }
The default style and control template for FileInputControl
is in a ResourceDictionary
in the Themes
subfolder and is merged into the Generic ResourceDictionary
. The XAML for this style is as follows:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Recipe_04_08="clr-namespace:Recipe_04_08;assembly="> <Style TargetType="{x:Type Recipe_04_08:FileInputControl}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type Recipe_04_08:FileInputControl}"> <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> <DockPanel> <Button x:Name="PART_Browse" DockPanel.Dock="Right" Margin="2,0,0,0"> Browse... </Button>
<TextBox IsReadOnly="True" Text="{Binding Path=FileName, RelativeSource= {RelativeSource TemplatedParent}}" /> </DockPanel> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
The XAML for the window that consumes this custom control is as follows:
<Window x:Class="Recipe_04_08.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Recipe_04_08="clr-namespace:Recipe_04_08;assembly=" Title="WPF Recipes 4_08" Height="72" Width="300"> <Grid> <Recipe_04_08:FileInputControl Margin="8" /> </Grid> </Window>
You need to specify that consumers of your custom control should define certain elements within the control template in order for the control to function correctly.
In the default control template for your custom control, name any elements that are required by your control to function correctly, according to the naming convention PART_ElementName
.
You should then document each part's existence by marking your class with System.Windows.TemplatePartAttribute
, specifying the name and System.Type
as parameters.
In the code for your custom control, add any event handlers to these elements dynamically by overriding the OnApplyTemplate
method and locating the actual implementation of the element by calling GetTemplateChild
.
By documenting any parts required by your custom control using TemplatePartAttribute
, you signal this requirement to consumers of your control and to design tools such as Expression Blend.
Furthermore, by attaching any necessary event handlers to the control parts programmatically, you ensure that they do not have to be specified in every template defined for your control by a consumer.
When locating these parts in the code for your custom control, it is recommended that you handle any omissions gracefully. If a template does not define a specific element, it should not cause an exception in your code. This not only allows consumers of your control to support just the functionality they require, but it also prevents issues when your control is used within design tools such as Expression Blend.
The following example demonstrates how to create a lookless custom control to encapsulate the functionality of browsing to a file and displaying the file name. Figure 4-8 shows the control in use.
The FileInputControl
class derives from Control
and uses the TemplatePart
attribute to signal that it expects a Button
control called PART_Browse
. It overrides the OnApplyTemplate
method and calls GetTemplateChild
to find the button defined by its actual template. If this exists, it adds an event handler to the button's Click
event.
The code for the control is as follows:
using System.Windows; using System.Windows.Controls; using Microsoft.Win32; namespace Recipe_04_09 { /// <summary> /// The TemplatePart attribute specifies that the control /// expects the Control Template to contain a Button called /// PART_Browse /// </summary> [TemplatePart(Name = "PART_Browse", Type = typeof(Button))]
public class FileInputControl : Control { static FileInputControl() { DefaultStyleKeyProperty.OverrideMetadata( typeof(FileInputControl), new FrameworkPropertyMetadata( typeof(FileInputControl))); } public override void OnApplyTemplate() { base.OnApplyTemplate(); // Use the GetTemplateChild method to locate // the button called PART_Browse Button browseButton = base.GetTemplateChild("PART_Browse") as Button; // Do not cause or throw an exception // if it wasn't supplied by the Template if(browseButton != null) browseButton.Click += new RoutedEventHandler(browseButton_Click); } void browseButton_Click(object sender, RoutedEventArgs e) { OpenFileDialog dlg = new OpenFileDialog(); if(dlg.ShowDialog() == true) { this.FileName = dlg.FileName; } } public string FileName { get { return (string) GetValue(FileNameProperty); } set { SetValue(FileNameProperty, value); } } public static readonly DependencyProperty FileNameProperty = DependencyProperty.Register(
"FileName", typeof(string), typeof(FileInputControl)); } }
The default style and control template for FileInputControl
is in a ResourceDictionary
in the Themes
subfolder and is merged into the generic ResourceDictionary
. The XAML for this style is as follows:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Recipe_04_09="clr-namespace:Recipe_04_09;assembly="> <Style TargetType="{x:Type Recipe_04_09:FileInputControl}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type Recipe_04_09:FileInputControl}"> <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> <DockPanel> <Button x:Name="PART_Browse" DockPanel.Dock="Right" Margin="2,0,0,0"> Browse... </Button> <TextBox IsReadOnly="True" Text="{Binding Path=FileName, RelativeSource= {RelativeSource TemplatedParent}}" /> </DockPanel> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
The XAML for the window that consumes this custom control is as follows:
<Window x:Class="Recipe_04_09.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Recipe_04_09="clr-namespace:Recipe_04_09;assembly=" Title="WPF Recipes 4_09" Height="72" Width="300"> <Grid> <Recipe_04_09:FileInputControl Margin="8" /> </Grid> </Window>
You need to support UI automation in your custom control to allow test scripts to interact with the UI.
Create a companion class called ControlNameAutomationPeer
for your custom control that derives from System.Windows.Automation.Peers.FrameworkElementAutomationPeer
.
Override the OnCreateAutomationPeer
method in your custom control, and return an instance of your companion class.
The FrameworkElementAutomationPeer
companion class describes your control to the automation system. Whenever an event occurs that should be communicated to the automation system, you can retrieve the companion class and call the Invoke
method of System.Windows.Automation.Provider.IInvokeProvider
.
The following example demonstrates how to create a lookless custom control to encapsulate the functionality of browsing to a file and displaying the file name.
The code for the control also defines a class called FileInputControlAutomationPeer
that provides UI automation support and returns an instance of this class in the OnCreateAutomationPeer
method.
The code for the control is as follows:
using System; using System.Windows; using System.Windows.Automation.Peers; using System.Windows.Automation.Provider; using System.Windows.Controls; using System.Windows.Markup; using Microsoft.Win32; namespace Recipe_04_10 { [TemplatePart(Name = "PART_Browse", Type = typeof(Button))] [ContentProperty("FileName")] public class FileInputControl : Control { static FileInputControl() { DefaultStyleKeyProperty.OverrideMetadata( typeof(FileInputControl), new FrameworkPropertyMetadata( typeof(FileInputControl))); } public override void OnApplyTemplate() { base.OnApplyTemplate(); Button browseButton = base.GetTemplateChild("PART_Browse") as Button; if(browseButton != null) browseButton.Click += new RoutedEventHandler(browseButton_Click); } void browseButton_Click(object sender, RoutedEventArgs e) { OpenFileDialog dlg = new OpenFileDialog(); if(dlg.ShowDialog() == true) { this.FileName = dlg.FileName; } }
public string FileName { get { return (string) GetValue(FileNameProperty); } set { SetValue(FileNameProperty, value); } } public static readonly DependencyProperty FileNameProperty = DependencyProperty.Register( "FileName", typeof(string), typeof(FileInputControl)); /// <summary> /// Identifies SimpleButton.Click routed event. /// </summary> public static readonly RoutedEvent ClickEvent = EventManager.RegisterRoutedEvent( "Click", RoutingStrategy.Bubble, typeof(EventHandler), typeof(FileInputControl)); /// <summary> /// Occurs when a Simple button is clicked. /// </summary> public event RoutedEventHandler Click { add { AddHandler(ClickEvent, value); } remove { RemoveHandler(ClickEvent, value); } } /// <summary> /// Overriding of this method provides an UI Automation support /// </summary> /// <returns></returns>
protected override AutomationPeer OnCreateAutomationPeer() { return new FileInputControlAutomationPeer(this); } } /// <summary> /// Class that provides UI Automation support /// </summary> public class FileInputControlAutomationPeer : FrameworkElementAutomationPeer, IInvokeProvider { public FileInputControlAutomationPeer(FileInputControl control) : base(control) { } protected override string GetClassNameCore() { return "FileInputControl"; } protected override string GetLocalizedControlTypeCore() { return "FileInputControl"; } protected override AutomationControlType GetAutomationControlTypeCore() { return AutomationControlType.Button; } public override object GetPattern(PatternInterface patternInterface) { if(patternInterface == PatternInterface.Invoke) { return this; } return base.GetPattern(patternInterface); } private FileInputControl MyOwner {
get { return (FileInputControl) base.Owner; } } #region IInvokeProvider Members public void Invoke() { RoutedEventArgs newEventArgs = new RoutedEventArgs(FileInputControl.ClickEvent); MyOwner.RaiseEvent(newEventArgs); } #endregion } }
The control is then used in the following window. It contains a button that, when clicked, invokes the Click
event of the FileInputControl
via the automation peer.
The code-behind for this window is as follows:
using System.Windows; using System.Windows.Automation.Peers; using System.Windows.Automation.Provider; using Recipe_04_10; namespace Recipe_04_10 { public partial class Window1 : Window { public Window1() { InitializeComponent(); ctlFileInput.Click += new RoutedEventHandler(ctlFileInput_Click); } private void Button_Click(object sender, RoutedEventArgs e) { // Get the AutomationPeer for this control FileInputControlAutomationPeer peer = new FileInputControlAutomationPeer(ctlFileInput); IInvokeProvider invokeProvider = peer.GetPattern(PatternInterface.Invoke) as IInvokeProvider;
// Call the Invoke method invokeProvider.Invoke(); } private void ctlFileInput_Click( object sender, RoutedEventArgs e) { MessageBox.Show("Invoked via the Automation Peer"); } } }
Create a class that derives from System.Windows.FrameworkElement
, and override the OnRender
method of the base class System.Windows.UIElement
. Add code to render the custom element to the System.Windows.Media.DrawingContext
.
The OnRender
method of UIElement
is provided with a DrawingContext
. This context provides methods for drawing text and geometries.
When the parent of the UIElement
detects that the size has changed, OnRender
is called automatically. It can also be invoked when any data or properties change by calling the InvalidateVisual
method of UIElement
.
The following example demonstrates how to render a custom pie chart control using the OnRender
method.
The PieChartControl
contains a Slices
property that tells the control the slices it needs to draw. In the OnRender
method, it draws each slice to the DrawingContext
object.
The code for the PieChartControl
is as follows:
using System; using System.Collections.Generic; using System.Windows; using System.Windows.Media;
namespace Recipe_04_11 { public class PieChartControl : FrameworkElement { #region Slices public List<double> Slices { get { return (List<double>) GetValue(SlicesProperty); } set { SetValue(SlicesProperty, value); } } // Using a DependencyProperty as the backing store for slices. // This enables animation, styling, binding, etc... public static readonly DependencyProperty SlicesProperty = DependencyProperty.Register("Slices", typeof(List<double>), typeof(PieChartControl), new FrameworkPropertyMetadata( null, FrameworkPropertyMetadataOptions. AffectsRender, new PropertyChangedCallback( OnPropertyChanged))); #endregion /// <summary> /// Override the OnRender and draw the slices /// for the pie chart /// </summary> /// <param name="drawingContext"></param> protected override void OnRender(DrawingContext drawingContext) { List<double> segments = this.Slices; if(segments != null) { Size radius = new Size( this.RenderSize.Width * 0.5, this.RenderSize.Height * 0.5);
Point startPoint = new Point(radius.Width, 0); foreach(double slice in segments) { startPoint = DrawSlice( drawingContext, slice, startPoint, radius); } } } private Point DrawSlice( DrawingContext drawingContext, double slice, Point startPoint, Size radius) { // double theta = (slice.Percentage / 100) * 360; double theta = (slice / 100) * 360; // nb. This caters for the condition where we have one slice theta = (theta == 360) ? 359.99 : theta; //Note - we need to translate the point first. // Could be rolled into a single affine transformation. Point endPoint = RotatePoint( new Point( startPoint.X - radius.Width, startPoint.Y - radius.Height), theta); endPoint = new Point( endPoint.X + radius.Width, endPoint.Y + radius.Height); bool isLargeArc = (theta > 180); PathGeometry geometry = new PathGeometry(); PathFigure figure = new PathFigure(); geometry.Figures.Add(Figure); figure.IsClosed = true; figure.StartPoint = startPoint;
figure.Segments.Add( new ArcSegment(endPoint, radius, 0, isLargeArc, SweepDirection.Clockwise, false)); figure.Segments.Add(new LineSegment(startPoint, false)); figure.Segments.Add(new LineSegment(endPoint, false)); figure.Segments.Add( new LineSegment( new Point( radius.Width, radius.Height), false)); SolidColorBrush brush = new SolidColorBrush(GetRandomColor()); drawingContext.DrawGeometry(brush, new Pen(brush, 1), geometry); startPoint = endPoint; return startPoint; } private const double _pi_by180 = Math.PI / 180; private Point RotatePoint(Point a, double phi) { double theta = phi * _pi_by180; double x = Math.Cos(theta) * a.X + -Math.Sin(theta) * a.Y; double y = Math.Sin(theta) * a.X + Math.Cos(theta) * a.Y; return new Point(x, y); } protected static void OnPropertyChanged( DependencyObject o, DependencyPropertyChangedEventArgs args) { PieChartControl pcc = o as PieChartControl; if(null != pcc) pcc.InvalidateVisual(); } private static Random seed = new Random(); private Color GetRandomColor() { Color newColor = new Color(); newColor.A = (byte) 255; newColor.R = (byte) seed.Next(0, 256); newColor.G = (byte) seed.Next(0, 256); newColor.B = (byte) seed.Next(0, 256);
return newColor; } } }
The following XAML defines a window displaying three PieChartControl
controls.
<Window x:Class="Recipe_04_11.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Recipe_04_11="clr-namespace:Recipe_04_11;assembly=" Title="WPF Recipes 4_11" Height="120" Width="180"> <Grid> <StackPanel Orientation="Horizontal" Margin="4"> <Recipe_04_11:PieChartControl x:Name="pieChart1" Width="36" Height="36" Margin="8" /> <Recipe_04_11:PieChartControl x:Name="pieChart2" Width="36" Height="36" Margin="8" /> <Recipe_04_11:PieChartControl x:Name="pieChart3" Width="36" Height="36" Margin="8" /> </StackPanel> </Grid> </Window>
In the constructor for the window, the pie charts are given their slices:
using System.Collections.Generic; using System.Windows; namespace Recipe_04_11 { public partial class Window1 : Window
{ public Window1() { InitializeComponent(); // Set up the slices pieChart1.Slices = new List <double>(); pieChart1.Slices.Add(30); pieChart1.Slices.Add(60); pieChart1.Slices.Add(160); pieChart2.Slices = new List <double>(); pieChart2.Slices.Add(30); pieChart2.Slices.Add(90); pieChart3.Slices = new List <double>(); pieChart3.Slices.Add(90); pieChart3.Slices.Add(180); } } }
Figure 4-9 shows the resulting window.
Create a control that inherits from TextBox
, add a System.Windows.DependencyProperty
to the code-behind called Number
, and specify a type of double
.
Override the OnPreviewTextInput
method of the TextBox
control, and if the text that is being input cannot be parsed to a double
, set the Handled
property of the System.Windows.Input.TextCompositionEventArgs
to True
.
Add code to ensure that when the Number
property is changed, the Text
property is also changed, and vice versa.
If you need the TextBox
to contain only integer values, then simply change the type of Number
property to int
, and use int.TryParse
instead of double.TryParse
to check whether a new text input should be allowed.
By inheriting from a TextBox
control, you get a custom control with all the behavior and appearance properties of a TextBox
, but you also get the ability to override and modify certain aspects or features for the specific needs of a situation.
In this case, you override the OnPreviewTextInput
method to intercept the inputting of text into the base TextBox
and allow text to be input only if it can be parsed to a double
. This is possible because the TextCompositionEventArgs
class has a Handled
property, and the text is input only if this property is not set to False
.
By using DependencyProperty
to store the numeric value of the Text
, you can use the full range of WPF features such as data binding, styles, and animations to interact with the control.
The following code-behind shows a class called NumericTextboxControl
that inherits from the TextBox
class and adds a Number
property of type double
, backed by a DependencyProperty
. There is code in the OnPreviewTextInput
method to allow only text that can be converted to a double
as input. Then, there is code in the OnNumberChanged
and OnTextChanged
methods to ensure that the values in the Text
and Number
properties are synchronized.
using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace Recipe_04_12 { public partial class NumericTextboxControl : TextBox { // Flag is True if the Text property is changed private bool isTextChanged = false; // Flag is True if the Number property is changed private bool isNumberChanged = false;
public NumericTextboxControl() { InitializeComponent(); } /// <summary> /// Public property to store the numeric /// value of the control's Text property /// </summary> public double Number { get { return (double) GetValue(NumberProperty); } set { SetValue(NumberProperty, value); } } public static readonly DependencyProperty NumberProperty = DependencyProperty.Register("Number", typeof(double), typeof(NumericTextboxControl), new PropertyMetadata( new PropertyChangedCallback( OnNumberChanged))); private static void OnNumberChanged( DependencyObject sender, DependencyPropertyChangedEventArgs e) { NumericTextboxControl control = (NumericTextboxControl) sender; if(!control.isTextChanged) { // Number property has been changed from the outside, // via a binding or control, so set the Text property control.isNumberChanged = true; control.Text = control.Number.ToString(); control.isNumberChanged = false; } }
protected override void OnTextChanged(TextChangedEventArgs e) { if(!isNumberChanged) { // Text property has been changed from // text input, so set the Number property // nb. It will default to 0 if the text // is empty or "-" isTextChanged = true; double number; double.TryParse(this.Text, out number); this.Number = number; isTextChanged = false; } base.OnTextChanged(e); } protected override void OnPreviewTextInput(TextCompositionEventArgs e) { // Get the preview of the new text string newTextPreview = this.Text.Insert( this.SelectionStart, e.Text); // Try to parse it to a double double number; if(!double.TryParse(newTextPreview, out number) && newTextPreview != "-") { // Mark the event as being handled if // the new text can't be parsed to a double e.Handled = true; } base.OnPreviewTextInput(e); } } }
The following XAML shows how to use a NumericTextBoxControl
in a window. There is also a button to demonstrate changing the value of the Number
property and updating the text, as well as a System.Windows.Controls.TextBlock
control that demonstrates binding to the Number
property.
<Window x:Class="Recipe_04_12.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Recipe_04_12="clr-namespace:Recipe_04_12;assembly=" Title="WPF Recipes 4_12" Height="120" Width="300"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="0.5*"/> <ColumnDefinition Width="0.5*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <TextBlock Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Center"> Only accepts numbers: </TextBlock> <Recipe_04_12:NumericTextboxControl x:Name="numTextBox" Width="80" Height="20" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Center" /> <Button Click="Button_Click" Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Width="120" Height="24" > Increment the number </Button> <StackPanel Orientation="Horizontal" VerticalAlignment="Center" Grid.Row="1" Grid.Column="1"> <TextBlock>The number is:</TextBlock>
<TextBlock Margin="4,0,0,0" Text="{Binding ElementName=numTextBox, Path=Number, UpdateSourceTrigger=PropertyChanged}" /> </StackPanel> </Grid> </Window>
Create a class that derives from System.Windows.Controls.Canvas
, and override the MeasureOverride
method. In this method, iterate through the FrameworkElements
in the Children
collection, call the Measure
method of each child, and determine the highest value for the Top
and Left
properties. Then return these as the dimensions the canvas should occupy based on the layout of its children.
This new Canvas
control can be wrapped in a System.Windows.Controls.ScrollViewer
, and if the Canvas
contains child elements that require scrolling in order to be brought into view, the ScrollViewer
now provides the correct scroll amount.
The default value for the VerticalScrollBarVisibility
property of a ScrollViewer
is System.Windows.Controls.ScrollBarVisibility.Visible
, but the default value for the HorizontalScrollBarVisibility
property is Hidden
. So if this property is not explicitly changed to Visible
, the ScrollViewer
will not show the horizontal scrollbar regardless of whether there are any child elements out of view and to the right.
By default, a Canvas
has no height or width. This causes issues if you want to use a Canvas
in a ScrollViewer
because the ScrollViewer
doesn't ever see the Canvas
spill out of its viewport. When overriding the Canvas
's MeasureOverride
method, you can determine a bounding rectangle for all the child items in the Canvas
and return this as the Canvas
's size. Then if the Canvas
contains child elements that require scrolling to be brought into view, the ScrollViewer
provides the correct scroll amount.
The following example demonstrates how to create a ScrollableCanvasControl
, a custom reusable canvas control that can be wrapped in a ScrollViewer
and display horizontal and vertical scrollbars to scroll the child items into view. This user control is then used in a window, which is shown in Figure 4-10.
The code for the ScrollableCanvasControl
is as follows:
using System; using System.Windows; using System.Windows.Controls; namespace Recipe_04_13 { public class ScrollableCanvasControl : Canvas { static ScrollableCanvasControl() { DefaultStyleKeyProperty.OverrideMetadata( typeof(ScrollableCanvasControl), new FrameworkPropertyMetadata( typeof(ScrollableCanvasControl))); } protected override Size MeasureOverride( Size constraint) { double bottomMost = 0d; double rightMost = 0d; // Loop through the child FrameworkElements, // and track the highest Top and Left value // amongst them. foreach(object obj in Children) { FrameworkElement child = obj as FrameworkElement; if(child != null) { child.Measure(constraint);
bottomMost = Math.Max( bottomMost, GetTop(child) + child.DesiredSize.Height); rightMost = Math.Max( rightMost, GetLeft(child) + child.DesiredSize.Width); } } if(double.IsNaN(bottomMost) || double.IsInfinity(bottomMost)) { bottomMost = 0d; } if(double.IsNaN(rightMost) || double.IsInfinity(rightMost)) { rightMost = 0d; } // Return the new size return new Size(rightMost, bottomMost); } } }
The following XAML defines a window with two ScrollView
controls side by side. The one on the left contains a normal Canvas
, which in turn contains two System.Windows.Controls.Button
controls. One of the buttons is positioned in the view; one is positioned below the bottom of the window. Because this is a normal Canvas
, the second button is not displayed and cannot be scrolled into view. The Canvas
on the right is a ScrollableCanvasControl
containing identical buttons. However, this time, the vertical scrollbar is displayed, and the bottom button can be scrolled into view. Figure 4-10 shows the results.
<Window x:Class="Recipe_04_13.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Recipe_04_13="clr-namespace:Recipe_04_13;assembly=" Title="WPF Recipes 4_13" Height="200" Width="400" >
<Window.Resources> <Style TargetType="Button"> <Setter Property="Width" Value="Auto" /> <Setter Property="Height" Value="24" /> </Style> </Window.Resources> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="0.5*"/> <ColumnDefinition Width="0.5*"/> </Grid.ColumnDefinitions> <ScrollViewer Grid.Column="0"> <Canvas> <Button Canvas.Top="80" Canvas.Left="80"> In View </Button> <Button Canvas.Top="300" Canvas.Left="80"> Out of view </Button> </Canvas> </ScrollViewer> <ScrollViewer Grid.Column="1"> <Recipe_04_13:ScrollableCanvasControl> <Button Canvas.Top="80" Canvas.Left="80"> In View </Button> <Button Canvas.Top="300" Canvas.Left="80"> Out of View </Button> </Recipe_04_13:ScrollableCanvasControl> </ScrollViewer> </Grid> </Window>
Create a class that derives from System.Windows.Controls.Canvas
, and override the MeasureOverride
method. In this method, iterate through the FrameworkElements
in the Children
collection, call the Measure
method of each child, and determine the highest value for the Top
and Left
properties. Then return these as the dimensions the canvas should occupy based on the layout of its children.
This new Canvas
control can be wrapped in a System.Windows.Controls.ScrollViewer
, and if the Canvas
contains child elements that require scrolling in order to be brought into view, the ScrollViewer
now provides the correct scroll amount.
Set the System.Windows.Media.Transform.LayoutTransform
property of the Canvas
to a System.Windows.Media.ScaleTransform
, and bind the ScaleX
and ScaleY
properties of the ScaleTransform
to the Value
property of a System.Windows.Controls.Slider
control.
In overriding the Canvas
's MeasureOverride
method, you can determine a bounding rectangle for all the child items in the Canvas
and return this as the Canvas
's size. Then if the Canvas
contains child elements that require scrolling to be brought into view, the ScrollViewer
provides the correct scroll amount.
By setting the LayoutTransform
property of the Canvas
to a ScaleTransform
, you can automatically transform the scale of the Canvas
control by a factor provided by the value of a Slider
control.
The following example demonstrates how to create a ZoomableCanvasControl
, which is a custom reusable Canvas
control that can be wrapped in a ScrollViewer
, and it sets the LayoutTransform
property of the Canvas
to a ScaleTransform
.
This user control is then used in a window that contains a Slider
control, and it binds the ScaleX
and ScaleY
properties of the ScaleTransform
to the value of the Slider
control.
Figure 4-11 shows the resulting window. As the Slider
moves left and right, the contents of the Canvas
zoom in and out.
The code for the ZoomableCanvasControl
is as follows:
using System; using System.Windows; using System.Windows.Controls; namespace Recipe_04_14 { public class ZoomableCanvasControl : Canvas { static ZoomableCanvasControl() { DefaultStyleKeyProperty.OverrideMetadata( typeof(ZoomableCanvasControl), new FrameworkPropertyMetadata( typeof(ZoomableCanvasControl))); } protected override Size MeasureOverride( Size constraint) { double bottomMost = 0d; double rightMost = 0d; // Loop through the child FrameworkElements, // and track the highest Top and Left value // amongst them. foreach(object obj in Children) { FrameworkElement child = obj as FrameworkElement; if(child != null) { child.Measure(constraint); bottomMost = Math.Max( bottomMost, GetTop(child) + child.DesiredSize.Height);
rightMost = Math.Max( rightMost, GetLeft(child) + child.DesiredSize.Width); } } if(double.IsNaN(bottomMost) || double.IsInfinity(bottomMost)) { bottomMost = 0d; } if(double.IsNaN(rightMost) || double.IsInfinity(rightMost)) { rightMost = 0d; } // Return the new size return new Size(rightMost, bottomMost); } } }
The following XAML defines a window with a ZoomableCanvasControl
inside a ScrollViewer
control. There is a Slider
control docked to the bottom of the window, whose Value
property is bound to the ScaleX
and ScaleY
properties of a ScaleTransform
within the ZoomableCanvasControl
.
<Window x:Class="Recipe_04_14.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Recipe_04_14="clr-namespace:Recipe_04_14;assembly=" Title="WPF Recipes 4_14" Height="300" Width="300" > <Window.Resources> <Style TargetType="Button"> <Setter Property="Width" Value="Auto" /> <Setter Property="Height" Value="24" /> </Style> </Window.Resources> <DockPanel>
<Slider DockPanel.Dock="Bottom" x:Name="zoomSlider" Minimum="0.1" Maximum="5" Value="1" /> <ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"> <Recipe_04_14:ZoomableCanvasControl x:Name="zoomControl"> <Canvas.LayoutTransform> <ScaleTransform ScaleX="{Binding Path=Value, ElementName=zoomSlider}" ScaleY="{Binding Path=Value, ElementName=zoomSlider}" /> </Canvas.LayoutTransform> <Rectangle Canvas.Top="0" Canvas.Left="0" StrokeThickness="2" Stroke="Red" Width="50" Height="50" /> <Rectangle Canvas.Top="50" Canvas.Left="50" StrokeThickness="2" Stroke="Blue" Width="150" Height="150" /> <Rectangle Canvas.Top="200" Canvas.Left="200" StrokeThickness="2" Stroke="Green" Width="200" Height="200" /> </Recipe_04_14:ZoomableCanvasControl> </ScrollViewer> </DockPanel> </Window>
Figure 4-11 shows the resulting window.
Create a class that derives from System.Windows.Controls.Canvas
, and override the OnPreviewMouseLeftButtonDown
, OnPreviewMouseLeftButtonUp
, and OnPreviewMouseMove
methods. Add logic to these methods to determine when the left mouse button is pressed down on an element within the Canvas
and where the element should be moved to if the mouse is moved before the button is released.
In the Canvas
control's OnPreviewMouseLeftButtonDown
method, store the current mouse position and the current position of the selected UI element on the Canvas
. In the OnPreviewMouseMove
method, get the new position of the mouse, and use it to calculate the desired position of the UI element. Call the Canvas.SetLeft
and Canvas.SetTop
methods to set this position.
The following example demonstrates how to create a DragCanvasControl
, a custom reusable Canvas
control that overrides the OnPreviewMouseLeftButtonDown
, OnPreviewMouseLeftButtonUp
, and OnPreviewMouseMove
methods to automatically drag elements inside the Canvas
when the left mouse button is pressed down and the mouse is moved.
Figure 4-12 shows the resulting window. The shape elements in the canvas can be dragged around the Canvas
by clicking them.
The code for the DragCanvasControl
is as follows:
using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace Recipe_04_15 { public class DragCanvasControl : Canvas { private Point startPoint; private Point selectedElementOrigins; private bool isDragging; private UIElement selectedElement; static DragCanvasControl() { DefaultStyleKeyProperty.OverrideMetadata( typeof(DragCanvasControl), new FrameworkPropertyMetadata( typeof(DragCanvasControl))); } protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e) { base.OnPreviewMouseLeftButtonDown(e); if(e.Source != this) { if(!isDragging) { startPoint = e.GetPosition(this); selectedElement = e.Source as UIElement; this.CaptureMouse(); isDragging = true; selectedElementOrigins = new Point( Canvas.GetLeft(selectedElement), Canvas.GetTop(selectedElement)); } e.Handled = true; } }
protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e) { base.OnPreviewMouseLeftButtonUp(e); if(this.IsMouseCaptured) { isDragging = false; this.ReleaseMouseCapture(); e.Handled = true; } } protected override void OnPreviewMouseMove(MouseEventArgs e) { base.OnPreviewMouseMove(e); if(this.IsMouseCaptured) { if(isDragging) { Point currentPosition = e.GetPosition(this); double elementLeft = (currentPosition.X - startPoint.X) + selectedElementOrigins.X; double elementTop = (currentPosition.Y - startPoint.Y) + selectedElementOrigins.Y; Canvas.SetLeft(selectedElement, elementLeft); Canvas.SetTop(selectedElement, elementTop); } } } } }
The following XAML defines a window with a DragCanvasControl
containing three shape elements:
<Window x:Class="Recipe_04_15.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Recipe_04_15="clr-namespace:Recipe_04_15;assembly=" Title="WPF Recipes 4_15" Height="200" Width="300">
<Recipe_04_15:DragCanvasControl> <Rectangle Canvas.Top="8" Canvas.Left="8" Width="32" Height="32" Fill="Blue" /> <Ellipse Canvas.Top="36" Canvas.Left="48" Width="40" Height="24" Fill="Yellow" /> <Ellipse Canvas.Top="60" Canvas.Left="96" Width="32" Height="32" Fill="Red" /> </Recipe_04_15:DragCanvasControl> </Window>
Figure 4-12 shows the resulting window.