代码改变世界

Windows Phone自定义主题

2012-03-12 09:12 by Windie Chai, ... 阅读, ... 评论, 收藏, 编辑

我们知道Windows Phone默认的主题系统是由黑白两色为背景和一些强调色组成的,用户可以随意切换。通常来说,应用开发者无需关心这一部分,系统会去更新相关的资源,然后再体现在应用中。

但有一些时候,我们基于品牌等因素的考量,可能不想使用Windows Phone的默认主题。比如我开发的“豆芽”是豆瓣网的一个客户端,我希望尽可能贴近豆瓣网本身清新的风格,而不是给用户呈现一个和豆瓣网风格大相径庭的黑色背景的界面;再比如我想让应用使用Windows Phone的默认字体(等线),而不是SDK的默认字体(雅黑)。

这些都需要我们去自定义应用的主题。

在介绍如何创建自定义主题之前,先来简单的描述一下Windows Phone主题的原理。

在Windows Phone中,系统预定义了许多资源,这些资源包括了画笔、颜色、字体、粗细、字号、文本样式等等最基本的元素(详细的资源名称可以查看这里)。此外,Windows Phone中的所有控件都会有自己的样式,样式中还包括了定义控件布局的模板,而模板又利用系统内置的资源定义了控件在各种状态下的外观(所以我们在XAML中随处可以见类似{StaticResource PhoneBackgroundBrush}这样的对内置资源的引用)。

所以我们可以想到,修改内置资源或者修改控件的样式都可以达到自定义主题的效果。

在早期的Windows Phone v7.0),我们可以使用前一种方式,只需要在应用中增加一个ResourceDictionary的XAML文件,里边添加若干和系统资源相同键名的资源,即可实现对系统资源的覆盖。

但这种方法在Mango (v7.1)中无效了,它被当作一个Bug修复了,所以我们只能另寻方法。代价最小的一种方法是在App初始化的时候动态的读取我们定义的ResourceDictionary,并替换系统内置资源。具体的步骤可以参考这里 ,我就不赘述了。

此外,还可以利用Mango带来的另外一个变化,新的Silverlight 4带来的“隐式样式”(Implicit Style)。隐式样式是指只有TargetType却没有指定Key的Style,在Silverlight 4中,会将这个Style应用到所有匹配的TargetType对象上。

我们可以利用“隐式样式”来更改内置控件的样式,只需要将需要修改的控件的样式添加到应用的ResourceDictionary中,将其Key值去掉即可(当然不去掉也可以,这就需要手工设置所有匹配控件的Style属性)。

但一般情况下,既然我们想要更改应用级别的主题,基本上我们会修改整套配色方案,如果单纯用“隐式样式”来实现的话,我们就需要实现所有控件的隐式样式,看起来似乎也不是一件简单的事情。

本文来介绍另外一种方法,这种方法不仅实现了所有控件的样式,还一并接管了所有的内置资源,但它的实现过程却一点儿也不复杂。

首先

我们在项目中添加一个XAML文件,用来存放新的主题,这里将文件名定为“CustomTheme.xaml”,下面是它的内容:

<ResourceDictionary
 
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 
xmlns:x
="http://schemas.microsoft.com/winfx/2006/xaml"
 
xmlns:System
="clr-namespace:System;assembly=mscorlib">
 
</ResourceDictionary>

然后编辑App.xaml,添加相应的ResourceDictionary:

<Application.Resources>
 
<ResourceDictionary>
 
<ResourceDictionary.MergedDictionaries>
 
<ResourceDictionary Source="CustomTheme.xaml"/>
 
</ResourceDictionary.MergedDictionaries>
 
</ResourceDictionary>
 
</Application.Resources>

接着打开Windows Phone SDK附带的设计资源文件夹:C:\Program Files (x86)\Microsoft SDKs\Windows Phone\v7.1\Design。我们会看到若干文件夹,这些文件夹都对应于Windows Phone中的一个主题搭配,譬如LightGreen表示浅色背景色和绿色强调色的搭配。

挑选一个最贴近我们想要的主题配色的文件夹打开,我们会看到两个XAML文件,其中ThemeResources.xaml中定义了系统资源,System.Windows.xaml中定义了大多数控件的样式。

将ThemeResources.xaml和System.Windows.xaml添加到项目中,由于System.Windows.xaml引用了ThemeResources.xaml中定义的资源,所以在System.Windows.xaml的根元素下添加一个ResourceDictionary来引用ThemeResources.xaml(否则在运行时会抛出找不到资源的异常):

<ResourceDictionary.MergedDictionaries>

<ResourceDictionary Source="ThemeResources.xaml"/>

</ResourceDictionary.MergedDictionaries>

然后在我们的CustomTheme.xaml中也添加一个对System.Windows.xaml引用的ResourceDictionary:

<ResourceDictionary.MergedDictionaries>

<ResourceDictionary Source="System.Windows.xaml"/>

</ResourceDictionary.MergedDictionaries>

