WPF Style & Template

1.Style

Style就是一堆Setter/Trigger.
WPF元素可以通过元素的Style属性设置(Style属性在FrameworkElement中定义)使用样式。
Setter是Style中最重要的属性,Setter只支持依赖属性;
某些依赖属性无法使用简单的字符串设置值,这时使用嵌套元素的方法实现,比如ImageBrush。

<Window.Resources>
<Style x:Key="bigFontButtonStyle">
    <Setter Property="Button.FontFamily" Value="Times New Roman"/>
    <Setter Property="Button.FontSize" Value="18"/>
    <Setter Property="Button.FontWeight" Value="Bold"/>
    <Setter Property="Button.Foreground" Value="Red"/>
    <Setter Property="Button.Background">
        <Setter.Value>
            <ImageBrush TileMode="Tile"
                    ViewportUnits="Absolute"
                    Viewport="0 0 32 32"
                    ImageSource="/images/open32.png"
                    Opacity="0.3"/>
        </Setter.Value>
    </Setter>
</Style>
</Window.Resources>

<Button Content="button" Style="{StaticResource bigFontButtonStyle}"/>

希望在多个Style中共用的画刷等资源,可以定义为资源,然后在各个样式中使用.
1)新建一个目录Resources
2)添加 Resource Dictionary
3)在Resource Dictionary xaml文件中定义资源
4)在需要使用它的窗口中使用

<!--定义了一个用于共享的image brush, MyRes.xaml-->
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:WpfApp5">

    <ImageBrush x:Key="MyImgBrush" TileMode="Tile"
                    ViewportUnits="Absolute"
                    Viewport="0 0 32 32"
                    ImageSource="/images/open32.png"
                    Opacity="0.3"/>

</ResourceDictionary>
<!-- 合并资源 -->
<!--指定程序集,并使用component关键字
<ResourceDictionary Source="/CustomControls;component/themes/ColorPicker.xaml"/>-->
<Window.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="Resources/MyRes.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Window.Resources>
<!-- 使用 -->
<Button Content="tuple" Background="{StaticResource MyImgBrush}" Height="50"/>

<!--Resource Dictionary内部也可以使用资源合并-->
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:WpfApp5">
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="/Resources/MyRes.xaml"/>
    </ResourceDictionary.MergedDictionaries>

    <ControlTemplate x:Key="customButtonTemplate" TargetType="{x:Type Button}">
        <Border Name="Border" BorderThickness="2" CornerRadius="2"
                    Background="{StaticResource MyImgBrush}"
                    BorderBrush="{TemplateBinding BorderBrush}">
            <Grid>
                <Rectangle Name="FocusCue" Visibility="Hidden" Stroke="Red"
                           StrokeThickness="1" StrokeDashArray="1 2"
                           SnapsToDevicePixels="True"/>
                <ContentPresenter Margin="{TemplateBinding Padding}"
                                      RecognizesAccessKey="True"/>
            </Grid>
        </Border>
        <ControlTemplate.Triggers>
            <Trigger Property="IsKeyboardFocused" Value="True">
                <Setter TargetName="FocusCue" Property="Visibility" Value="Visible"/>
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>

</ResourceDictionary>

用Setter设置属性时,需要指定类和属性名,类名不一定非得是定义该属性得类名,也可以是派生类的类名;
如果要设置的属性是针对某个特定的类型的,那么可以使用TargetType

<Style x:Key="bigFontButtonStyle" TargetType="Button">
    <Setter Property="FontFamily" Value="Times New Roman"/>
    <Setter Property="Foreground" Value="Red"/>
    <Setter Property="Background">
        <Setter.Value>
            <ImageBrush TileMode="Tile"
                    ViewportUnits="Absolute"
                    Viewport="0 0 32 32"
                    ImageSource="/images/open32.png"
                    Opacity="0.3"/>
        </Setter.Value>
    </Setter>
</Style>

WPF很少使用EventSetter

<Style x:Key="MouseOverStyle">
    <EventSetter Event="TextBlock.MouseEnter" Handler="elem_MouseEnter"/>
    <EventSetter Event="TextBlock.MouseLeave" Handler="elem_MouseLeave"/>
