Marvin's Blog【程式人生】

Ability will never catch up with the demand for it

30 Mar 2020

UWP设计文档阅读笔记(五):关于XAML样式

Design UI > Controls > XAML Styles

XAML styles

XAML的样式可以作为一种资源被不同的XAML模块共享。定义共享样式的方法有两个,一个是在App.xaml中定义这些样式;二是把样式单独保存成资源字典文件。每个XAML页面也可以定义自己的样式,如果样式的键值与共享的样式冲突,那么本地的样式优先被使用。

定义一个样式的时候,需要指定样式的施用目标(TargetType),以及这个样式所修改的目标部属列表。 TargetType的指定方式是采用字符串,描述的是一个FrameworkElement类型,或者从FrameworkElement派生出来的类型。XAML必须能从组件(assembly)中找出TargetType的具体信息,否则就会出错。

Style的内容是一列具有Property和Value参数的Setter元素。Setter的Value参数可以作为Setter元素的属性来设置,也可以在Setter的子元素指定。

前面提到,样式可以作为资源存在。如何使用资源中的样式呢?有两种方式:

  • 隐式应用。如果样式只具有TargetType而不具有x:key属性,那么这个样式会隐式应用于所有的TargetType类型的对象
  • 显示应用。如果样式既指定了TargetType又指定了x:Key属性,那么这个样式必须在目标对象上显示地将Style部属通过{StaticResource}标记扩展来绑定指定的x:Key值。

那些没有显示应用样式的对象,会自动隐式应用样式。

https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/xaml-styles#apply-an-implicit-or-explicit-style

通过BaseOn部属,可以让一个样式基于另外一个样式,也就是继承自另外一个样式。派生出来的样式必须和被继承的样式应用于同一类型(或者其派生类型)。如果被继承的样式应用于ContentControl,那么派生出来的样式可以指定应用对象为ContentControl,或者其派生类型Button、ScrollViewer。

在Microsoft Visual Studio XAML设计界面中可以很方便通过右击元素来编辑其样式。

可以在App或者页面基本修改系统图刷,这样当前所有的控件都会使用修改过的系统图刷。一个示例:

<Page.Resources>
    <ResourceDictionary>
        <ResourceDictionary.ThemeDictionaries>
            <ResourceDictionary x:Key="Light">
                 <SolidColorBrush x:Key="ButtonBackground" Color="Transparent"/>
                 <SolidColorBrush x:Key="ButtonForeground" Color="MediumSlateBlue"/>
                 <SolidColorBrush x:Key="ButtonBorderBrush" Color="MediumSlateBlue"/>
            </ResourceDictionary>
        </ResourceDictionary.ThemeDictionaries>
    </ResourceDictionary>
</Page.Resources>

注意,上面的例子中样式是指定在ResourceDictionary.ThemeDictionaries中的。

对于PointerOver 、PointerPressed 或者Disabled 等按键状态,其对应的图刷键名例子有:ButtonBackgroundPointerOver、ButtonForegroundPointerPressed、ButtonBorderBrushDisabled。注意会发现,状态的名字会附到原始键名之后。

也可以直接在控件的资源列表上指定样式,这样样式仅会应用于该控件。示例:

<CheckBox Content="Special CheckBox" Margin="5">
    <CheckBox.Resources>
        <ResourceDictionary>
            <ResourceDictionary.ThemeDictionaries>
                <ResourceDictionary x:Key="Light">
                    <SolidColorBrush x:Key="CheckBoxForegroundUnchecked"
                        Color="Purple"/>
<!-- more -->
                </ResourceDictionary>
            </ResourceDictionary.ThemeDictionaries>
        </ResourceDictionary>
    </CheckBox.Resources>
</CheckBox>

尽可能使用Windows Runtime的默认XAML样式资源。如果必须定义自己的样式,可以基于默认样式扩展。实在不行的话,就将原样式拷贝过来修改。

样式setter也可以应用在Control的Template属性。事实上大部分样式都是据此定义的,更多参考Control templates

Control templates

通过控件模板可以修改控件的在视觉上的结构或者行为。这种方式比使用样式具有更高的灵活度,因为控件模板更能针对元素的不同状态做出自定义的显示效果,而样式相对而言比较静态。参考ControlTemplate

控件模板和样式一样,都可以指定x:Key和TargetType属性。

以CheckBox为例,默认情况下,CheckBox有三种状态,选中、未选中、未知。通过控件模板,可以修改这三种状态下的显示效果。

一个控件模板必须有一个FrameworkElement作为其根节点,然后再通过这个根节点来包含其他节点。指定不同的FrameworkElement来作为控件模板的内容,可以修改控件的显示结构。

在控件模板中,通过TemplateBinding 来绑定目标控件的部属值。参考TemplateBinding markup extension

在Win10 1809 (SDK 17763),可以是同x:Bind来替换TemplateBinding

