Windows Presentation Foundation (WPF)使你能够创建可自定义其外观的控件。 可以通过创建新的ControlTemplate来更改CheckBox的外观,超出仅仅设置属性能够做到的范围。 下图显示了一个 CheckBox 使用默认 ControlTemplate 以及一个 CheckBox 使用自定义 ControlTemplate 的示例。
使用默认控件模板的 CheckBox
使用自定义控件模板的 CheckBox
如果在创建控件时遵循部件和状态模型,则可以自定义控件的外观。 Blend for Visual Studio 等设计器工具支持部件和状态模型,因此在遵循此模型时,可在这些类型的应用程序中自定义控件。 本主题讨论部件和状态模型,以及如何在创建自己的控件时遵循它。 本主题使用自定义控件 NumericUpDown
的示例来说明此模型的理念。 该 NumericUpDown
控件显示一个数值,用户可以通过单击控件的按钮来增加或减少该值。 下图显示了 NumericUpDown
本主题中讨论的控件。
自定义 NumericUpDown 控件
本主题包含以下部分:
先决条件
本主题假定你知道如何为现有控件创建新 ControlTemplate 控件,熟悉控件协定中的元素是什么,并了解 在创建控件模板中讨论的概念。
注释
若要创建可以自定义其外观的控件,必须创建一个从 Control 类继承的控件或其子类之一,而不是 UserControl。 从 UserControl 继承的控件可以快速创建,但它不使用 ControlTemplate,因此无法自定义其外观。
部件和状态模型
部件和状态模型指定如何定义控件的视觉结构和视觉行为。 若要遵循部件和状态模型,应执行以下作:
在控件的 ControlTemplate 中定义视觉结构和视觉行为。
当控件的逻辑与控件模板的某些部分交互时,请遵循某些最佳做法。
提供控制合同以明确应包含在ControlTemplate中的内容。
在控件中 ControlTemplate 定义视觉结构和视觉行为时,应用程序作者可以通过创建新的 ControlTemplate 而不是编写代码来更改控件的视觉结构和视觉行为。 必须提供一个控制合约,以告知应用程序作者应该在ControlTemplate中定义哪些FrameworkElement对象和状态。 当您与 ControlTemplate 中的部件交互时,应遵循一些最佳做法,以确保控件能正确处理一个不完整的 ControlTemplate。 如果遵循这三个原则,应用程序作者将能够像为 WPF 附带的控件一样轻松地为控件创建一个 ControlTemplate 控件。 以下部分详细介绍了其中每个建议。
在 ControlTemplate 中定义控件的视觉结构和视觉行为
使用部件和状态模型创建自定义控件时,您可以在控件的 ControlTemplate 中定义其视觉结构和视觉行为,而不是在其逻辑中。 控件的可视结构是构成控件的对象的组合 FrameworkElement 。 视觉行为是控件处于特定状态时显示的方式。 有关创建 ControlTemplate 指定控件的视觉结构和视觉行为的详细信息,请参阅 创建控件的模板。
在NumericUpDown
控件示例中,视觉结构包括两个RepeatButton控件和一个TextBlock。 如果在控件的代码 NumericUpDown
中添加这些控件--在它的构造函数中--这些控件的位置将是不可更改的。 应在 ControlTemplate中定义控件的视觉结构和视觉行为,而不是在代码中定义。 然后,应用程序开发人员可以自定义按钮和 TextBlock 的位置,并指定在 Value
为负数时发生的行为,因为 ControlTemplate 可以被替换。
以下示例显示了 NumericUpDown
控件的可视结构,其中包括用于增加 Value
的 RepeatButton、用于减少 Value
的 RepeatButton 和用于显示 Value
的 TextBlock。
<ControlTemplate TargetType="src:NumericUpDown">
<Grid Margin="3"
Background="{TemplateBinding Background}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Border BorderThickness="1" BorderBrush="Gray"
Margin="7,2,2,2" Grid.RowSpan="2"
Background="#E0FFFFFF"
VerticalAlignment="Center"
HorizontalAlignment="Stretch">
<!--Bind the TextBlock to the Value property-->
<TextBlock Name="TextBlock"
Width="60" TextAlignment="Right" Padding="5"
Text="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type src:NumericUpDown}},
Path=Value}"/>
</Border>
<RepeatButton Content="Up" Margin="2,5,5,0"
Name="UpButton"
Grid.Column="1" Grid.Row="0"/>
<RepeatButton Content="Down" Margin="2,0,5,5"
Name="DownButton"
Grid.Column="1" Grid.Row="1"/>
<Rectangle Name="FocusVisual" Grid.ColumnSpan="2" Grid.RowSpan="2"
Stroke="Black" StrokeThickness="1"
Visibility="Collapsed"/>
</Grid>
</Grid>
</ControlTemplate>
控件的 NumericUpDown
可视行为是,如果值为负值,则值为红色字体。 如果在代码中更改Foreground的TextBlock值,并且Value
为负时,NumericUpDown
将始终显示红色负值。 您可以通过将VisualState对象添加到ControlTemplate来指定控件在ControlTemplate中的视觉行为。 以下示例显示了VisualState对象在Positive
和Negative
状态下的情况。
Positive
并且 Negative
是互斥的(控件始终位于两者中的一个),因此本示例将 VisualState 对象放入单个 VisualStateGroup对象中。 当控件进入 Negative
状态时,Foreground 变为 TextBlock 红色。 当控件处于 Positive
状态时,返回 Foreground 其原始值。 在ControlTemplate中定义对象的问题在创建控件模板中进一步讨论。
注释
请务必在 VisualStateManager.VisualStateGroups 根 FrameworkElementControlTemplate 上设置附加属性。
<ControlTemplate TargetType="local:NumericUpDown">
<Grid Margin="3"
Background="{TemplateBinding Background}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="ValueStates">
<!--Make the Value property red when it is negative.-->
<VisualState Name="Negative">
<Storyboard>
<ColorAnimation To="Red"
Storyboard.TargetName="TextBlock"
Storyboard.TargetProperty="(Foreground).(Color)"/>
</Storyboard>
</VisualState>
<!--Return the TextBlock's Foreground to its
original color.-->
<VisualState Name="Positive"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
在代码中使用控件模板的部分
ControlTemplate作者可能会故意或错误地省略FrameworkElement或VisualState对象,但控件的逻辑可能需要这些部分才能正常运行。 部件和状态模型规定,您的控件应具备对ControlTemplate没有FrameworkElement或VisualState对象的抵抗力。 控件不应在ControlTemplate中缺少FrameworkElement、VisualState或VisualStateGroup时引发异常或报告错误。 本部分介绍与 FrameworkElement 对象交互和管理状态的建议做法。
预期会缺少 FrameworkElement 对象
在 ControlTemplate 中定义 FrameworkElement 对象时,控件的逻辑可能需要与其中一些对象进行交互。 例如,控件NumericUpDown
订阅按钮Click的事件以增加或减少Value
,并将属性TextBlock设置为 TextValue
。 如果自定义 ControlTemplate 省略 TextBlock 或按钮,控件会丢失某些功能,但应确保控件不会导致错误。 例如,如果 a ControlTemplate 不包含要更改 Value
的按钮,则会 NumericUpDown
丢失该功能,但使用该 ControlTemplate 按钮的应用程序将继续运行。
确保以下做法能让您的控件正确响应缺少 FrameworkElement 对象的情况:
设置
x:Name
代码中需要引用的每个 FrameworkElement 属性。为需要与之交互的每个 FrameworkElement 属性定义专用属性。
管理您的控件在 FrameworkElement 属性的 set 访问器中处理的任何事件的订阅与取消。
设置 FrameworkElement 在方法的步骤 2 OnApplyTemplate 中定义的属性。 这是FrameworkElementControlTemplate中最早可供控件使用的。 使用
x:Name
的FrameworkElement从ControlTemplate获得它。在访问FrameworkElement的成员之前,请检查其是否为
null
。null
如果是,请不要报告错误。
以下示例演示控件 NumericUpDown
如何根据前面的列表中的建议与 FrameworkElement 对象交互。
在定义ControlTemplate中控件NumericUpDown
的可视结构的示例中,增加Value
的RepeatButton已将其x:Name
属性设置为UpButton
。 以下示例声明了一个名为UpButtonElement
的属性,该属性表示在ControlTemplate中声明的RepeatButton。
set
访问器首先检查 UpDownElement
是否等于 null
,如果不相等,就取消订阅按钮的 Click 事件,然后设置属性,最后订阅新的 Click 事件。 另一个属性也已定义, RepeatButton但此处未显示,另一个调用 DownButtonElement
。
private RepeatButton upButtonElement;
private RepeatButton UpButtonElement
{
get
{
return upButtonElement;
}
set
{
if (upButtonElement != null)
{
upButtonElement.Click -=
new RoutedEventHandler(upButtonElement_Click);
}
upButtonElement = value;
if (upButtonElement != null)
{
upButtonElement.Click +=
new RoutedEventHandler(upButtonElement_Click);
}
}
}
Private m_upButtonElement As RepeatButton
Private Property UpButtonElement() As RepeatButton
Get
Return m_upButtonElement
End Get
Set(ByVal value As RepeatButton)
If m_upButtonElement IsNot Nothing Then
RemoveHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
End If
m_upButtonElement = value
If m_upButtonElement IsNot Nothing Then
AddHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
End If
End Set
End Property
下面的示例演示 NumericUpDown
控件的 OnApplyTemplate。 该示例使用GetTemplateChild该方法从中ControlTemplate获取FrameworkElement对象。 请注意,该示例保护GetTemplateChild查找FrameworkElement时指定名称不符合预期类型的情况。 最好忽略具有指定 x:Name
但类型错误的元素。
public override void OnApplyTemplate()
{
UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
//TextElement = GetTemplateChild("TextBlock") as TextBlock;
UpdateStates(false);
}
Public Overloads Overrides Sub OnApplyTemplate()
UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)
UpdateStates(False)
End Sub
通过遵循前面的示例中展示的方法,可以确保当FrameworkElement缺失时ControlTemplate仍然能够继续运行。
使用 VisualStateManager 管理状态
跟踪 VisualStateManager 控件的状态,并执行在状态之间转换所需的逻辑。 当您将VisualState对象添加到ControlTemplate时,它们被添加到VisualStateGroup中,同时将VisualStateGroup添加到VisualStateManager.VisualStateGroups附加属性中,以便VisualStateManager可以访问这些对象。
以下示例重复前面的示例,该示例显示VisualState与Positive
Negative
控件的状态对应的对象。 在StoryboardNegative
VisualState中,Foreground的TextBlock变为红色。 当 NumericUpDown
控件处于 Negative
状态时,Negative
情节提要将开始。 然后,Storyboard在控件返回到Positive
状态时在Negative
状态停止。
Positive
VisualState不需要包含Storyboard,因为当Negative
的Storyboard停止时,Foreground会恢复成其原始颜色。
<ControlTemplate TargetType="local:NumericUpDown">
<Grid Margin="3"
Background="{TemplateBinding Background}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="ValueStates">
<!--Make the Value property red when it is negative.-->
<VisualState Name="Negative">
<Storyboard>
<ColorAnimation To="Red"
Storyboard.TargetName="TextBlock"
Storyboard.TargetProperty="(Foreground).(Color)"/>
</Storyboard>
</VisualState>
<!--Return the TextBlock's Foreground to its
original color.-->
<VisualState Name="Positive"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
请注意,虽然TextBlock被赋予了一个名称,但因为控件的逻辑从未引用过TextBlock,TextBlock不在NumericUpDown
的控制合约中。 引用在 ControlTemplate 内的元素有名称,但不需要成为控件协定的一部分,因为新的控件 ControlTemplate 可能不需要引用这些元素。 例如,有人为NumericUpDown
创建了新的ControlTemplate,可能会决定不通过更改Foreground来指示Value
为负值。 在这种情况下,代码和 ControlTemplate 都不按名称引用 TextBlock 。
控件的逻辑负责更改控件的状态。 以下示例显示,当Value
为0或更大时,NumericUpDown
控件调用GoToState方法进入Positive
状态;当Value
小于0时,进入Negative
状态。
if (Value >= 0)
{
VisualStateManager.GoToState(this, "Positive", useTransitions);
}
else
{
VisualStateManager.GoToState(this, "Negative", useTransitions);
}
If Value >= 0 Then
VisualStateManager.GoToState(Me, "Positive", useTransitions)
Else
VisualStateManager.GoToState(Me, "Negative", useTransitions)
End If
该方法GoToState执行启动和停止情节板所需的逻辑,并确保操作的适当性。 当控件调用 GoToState 更改其状态时, VisualStateManager 请执行以下操作:
如果控件正在转向VisualState且符合Storyboard,则动画开始。 然后,如果控件来源于VisualState且具备Storyboard,则故事板结束。
如果控件已处于指定状态, GoToState 则不执行任何作并返回
true
。如果指定的状态不存在于 ControlTemplate 的
control
中,GoToState 则不执行任何操作,并返回false
。
使用 VisualStateManager 的最佳做法
建议采取以下措施以维护控件的状态:
使用属性跟踪其状态。
创建在状态之间转换的帮助程序方法。
控件 NumericUpDown
使用其 Value
属性来跟踪它是否处于 Positive
或 Negative
处于状态。 该NumericUpDown
控件还定义Focused
和UnFocused
状态,以跟踪IsFocused属性。 如果使用与控件属性不自然对应的状态,可以定义一个私有属性来跟踪这些状态。
更新所有状态的单个方法集中调用 VisualStateManager 并使您的代码易于管理。 下面的示例演示控件 NumericUpDown
的帮助程序方法 UpdateStates
。 如果 Value
大于或等于 0,则 Control 处于 Positive
状态。 当小于 0 时 Value
,控件处于 Negative
状态。 当 IsFocused 为 true
时,控件处于 Focused
状态;否则,它处于 Unfocused
状态。 无论状态发生何种更改,控件都可以在需要更改其状态时调用 UpdateStates
。
private void UpdateStates(bool useTransitions)
{
if (Value >= 0)
{
VisualStateManager.GoToState(this, "Positive", useTransitions);
}
else
{
VisualStateManager.GoToState(this, "Negative", useTransitions);
}
if (IsFocused)
{
VisualStateManager.GoToState(this, "Focused", useTransitions);
}
else
{
VisualStateManager.GoToState(this, "Unfocused", useTransitions);
}
}
Private Sub UpdateStates(ByVal useTransitions As Boolean)
If Value >= 0 Then
VisualStateManager.GoToState(Me, "Positive", useTransitions)
Else
VisualStateManager.GoToState(Me, "Negative", useTransitions)
End If
If IsFocused Then
VisualStateManager.GoToState(Me, "Focused", useTransitions)
Else
VisualStateManager.GoToState(Me, "Unfocused", useTransitions)
End If
End Sub
如果将状态名称 GoToState 传递给控件已处于该状态时, GoToState 则不执行任何作,因此无需检查控件的当前状态。 例如,如果 Value
从一个负数更改为另一个负数,则 Negative
状态的场景不会中断,用户也不会注意到控件的变化。
VisualStateManager 使用 VisualStateGroup 对象来确定调用 GoToState 时要退出的状态。 控件在其ControlTemplate中每个VisualStateGroup具体定义一种状态,并且只有当它从相同的VisualStateGroup中进入另一状态时,才会离开当前状态。 例如,控件ControlTemplateNumericUpDown
在一个VisualStateGroup控件中定义了Positive
和Negative
VisualState对象,在另一个控件中定义了Focused
和Unfocused
VisualState对象。 在本主题的完整示例部分中,可以看到Focused
和Unfocused
VisualState的定义。当控件从Positive
状态切换到Negative
状态时,或从Negative
状态切换到Positive
状态时,该控件仍保持在Focused
或Unfocused
状态中。
有三个典型的位置,控件的状态可能会更改:
当 ControlTemplate 应用于 Control 时。
属性更改时。
事件发生时。
以下示例演示了在这些情况下更新控件的状态 NumericUpDown
。
应在OnApplyTemplate方法中更新控件的状态,以便应用ControlTemplate时控件显示为正确的状态。 下面的示例在 OnApplyTemplate 中调用 UpdateStates
,以确保控件处于适当状态。 例如,假设你创建了一个 NumericUpDown
控件,然后将其设置为 Foreground 绿色和 Value
-5。 如果在应用于NumericUpDown
控件时ControlTemplate未调用UpdateStates
控件,则控件不处于Negative
状态,值为绿色而不是红色。 必须调用 UpdateStates
以将控件 Negative
置于状态。
public override void OnApplyTemplate()
{
UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
//TextElement = GetTemplateChild("TextBlock") as TextBlock;
UpdateStates(false);
}
Public Overloads Overrides Sub OnApplyTemplate()
UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)
UpdateStates(False)
End Sub
当属性发生更改时,通常需要更新控件的状态。 下面的示例演示了整个 ValueChangedCallback
方法。 因为当 Value
发生变化时会调用 ValueChangedCallback
,所以如果 Value
从正变为负或反之亦然,该方法会调用 UpdateStates
。 当Value
发生变化但仍保持正值或负值时,调用UpdateStates
是可以接受的,因为在这种情况下,控件不会改变状态。
private static void ValueChangedCallback(DependencyObject obj,
DependencyPropertyChangedEventArgs args)
{
NumericUpDown ctl = (NumericUpDown)obj;
int newValue = (int)args.NewValue;
// Call UpdateStates because the Value might have caused the
// control to change ValueStates.
ctl.UpdateStates(true);
// Call OnValueChanged to raise the ValueChanged event.
ctl.OnValueChanged(
new ValueChangedEventArgs(NumericUpDown.ValueChangedEvent,
newValue));
}
Private Shared Sub ValueChangedCallback(ByVal obj As DependencyObject,
ByVal args As DependencyPropertyChangedEventArgs)
Dim ctl As NumericUpDown = DirectCast(obj, NumericUpDown)
Dim newValue As Integer = CInt(args.NewValue)
' Call UpdateStates because the Value might have caused the
' control to change ValueStates.
ctl.UpdateStates(True)
' Call OnValueChanged to raise the ValueChanged event.
ctl.OnValueChanged(New ValueChangedEventArgs(NumericUpDown.ValueChangedEvent, newValue))
End Sub
在事件发生时,可能还需要更新状态。 以下示例展示了NumericUpDown
在Control上调用UpdateStates
来处理GotFocus事件。
protected override void OnGotFocus(RoutedEventArgs e)
{
base.OnGotFocus(e);
UpdateStates(true);
}
Protected Overloads Overrides Sub OnGotFocus(ByVal e As RoutedEventArgs)
MyBase.OnGotFocus(e)
UpdateStates(True)
End Sub
帮助你 VisualStateManager 管理控件的状态。 通过使用 , VisualStateManager可确保控件在状态之间正确转换。 如果按照本部分中介绍的建议进行作 VisualStateManager,则控件的代码将保持可读且可维护。
提供控制合同
你提供一个控制协定,以便 ControlTemplate 作者知道要放入模板的内容。 控制合同有三个元素:
控件逻辑使用的视觉元素。
控件的状态和每个状态所属的组。
对控件外观有影响的公共属性。
创建新 ControlTemplate 人员需要知道控件逻辑使用的对象 FrameworkElement 、每个对象的类型及其名称。 ControlTemplate作者还需要知道控件可以处于的每个可能状态的名称,以及VisualStateGroup该状态所在的状态。
返回该 NumericUpDown
示例时,控件需要 ControlTemplate 具有以下 FrameworkElement 对象:
一个 RepeatButton 调用
UpButton
。某个RepeatButton被称为
DownButton.
该控件可以处于以下状态:
在
ValueStates
VisualStateGroupPositive
Negative
在
FocusStates
VisualStateGroupFocused
Unfocused
要指定控制所期望的FrameworkElement对象,您可以使用TemplatePartAttribute来指定预期元素的名称和类型。 若要指定控件的可能状态,请使用TemplateVisualStateAttribute来指定状态的名称及其所属的VisualStateGroup。 将 TemplatePartAttribute 和 TemplateVisualStateAttribute 放置在控件的类定义上。
影响控件外观的任何公共属性也是您的控件合约的一部分。
以下示例指定 FrameworkElement 控件的对象和状态 NumericUpDown
。
[TemplatePart(Name = "UpButtonElement", Type = typeof(RepeatButton))]
[TemplatePart(Name = "DownButtonElement", Type = typeof(RepeatButton))]
[TemplateVisualState(Name = "Positive", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Negative", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Focused", GroupName = "FocusedStates")]
[TemplateVisualState(Name = "Unfocused", GroupName = "FocusedStates")]
public class NumericUpDown : Control
{
public static readonly DependencyProperty BackgroundProperty;
public static readonly DependencyProperty BorderBrushProperty;
public static readonly DependencyProperty BorderThicknessProperty;
public static readonly DependencyProperty FontFamilyProperty;
public static readonly DependencyProperty FontSizeProperty;
public static readonly DependencyProperty FontStretchProperty;
public static readonly DependencyProperty FontStyleProperty;
public static readonly DependencyProperty FontWeightProperty;
public static readonly DependencyProperty ForegroundProperty;
public static readonly DependencyProperty HorizontalContentAlignmentProperty;
public static readonly DependencyProperty PaddingProperty;
public static readonly DependencyProperty TextAlignmentProperty;
public static readonly DependencyProperty TextDecorationsProperty;
public static readonly DependencyProperty TextWrappingProperty;
public static readonly DependencyProperty VerticalContentAlignmentProperty;
public Brush Background { get; set; }
public Brush BorderBrush { get; set; }
public Thickness BorderThickness { get; set; }
public FontFamily FontFamily { get; set; }
public double FontSize { get; set; }
public FontStretch FontStretch { get; set; }
public FontStyle FontStyle { get; set; }
public FontWeight FontWeight { get; set; }
public Brush Foreground { get; set; }
public HorizontalAlignment HorizontalContentAlignment { get; set; }
public Thickness Padding { get; set; }
public TextAlignment TextAlignment { get; set; }
public TextDecorationCollection TextDecorations { get; set; }
public TextWrapping TextWrapping { get; set; }
public VerticalAlignment VerticalContentAlignment { get; set; }
}
<TemplatePart(Name:="UpButtonElement", Type:=GetType(RepeatButton))>
<TemplatePart(Name:="DownButtonElement", Type:=GetType(RepeatButton))>
<TemplateVisualState(Name:="Positive", GroupName:="ValueStates")>
<TemplateVisualState(Name:="Negative", GroupName:="ValueStates")>
<TemplateVisualState(Name:="Focused", GroupName:="FocusedStates")>
<TemplateVisualState(Name:="Unfocused", GroupName:="FocusedStates")>
Public Class NumericUpDown
Inherits Control
Public Shared ReadOnly TextAlignmentProperty As DependencyProperty
Public Shared ReadOnly TextDecorationsProperty As DependencyProperty
Public Shared ReadOnly TextWrappingProperty As DependencyProperty
Public Property TextAlignment() As TextAlignment
Public Property TextDecorations() As TextDecorationCollection
Public Property TextWrapping() As TextWrapping
End Class
完整的示例
以下示例是ControlTemplate的整个NumericUpDown
控件。
<!--This is the contents of the themes/generic.xaml file.-->
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:VSMCustomControl">
<Style TargetType="{x:Type local:NumericUpDown}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:NumericUpDown">
<Grid Margin="3"
Background="{TemplateBinding Background}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="ValueStates">
<!--Make the Value property red when it is negative.-->
<VisualState Name="Negative">
<Storyboard>
<ColorAnimation To="Red"
Storyboard.TargetName="TextBlock"
Storyboard.TargetProperty="(Foreground).(Color)"/>
</Storyboard>
</VisualState>
<!--Return the control to its initial state by
return the TextBlock's Foreground to its
original color.-->
<VisualState Name="Positive"/>
</VisualStateGroup>
<VisualStateGroup Name="FocusStates">
<!--Add a focus rectangle to highlight the entire control
when it has focus.-->
<VisualState Name="Focused">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="FocusVisual"
Storyboard.TargetProperty="Visibility" Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<!--Return the control to its initial state by
hiding the focus rectangle.-->
<VisualState Name="Unfocused"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Border BorderThickness="1" BorderBrush="Gray"
Margin="7,2,2,2" Grid.RowSpan="2"
Background="#E0FFFFFF"
VerticalAlignment="Center"
HorizontalAlignment="Stretch">
<!--Bind the TextBlock to the Value property-->
<TextBlock Name="TextBlock"
Width="60" TextAlignment="Right" Padding="5"
Text="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type local:NumericUpDown}},
Path=Value}"/>
</Border>
<RepeatButton Content="Up" Margin="2,5,5,0"
Name="UpButton"
Grid.Column="1" Grid.Row="0"/>
<RepeatButton Content="Down" Margin="2,0,5,5"
Name="DownButton"
Grid.Column="1" Grid.Row="1"/>
<Rectangle Name="FocusVisual" Grid.ColumnSpan="2" Grid.RowSpan="2"
Stroke="Black" StrokeThickness="1"
Visibility="Collapsed"/>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
下面的示例显示了NumericUpDown
的逻辑。
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
namespace VSMCustomControl
{
[TemplatePart(Name = "UpButtonElement", Type = typeof(RepeatButton))]
[TemplatePart(Name = "DownButtonElement", Type = typeof(RepeatButton))]
[TemplateVisualState(Name = "Positive", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Negative", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Focused", GroupName = "FocusedStates")]
[TemplateVisualState(Name = "Unfocused", GroupName = "FocusedStates")]
public class NumericUpDown : Control
{
public NumericUpDown()
{
DefaultStyleKey = typeof(NumericUpDown);
this.IsTabStop = true;
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(
"Value", typeof(int), typeof(NumericUpDown),
new PropertyMetadata(
new PropertyChangedCallback(ValueChangedCallback)));
public int Value
{
get
{
return (int)GetValue(ValueProperty);
}
set
{
SetValue(ValueProperty, value);
}
}
private static void ValueChangedCallback(DependencyObject obj,
DependencyPropertyChangedEventArgs args)
{
NumericUpDown ctl = (NumericUpDown)obj;
int newValue = (int)args.NewValue;
// Call UpdateStates because the Value might have caused the
// control to change ValueStates.
ctl.UpdateStates(true);
// Call OnValueChanged to raise the ValueChanged event.
ctl.OnValueChanged(
new ValueChangedEventArgs(NumericUpDown.ValueChangedEvent,
newValue));
}
public static readonly RoutedEvent ValueChangedEvent =
EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Direct,
typeof(ValueChangedEventHandler), typeof(NumericUpDown));
public event ValueChangedEventHandler ValueChanged
{
add { AddHandler(ValueChangedEvent, value); }
remove { RemoveHandler(ValueChangedEvent, value); }
}
protected virtual void OnValueChanged(ValueChangedEventArgs e)
{
// Raise the ValueChanged event so applications can be alerted
// when Value changes.
RaiseEvent(e);
}
private void UpdateStates(bool useTransitions)
{
if (Value >= 0)
{
VisualStateManager.GoToState(this, "Positive", useTransitions);
}
else
{
VisualStateManager.GoToState(this, "Negative", useTransitions);
}
if (IsFocused)
{
VisualStateManager.GoToState(this, "Focused", useTransitions);
}
else
{
VisualStateManager.GoToState(this, "Unfocused", useTransitions);
}
}
public override void OnApplyTemplate()
{
UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
//TextElement = GetTemplateChild("TextBlock") as TextBlock;
UpdateStates(false);
}
private RepeatButton downButtonElement;
private RepeatButton DownButtonElement
{
get
{
return downButtonElement;
}
set
{
if (downButtonElement != null)
{
downButtonElement.Click -=
new RoutedEventHandler(downButtonElement_Click);
}
downButtonElement = value;
if (downButtonElement != null)
{
downButtonElement.Click +=
new RoutedEventHandler(downButtonElement_Click);
}
}
}
void downButtonElement_Click(object sender, RoutedEventArgs e)
{
Value--;
}
private RepeatButton upButtonElement;
private RepeatButton UpButtonElement
{
get
{
return upButtonElement;
}
set
{
if (upButtonElement != null)
{
upButtonElement.Click -=
new RoutedEventHandler(upButtonElement_Click);
}
upButtonElement = value;
if (upButtonElement != null)
{
upButtonElement.Click +=
new RoutedEventHandler(upButtonElement_Click);
}
}
}
void upButtonElement_Click(object sender, RoutedEventArgs e)
{
Value++;
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
Focus();
}
protected override void OnGotFocus(RoutedEventArgs e)
{
base.OnGotFocus(e);
UpdateStates(true);
}
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
UpdateStates(true);
}
}
public delegate void ValueChangedEventHandler(object sender, ValueChangedEventArgs e);
public class ValueChangedEventArgs : RoutedEventArgs
{
private int _value;
public ValueChangedEventArgs(RoutedEvent id, int num)
{
_value = num;
RoutedEvent = id;
}
public int Value
{
get { return _value; }
}
}
}
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Controls.Primitives
Imports System.Windows.Input
Imports System.Windows.Media
<TemplatePart(Name:="UpButtonElement", Type:=GetType(RepeatButton))> _
<TemplatePart(Name:="DownButtonElement", Type:=GetType(RepeatButton))> _
<TemplateVisualState(Name:="Positive", GroupName:="ValueStates")> _
<TemplateVisualState(Name:="Negative", GroupName:="ValueStates")> _
<TemplateVisualState(Name:="Focused", GroupName:="FocusedStates")> _
<TemplateVisualState(Name:="Unfocused", GroupName:="FocusedStates")> _
Public Class NumericUpDown
Inherits Control
Public Sub New()
DefaultStyleKeyProperty.OverrideMetadata(GetType(NumericUpDown), New FrameworkPropertyMetadata(GetType(NumericUpDown)))
Me.IsTabStop = True
End Sub
Public Shared ReadOnly ValueProperty As DependencyProperty =
DependencyProperty.Register("Value", GetType(Integer), GetType(NumericUpDown),
New PropertyMetadata(New PropertyChangedCallback(AddressOf ValueChangedCallback)))
Public Property Value() As Integer
Get
Return CInt(GetValue(ValueProperty))
End Get
Set(ByVal value As Integer)
SetValue(ValueProperty, value)
End Set
End Property
Private Shared Sub ValueChangedCallback(ByVal obj As DependencyObject,
ByVal args As DependencyPropertyChangedEventArgs)
Dim ctl As NumericUpDown = DirectCast(obj, NumericUpDown)
Dim newValue As Integer = CInt(args.NewValue)
' Call UpdateStates because the Value might have caused the
' control to change ValueStates.
ctl.UpdateStates(True)
' Call OnValueChanged to raise the ValueChanged event.
ctl.OnValueChanged(New ValueChangedEventArgs(NumericUpDown.ValueChangedEvent, newValue))
End Sub
Public Shared ReadOnly ValueChangedEvent As RoutedEvent =
EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Direct,
GetType(ValueChangedEventHandler), GetType(NumericUpDown))
Public Custom Event ValueChanged As ValueChangedEventHandler
AddHandler(ByVal value As ValueChangedEventHandler)
Me.AddHandler(ValueChangedEvent, value)
End AddHandler
RemoveHandler(ByVal value As ValueChangedEventHandler)
Me.RemoveHandler(ValueChangedEvent, value)
End RemoveHandler
RaiseEvent(ByVal sender As Object, ByVal e As RoutedEventArgs)
Me.RaiseEvent(e)
End RaiseEvent
End Event
Protected Overridable Sub OnValueChanged(ByVal e As ValueChangedEventArgs)
' Raise the ValueChanged event so applications can be alerted
' when Value changes.
MyBase.RaiseEvent(e)
End Sub
#Region "NUDCode"
Private Sub UpdateStates(ByVal useTransitions As Boolean)
If Value >= 0 Then
VisualStateManager.GoToState(Me, "Positive", useTransitions)
Else
VisualStateManager.GoToState(Me, "Negative", useTransitions)
End If
If IsFocused Then
VisualStateManager.GoToState(Me, "Focused", useTransitions)
Else
VisualStateManager.GoToState(Me, "Unfocused", useTransitions)
End If
End Sub
Public Overloads Overrides Sub OnApplyTemplate()
UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)
UpdateStates(False)
End Sub
Private m_downButtonElement As RepeatButton
Private Property DownButtonElement() As RepeatButton
Get
Return m_downButtonElement
End Get
Set(ByVal value As RepeatButton)
If m_downButtonElement IsNot Nothing Then
RemoveHandler m_downButtonElement.Click, AddressOf downButtonElement_Click
End If
m_downButtonElement = value
If m_downButtonElement IsNot Nothing Then
AddHandler m_downButtonElement.Click, AddressOf downButtonElement_Click
End If
End Set
End Property
Private Sub downButtonElement_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
Value -= 1
End Sub
Private m_upButtonElement As RepeatButton
Private Property UpButtonElement() As RepeatButton
Get
Return m_upButtonElement
End Get
Set(ByVal value As RepeatButton)
If m_upButtonElement IsNot Nothing Then
RemoveHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
End If
m_upButtonElement = value
If m_upButtonElement IsNot Nothing Then
AddHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
End If
End Set
End Property
Private Sub upButtonElement_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
Value += 1
End Sub
Protected Overloads Overrides Sub OnMouseLeftButtonDown(ByVal e As MouseButtonEventArgs)
MyBase.OnMouseLeftButtonDown(e)
Focus()
End Sub
Protected Overloads Overrides Sub OnGotFocus(ByVal e As RoutedEventArgs)
MyBase.OnGotFocus(e)
UpdateStates(True)
End Sub
Protected Overloads Overrides Sub OnLostFocus(ByVal e As RoutedEventArgs)
MyBase.OnLostFocus(e)
UpdateStates(True)
End Sub
#End Region
End Class
Public Delegate Sub ValueChangedEventHandler(ByVal sender As Object,
ByVal e As ValueChangedEventArgs)
Public Class ValueChangedEventArgs
Inherits RoutedEventArgs
Public Sub New(ByVal id As RoutedEvent,
ByVal num As Integer)
Value = num
RoutedEvent = id
End Sub
Public ReadOnly Property Value() As Integer
End Class