WPF-Control Templates

控件模板

理解逻辑树和可视化树

通过可视化树可以完成一下两项非常有用的工作

  • 可使用样式改变可视化树中的元素
  • 可为控件创建新模板

LogicalTreeHelper:

  • FindLogicalNode()
  • BringIntoView()
  • GetParent()
  • GetChildren()

VisualTreeHelper:

  • GetChildrenCount()
  • GetChild()
  • GetParent()

Note:可以使用Snoop 深入研究其他应用程序的可视化树。snoopwpf.codeplex.com

简单的递归VisualTree窗口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public partial class VisualTreeDisplay : System.Windows.Window
{

public VisualTreeDisplay()
{
InitializeComponent();
}

public void ShowVisualTree(DependencyObject element)
{
// Clear the tree.
treeElements.Items.Clear();

// Start processing elements, begin at the root.
ProcessElement(element, null);
}

private void ProcessElement(DependencyObject element, TreeViewItem previousItem)
{
// Create a TreeViewItem for the current element.
TreeViewItem item = new TreeViewItem();
item.Header = element.GetType().Name;
item.IsExpanded = true;

// Check whether this item should be added to the root of the tree
//(if it's the first item), or nested under another item.
if (previousItem == null)
{
treeElements.Items.Add(item);
}
else
{
previousItem.Items.Add(item);
}

// Check if this element contains other elements.
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
{
// Process each contained element recursively.
ProcessElement(VisualTreeHelper.GetChild(element, i), item);
}
}
}

对当前元素,添加它的子元素的对象,然后递归子元素及子元素的子元素的对象。

然后可以用其他窗口显示此树

1
2
3
VisualTreeDisplay treeDisplay = new VisualTreeDisplay();
treeDisplay.ShowVisualTree(this);
treeDisplay.Show();

理解模板

每个控件都有一个内置的方法,用于确定如何渲染控件。改方法称为控件模板,是用XAML标记块定义的。

Button类的模板简化版本。

1
2
3
4
5
6
7
8
<ControlTemplate ... >
<mwt:ButtonChrome Name="Chrome" ...>
<ContentPresenter Content="{TemplateBinding ContentControl.Content}" .../>
</mwt:ButtonChrome>
<ControlTemplate.Triggers>
...
</ControlTemplate.Triggers>
</ControlTemplate>

如果希望构建全新按钮,只需要创建新的模板控件。

Note:ButtonChrome类也是继承自Decorator类,和Border类似,是为了添加图形装饰使用。

按钮获得焦点/被单击以及被禁用时,触发器控制按钮如何进行变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<Trigger Property="UIElement.IsKeyboardFocused">
<Setter Property="mwt:ButtonChrome.RenderDefaulted" TargetName="Chrome">
<Setter.Value>
<s:Boolen>True</s:Boolen>
</Setter.Value>
</Setter>
<Trigger.Value>
<s:Boolean>True</s:Boolean>
</Trigger.Value>
</Trigger>

<Trigger Property="ToggleButton.IsChecked">
<Setter Property="mwt:ButtonChrome.RenderPressed" TargetName="Chrome">
<Setter.Value>
<s:Boolean>True</s:Boolean>
</Setter.Value>
</Setter>
<Trigger.Value>
<s:Boolean>True</s:Boolean>
</Trigger.Value>
</Trigger>

触发器未必需要使用TargetName属性。如下

1
2
3
4
5
6
7
8
9
10
<Trigger Property ="UIElement.IsEnabled">
<Setter Property="TextElement.Foreground">
<Setter.Value>
<SolidColorBrush>#FFADADAD</SolidColorBrush>
</Setter.Value>
</Setter>
<Trigger.Value>
<s:Boolean>False</s:Boolean>
</Trigger.Value>
</Trigger>

模板类型

模板有三种类型,都继承自FrameworkTemplate基类。

  • ControlTemplate
  • DataTemplate
  • HierarchicalDatatemplate
  • ItemsControl

修饰类

ButtonChrome类,定义Microsoft.Windows.Themes. 其中还包括BulletChrome,ScrollChrome, ListBoxChrome, SystemDropShadowChrome.

稍高级别上System.Windows.Controls.Primitives命名空间中包含大量可独立使用的元素。这些元素包括ScrollBar, ResizeGrip, Thumb, TickBar等。

