提到样式应该要追溯到 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> <Grid.Resources> <ControlTemplate x:Key="buttonTemplate" TargetType="{x:Type Button}"> <Grid > <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> <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) { try { 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> <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等);样式触发器高于模板触发器。