[WPF Bug清单]之(10)——CheckBox在不同主题下具有不同的行为
我们都知道Window有多种主题(Theme)。一般情况下,显然我们会希望不同主题下,我们的应用程序的行为不会有变化。或者说,我们不希望为了特定的主题,为控件写特定的逻辑。然而不幸的是,.NET Framework里一些控件自带的主题就存在问题,使得我们不得不在使用时,为这个控件在特定的主题下特殊处理。
下面举一个例子。在ListBox里放CheckBox,组成一个CheckBoxList应该是一个比较常见的应用。从理论上来说,在WPF里最简单的方式就是在ListBox的ItemTemplate里或是ItemContainerStyle里放一个CheckBox就可以了。
但是实际上,在做这个简单的CheckBoxList的时候,会遇到一个又一个的问题。首先重申一下文本的意图,怕自己又没有说明白误导大家。本文不是讨论CheckBoxList里的蓝条问题,而是在讨论CheckBox在不同主题下的不同行为的问题。CheckBoxList仅仅是个例子。
先来看看效果图。
图1. 两种主题下的CheckBox
在上图中,左侧是Classic主题下的CheckBox。右侧是XP默认的Luna主题下的CheckBox。
问题1. CheckBox的IsChecked状态与ListBoxItem的IsSelected状态不同步。如果你想保留选中时的蓝条,那么比较好办,把这两个属性Binding到一起就可以了。如果你不想要那个选中时的蓝条,会稍稍复杂一些。解决方案很多,就不赘述了。示例程序中,为减少干扰,不对这个问题进行解决。
问题2. CheckBox所在的Item被选中时,为蓝色。CheckBox里的文字为黑色,这个与ListBoxItem的默认颜色行为不一致。为了让CheckBox在被选中时文字为白色并不难,写个Binding就OK了。这个根本不是问题,但是解决这个问题,造成了下面的问题,才是主要问题。(当然,如果你隐藏了蓝条,就没有任何问题。)
问题3. 这个是这篇文章的主要议题,看看下面几个图就知道了。我们对两边的CheckBoxList做同样的操作。先来右边的。
图2. 选中最后一个CheckBox
图3. 点击刚才选中CheckBox边上的空白,使其选中
注意,这里CheckBox里的勾还是可见的。很费话是吧,怎么可能不见?下面让你来见识一下,Classic主题下的勾就看不到了。跟没有选中一样。
图4. 选中经典CheckBox,勾可见
图5. 点击空白,选中它,勾不见了
再给个提示,注意图2和图3,之间的变化,Item3被选中之后,变成了白色。而勾的颜色没有变。再来看图4和图5。
应该已经猜到了吧?没有错,Classic主题下,CheckBox里的勾也成了白色的。
熟悉WPF的人应该也已经猜到了,这个是由于不同主题下,CheckBox的默认Template的实现不同所导致的。下面是CheckBox在不同主题下的代码。(直接来自于Blend,根本来源是.NET Framework里的PresentationFramework.Classic和PresentationFramework.Luna两个DLL。)
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Microsoft_Windows_Themes="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Classic">
<!-- Resource dictionary entries should be defined here. -->
<Style x:Key="CheckRadioFocusVisual">
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<Rectangle Stroke="Black" StrokeDashArray="1 2" StrokeThickness="1" Margin="14,0,0,0" SnapsToDevicePixels="true"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="EmptyCheckBoxFocusVisual">
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<Rectangle Stroke="Black" StrokeDashArray="1 2" StrokeThickness="1" Margin="1" SnapsToDevicePixels="true"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="ClassicCheckBoxStyle" TargetType="{x:Type CheckBox}">
<Setter Property="FocusVisualStyle" Value="{StaticResource CheckRadioFocusVisual}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}"/>
<Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
<Setter Property="BorderBrush" Value="{x:Static Microsoft_Windows_Themes:ClassicBorderDecorator.ClassicBorderBrush}"/>
<Setter Property="BorderThickness" Value="2"/>
<Setter Property="Padding" Value="2,0,0,0"/>
<Setter Property="FocusVisualStyle" Value="{StaticResource EmptyCheckBoxFocusVisual}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type CheckBox}">
<BulletDecorator SnapsToDevicePixels="true" Background="Transparent">
<BulletDecorator.Bullet>
<Microsoft_Windows_Themes:ClassicBorderDecorator x:Name="CheckMark" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}" BorderStyle="Sunken" BorderThickness="{TemplateBinding BorderThickness}">
<!-- The following Path Binding Fill to Foreground of Templated parent, which is different from Luna's template -->
<Path x:Name="CheckMarkPath" Fill="{TemplateBinding Foreground}" FlowDirection="LeftToRight" Margin="1,1,1,1"
Width="7" Height="7" Data="M 0 2.0 L 0 4.8 L 2.5 7.4 L 7.1 2.8 L 7.1 0 L 2.5 4.6 Z"/>
</Microsoft_Windows_Themes:ClassicBorderDecorator>
</BulletDecorator.Bullet>
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" RecognizesAccessKey="True"/>
</BulletDecorator>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="false">
<Setter Property="Visibility" TargetName="CheckMarkPath" Value="Hidden"/>
</Trigger>
<Trigger Property="IsChecked" Value="{x:Null}">
<Setter Property="Background" TargetName="CheckMark" Value="{DynamicResource {x:Static SystemColors.ControlLightLightBrushKey}}"/>
<Setter Property="Fill" TargetName="CheckMarkPath" Value="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}"/>
</Trigger>
<Trigger Property="IsPressed" Value="true">
<Setter Property="Background" TargetName="CheckMark" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Background" TargetName="CheckMark" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
<Setter Property="Fill" TargetName="CheckMarkPath" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
代码1. Classic主题下CheckBox的默认Template
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Microsoft_Windows_Themes="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Luna">
<!-- Resource dictionary entries should be defined here. -->
<LinearGradientBrush x:Key="CheckRadioFillNormal">
<GradientStop Color="#FFD2D4D2" Offset="0"/>
<GradientStop Color="#FFFFFFFF" Offset="1"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="CheckRadioStrokeNormal">
<GradientStop Color="#FF004C94" Offset="0"/>
<GradientStop Color="#FF003C74" Offset="1"/>
</LinearGradientBrush>
<Style x:Key="LunaCheckBoxStyle" TargetType="{x:Type CheckBox}">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
<Setter Property="Background" Value="{StaticResource CheckRadioFillNormal}"/>
<Setter Property="BorderBrush" Value="{StaticResource CheckRadioStrokeNormal}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="FocusVisualStyle" Value="{StaticResource EmptyCheckBoxFocusVisual}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type CheckBox}">
<BulletDecorator SnapsToDevicePixels="true" Background="Transparent">
<BulletDecorator.Bullet>
<Microsoft_Windows_Themes:BulletChrome Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" IsChecked="{TemplateBinding IsChecked}"
RenderMouseOver="{TemplateBinding IsMouseOver}" RenderPressed="{TemplateBinding IsPressed}"/>
</BulletDecorator.Bullet>
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" RecognizesAccessKey="True"/>
</BulletDecorator>
<ControlTemplate.Triggers>
<Trigger Property="HasContent" Value="true">
<Setter Property="FocusVisualStyle" Value="{StaticResource CheckRadioFocusVisual}"/>
<Setter Property="Padding" Value="2,0,0,0"/>
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
代码2. Luna主题Normal配色方案下,CheckBox的默认Template
从上面两段代码可以看出,Classic主题下的CheckBox的勾的颜色绑定到了CheckBox本身的Foreground属性上,但是Luna主题下的没有这样做(Luna下的CheckBox的勾是用BulletChrome画的)。也许微软这样做有自己的考虑,但是从目前的结果来看,没有带来实现的好处,却带来了不小的麻烦,因为这个小问题并不是想象中那么容易完美解决的。
给出几个方案。
1. 不把CheckBox的Foreground与ListBoxItem的Foreground绑定。然后解决CheckBox黑色文字与蓝条的冲突问题。方案之一就是改变蓝条成灰条,橙条,颜色随你。
2. 重写Classic主题下CheckBox的Template,可以,首先这个Template的代码不算少,而且你还要写代码去在程序启动时判断是否要加载这个特殊的Template。这个还要读注册表。
3. 算了,我不要蓝条还不行吗?有时客户或是公司的UX和QA会不同意,他们不会因为你不好做就原谅你的。除非你很能忽悠。
好了,问题讲完了。不过这里的CheckBox只是一个例子,WPF里类似的问题还是不少的。比如很多控件的FocusVisualStyle根本无效。这个将在之后的文章中介绍。
已经给WPF找了10个Bug个了,当然是按自己的标准找的Bug(被QA、UX和客户磨练出来了),也许有人不接受,认为这些不算是Bug,但是讨论这个实在没有什么意义。这个系列文章的主要目标,是为了给计划使用和正在使用WPF进行项目开发的人,一些提示,在自己遇到的陷阱边上立个牌子,让大家少走一些弯路,毕竟这些问题在微软的文档大都是没有涉及到的,能达到这个目标就足够了。
在这里更要感谢曾经给过我支持和鼓励的、关注着这个系列文章的园友们,没有你们的支持,我也坚持不到第10篇的。谢谢大家。
(2009-7-25 10:00)补充一下:为什么说这个问题不小呢?除了不太好解决外,之前软件开发,只要考虑各个系统,QA们要在不同系统上测试。现在好了,还要在不同主题下测。-_-