ResizeGrip 例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<ControlTemplate TargetType="{x:Type ResizeGrip}">
<Grid Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
<Path Margin="0,0,2,2" Data="M9,0L11,0 11,11 0,11 0,9 3,9 3,6 6,6 6,3 9,3z"
HorizontalAlignment="Right" VerticalAlignment="Bottom">
<Path.Fill>
<DrawingBrush ViewboxUnits="Absolute" TileMode="Tile" Viewbox="0,0,3,3"
Viewport="0,0,3,3" ViewportUnits="Absolute">
<DrawingBrush.Drawing>
<DrawingGroup>
<DrawingGroup.Children>
<GeometryDrawing Geometry="M0,0L2,0 2,2 0,2z">
<GeometryDrawing.Brush>
<LinearGradientBrush EndPoint="1,0.75" StartPoint="0,0.25">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0.3" Color="#FFFFFFFF"/>
<GradientStop Offset="0.75" Color="#FFBBC5D7"/>
<GradientStop Offset="1" Color="#FF6D83A9"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</GeometryDrawing.Brush>
</GeometryDrawing>
</DrawingGroup.Children>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</Path.Fill>
</Path>
</Grid>
</ControlTemplate>

剖析控件

可以通过编程获取所需的信息。基本思想是从Template属性中获取控件的模板,然后使用XamlWriter类,将该模板串行化到XAML文件中。

要构建这么一个程序的诀窍是使用反射(reflection)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Window_Loaded(object sender,EventArgs e)
{
Type controlType = typeof(Control);
List<Type> derivedTypes = new List<Type>();
//Search all the types in the assembly where the Control Class is defined.
Assembly assembly = Assembly.GetAssembly(typeof(Control));
foreach (Type type in assembly.GetTypes())
{
if (type.IsSubclassOf(controlType) && !type.IsAbstract && type.IsPublic)
{
derivedTypes.Add(type);
}
}

derivedTypes.Sort(new TypeComparer());
lstTypes.ItemsSource = derivedTypes;
}

private void lstTypes_SelectionChanged(object sender,SelectionChangedEventArgs e)
{
try
{
Type type = (Type)lstTypes.SelectedItem;
ConstructorInfo info = type.GetConstructor(System.Type.EmptyTypes);
Control control = (Control)info.Invoke(null);

Window win = control as Window;
if (win !=null)
{
win.WindowState = WindowState.Minimized;
win.ShowInTaskbar = false;
win.Show();
}
else
{
control.Visibility = Visibility.Collapsed;
grid.Children.Add(control);
}

ControlTemplate template = control.Template;

XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
StringBuilder sb = new StringBuilder();
XmlWriter writer = XmlWriter.Create(sb, settings);
XamlWriter.Save(template, writer);

txtTemplate.Text = sb.ToString();

if (win!=null)
{
win.Close();
}
else
{
grid.Children.Remove(control);
}
}
catch (Exception err)
{
txtTemplate.Text = "<< Error generation template: " + err.Message + ">>";
}
}

public class TypeComparer:IComparer<Type>
{
public int Compare(Type x,Type y)
{
return x.Name.CompareTo(y.Name);
}
}
}

创建控件模板

简单按钮

基本框架

1
2
3
4
5
6
7
8
9
10
11
12
13
<Window.Resources>
<ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
<Border BorderBrush="Orange" BorderThickness="3" CornerRadius="2"
Background="Red" TextBlock.Foreground="White">
<ContentPresenter RecognizesAccessKey="True"></ContentPresenter>
</Border>
</ControlTemplate>
</Window.Resources>
<Grid>
<StackPanel Margin="5">
<Button Margin="10" Padding="5" Template="{StaticResource ButtonTemplate}">A SImple Button</Button>
</StackPanel>
</Grid>

其中,必须要有ContentPresenter.所有的内容控件都需要ContentPresenter元素,它是表示“在此插入内容的标记器”。

模板绑定

但是仅仅这样StackPanel忽略了Padding属性,通过使用模板绑定,模板可以从应用模板的控件中提取一个值。

1
2
<ContentPresenter RecognizesAccessKey="True"
Margin="{TemplateBinding Padding}"></ContentPresenter>

就可以得到期望的效果.模板绑定只支持单向数据绑定。如果遇到模板绑定不生效,就可以改用数据绑定。

Note:模板绑定支持WPF的变化监测基础结构,所有依赖项属性都包含该基础结构。ContentPresenter元素之所以能够工作,是因为它有一个模板绑定将ContentPresenter.Content属性设置为Button.Content属性。