前面提到,控件模板可以基于控件状态做不同的显示。比如CheckBox有选中、未选中以及未知三种状态。通过使用VisualState对象,控件模板可以控制控件在这三种不同状态下的显示。VisualState.Name用于匹配控件的状态,VisualState的子内容(Setter或者Storyboard)来指定显示。多个VisualState通过VisualStateManager.VisualStateGroups 附着部属(在控件模板根元素上)进行管理,示例如下:

        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="CheckStates">
                <VisualState x:Name="Checked">
                    <VisualState.Setters>
                        <Setter Target="CheckGlyph.Opacity" Value="1"/>
                    </VisualState.Setters>
                    <!-- This Storyboard is equivalent to the Setter. -->
                    <!--<Storyboard>
                        <DoubleAnimation Duration="0" To="1"
                         Storyboard.TargetName="CheckGlyph" Storyboard.TargetProperty="Opacity"/>
                    </Storyboard>-->
                </VisualState>
                <VisualState x:Name="Unchecked"/>
                <VisualState x:Name="Indeterminate">
                    <VisualState.Setters>
                        <Setter Target="IndeterminateGlyph.Opacity" Value="1"/>
                    </VisualState.Setters>
                    <!-- This Storyboard is equivalent to the Setter. -->
                    <!--<Storyboard>
                        <DoubleAnimation Duration="0" To="1"
                         Storyboard.TargetName="IndeterminateGlyph" Storyboard.TargetProperty="Opacity"/>
                    </Storyboard>-->
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>

想了解Storyboard和Setter的差异,参考Storyboarded animations for visual states

在Microsoft Visual Studio Document Outline可以快速右击元素,来修改其主题和样式。

Controls and accessibility涉及到一些控件可访问性的问题。

控件模板中可以用{ThemeResource} markup extension来绑定主题资源。

ResourceDictionary and XAML resource references

所谓资源,一般情况下是一组可重用的对象。下类XAML元素经常被定义作为资源使用:

  • 样式
  • 控件模板
  • 动画组件(animation components)
  • 自定义图刷Brush

通过元素的Resources部属可以定义资源字典(ResourceDictionary):

   <Page.Resources>
        <x:String x:Key="greeting">Hello world</x:String>
        <x:String x:Key="goodbye">Goodbye world</x:String>
    </Page.Resources>

资源不一定需要是字符串,也可以是其他可以被共享的对象(比如SolidColorBrush )。值得注意的是,像控件、形状这些FrameworkElement不可共享,也就不能作为资源使用。

通常情况下,资源要用x:Key来索引,但也有例外:

  • 对于样式和控件模板,它们有TargetType属性,可以在不使用x:Key的情况下找到目标对象。这种情况下,其索引是目标对象的类型,而不是字符串。比如TargetType是Button的情况下,其索引是typeof(Button)。
  • DataTemplate也是类似情况
  • x:Name同样可以用来代替x:Key来定义索引,可视x:Name在页面加载的时候才初始化,性能上不及x:Key。

通过{StaticResource}标记扩展来引用资源(通过指定x:Key或者x:Name)。在控件没有设置Style、ContentTemplate或者ItemTemplate部属的情况下,XAML框架会通过TargetType(而不是x:key、x:Name)隐式地查找可用资源。

你可以在代码中查找资源字典,但是只能涉及本地资源,比如Page.Resources中定义的资源。它不会像StaticResource标记扩展一样,可以从Application.Resources候补查阅,而是要通过间接的手段。下面的C#代码从Page.Resources里面查询redButtonStyle:

    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
            Style redButtonStyle = (Style)this.Resources["redButtonStyle"];
        }
    }

下面的代码从Application.Resources查询appButtonStyle:

    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
            Style appButtonStyle = (Style)Application.Current.Resources["appButtonStyle"];
        }
    }

也可以在代码中向字典添加资源,但是有两个限制:

  • 资源必须在使用前添加
  • 不能再App的构造函数中添加资源

对于上面两个限制,最好的办法是在Application.OnLaunched 中添加资源:

// App.xaml.cs
    
sealed partial class App : Application
{
    protected override void OnLaunched(LaunchActivatedEventArgs e)
    {
        Frame rootFrame = Window.Current.Content as Frame;
        if (rootFrame == null)
        {
            SolidColorBrush brush = new SolidColorBrush(Windows.UI.Color.FromArgb(255, 0, 255, 0)); // green
            this.Resources["brush"] = brush;
            // … Other code that VS generates for you …
        }
    }
}

FrameworkElement是Control的祖先类,它拥有一个Resources部属,用于管理资源。所以从FrameworkElement派生的来的类对象都可以自定义资源:

        <Border x:Name="border">
            <Border.Resources>
                <x:String x:Key="greeting">Hola mundo</x:String>
            </Border.Resources>
            <!-- Displays "Hola mundo" -->
            <TextBlock x:Name="textBlock2" Text="{StaticResource greeting}"/>
        </Border>

也可以在代码中访问:

            this.InitializeComponent();
            textBlock3.Text = (string)border.Resources["greeting"];

