创建具有可自定义外观的控件
Windows Presentation Foundation (WPF) 使你能够创建可自定义其外观的控件。 例如,可以通过创建新的CheckBox 来更改 ControlTemplate 的外观(可超过设置属性的功能)。 下图显示了一个使用默认 ControlTemplate 的 CheckBox ,以及一个使用自定义 ControlTemplate 的 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
控件的可视结构,其中包括 RepeatButton(用于增大 Value
)、RepeatButton(用于减小 Value
)和 TextBlock(用于显示 Value
)。
<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
控件的视觉行为是值在为负数时为红色字体。 如果在 Value
为负数时在代码中更改 TextBlock 的 Foreground,则 NumericUpDown
会始终显示红色负值。 通过将 VisualState 对象添加到 ControlTemplate,在 ControlTemplate 中指定控件的视觉行为。 以下示例演示 Positive
和 Negative
状态的 VisualState 对象。 Positive
和 Negative
相互排斥(控件始终恰好位于这两个控件中的一个),因此该示例将 VisualState 对象置于单个 VisualStateGroup 中。 当控件进入 Negative
状态时,TextBlock 的 Foreground 会变为红色。 当控件处于 Positive
状态时,Foreground 会恢复为其原始值。 在创建控件模板中进一步讨论了如何在 ControlTemplate 中定义 VisualState 对象。
注意
请务必对 ControlTemplate 的根 FrameworkElement 设置 VisualStateManager.VisualStateGroups 附加属性。
<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 的部件
ControlTemplate 作者可能会有意或错误地省略 FrameworkElement 或 VisualState 对象,但控件的逻辑可能需要这些部件才能正常运行。 部件和状态模型指定,控件应可复原到缺少 FrameworkElement 或 VisualState 对象的 ControlTemplate。 如果 ControlTemplate 中缺少 FrameworkElement、VisualState 或 VisualStateGroup,控件不应引发异常或报告错误。 本部分介绍有关与 FrameworkElement 对象交互和管理状态的建议做法。
预计缺少 FrameworkElement 对象
在 ControlTemplate 中定义 FrameworkElement 对象时,控件的逻辑可能需要与其中一些对象进行交互。 例如,NumericUpDown
控件订阅按钮的 Click 事件以增大或减小 Value
,并将 TextBlock 的 Text 属性设置为 Value
。 如果自定义 ControlTemplate 省略 TextBlock 或按钮,则控件失去一些功能是可以接受的,但应确保控件不会导致错误。 例如,如果 ControlTemplate 不包含用于更改 Value
的按钮,则 NumericUpDown
会失去该功能,但使用 ControlTemplate 的应用程序会继续运行。
以下做法可确保控件正确响应缺少 FrameworkElement 对象:
为代码中需要引用的每个 FrameworkElement 设置
x:Name
属性。为需要与之交互的每个 FrameworkElement 定义私有属性。
订阅和取消订阅控件在 FrameworkElement 属性 set 访问器中处理的任何事件。
设置步骤 2 中在 OnApplyTemplate 方法中定义的 FrameworkElement 属性。 这是 ControlTemplate 中的 FrameworkElement 可供控件使用的最早时间。 使用 FrameworkElement 的
x:Name
从 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
按照前面示例中演示的做法操作,可确保控件在 ControlTemplate 缺少 FrameworkElement 时继续运行。
使用 VisualStateManager 管理状态
VisualStateManager 会跟踪控件的状态,并执行在状态之间转换所需的逻辑。 将 VisualState 对象添加到 ControlTemplate 时,会将它们添加到 VisualStateGroup,并将 VisualStateGroup 添加到 VisualStateManager.VisualStateGroups 附加属性,以便 VisualStateManager 有权访问它们。
以下示例重复前面显示与控件的 Positive
和 Negative
状态对应的 VisualState 对象的示例。 Negative
VisualState 中的 Storyboard 会将 TextBlock 的 Foreground 转变为红色。 当 NumericUpDown
控件处于 Negative
状态时,Negative
状态下的情节提要会开始。 随后 Negative
状态下的 Storyboard 会在控件恢复为 Positive
状态时停止。 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 不在 NumericUpDown
的控件协定中,因为控件的逻辑从不引用 TextBlock。 在 ControlTemplate 中引用的元素具有名称,但不需要是控件协定的一部分,因为控件的新 ControlTemplate 可能不需要引用该元素。 例如,为 NumericUpDown
创建新 ControlTemplate 的用户可能会决定不通过更改 Foreground 来指示 Value
是负值。 在这种情况下,代码和 ControlTemplate 都不会按名称引用 TextBlock。
控件的逻辑负责更改控件的状态。 以下示例演示 NumericUpDown
控件在 Value
为 0 或更大时调用 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
。如果指定的状态在
control
的 ControlTemplate 中不存在于,则 GoToState 不执行任何操作并返回false
。
使用 VisualStateManager 的最佳做法
建议执行以下操作来维护控件的状态:
使用属性跟踪其状态。
创建帮助程序方法以在状态之间转换。
NumericUpDown
控件使用其 Value
属性跟踪它是处于 Positive
还是 Negative
状态。 NumericUpDown
控件还定义了 Focused
和 UnFocused
状态,会跟踪 IsFocused 属性。 如果使用不是天然对应于控件属性的状态,则可以定义私有属性以跟踪状态。
更新所有状态的单个方法可集中对 VisualStateManager 的调用并使代码可管理。 下面的示例演示 NumericUpDown
控件的帮助程序方法 UpdateStates
。 当 Value
大于或等于 0 时,Control 处于 Positive
状态。 当 Value
小于 0 时,控件处于 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
状态的情节提要不会中断,用户不会看到控件发生更改。
在调用 GoToState 时,VisualStateManager 使用 VisualStateGroup 对象确定要退出的状态。 对于在其 ControlTemplate 中定义的每个 VisualStateGroup,控件始终处于一个状态,并且只有当它进入同一个 VisualStateGroup 中的另一个状态时,才会离开一个状态。 例如,NumericUpDown
控件的 ControlTemplate 在一个 VisualStateGroup 中定义了 Positive
和 Negative
VisualState 对象,在另一个中定义了 Focused
和 Unfocused
VisualState 对象。 (可以查看在本主题的完整示例部分中定义的 Focused
和 Unfocused
VisualState)当控件从 Positive
状态转换为 Negative
状态或进行相反转换时,控件仍保持 Focused
或 Unfocused
状态。
控件的状态可能会在三种典型情况下进行更改:
当 ControlTemplate 应用于 Control 时。
当属性更改时。
当事件发生时。
以下示例演示如何在这些情况下更新 NumericUpDown
控件的状态。
应在 OnApplyTemplate 方法中更新控件的状态,以便在应用 ControlTemplate 时,控件可显示正确的状态。 以下示例在 OnApplyTemplate 中调用 UpdateStates
以确保控件处于合适的状态。 例如,假设你创建了 NumericUpDown
控件,然后将其 Foreground 设置为绿色,并将 Value
设置为 -5。 如果在将 ControlTemplate 应用于 NumericUpDown
控件时未调用 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 对象:
一个名为
UpButton
的 RepeatButton。一个名为
DownButton.
的 RepeatButton
该控件可以处于以下状态:
在
ValueStates
VisualStateGroup 中Positive
Negative
在
FocusStates
VisualStateGroup 中Focused
Unfocused
若要指定控件所需的 FrameworkElement 对象,可使用 TemplatePartAttribute,它指定预期元素的名称和类型。 若要指定控件的可能状态,可使用 TemplateVisualStateAttribute,它指定状态的名称及其所属的 VisualStateGroup。 将 TemplatePartAttribute 和 TemplateVisualStateAttribute 放置在控件的类定义中。
影响控件外观的任何公共属性也是控件协定的一部分。
以下示例为 NumericUpDown
控件指定 FrameworkElement 对象和状态。
[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
完整的示例
以下示例是 NumericUpDown
控件的整个 ControlTemplate。
<!--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