改变属性的触发器

上面的简单按钮令人胆寒,鼠标移上去没有任何反应。可以添加触发器让它活。

修改!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
<Border Name="Border" BorderBrush="Orange" BorderThickness="3" CornerRadius="2"
Background="Red" TextBlock.Foreground="White">
<ContentPresenter RecognizesAccessKey="True"
Margin="{TemplateBinding Padding}"></ContentPresenter>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Border" Property="Background" Value="DarkRed"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="BorderBrush" Value="DarkKhaki"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>

在按钮和其他控件中,经常还要添加另外一个元素-焦点指示器。简单地使用Trigger根据Button.IsKeyboardFocused属性来显隐该元素。

1
2
3
4
5
6
7
<Grid>
<Rectangle Name="FocusCue" Visibility="Hidden" Stroke="Black"
StrokeThickness="1" StrokeDashArray="1 2"
SnapsToDevicePixels="True"></Rectangle>
<ContentPresenter RecognizesAccessKey="True"
Margin="{TemplateBinding Padding}"></ContentPresenter>
</Grid>
1
2
3
4
5
6
7
<Trigger Property="IsKeyboardFocused" Value="True">
<Setter TargetName="FocusCue" Property="Visibility" Value="Visible"/>
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter TargetName="Border" Property="TextBlock.Foreground" Value="Gray"/>
<Setter TargetName="Border" Property="Background" Value="MistyRose"/>
</Trigger>

最后加了个润色disable的灰色按钮。

Style and Template

样式功能没有Template强大,只能在原本的模板上调整

使用动画的触发器

下面是使用动画Trigger的简单按钮。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<ControlTemplate.Triggers>
<EventTrigger RoutedEvent="MouseEnter">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetName="Border"
Storyboard.TargetProperty="Background.Color"
To="Blue" Duration="0:0:1" AutoReverse="True"
RepeatBehavior="Forever"></ColorAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="MouseLeave">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetName="Border"
Storyboard.TargetProperty="Background.Color"
Duration="0:0:0.5"></ColorAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</ControlTemplate.Triggers>

可以使用两种等价的方法添加鼠标悬停动画-

  1. 创建相应MouseEnter和MouseLeave事件的事件触发器
  2. 创建当IsMouseOver 属性发生变化时添加进入或退出动作的属性触发器

EventTrigger驱动动画的其他任务:

  • 显示或隐藏元素。 改变Opacity
  • 改变形状和位置。 TranslateTransform. ScaleTransform RotateTransform
  • 改变光照或着色。改变绘制背景的画刷的动画。ColorAnimation动画改变SolidBrush。LinearGradientBrush or RadialGradientBrush。

组织模板资源

在大型程序中,在单独资源字典中定义资源是一个更好的主意。

有经验的开发人员更愿意为每个控件模板创建单独的资源字典。为使用它,可使用MergedDictionaries集合完成工作。例如,假如按钮模板放在Resources文件夹的Button.xaml文件中,就可以在App.xaml中如下引用

1
2
3
4
5
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources\Button.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

分解按钮控件模板

控件模板中可能封装了大量的不同细节,如形状、几何图形、画刷等。从控件模板中把细节提取并定义为单独的资源就可以更方便的重用。!

定义有顺序,包括控件模板使用的资源、控件模板以及每个按钮应用控件模板的样式规则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<RadialGradientBrush RadiusX="1" RadiusY="5" GradientOrigin="0.5,0.3" x:Key="HighlightBackground">
<GradientStop Color="White" Offset="0"/>
<GradientStop Color="Blue" Offset="0.4"/>
</RadialGradientBrush>

<RadialGradientBrush RadiusX="1" RadiusY="5" GradientOrigin="0.5,0.3"
x:Key="PressedBackground">
<GradientStop Color="White" Offset="0" />
<GradientStop Color="Blue" Offset="1" />
</RadialGradientBrush>

<SolidColorBrush Color="Blue" x:Key="DefaultBackground"></SolidColorBrush>
<SolidColorBrush Color="Gray" x:Key="DisabledBackground"></SolidColorBrush>

<RadialGradientBrush RadiusX="1" RadiusY="5" GradientOrigin="0.5,0.3"
x:Key="Border">
<GradientStop Color="White" Offset="0" />
<GradientStop Color="Blue" Offset="1" />
</RadialGradientBrush>