</Style>
private void elem_MouseEnter(object sender, MouseEventArgs e)
{
    ((TextBlock)sender).Background = new SolidColorBrush(Colors.LightGoldenrodYellow);
}
private void elem_MouseLeave(object sender, MouseEventArgs e)
{
    ((TextBlock)sender).Background = null;
}

WPF Style支持继承(通过BasedOn属性),但不推荐使用,除非有特别原因,否则不要使用

<Window.Resources>
    <Style x:Key="bigFontButtonStyle">
        <Setter Property="Control.FontFamily" Value="Times New Roman"/>
        <Setter Property="Control.FontSize" Value="18"/>
        <Setter Property="Control.FontWeight" Value="Bold"/>
    </Style>
    <Style x:Key="enhenceBigFontButtonStyle" BasedOn="{StaticResource bigFontButtonStyle}">
        <Setter Property="Control.Foreground" Value="Red"/>
        <Setter Property="Control.Background">
            <Setter.Value>
                <ImageBrush TileMode="Tile"
                        ViewportUnits="Absolute"
                        Viewport="0 0 32 32"
                        ImageSource="/images/open32.png"
                        Opacity="0.3"/>
            </Setter.Value>
        </Setter>
    </Style>
</Window.Resources>

可以通过类型自动应用样式:只要指定TargetType并去掉x:Key即可

<Style TargetType="Button">
    <Setter Property="Control.Foreground" Value="Red"/>
    <Setter Property="Control.FontWeight" Value="Bold"/>
</Style>
<!--上面的定义等价于下面的定义-->
<Style x:Key="{x:Type Button}">
    <Setter Property="Control.Foreground" Value="Red"/>
    <Setter Property="Control.FontWeight" Value="Bold"/>
</Style>

<StackPanel>
    <!--取消使用上述style-->
    <Button Content="tuple" Style="{x:Null}"/>
    <!--自动使用上述style-->
    <Button Content="button"/>
</StackPanel>

普通触发器Trigger

//触发器的优点是不需要为恢复写任何代码,多个触发器冲突时,后面的覆盖前面的
<Style x:Key="mybuttonstyle">
    <Setter Property="Control.FontWeight" Value="Bold"/>
    <Setter Property="Control.Foreground" Value="Red"/>
    <Style.Triggers>
        <Trigger Property="Control.IsFocused" Value="True">
            <Setter Property="Control.Foreground" Value="Green"/>
        </Trigger>
        <Trigger Property="Button.IsPressed" Value="True">
            <Setter Property="Control.Foreground" Value="Blue"/>
        </Trigger>
    </Style.Triggers>
</Style>

//多个条件的,使用MultiTrigger
<Style x:Key="myButtonStyle">
    <Setter Property="Control.FontWeight" Value="Bold"/>
    <Style.Triggers>
        <MultiTrigger>
            <MultiTrigger.Conditions>
                <Condition Property="Control.IsFocused" Value="True"/>
                <Condition Property="Control.IsMouseOver" Value="True"/>
            </MultiTrigger.Conditions>
            <MultiTrigger.Setters>
                <Setter Property="Control.Foreground" Value="Blue"/>
            </MultiTrigger.Setters>
        </MultiTrigger>
    </Style.Triggers>
</Style>

事件触发器
普通触发器等待属性发生变化,事件触发器等待特定事件被引发,事件触发器通常被用于动画

<Style x:Key="myButtonStyle">
    <Setter Property="Control.FontWeight" Value="Bold"/>
    <Style.Triggers>
        <!--- 在0.2s内,字体大小从当前值改变到22 -->
        <EventTrigger RoutedEvent="Mouse.MouseEnter">
            <EventTrigger.Actions>
                <BeginStoryboard>
                    <Storyboard>
                        <!--- DoubleAnimation在指定时间内将double值从当前值改变到指定值-->
                        <DoubleAnimation Duration="0:0:0.2"
                                            Storyboard.TargetProperty="FontSize"
                                            To="22"/>
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger.Actions>
        </EventTrigger>

        <!--- 在2s内,字体大小从当前值改变回原来的值 -->
        <EventTrigger RoutedEvent="Mouse.MouseLeave">
            <EventTrigger.Actions>
                <BeginStoryboard>
                    <Storyboard>
                        <!--- 没有设置To,WPF假定使用第一次动画前的值 -->
                        <DoubleAnimation Duration="0:0:1"
                                            Storyboard.TargetProperty="FontSize"/>
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger.Actions>
        </EventTrigger>
    </Style.Triggers>