现在这几个文件的关系是:App.xaml引用了CustomTheme.xaml,CustomTheme.xaml引用了System.Windows.xaml,System.Windows.xaml引用了ThemeResources.xaml。也就是说,在应用运行时,这几个XAML会被全部加载。

至此,我们的应用中已经包含了Windows Phone的几乎所有资源和样式,但无论我们怎么修改它们的值,都不会影响应用在运行时呈现的外观。因为我们所有的模版和页面元素依然引用了系统内置的资源,而这些资源是不可覆盖的。

这里有个奇怪的现象,如果我们修改了这些资源的值,是可以在设计器中看到效果的,但丝毫不会影响运行时的效果,我相信这也是一个迟早要修复的Bug。

回到正题,既然我们已经包含了Windows Phone的几乎所有的资源和样式,那还何必去覆盖系统预置的资源和模版呢?直接使用我们自己的不就好了吗?

没错,就是这样。

我们观察到Windows Phone内置的资源和样式的名称都以“Phone”开头,非常有规律,也就非常容易替换。所以接下来我们在整个项目(或解决方案,取决于你的实际情况)里搜索x:Key=”Phone这个字符串,将其替换为x:Key=”Custom,这样会将所有资源的名称修改为CustomXXX;然后在搜索{StaticResource Phone开头的字符串,将其替换为{StaticResource Custom,这样会把所有对系统资源的引用修改为对应用内资源的引用。

如下图所示:

完成这步操作之后,我们的应用就已经基本和Windows Phone内置主题说再见了,这时我们可以修改一些资源的值,运行一下看看效果:

不仅仅是颜色,因为我们已经将系统内置的绝大多数样式都包含了进来了,所以还可以修改控件的布局。譬如我们想更改CheckBox的对钩的样式,没有问题,它在模版中使用一个名为CheckMark的Path来表示的,直接修改就好:

前面我一直在强调我们只包含了“绝大多数模版”,是因为还有一些控件的模版并未在System.Windows.xaml中定义,譬如Panorama和Pivot,对于这两个控件的样式我们该如何自定义呢?

需要一些小手段,我们用.NET Reflector这个工具打开C:\Program Files (x86)\Microsoft SDKs\Windows Phone\v7.1\Libraries\Silverlight\Microsoft.Phone.Controls.dll,查看它的Resources,将MicrosoftPhone.Controls.g.resources中的themes/generic.xaml保存下来。

这个文件定义了Panorama和Pivot控件的样式,同样将其添加到项目中。

然后在项目中添加对Microsoft.Phone.Controls.dll的引用并将generic.xaml文件根元素中定义的两个命名空间(local和localPrimitives)的值修改一下:

xmlns:local="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls"

xmlns:localPrimitives="clr-namespace:Microsoft.Phone.Controls.Primitives;assembly=Microsoft.Phone.Controls"

现在我们需要把generic.xaml插入到之前创建好的“资源引用链”中,先在generic.xaml文件的根元素下增加对System.Windows.xaml的引用:

<ResourceDictionary.MergedDictionaries>

<ResourceDictionary Source="System.Windows.xaml"/>

</ResourceDictionary.MergedDictionaries>

再将CustomTheme.xaml中对System.Windows.xaml的引用改为对generic.xaml的引用。

现在这几个文件的关系变成了:App.xaml应用了CustomTheme.xaml,CustomTheme.xaml引用了generic.xaml,generic.xaml引用了System.Windows.xaml,System.Windows.xaml引用了ThemeResources.xaml。

OK,现在再添加一个Panorama页面,修改一下布局(Title的尺寸),试试效果:

方法介绍完了,简单归纳一下,就是找出系统内置的资源和样式,添加到应用的项目中,修改所有资源和样式的Key值,修改所有对系统内置资源和样式的引用,听起来似乎工程浩大,其实只是几步简单的复制粘贴和查找替换。

唯一比较麻烦的是要维护几个xaml文件之间的互相引用,还要保持一定的顺序,其实你也可以不必这么做,如果你愿意的话,完全可以把ThemeResources.xaml、System.Windows.xaml和generic.xaml文件根元素中的内容依次复制到CustomTheme.xaml文件中,这样只需要CustomTheme.xaml一个文件就可以了,但我个人认为把这几个文件混在一块并不利于维护。

当然,这种方法也并不是十分完美的,在我看来,它存在如下一些缺陷:

  • 每次添加一个新的页面,都要检查其中对系统内置资源的引用,将其修改为引用我们接管的资源
  • 假如微软在将来的版本中修改了内置样式的模板,我们也得做对应的修改,这就会比较棘手,我的方法是,在修改这些资源的值的时候,在其旁边加一个注释来表明这个值被修改过(譬如<!–Changed–>),将来微软升级了SDK,我们可以先备份一下现有XAML文件,然后重复之前的步骤接管系统内置资源和样式,再根据备份文件中的特定注释来逐一修改。

此外,你可能会担心最后在接管Panorama和Pivot样式的时候使用了一点Hack的手段,会不会被市场拒绝,对此我也不能给出确切的答案,我只能说,“豆芽”也使用了这种方法,目前还没有遇到这方面的问题。

轻触这里下载源码