<!-- The button control template. -->
<ControlTemplate x:Key="GradientButtonTemplate" TargetType="{x:Type Button}">
<Border Name="Border" BorderBrush="{StaticResource Border}" BorderThickness="2"
CornerRadius="2" Background="{StaticResource DefaultBackground}"
TextBlock.Foreground="White">
<Grid>
<Rectangle Name="FocusCue" Visibility="Hidden" Stroke="Black"
StrokeThickness="1" StrokeDashArray="1 2" SnapsToDevicePixels="True">
</Rectangle>
<ContentPresenter Margin="{TemplateBinding Padding}"
RecognizesAccessKey="True"></ContentPresenter>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background"
Value="{StaticResource HighlightBackground}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Border" Property="Background"
Value="{StaticResource PressedBackground}" />
</Trigger>
<Trigger Property="IsKeyboardFocused" Value="True">
<Setter TargetName="FocusCue" Property="Visibility"
Value="Visible"></Setter>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Border" Property="Background"
Value="{StaticResource DisabledBackground}"></Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>

<!-- The style that applies the button control template. -->
<Style TargetType="{x:Type Button}">
<Setter Property="Control.Template"
Value="{StaticResource GradientButtonTemplate}"></Setter>
</Style>

通过样式应用模板

上面的方式存在局限性,控件模板本质上硬编码了一些细节,如颜色方案。那么如果重新编写,使用模板绑定从控件属性中取出信息,这样就更方便定制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<ControlTemplate x:Key="CustomButtonTemplate" TargetType="{x:Type Button}">
<Border Name="Border" BorderThickness="2" CornerRadius="2"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}">
<Grid>
<Rectangle Name="FocusCue" Visibility="Hidden" Stroke="Black"
StrokeThickness="1" StrokeDashArray="1 2" SnapsToDevicePixels="True">

</Rectangle>
<ContentPresenter Margin="{TemplateBinding Padding}"
RecognizesAccessKey="True"></ContentPresenter>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsKeyboardFocused" Value="True">
<Setter TargetName="FocusCue" Property="Visibility"
Value="Visible"></Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>

<Style x:Key="CustomButtonStyle" TargetType="{x:Type Button}">
<Setter Property="Control.Template"
Value="{StaticResource CustomButtonTemplate}"></Setter>
<Setter Property="BorderBrush" Value="{StaticResource Border}"/>
<Setter Property="Background" Value="{StaticResource DefaultBackground}"/>
<Setter Property="TextBlock.Foreground" Value="White"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{StaticResource HighlightBackground}"></Setter>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="{StaticResource PressedBackground}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="{StaticResource DisabledBackground}">

</Setter>
</Trigger>
</Style.Triggers>
</Style>

为了使用这个新模板,需要设置按钮的Style属性而不是Template属性。

1
<Button Margin="10" Padding="5" Style="{StaticResource CustomButtonStyle}">Cumsj</Button>

自动应用模板

如果需要在整个应用程序中改变每个按钮的外观?

技巧是使用类型样式。

1
2
3
<Style TargetType="{x:Type Button}">
<Setter Property="Control.Template" Value="{StaticResource ButtonTemplate}"
</Style>

如果对单个按钮不想使用该样式

<Button Style="{x:Null}" ...></Button>

Note:如果遵循正确的设计原则,单独定义按钮,这种技术效果更好。

包含基于类型的样式的组合的资源字典一般被称为主题theme 。使用面向搜索引擎编程的技术可以很容易地插入第三方的精彩界面。

由用户选择的皮肤

在使用时想换,可以啊

1
2
3
4
5
6
7
private void chkGreen_Checked(object sender,RoutedEventArgs e)
{
ResourceDictionary resourceDictionary = new ResourceDictionary();
resourceDictionary.Source = new Uri(
"Resources/GradientButtonVariant.xaml", UriKind.Relative);
this.Resources.MergedDictionaries[0] = resourceDictionary;
}

上面的代码加载了GradientButtonVariant资源字典并且放到MergedDictionaries[0]集合的第一个位置。

如果想要为整个应用更改皮肤

Application.Current.Resources.MergedDictionaries[0]=newDictionary;

还可以使用pack URI语法加载在另一个程序集中定义的资源字典。

1
2
3
4
ResourceDictionary resourceDictionary = new ResourceDictionary();
resourceDictionary.Source = new Uri(
"ControlTemplateLibrary;component/GradientButtonVariant.xaml",UriKind.Relative);
this.Resources.MergedDictionaries[0] = resourceDictionary;

还有一种方法–可以手动设置button对象的Template或者Style属性来选用新的模板,但是这种方法需要Dynamic Resource引用实时加载。

还有一种通过编写代码来加载资源字典的方法。此为强类型的,并且可以添加属性方法及其他功能。

Vs不能自动完成为资源字典创建代码隐藏类。手动如下

1
2
3
4
5
6
7
public partial class GradientButtonVariant:ResourceDictionary
{
public GradientButtonVariant()
{
InitializeComponent();
}
}

然后在资源字典中开类

1
2
3
4
5
<ResourceDictionary x:Class="ControlTemplates.GradientButtonVariant" ...>



</ResourceDictionary>

然后就可以在该命名空间下使用代码创建它了

1
2
3
4
5
private void button_Click(object sender, RoutedEventArgs e)
{
GradientButtonVariant newDic = new GradientButtonVariant();
this.Resources.MergedDictionaries[0] = newDic;
}

在Solution Explorer中,如果希望cs嵌套在xaml下面,可以notepad中修改.csproj项目文件。在部分,找到代码隐藏文件,添加DependentUpon

1
2
3
<Compile Include="TestHer3.xaml.cs">
<DependentUpon>TestHer3.xaml</DependentUpon>
</Compile>

构建更复杂的模板

隐含约定,如果使用自定义控件模板替代控件的标准模板,新模板必须满足控件代码实现的所有需要。

嵌套的模板

假设修改熟悉的ListBox控件。这玩意相当的复杂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
<SolidColorBrush x:Key="StandardBorderBrush" Color="#888" />
<SolidColorBrush x:Key="StandardBackgroundBrush" Color="#FFF" />
<SolidColorBrush x:Key="HoverBorderBrush" Color="#DDD" />
<SolidColorBrush x:Key="SelectedBackgroundBrush" Color="Gray" />
<SolidColorBrush x:Key="SelectedForegroundBrush" Color="White" />


<LinearGradientBrush x:Key="ListBoxBackgroundBrush" StartPoint="0,0" EndPoint="1,0.001">
<GradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Color="White" Offset="0.0" />
<GradientStop Color="White" Offset="0.6" />
<GradientStop Color="#DDDDDD" Offset="1.2"/>
</GradientStopCollection>
</GradientBrush.GradientStops>
</LinearGradientBrush>


<LinearGradientBrush x:Key="StandardBrush" StartPoint="0,0" EndPoint="0,1">
<GradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Color="#FFF" Offset="0.0"/>
<GradientStop Color="#CCC" Offset="1.0"/>
</GradientStopCollection>
</GradientBrush.GradientStops>
</LinearGradientBrush>

<SolidColorBrush x:Key="GlyphBrush" Color="#444" />
<LinearGradientBrush x:Key="PressedBrush" StartPoint="0,0" EndPoint="0,1">
<GradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Color="#BBB" Offset="0.0"/>
<GradientStop Color="#EEE" Offset="0.1"/>
<GradientStop Color="#EEE" Offset="0.9"/>
<GradientStop Color="#FFF" Offset="1.0"/>
</GradientStopCollection>
</GradientBrush.GradientStops>
</LinearGradientBrush>

<Style x:Key="ScrollBarLineButtonStyle" TargetType="{x:Type RepeatButton}">
<Setter Property="Focusable" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type RepeatButton}">
<Grid Margin="1">
<Ellipse Name="Border" StrokeThickness="1" Stroke="{StaticResource StandardBorderBrush}"
Fill="{StaticResource StandardBrush}"></Ellipse>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"></ContentPresenter>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsPressed" Value="true">
<Setter TargetName="Border" Property="Fill" Value="{StaticResource PressedBrush}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

<Style x:Key="ScrollBarPageButtonStyle" TargetType="{x:Type RepeatButton}">
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="Focusable" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type RepeatButton}">
<Border Background="Transparent" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

<Style x:Key="ScrollBarThumbStyle" TargetType="{x:Type Thumb}">
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="Focusable" Value="False"/>
<Setter Property="Margin" Value="1,0,1,0" />
<Setter Property="Background" Value="{StaticResource StandardBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource StandardBorderBrush}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Thumb}">
<Ellipse Stroke="{StaticResource StandardBorderBrush}"
Fill="{StaticResource StandardBrush}"></Ellipse>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

