添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
温暖的弓箭  ·  SwiftUI TextField & ...·  2 年前    · 

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