Monday, July 25, 2022

Setting Background of DatePicker, ComboBox, CheckBox

We are implementing a new UI standard that improves the readability of disabled controls. Take a look at some sample enabled and disabled controls below. You can see the disabled controls are inconsistent in look and are quite difficult to read in some cases.


Here's the XAML for the window above. 

<Window x:Class="DisabledControls.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:DisabledControls"
        mc:Ignorable="d"
        Title="Disabled Styles" Height="450" Width="800">
    <Window.Resources>
        <Color x:Key="PageBackgroundColor" >#E7E7F0</Color>
        <SolidColorBrush x:Key="PageBackgroundBrush" Color="{StaticResource PageBackgroundColor}"/>
    </Window.Resources>
    <Window.Background>
        <Binding Source="{StaticResource  PageBackgroundBrush}"/>
    </Window.Background>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="24"/>
            <RowDefinition Height="24"/>
            <RowDefinition Height="24"/>
            <RowDefinition Height="24"/>
            <RowDefinition Height="24"/>
            <RowDefinition Height="24"/>
            <RowDefinition Height="60"/>
        
</Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="200"/>
            <ColumnDefinition Width="200"/>
        </Grid.ColumnDefinitions>
        <TextBlock Grid.Row="0" Grid.Column="0" Text="Enabled Controls"/>
        <TextBlock Grid.Row="0" Grid.Column="1" Text="Disabled Controls"/>
        <TextBox Grid.Row="1" Grid.Column="0" Text="Enabled TextBox"/>
        <TextBox Grid.Row="1" Grid.Column="1" Text="Disabled TextBox" IsEnabled="False"/>
        <DatePicker Grid.Row="2" Grid.Column="0" SelectedDate="1/1/2000"/>
        <DatePicker Grid.Row="2" Grid.Column="1" IsEnabled="False" SelectedDate="1/1/2000"/>
        <ComboBox Grid.Row="3" Grid.Column="0" SelectedIndex="0">
            <ComboBoxItem>Enabled Dropdown</ComboBoxItem>
        </ComboBox>
        <ComboBox Grid.Row="3" Grid.Column="1" SelectedIndex="0" IsEnabled="False">
            <ComboBoxItem>Disabled Dropdown</ComboBoxItem>
        </ComboBox>
        <ComboBox Grid.Row="4" Grid.Column="0" SelectedIndex="0" IsEditable="True">
            <ComboBoxItem>Enabled Combobox</ComboBoxItem>
        </ComboBox>
        <ComboBox Grid.Row="4" Grid.Column="1" SelectedIndex="0" IsEditable="True" IsEnabled="False">
            <ComboBoxItem>Disabled Combobox</ComboBoxItem>
        </ComboBox>
        <CheckBox Grid.Row="5" Grid.Column="0" Content="Enabled Checkbox" IsChecked="True"/>
        <CheckBox Grid.Row="5" Grid.Column="1" Content="Disabled Checkbox" IsChecked="True" IsEnabled="False"/>
    </Grid>
</Window>

 We decided to style disabled controls so the background is transparent and the text is black. Here's how the TextBox looks with this new standard.

TextBox with new disabled style

At first I assumed all I had to do was write a default style with an IsEnabled trigger that overrode the foreground and background. But Microsoft grays out the text by setting the opacity of the border element to 0.5. I could add a default border style to my textbox style to override this, but it's easier to simply override the textbox template like this.

    <Window.Resources>
        <Color x:Key="PageBackgroundColor" >#E7E7F0</Color>
        <Color x:Key="ControlBorderColor">DarkGray</Color>
 
        <Thickness x:Key="ControlBorderThickness">0.5</Thickness>
 
        <SolidColorBrush x:Key="PageBackgroundBrush" Color="{StaticResource PageBackgroundColor}"/>
        <SolidColorBrush x:Key="ControlBorderBrush" Color="{StaticResource ControlBorderColor}"/>
 
        <Style TargetType="TextBox">
            <Style.Triggers>
                <Trigger Property="IsEnabled" Value="False">
                    <Trigger.Setters>
                        <Setter Property="Template">
                            <Setter.Value>
                                <ControlTemplate TargetType="TextBox">
                                    <Border BorderThickness="{StaticResource ControlBorderThickness}" BorderBrush="{StaticResource ControlBorderBrush}" Padding="2" Height="{TemplateBinding Height}">
                                        <TextBlock Text="{TemplateBinding Text}" Width="{TemplateBinding Width}" Background="Transparent"/>
                                    </Border>
                                </ControlTemplate>
                            </Setter.Value>
                        </Setter>
                    </Trigger.Setters>
                </Trigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>
 