<ControlTemplate x:Key="VerticalScrollBar" TargetType="{x:Type ScrollBar}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition MaxHeight="18"/>
<RowDefinition Height="*"/>
<RowDefinition MaxHeight="18"/>
</Grid.RowDefinitions>

<RepeatButton Grid.Row="0" Height="18"
Style="{StaticResource ScrollBarLineButtonStyle}"
Command="ScrollBar.LineUpCommand" >
<Path
Fill="{StaticResource GlyphBrush}"
Data="M 0 4 L 8 4 L 4 0 Z"></Path>
</RepeatButton>
<Track Name="PART_Track" Grid.Row="1"
IsDirectionReversed="True" ViewportSize="0">
<Track.DecreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageUpCommand" Style="{StaticResource ScrollBarPageButtonStyle}">
</RepeatButton>
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb Style="{StaticResource ScrollBarThumbStyle}">
</Thumb>
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageDownCommand" Style="{StaticResource ScrollBarPageButtonStyle}">
</RepeatButton>
</Track.IncreaseRepeatButton>
</Track>
<RepeatButton
Grid.Row="3" Height="18"
Style="{StaticResource ScrollBarLineButtonStyle}"
Command="ScrollBar.LineDownCommand">
<Path
Fill="{StaticResource GlyphBrush}"
Data="M 0 0 L 4 4 L 8 0 Z"></Path>
</RepeatButton>
</Grid>
</ControlTemplate>


<Style TargetType="{x:Type ScrollBar}">
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="OverridesDefaultStyle" Value="true"/>
<Style.Triggers>

<Trigger Property="Orientation" Value="Vertical">
<Setter Property="Width" Value="18"/>
<Setter Property="Height" Value="Auto" />
<Setter Property="Template" Value="{StaticResource VerticalScrollBar}" />
</Trigger>
</Style.Triggers>
</Style>


<Style TargetType="{x:Type ListBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBox}">
<Border
Name="Border"
Background="{StaticResource ListBoxBackgroundBrush}"
BorderBrush="{StaticResource StandardBorderBrush}"
BorderThickness="1"
CornerRadius="3">
<ScrollViewer
Focusable="False">
<ItemsPresenter Margin="2"></ItemsPresenter>
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Border
Name="Border" BorderThickness="2" CornerRadius="3"
Padding="1"
SnapsToDevicePixels="True">
<ContentPresenter />
</Border>
<ControlTemplate.Triggers>
<EventTrigger RoutedEvent="ListBoxItem.MouseEnter">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="FontSize"
To="20" Duration="0:0:1"></DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
<EventTrigger RoutedEvent="ListBoxItem.MouseLeave">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="FontSize"
BeginTime="0:0:0.5" Duration="0:0:0.2"></DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>

<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="BorderBrush" Value="{StaticResource HoverBorderBrush}"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Border" Property="Background" Value="{StaticResource SelectedBackgroundBrush}"/>
<Setter TargetName="Border" Property="TextBlock.Foreground" Value="{StaticResource SelectedForegroundBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

控件模板示例

ControlTemplateExamples.获取的方法是http://tinyurl.com/9jtk93x网站下载。

Note:SimpleStyles是隐藏的WPF宝藏。他们提供了比默认的控件模板更易于理解和增强的模板,如果需要使用自定义外观增强通用控件,应该使用它作为起点。

可视化状态

控件可以使用TemplatePart特性指示控件模板应当包含具有特定名称的元素,可使用TemplateVisualState特性指示他们支持的可视化状态。例如按钮应该提供如下可视化状态。

1
2
3
4
5
6
[TemplateVisualState{Name="Normal"   ,GroupName="CommonStates"]
[TemplateVisualState{Name="MouseOver",GroupName="CommonStates"]
[TemplateVisualState{Name="Pressed" ,GroupName="CommonStates"]
[TemplateVisualState{Name="Disabled" ,GroupName="CommonStates"]
[TemplateVisualState{Name="Unfocused",GroupName="FocusStates"]
[TemplateVisualState{Name="Focused" ,GroupName="FocusStates"]
1
2
public class Button : ButtonBase
{ ... }

小结

​ 学习了如何使用基本的模板构建技术为WPF核心控件更换皮肤,而不必重新实现任何核心按钮功能。可以在整个应用程序中重用按钮模板,并能立即使用全新的设计替换该模板。恭喜。