</Style>

2.模板

WPF的控件都是设计成无外观的(lookless),但控件行为是固化到控件类中的,外观可以自己定义。

WPF有3种类型的模板:
控件模板(由ControlTemplate类表示),
数据模板(由DataTemplate和HierarchicalDataTemplate类表示),
以及用于ItemsControl控件的面板模板(由ItemsPanelTemplate类表示)。

数据模板用于从对象中提取数据,并在内容控件或者列表控件的各个项中显示数据,在数据绑定中,数据模板非常有用;在一定程度上数据模板和控件模板相互重叠。

System.Windows.Controls.Primitives提供可以在各种控件中使用的基本要素,而Micosoft.Windows.Themes包含了用于渲染这些细节的基本绘图逻辑。SnapToDevicePixels确保单个像素的线条不会被放到两个像素的“中间”,这在自定义模板中也非常有用。

下面定义了一个模板,使用TemplateBinding把模板里面的某个属性和使用该模板的控件的属性绑定到一起,并使用触发器设置相关属性的值。

//可以共用的资源,可以提取处理,定义成资源
<RadialGradientBrush x:Key="HighlightBackground"
                        RadiusX="1" RadiusY="5" GradientOrigin="0.5,0.3">
    <GradientStop Color="White" Offset="0"/>
    <GradientStop Color="Blue" Offset=".4"/>
</RadialGradientBrush>
<RadialGradientBrush x:Key="PressedBackground"
                        RadiusX="1" RadiusY="5" GradientOrigin="0.5,0.3">
    <GradientStop Color="White" Offset="0"/>
    <GradientStop Color="Blue" Offset="1"/>
</RadialGradientBrush>

//模板定义
<ControlTemplate x:Key="MyButtonTemplate" TargetType="{x:Type Button}">
    <Border Name="Border" 
            //按钮的边框颜色
            BorderBrush="{TemplateBinding BorderBrush}"
            //按钮的边框宽度
            BorderThickness="{TemplateBinding BorderThickness}"
            //按钮的背景
            Background="{TemplateBinding Background}"
            //按钮文本颜色(ContentPresenter里面默认是一个透明的TextBlock)
            TextBlock.Foreground="{TemplateBinding Foreground}">
        <Grid>
            //focus时显示虚线
            <Rectangle Name="FocusCue" Visibility="Hidden" Stroke="Green"
                       StrokeThickness="1" StrokeDashArray="1 2" SnapsToDevicePixels="True"/>
            //文本内容的margin和文本的对齐(注意不要和按钮对齐方式混淆)
            <ContentPresenter Margin="{TemplateBinding Padding}" 
                       HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"/>
        </Grid>
    </Border>

    <ControlTemplate.Triggers>
        //鼠标移到按钮上,背景变成HighlightBackground
        <Trigger Property="IsMouseOver" Value="True">
            <Setter TargetName="Border" Property="Background" 
                    Value="{StaticResource HighlightBackground}"/>
        </Trigger>
        //鼠标按下,背景变成PressedBackground,边框变成DarkKhaki
        <Trigger Property="IsPressed" Value="True">
            <Setter TargetName="Border" Property="Background" 
                    Value="{StaticResource PressedBackground}"/>
            <Setter TargetName="Border" Property="BorderBrush" Value="DarkKhaki"/>
        </Trigger>
        //foucs显示虚线,通过触发器显示隐藏是最常见的用法
        <Trigger Property="IsKeyboardFocused" Value="True">
            <Setter TargetName="FocusCue" Property="Visibility" Value="Visible"/>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

