[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们要在不同系统上测试。现在好了,还要在不同主题下测。-_-
原文地址:https://www.cnblogs.com/nankezhishi/p/WPFBugCheckBoxList.html