You may think "that's a lot of XAML to change a background - I thought XAML was concise". You'd be right. The DatePicker gets a lot worse.

The DatePicker has several parts including a border, grid, textbox, button, and popup. It has a background property but setting this has no effect. We need to tell the textbox to be transparent and black and we need to gray out the button. In addition, the Datepicker has two rectangles it uses to gray out the text by making them semi-transparent. So we have a shopping list of things we have to do.

  1. Hide the rectangles
  2. Retemplate the TextBox (note the DatePicker actually uses a DatePickerTextBox)
  3. Gray the button
Here's the Datepicker style we need.

        <Style TargetType="DatePicker" BasedOn="{StaticResource {x:Type DatePicker}}">
            <Style.Resources>
                <Style TargetType="Rectangle">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding IsEnabled, RelativeSource={RelativeSource AncestorType=DatePicker}}" Value="False">
                            <Setter Property="Opacity" Value="0"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
                <Style TargetType="DatePickerTextBox">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding IsEnabled, RelativeSource={RelativeSource AncestorType=DatePicker}}" Value="False">
                            <Setter Property="Template">
                                <Setter.Value>
                                    <ControlTemplate TargetType="DatePickerTextBox">
                                        <TextBlock Text="{TemplateBinding Text}"/>
                                    </ControlTemplate>
                                </Setter.Value>
                            </Setter>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
                <Style TargetType="Button">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding IsEnabled, RelativeSource={RelativeSource AncestorType=DatePicker}}" Value="False">
                            <Setter Property="Opacity" Value="0.5"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </Style.Resources>
        </Style>

 Now the disabled DatePicker looks like this.

DatePicker with new disabled style

The ComboBox has similar issues that I solved in the same way - I retemplate the control to be a simple TextBlock. Note I don't attempt to reproduce the down arrow. The style looks like this.

        <Style TargetType="{x:Type ComboBox}">
            <Style.Triggers>
                <Trigger Property="IsEnabled" Value="False">
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="ComboBox">
                                <Border Width="{TemplateBinding Width}"
                                    Height="{TemplateBinding Height}"
                                    BorderBrush="{StaticResource ControlBorderBrush}" BorderThickness="{StaticResource ControlBorderThickness}" Padding="2">
                                    <TextBlock Text="{TemplateBinding Text}"/>
                                </Border>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Trigger>
            </Style.Triggers>
        </Style>
 
And the result looks like this.

Combobox with new disabled style

The checkbox also has a Background property but that seems to be ignored too. I completely retemplated the CheckBox with a TextBlock instead using this style.

        <Style TargetType="{x:Type CheckBox}">
            <Style.Triggers>
                <Trigger Property="IsEnabled" Value="False">
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="CheckBox">
                                <StackPanel Orientation="Horizontal" VerticalAlignment="Top">
                                    <Border Height="12" Width="12" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{StaticResource ControlBorderThickness}" Margin="1">
                                        <TextBlock Text="&#xFC;" FontFamily="Wingdings" FontSize="11" FontWeight="Bold">
                                            <TextBlock.Style>
                                                <Style TargetType="TextBlock">
                                                    <Style.Triggers>
                                                        <DataTrigger Binding="{Binding IsChecked, RelativeSource={RelativeSource AncestorType=CheckBox}}" Value="False">
                                                            <Setter Property="Visibility" Value="Hidden"/>
                                                        </DataTrigger>
                                                    </Style.Triggers>
                                                </Style>
                                            </TextBlock.Style>
                                        </TextBlock>
                                    </Border>
                                    <TextBlock Text="{TemplateBinding Content}" Margin="2,0,0,0"/>
                                </StackPanel>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Trigger>
            </Style.Triggers>
        </Style> 

Adding support for ListBox:

 If you disable a ListBox you cannot scroll it. The same applies if you set IsHitTestVisible to false. There is no IsReadOnly property on a ListBox. So in the absence of a useful property, we need to get the look and feel we want from a style. This actually makes things a lot easier because we don't have to fight Microsoft's desire to make all disabled controls difficult to read.

Add a read only ListBox style to the window's resources.

        <Style x:Key="ReadOnlyListBox" TargetType="{x:Type ListBox}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ListBox}">
                        <Border x:Name="Bd" SnapsToDevicePixels="true" Background="Transparent" Padding="1"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}">
                            <ScrollViewer Padding="{TemplateBinding Padding}" Focusable="false">
                                <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" IsHitTestVisible="False"/>
                            </ScrollViewer>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsGrouping" Value="true">
                                <Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

