提到样式应该要追溯到 Html中的CSS样式了,通常是为了使多个元素(控件)达到一个效果(外观或者拥有相同的功能);当然,在WPF中也有类似于CSS这样,不过肯定是比CSS更要强大的哦。
基本使用
<StackPanel>
<Button Height="50" Margin="0 10 0 0" Background="Beige" Width="100">
<Button.RenderTransform>
<RotateTransform Angle="45"></RotateTransform>
</Button.RenderTransform>
</Button>
<Button Height="50" Margin="0 10 0 0" Background="Beige" Width="100">
<Button.RenderTransform>
<RotateTransform Angle="45"></RotateTransform>
</Button.RenderTransform>
</Button>
<Button Height="50" Margin="0 10 0 0" Background="Beige" Width="100">
<Button.RenderTransform>
<RotateTransform Angle="45"></RotateTransform>
</Button.RenderTransform>
</Button>
</StackPanel>
上边的三个按钮使用了同样的倾斜规则,好在我们只有3个按钮,如果我们有几十个或者更多呢,实在是看着不舒服,也是对资源的极其浪费.如果我们提取出样式会是怎样的呢:
<UserControl.Resources>
<Style TargetType="Button">
<Setter Property="Height" Value="50"></Setter>
<Setter Property="Width" Value="100"></Setter>
<Setter Property="Margin" Value="0 10 0 0"></Setter>
<Setter Property="Background" Value="Beige"></Setter>
<Setter Property="RenderTransform">
<Setter.Value>
<RotateTransform Angle="45"></RotateTransform>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
添加了这样一个Style样式,切忌要放到Resource中哦(当前是Usercontrol,也可以是Grid或者其他元素的),使用如下:
<Grid x:Name="LayoutRoot" Background="White">
<StackPanel>
<Button>
</Button>
<Button>
</Button>
<Button>
</Button>
</StackPanel>
哈哈,是不是很奇怪呢,我对Button什么也没有做,看到的效果依然如此
为什么呢,当添加了Style之后,并且没有对Style设置Key,同时呢设置了TargetType,那么此TargetType的控件就会自动用上你给的样式哦.
样式的继承
上边的使用是最简单的样式使用方式,下面有一个新的需求,我们添加了一个RadioButton,同样也希望旋转45度,但是呢RadioButton也有自己不同的地方,这时候呢又想重用之前的样式代码,怎么办呢?还好我们有样式继承功能BaseOn属性,看看下边怎么做.
<Window.Resources>
<Style TargetType="{x:Type Control}">
<Setter Property="Height" Value="50"></Setter>
<Setter Property="Width" Value="100"></Setter>
<Setter Property="Margin" Value="0 10 0 0"></Setter>
<Setter Property="Background" Value="Beige"></Setter>
<Setter Property="RenderTransform">
<Setter.Value>
<RotateTransform Angle="45"></RotateTransform>
</Setter.Value>
</Setter>
</Style>
<Style BasedOn="{StaticResource {x:Type Control}}" TargetType="RadioButton">
<Setter Property="Foreground" Value="Blue"></Setter>
<Setter Property="FontSize" Value="50"></Setter>
</Style>
<Style BasedOn="{StaticResource {x:Type Control}}" TargetType="Button">
<Setter Property="FontSize" Value="22"></Setter>
<Setter Property="Foreground" Value="Red"></Setter>
</Style>
</Window.Resources>
在上边中主要就是使用了BaseOn属性来设置一个看起来比较费解的值.第一个Style标签是我们样式的基类,基类没有名字,只指定了TargetType="{x:Type Control}",即样式应用到所有的控件;两个子样式都BaseOn了这个基样式,BaseOn="{StaticResource {x:Type Control}}",这个式子蛮奇怪,那是因为我们的基类样式没有指定Key,如果指定了,那么我们的写法就是 BaseOn="{StaticResource baseStyleName}",如果样式没有指定key,那么就认为是TargetType的值了。效果如下:
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Content="Test"></Button>
<Button Content="Test"></Button>
<RadioButton >男</RadioButton>
</StackPanel>
三个按钮都应用了样式,当然有公共的和自己的样式.
样式触发器
样式中还有一个很好玩的功能,就是Triggers集合,这个是专门的触发器集合.首先来看如何通过触发器实现按钮悬浮时候改变字体颜色的功能.
<Style BasedOn="{StaticResource {x:Type Control}}" TargetType="{x:Type Button}">
<Setter Property="FontSize" Value="22"></Setter>
<Setter Property="Foreground" Value="Red"></Setter>
<Style.Triggers>
<Trigger Property="IsPressed" Value="true">
<Setter Property = "Foreground" Value="Green"/>
</Trigger>
</Style.Triggers>
</Style>
WPF中有3种触发器:
1.属性触发器:即上例子;
2.数据触发器(DataTrigger):普通的.net属性或者是依赖属性改变时触发;
3.事件触发器(EventTrigger):触发路由事件时会被调用。
事件触发器:
<ListBox.Triggers>
<EventTrigger RoutedEvent="ListBox.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="0" To="1" Duration="0:0:5"></DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</ListBox.Triggers>
上边的代码制作了一个ListBox的动画效果,使ListBox的透明度从0变到1,通过EventTrigger来实现.EventTrigger的RoutedEvent属性表示发生的事件,上边代码表示ListBox的Loaded事件.与属性触发器不同,EventTrigger中是TriggerAction的集合,即发生的事件和执行的操作.BeginStroyboard是一种操作,用于开始动画效果.
数据触发器:
DataTrigger之所以能够支持普通.Net属性,是因为它比属性触发器多了一个属性Binding,通过绑定来指定相关属性。
public class Student
private int _age;
public int Age
get { return _age; }
set { _age = value; }
private string _name;
public string Name
get { return _name; }
set { _name = value; }
一个自定义的类,用于绑定ListBox.
List<Student> students = new List<Student> {
new Student{ Age=18, Name="Listen"},
new Student{ Age=12, Name="Fly"},
new Student{ Age=18, Name="Colors"},
new Student{ Age=9, Name="Blue"},
this.lstStudent.ItemsSource = students;
在后置代码中进行初始化List集合,并赋值给ListBox.
<Window.Resources>
<Style TargetType="ListBoxItem">
<Setter Property="Margin" Value="0 , 2, 0, 2"></Setter>
<Setter Property="Padding" Value="0 , 2, 0, 2"></Setter>
<Setter Property="FontFamily" Value="微软雅黑"></Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Age}" Value="18">
<Setter Property="Background" Value="LightBlue"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
添加针对ListBoxItem的样式,其中的DataTrigger为数据触发器,Bindding就是绑定的属性,当Age的值为18的时候,就设置ListBoxItem的背景色为LightBlue。
<ListBox x:Name="lstStudent" DisplayMemberPath="Name">
</ListBox>
效果如下:

第一行和第三行的Age为18,背景色也发生了变化.
模板完整示例:
<Grid.Resources>
<ControlTemplate x:Key="buttonTemplate">
<Grid Width="100" Height="100">
<Ellipse x:Name="outerCircle" Width="100" Height="100">
<Ellipse.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="Blue"/>
<GradientStop Offset="1" Color="Red"/>
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Ellipse Width="80" Height="80">
<Ellipse.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="White"/>
<GradientStop Offset="1" Color="Transparent"/>
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="Button.IsMouseOver" Value="True">
<Setter TargetName="outerCircle" Property="Fill" Value="Orange"/>
</Trigger>
<Trigger Property="Button.IsPressed" Value="True">
<Setter Property="RenderTransform">
<Setter.Value>
<ScaleTransform ScaleX="0.9" ScaleY="0.9"/>
</Setter.Value>
</Setter>
<Setter Property ="RenderTransformOrigin" Value=".5,.5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Grid.Resources>
此模板实现了,嵌套了两个Ellipse,最主要的是悬浮到按钮上,外层Ellipse的颜色会变为Orange,点击按钮则会缩放按钮的大小为90%.
模板工作原理:
WPF中的模板分别为ControlTemplate(控件模板)、DataTemplate(数据模板)、HierarchicalDataTemplate(表示支持 HeaderedItemsControl(表示包含多个项目并具有标头的控件)的 DataTemplate ,例如 TreeViewItem 或 MenuItem)和ItemsPanelTemplate(项容器模板,例如ListBox的Items的容器模板),它们均继承自FrameworkTemplate,其中HierarchicalDataTemplate继承自DataTemplate。
模板通过改变控件的可视化树(visual Tree)来彻底改变其外观,比如上边代码将按钮的可视化树变为两个嵌套的Ellipse。 控件均派生自Control的类,只有Control及其派生类才有Template 这样的属性
模板绑定和模板触发器:
<Grid.Resources>
<ControlTemplate x:Key="buttonTemplate" TargetType="{x:Type Button}">
<Ellipse x:Name="outerCircle">
<Ellipse.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="{Binding RelativeSource={RelativeSource TemplatedParent},Path=Background.Color}"/>
<GradientStop Offset="1" Color="Red"/>
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Ellipse RenderTransformOrigin=".5,.5">
<Ellipse.RenderTransform>
<ScaleTransform ScaleX=".8" ScaleY=".8"/>
</Ellipse.RenderTransform>
<Ellipse.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="White"/>
<GradientStop Offset="1" Color="Transparent"/>
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Viewbox>
<ContentPresenter Margin="20" Content="{TemplateBinding Content}"/>
</Viewbox>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="Button.IsMouseOver" Value="True">
<Setter TargetName="outerCircle" Property="Fill" Value="{Binding RelativeSource={RelativeSource TemplatedParent},Path=BorderBrush}"/>
</Trigger>
<Trigger Property="Button.IsPressed" Value="True">
<Setter Property="RenderTransform">
<Setter.Value>
<ScaleTransform ScaleX="0.9" ScaleY="0.9"/>
</Setter.Value>
</Setter>
<Setter Property ="RenderTransformOrigin" Value=".5,.5"/>
</Trigger>
<Trigger Property
="IsEnabled" Value="False">
<Setter TargetName="outerCircle" Property="Fill" Value="Gray">
</Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Grid.Resources>
<StackPanel>
<Button Width="100" Height="100" Template="{StaticResource buttonTemplate}" Click="Button_Click" Content="OK" Background="CadetBlue" BorderBrush="BurlyWood"
<TextBlock Text="IsEnable = True" HorizontalAlignment="Center"/>
<Button Width="100" Height="100" Template="{StaticResource buttonTemplate}" Click="Button_Click" Content="Disabled" IsEnabled="False"></Button>
<TextBlock Text="IsEnable = False" HorizontalAlignment="Center"/>
</StackPanel>
</Grid>
重新修改了之前的示例代码,主要是修改了Ellipse的颜色,使用了LinearGradientBrush线性画刷,同时使用了RelativeResource相对资源绑定方式,此处的TemplatedParent应为Button本身,使用ContentPresenter来绑定按钮的Content,切记是使用TemplateBinding来绑定 。
模板绑定(TemplateBinding)类似一般的数据绑定,与一般的绑定相比有如下限制:
1.仅在模板的可视化树内部有效,在模板外部,甚至模板的Trigger中都无效。
2.不能应用在
Freezable
派生对象的属性上,如果你尝试绑定Brush的Color属性,则会失败哦。
我们上述代码有两处的数据绑定,一个是Ellipse的Fill属性中。因为Color属于Freezable派生的对象的属性,因此不能使用模板绑定。二是在Trigger中对IsMouseOver的处理,这事因为Trigger不属于控件模板的可视化树内容,因此使用模板无效。
模板和样式触发器比较类似,但是有区别哦:
1.样式触发器无法应用于模板的某个元素,而模板的触发器可以。比如上例子中可以在IsMouseOver为true的时候设置第一个Ellipse的Fill属性,而在样式中只能设置整个控件的某个属性。Setter的TargeName和Trigger的SourceName属性均用来指定模板中的某个子元素,该子元素必须有一个名字。
2.样式触发器优先级高于模板的触发器。
接下来和大家分享一个小程序,控件模板的浏览器程序(来自WPF葵花宝典一书):
<
Grid.RowDefinitions
>
<
RowDefinition
Height
="*"
/>
<
RowDefinition
Height
="20"
/>
</
Grid.RowDefinitions
>
<
Grid
Grid.Row
="0"
Name
="grid"
>
<
Grid.ColumnDefinitions
>
<
ColumnDefinition
Width
="*"
></
ColumnDefinition
>
<
ColumnDefinition
Width
="2"
></
ColumnDefinition
>
<
ColumnDefinition
Width
="3*"
></
ColumnDefinition
>
</
Grid.ColumnDefinitions
>
<
GridSplitter
Grid.Column
="1"
ResizeDirection
="Columns"
VerticalAlignment
="Stretch"
Width
="2"
Background
="Black"
HorizontalAlignment
="Center"
ShowsPreview
="True"
/>
<
TreeView
DisplayMemberPath
="Name"
Name
="lstTypes"
SelectedItemChanged
="lstTypes_SelectedItemChanged"
></
TreeView
>
<
TextBox
Grid.Column
="2"
Name
="txtTemplate"
TextWrapping
="Wrap"
VerticalScrollBarVisibility
="Visible"
FontFamily
="Consolas"
></
TextBox
>
</
Grid
>
<
TextBlock
x:Name
="txtbar"
Grid.Row
="1"
Height
="18"
HorizontalAlignment
="Left"
Margin
="10,0,0,0"
Text
="Wait"
></
TextBlock
>
</
Grid
>
后置代码如下:
private void Window_Loaded(object sender, RoutedEventArgs e)
// 获得Control的程序集
Assembly asbly = Assembly.GetAssembly(typeof(Control));
// 获得该程序集里的所有类型
Type[] atype = asbly.GetTypes();
// 使用该列表存储
SortedList<string, TreeViewItem> sortlst =
new SortedList<string, TreeViewItem>();
TreeViewItem item = new TreeViewItem();
item.Header = "Control";
item.Tag = typeof(Control);
sortlst.Add("Control", item);
lstTypes.Items.Add(item);
// 遍历所有的类型,然后将派生自contorl的类型添加到列表当中
foreach (Type typ in atype)
if (typ.IsPublic && (typ.IsSubclassOf(typeof(Control))))
item = new TreeViewItem();
item.Header = typ.Name;
item.Tag = typ;
sortlst.Add(typ.Name, item);
// 构建树
foreach (KeyValuePair<string, TreeViewItem> kvp in sortlst)
if (kvp.Key != "Control")
string strParent = ((Type)kvp.Value.Tag).BaseType.Name;
TreeViewItem itemParent = sortlst[strParent];
itemParent.Items.Add(kvp.Value);
private void lstTypes_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
Cursor oldcur = this.Cursor;
this.Cursor = Cursors.Wait;
// 获得选中的类型
TreeViewItem selectedItem = (TreeViewItem)lstTypes.SelectedItem;
if (selectedItem.HasItems)
this.Cursor = oldcur;
//wndBar.Hide();
return;
Type type = (Type)selectedItem.Tag;
// 实例化该type
ConstructorInfo info = type.GetConstructor(System.Type.EmptyTypes);
Control control = (Control)info.Invoke(null);
// 添加该控件 但是将属性状态设置为Collapsed.
control.Visibility = Visibility.Collapsed;
grid.Children.Add(control);
// 获得模板
ControlTemplate template = control.Template;
// 获得模板的XAML文件
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();
txtbar.Text = type.Name + "Control Template";
// 移出该控件
grid.Children.Remove(control);
this.Cursor = oldcur;
catch (Exception err)
txtTemplate.Text = "<< Error generating template: " + err.Message + ">>";
效果图如下:

样式和主题:
其实WPF中的主题或者是换肤其实就是通过样式和模板来实现的,当然通常会建立一个个xaml资源字典存放不同风格的样式,一般是在App.xaml中引入需要的样式字典。如下代码:
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/button.xaml"></ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
窗体代码:
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Menu Grid.Row="0" VerticalAlignment="Top">
<MenuItem Header="皮肤" >
<MenuItem Header="简单按钮风格" Name="simpleSkin" Click="MenuItem_Click" IsChecked="True"></MenuItem>
<MenuItem Header="个性化按钮风格" Name="fancySkin" Click="MenuItem_Click"></MenuItem>
</MenuItem>
</Menu>
<Button Grid.Row="1" Content="OK" VerticalAlignment="Center" HorizontalAlignment="Center"/>
</Grid>
后置代码:
private void MenuItem_Click(object sender, RoutedEventArgs e)
MenuItem menuItem = (MenuItem)sender;
if(menuItem == null) return;
if (menuItem == simpleSkin)
ResourceDictionary newDictionary = new ResourceDictionary();
newDictionary.Source = new Uri("Resources/button.xaml", UriKind.Relative);
Application.Current.Resources.MergedDictionaries[0] = newDictionary;
menuItem.IsChecked = true;
fancySkin.IsChecked = false;
else if (menuItem == fancySkin)
ResourceDictionary newDictionary = new ResourceDictionary();
newDictionary.Source = new Uri("Resources/fancyButton.xaml", UriKind.Relative);
Application.Current.Resources.MergedDictionaries[0] = newDictionary;
menuItem.IsChecked = true;
simpleSkin.IsChecked = false;
主要是通过Application.Current.Resources.MergedDictionaries.Add添加一个ResourceDictionary或者是直接修Application.Current.Resources.MergedDictionaries[0] 的值即可,主要代码是,通过设置ResourceDictionary的Source来指定需要的资源文件;这样在切换选中的菜单项之后就可以看到按钮样式的不同哦。

希望大家多多交流讨论。
Tip:WPF中的样式优先级: 模板中的样式优先级高于外部样式(Resource中的style等);样式触发器高于模板触发器。