资源字典可以相互合并。对于下面这个资源字典定义:

<!-- Dictionary1.xaml -->
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:MSDNSample">

    <SolidColorBrush x:Key="brush" Color="Red"/>

</ResourceDictionary>

只需将上面的资源合并到页面的资源字典中:

    <Page.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Dictionary1.xaml"/>
            </ResourceDictionary.MergedDictionaries>

            <x:String x:Key="greeting">Hello world</x:String>

        </ResourceDictionary>
    </Page.Resources>

上面的例子要指定一个ResourceDictionary元素,之前都没有看到。这是因为如果不需要合并资源的话,XAML可以帮你自动生成这个。

另外需要注意的是,MergedDictionaries里面的资源的优先级在本地资源之后。如果多个MergedDictionaries存在,那么他们的优先级是以在XAML中出现的次序相反:

            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Dictionary1.xaml"/>
                <ResourceDictionary Source="Dictionary2.xaml"/>
            </ResourceDictionary.MergedDictionaries>

上面例子中,Dictionary2.xaml的资源比1的资源优先级高。

虽然资源字典中的资源必须用不同的键值,但是MergedDictionaries引用的是不同的资源字典,同样的键值可以用在不同的资源字典。

ThemeResource和 StaticResource有点像,不过前者会受到主题更换的影响。

主题资源字典需要通过ThemeDictionaries来设置:

            <ResourceDictionary.ThemeDictionaries>
                <ResourceDictionary Source="Dictionary1.xaml" x:Key="Light"/>
                <ResourceDictionary Source="Dictionary2.xaml" x:Key="Dark"/>
            </ResourceDictionary.ThemeDictionaries>

可以在Win10SDK的\(Program Files)\Windows Kits\10\DesignTime\CommonConfiguration\Neutral\UAP\<SDK version>\Generic目录查看默认的主题。可以看到generic.xaml和themeresources.xaml。前者提供系统默认主题,后者是设计时候使用的。

XAML查找资源的顺序是先从元素自身的资源字典开始的,然后向上回溯。如果所需资源在当前XAML树中没有找到,那么则会查看 Application.Resources 。

对于控件模板,由于可以使用主题资源,所以它还会找到主题资源字典(以ResourceDictionary为根的XAML文件)

如果所需的资源在应用范围查找不到,那么还会在系统范围进行查找。如果还是失败,那么就会出现XAML处理异常,这是有可能在程序运行时发生的。

在一个字典中定义资源时,可以引用已经定义好(具有键值)的资源。XAML不支持先使用后定义的情形。

如果是在应用级别定义资源,那么不能引用同一个字典中的资源。因为这种情况下同一个字典中的资源被视为是同时定义的。

资源字典中的资源必须是可以被共享的。这是因为在内存中构建XAML树的时候,同一元素的对象不能出现在树的两个地方。

下面这些元素是可被共享的:

  • 样式或者模板
  • 图刷或者颜色
  • Storyboard派生的动画
  • GeneralTransform派生的变换
  • Matrix以及Matrix3D
  • Point值
  • 其他一些UI相关的结构,比如Thickness和CornerRadius
  • XAML intrinsic data types

对于自定义类型的对象,只要设计合理(比如需要具有默认构造函数,不从UIElement派生),是可以在XAML中被共享的。一个例子是 IValueConverter,在代码中实现,在XAML的资源字典中实例化。

UserControl 在资源查阅的行为上与其他控件有所不同,因为他继承了定义范围(definition scope)和使用范围(Usage Scope)两个概念。在定义范围内,UserControl无法访问App级别的资源;在使用范围内,UserControl可以访问App级别的资源。

可以把定义了资源字典的XAML作为XamlReader.Load输入之一。但这个XAML的所有资源必须自包含,因为当XamlReader.Load 解析这个资源字典的时候,它不会考虑其他的资源字典,也不会考虑应用级别的资源字典,也不支持ThemeResource

在C++/CX代码中,可以通过Lookup来访问FrameworkElement.Resources或者Application.Current.Resources。

在代码中访问和在XAML中访问资源效果不太一样。代码中访问的话,不会像XAML加载时后那样,除了访问本地的资源,还可以遍历Application.Resources。对于合并而引入的字典,会被当做主字典的一部分,可以获得访问。

代码中,访问XAML资源,如果资源不存在的话,会返回null。

可以使用Insert来在运行的时候往局部资源字典或者应用资源字典插入资源。当是这些不会使XAML中已经被使用的资源变得无效化。

XAML的资源字典可以包含一些需要本地化的字符串。如果是这种情况,应该把这些字符串保存成为项目资源,而不是资源字典中。把这些字符串从XAML中抽出,给使用这些字符串的元素一个x:Uid directive值。然后在资源文件中以XUIDValue.PropertyName的方式定义这些需要本地化的字符串。

如果需要更多的自定义,可以使用CustomXamlResourceLoader,然后通过CustomResource markup extension来使用这些资源。

(本篇完)

Categories

Tags

comments powered by Disqus