WPF之样式和控件模板


提到样式应该要追溯到 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等);样式触发器高于模板触发器。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM