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值。
那些没有显示应用样式的对象,会自动隐式应用样式。
通过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来使用这些资源。
(本篇完)