<Button x:Name="btn1"
    Content="My Button Template" Width="300"
    Template="{StaticResource MyButtonTemplate}"  
    //控件右对齐              
    HorizontalAlignment="Right" 
    //文本中间对齐
    HorizontalContentAlignment="Center" 
    //参见上面模板里面的TemplateBinding               
    BorderBrush="Yellow"
    BorderThickness="5"                    
    Background="BurlyWood"
    Foreground="Red"/>

3.通过样式应用模板

新建Custom Control时,VS会自动添加使用样式的模板。

<Style x:Key="MyButtonStyle" TargetType="{x:Type Button}">
    <Setter Property="Control.Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type Button}">
                <Border Name="Border" 
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}"
                        Background="{TemplateBinding Background}"
                        TextBlock.Foreground="{TemplateBinding Foreground}">
                    <Grid>
                        <Rectangle Name="FocusCue" Visibility="Hidden" Stroke="Green"
                        StrokeThickness="1" StrokeDashArray="1 2" SnapsToDevicePixels="True"/>
                        <ContentPresenter Margin="{TemplateBinding Padding}" 
                            HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"/>
                    </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}"/>
                        <Setter TargetName="Border" Property="BorderBrush" Value="DarkKhaki"/>
                    </Trigger>
                    <Trigger Property="IsKeyboardFocused" Value="True">
                        <Setter TargetName="FocusCue" Property="Visibility" Value="Visible"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
    <Style.Triggers>
        <!-- trigger可以放这里,也可以放ControlTemplate,
        但放这里无法设置模板内部的元素,因为访问不到.
        如果同时在模板和style里面设置了触发器,那么style里面的优先级更高
        -->
    </Style.Triggers>
</Style>

<Button x:Name="btn1"
        Content="My Button Template" Width="300"               
        Style="{StaticResource MyButtonStyle}"/>

4.复杂模板

控件模板和为其提供支持的代码之间有一个隐含的约定:如果使用自定义的模板替换默认模板,就需要确保新模板能够满足控件的实现代码的所有需要。对于简单控件,这个很容易,因为它对模板几乎或者完全没有真正的需求。但对于复杂控件就要小心了,因为控件外观和实现不是完全相互独立的。

UserControl从ContentControl派生;
UserControl改变了一些默认值:IsTabStop和Focusable改成false,并将HorizontalAlignment&VerticalAlignment设置为Stretch,从而填充可用空间;
UserControl应用了一个由ContentPresenter和Border组成的新的控件模板。
UserControl改变了路由事件的源。当事件从用户控件内的控件向外冒泡或隧道路由时,事件源变为指向用户控件而不是原始元素。
UserControl有控件模板,但很少改变控件模板,并且将模板作为类的一部分提供标记,创建控件后会使用InitializeComponent()方法处理这些标记。

无外观控件没有标记,需要的所有内容都在模板中。调用DefaultStyleKeyProperty.OverrideMetadata()通知WPF提供新的样式,否则就简单的使用默认样式。
模板中,创建链接到父控件属性的绑定表达式时,需要使用RelativeSource属性指示需要绑定到父控件,如果单向绑定可以满足需要通常使用轻量级的TemplateBinding表达式

<Slider Minimum="0" Maximum="255"
        Margin="{TemplateBinding Padding}"
        Value="{Binding Path=Red, 
                RelativeSource={RelativeSource TemplatedParent}}"/>

除非希望关联事件处理程序或者通过代码与元素进行交互,否则不要在控件模板中命名元素,需要命名时使用”PART_ElemName”的形式。

可以使用Style来应用样式,如果设置了一个新的样式,它会合并到默认样式中。如果新的样式与默认样式发生冲突,则新的样式会胜出并覆盖默认样式中的属性设置和触发器;而没有覆盖的细节仍然保留。
可以使用Style style = Application.Current.FindResource(typeof(Button))这样的方式查找默认样式。

如果不需要特定控件的功能,那么Control作为基类是最好的选择

猜你喜欢

转载自blog.csdn.net/hetoby/article/details/78062100