and add a couple of ListBoxes to our XAML.

        <ListBox Grid.Row="6" Grid.Column="0" SelectedIndex="0">
            <ListBoxItem>Enabled ListBoxItem One</ListBoxItem>
            <ListBoxItem>Enabled ListBoxItem Two</ListBoxItem>
            <ListBoxItem>Enabled ListBoxItem Three</ListBoxItem>
            <ListBoxItem>Enabled ListBoxItem Four</ListBoxItem>
            <ListBoxItem>Enabled ListBoxItem Five</ListBoxItem>
        </ListBox>
        <ListBox Grid.Row="6" Grid.Column="1" SelectedIndex="0" Style="{StaticResource ReadOnlyListBox}">
            <ListBoxItem>Disabled ListBoxItem One</ListBoxItem>
            <ListBoxItem>Disabled ListBoxItem Two</ListBoxItem>
            <ListBoxItem>Disabled ListBoxItem Three</ListBoxItem>
            <ListBoxItem>Disabled ListBoxItem Four</ListBoxItem>
            <ListBoxItem>Disabled ListBoxItem Five</ListBoxItem>
        </ListBox>


ReadOnly but scrollable ListBox

The entire XAML for this page including the new styles is...

<Window x:Class="DisabledControls.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:DisabledControls"
        mc:Ignorable="d"
        Title="Disabled Styles" Height="450" Width="800">
    <Window.Resources>
        <SolidColorBrush x:Key="PageBackgroundBrush" Color="#D7E2F7"/>
 
        <Style TargetType="TextBox">
            <Style.Triggers>
                <Trigger Property="IsEnabled" Value="False">
                    <Trigger.Setters>
                        <Setter Property="Template">
                            <Setter.Value>
                                <ControlTemplate TargetType="TextBox">
                                    <Border BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}" Padding="{TemplateBinding Padding}" Height="{TemplateBinding Height}">
                                        <TextBlock Text="{TemplateBinding Text}" Width="{TemplateBinding Width}" Background="Transparent"/>
                                    </Border>
                                </ControlTemplate>
                            </Setter.Value>
                        </Setter>
                    </Trigger.Setters>
                </Trigger>
            </Style.Triggers>
        </Style>
 
        <Style TargetType="DatePicker" BasedOn="{StaticResource {x:Type DatePicker}}">
            <Style.Resources>
                <Style TargetType="Rectangle">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding IsEnabled, RelativeSource={RelativeSource AncestorType=DatePicker}}" Value="False">
                            <Setter Property="Opacity" Value="0"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
                <Style TargetType="DatePickerTextBox">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding IsEnabled, RelativeSource={RelativeSource AncestorType=DatePicker}}" Value="False">
                            <Setter Property="Template">
                                <Setter.Value>
                                    <ControlTemplate TargetType="DatePickerTextBox">
                                        <TextBlock Text="{TemplateBinding Text}"/>
                                    </ControlTemplate>
                                </Setter.Value>
                            </Setter>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
                <Style TargetType="Button">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding IsEnabled, RelativeSource={RelativeSource AncestorType=DatePicker}}" Value="False">
                            <Setter Property="Opacity" Value="0.5"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </Style.Resources>
        </Style>
        <Style TargetType="{x:Type ComboBox}">
            <Style.Triggers>
                <Trigger Property="IsEnabled" Value="False">
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="ComboBox">
                                <Border Width="{TemplateBinding Width}"
                                    Height="{TemplateBinding Height}"
                                    BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Padding="2">
                                    <TextBlock Text="{TemplateBinding Text}"/>
                                </Border>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Trigger>
            </Style.Triggers>
        </Style>
 
        <Style TargetType="{x:Type CheckBox}">
            <Style.Triggers>
                <Trigger Property="IsEnabled" Value="False">
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="CheckBox">
                                <StackPanel Orientation="Horizontal" VerticalAlignment="Top">
                                    <Border Height="13" Width="13" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Margin="1">
                                        <TextBlock Text="&#xFC;" FontFamily="Wingdings" FontSize="11" FontWeight="Bold">
                                            <TextBlock.Style>
                                                <Style TargetType="TextBlock">
                                                    <Style.Triggers>
                                                        <DataTrigger Binding="{Binding IsChecked, RelativeSource={RelativeSource AncestorType=CheckBox}}" Value="False">
                                                            <Setter Property="Visibility" Value="Hidden"/>
                                                        </DataTrigger>
                                                    </Style.Triggers>
                                                </Style>
                                            </TextBlock.Style>
                                        </TextBlock>
                                    </Border>
                                    <TextBlock Text="{TemplateBinding Content}" Margin="2,0,0,0"/>
                                </StackPanel>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Trigger>
            </Style.Triggers>
        </Style>
 
        <Style TargetType="ListBox">
            <Style.Triggers>
                <Trigger Property="IsHitTestVisible" Value="False">
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="ListBox">
                                <Border Background="Transparent"
                                    BorderBrush="{TemplateBinding BorderBrush}"
                                    BorderThickness="{TemplateBinding BorderThickness}">
                                    <ScrollViewer HorizontalScrollBarVisibility="Auto">
                                        <StackPanel Orientation="Vertical"
                                                VerticalAlignment="Stretch"
                                                HorizontalAlignment="Stretch"
                                                IsItemsHost="True">
                                        </StackPanel>
                                    </ScrollViewer>
                                </Border>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Trigger>
            </Style.Triggers>
        </Style>
 
        <Style x:Key="ReadOnlyListBox" TargetType="{x:Type ListBox}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ListBox}">
                        <Border x:Name="Bd" SnapsToDevicePixels="true" Background="Transparent" Padding="1"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}">
                            <ScrollViewer Padding="{TemplateBinding Padding}" Focusable="false">
                                <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" IsHitTestVisible="False"/>
                            </ScrollViewer>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsGrouping" Value="true">
                                <Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <Window.Background>
        <Binding Source="{StaticResource  PageBackgroundBrush}"/>
    </Window.Background>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="24"/>
            <RowDefinition Height="24"/>
            <RowDefinition Height="24"/>
            <RowDefinition Height="24"/>
            <RowDefinition Height="24"/>
            <RowDefinition Height="24"/>
            <RowDefinition Height="70"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="200"/>
            <ColumnDefinition Width="200"/>
        </Grid.ColumnDefinitions>
        <TextBlock Grid.Row="0" Grid.Column="0" Text="Enabled Controls"/>
        <TextBlock Grid.Row="0" Grid.Column="1" Text="Disabled Controls"/>
        <TextBox Grid.Row="1" Grid.Column="0" Text="Enabled TextBox"/>
        <TextBox Grid.Row="1" Grid.Column="1" Text="Disabled TextBox" IsEnabled="False"/>
        <DatePicker Grid.Row="2" Grid.Column="0" SelectedDate="1/1/2000"/>
        <DatePicker Grid.Row="2" Grid.Column="1" IsEnabled="False" SelectedDate="1/1/2000"/>
        <ComboBox Grid.Row="3" Grid.Column="0" SelectedIndex="0">
            <ComboBoxItem>Enabled Dropdown</ComboBoxItem>
        </ComboBox>
        <ComboBox Grid.Row="3" Grid.Column="1" SelectedIndex="0" IsEnabled="False">
            <ComboBoxItem>Disabled Dropdown</ComboBoxItem>
        </ComboBox>
        <ComboBox Grid.Row="4" Grid.Column="0" SelectedIndex="0" IsEditable="True">
            <ComboBoxItem>Enabled Combobox</ComboBoxItem>
        </ComboBox>
        <ComboBox Grid.Row="4" Grid.Column="1" SelectedIndex="0" IsEditable="True" IsEnabled="False">
            <ComboBoxItem>Disabled Combobox</ComboBoxItem>
        </ComboBox>
        <CheckBox Grid.Row="5" Grid.Column="0" Content="Enabled Checkbox" IsChecked="True"/>
        <CheckBox Grid.Row="5" Grid.Column="1" Content="Disabled Checkbox" IsChecked="True" IsEnabled="False"/>
        <ListBox Grid.Row="6" Grid.Column="0" SelectedIndex="0">
            <ListBoxItem>Enabled ListBoxItem One</ListBoxItem>
            <ListBoxItem>Enabled ListBoxItem Two</ListBoxItem>
            <ListBoxItem>Enabled ListBoxItem Three</ListBoxItem>
            <ListBoxItem>Enabled ListBoxItem Four</ListBoxItem>
            <ListBoxItem>Enabled ListBoxItem Five</ListBoxItem>
        </ListBox>
        <ListBox Grid.Row="6" Grid.Column="1" SelectedIndex="0" Style="{StaticResource ReadOnlyListBox}">
            <ListBoxItem>Disabled ListBoxItem One</ListBoxItem>
            <ListBoxItem>Disabled ListBoxItem Two</ListBoxItem>
            <ListBoxItem>Disabled ListBoxItem Three</ListBoxItem>
            <ListBoxItem>Disabled ListBoxItem Four</ListBoxItem>
            <ListBoxItem>Disabled ListBoxItem Five</ListBoxItem>
        </ListBox>
    </Grid>
</Window>
 
 
If you think this is a ridiculous amount of work to make a few backgrounds transparent and some text black, I agree with you.

No comments:

Post a Comment