WPF Tricks: Coloring Icons at Runtime

Icons help users get their bearings in an application interface. Icons that are done well are easily recognizable, contributing significantly to the intuitive operability. Therefore, they belong in every application—but they can also cause unexpected problems for the developer.

The Problem

An icon is supposed to appear on a button. Both the foreground and background color of the control, and thus the color of the icon, are supposed to change according to the state (normal, hover, disabled). In my example, the color of the icon has to change from black to white for the hover because of the relatively dark background color. Since the background color is prescribed by the client’s CI, it cannot readily be changed.

Figure 1: The color of the icon is supposed to change according to the state (normal, hover, disabled)

Usually, different icon sets are required at this point, and the graphics have to be exchanged at runtime. Wouldn’t it be great if you could simply arbitrarily color the icons at runtime?

Here is how

We replace the control template of the button by way of style. The content itself is hidden behind a border and a rectangle for this purpose. It is important that both are the same size as the original content. The background color of the border is bound to the background color of the button by means of template binding. The fill color of the rectangle is bound to the foreground color. The content of the button is then displayed in this color.

We use the content presenter of the button, which is currently covered, as an opacity mask for the rectangle using a visual brush. Now, the rectangle is “punched out”: The rectangle remains visible wherever the content is visible. Everywhere else, it becomes transparent. Thus, the content of the button becomes visible in the defined foreground color!

An abbreviated version of the control template (without some of the properties):

    <Border
     Background="{TemplateBinding Background}" 
     BorderBrush="{TemplateBinding BorderBrush}" 
     BorderThickness="{TemplateBinding BorderThickness}">
     <Grid>
      <ContentPresenter x:Name="MyContentPresenter" 
                Content="{TemplateBinding Content}"/>
      <Border Background="{TemplateBinding Background}">
       <Rectangle 
        Fill="{TemplateBinding Foreground}" 
        Width="{Binding ElementName=MyContentPresenter, Path=ActualWidth}" 
        Height="{Binding ElementName=MyContentPresenter, Path=ActualHeight}">
        <Rectangle.OpacityMask>
         <VisualBrush Stretch="None" Visual="{Binding ElementName=MyContentPresenter}"/>
        </Rectangle.OpacityMask>
       </Rectangle>
      </Border>
     </Grid>
    </Border>

The button can furthermore be filled with any desired content:

    <Button>
     <StackPanel Orientation="Horizontal">
      <Image  Source="{StaticResource Icon}" Margin="2" />
      <TextBlock VerticalAlignment="Center">let’s go!</TextBlock>
     </StackPanel>
    </Button>

(The complete style is provided at the end of this post.)

Conclusion

This little trick enables you to color icons at runtime virtually any way you like, making different-colored icon sets unnecessary. The principle is very lightweight, but it has its limitations: Gray-scale or multi-colored icons, for example, cannot be used.

Obviously, the principle is not only useful for buttons; it can also be used for any other kind of control. Due to the binding of the color, even color gradients and animation are ultimately possible.


The complete style:

    <Style TargetType="Button">
     <Setter Property="Background" Value="WhiteSmoke" />
     <Setter Property="Foreground" Value="Black" />
     <Setter Property="SnapsToDevicePixels" Value="True" />
     <Setter Property="FontWeight" Value="Normal"/>
     <Setter Property="MinWidth" Value="50"/>
     <Setter Property="Padding" Value="1"/>
     <Setter Property="BorderBrush" Value="LightGray"/>
     <Setter Property="BorderThickness" Value="1"/>
     <Setter Property="VerticalContentAlignment" Value="Center"/>
     <Setter Property="HorizontalContentAlignment" Value="Center"/>
     <Setter Property="Template">
      <Setter.Value>
       <ControlTemplate TargetType="{x:Type Button}">
        <Border
         Background="{TemplateBinding Background}" 
         BorderBrush="{TemplateBinding BorderBrush}" 
         BorderThickness="{TemplateBinding BorderThickness}" 
         MinWidth="{TemplateBinding MinWidth}">
         <Grid>
          <ContentPresenter x:Name="MyContentPresenter" 
           Content="{TemplateBinding Content}" 
           HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 
           VerticalAlignment="{TemplateBinding VerticalContentAlignment}" 
           Margin="{TemplateBinding Padding}"/>
          <Border Background="{TemplateBinding Background}">
           <Rectangle 
            Fill="{TemplateBinding Foreground}" 
            Margin="{TemplateBinding Padding}"
            Width="{Binding ElementName=MyContentPresenter, Path=ActualWidth}" 
            Height="{Binding ElementName=MyContentPresenter, Path=ActualHeight}" 
            HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 
            VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
            <Rectangle.OpacityMask>
             <VisualBrush Stretch="None" Visual="{Binding ElementName=MyContentPresenter}"/>
            </Rectangle.OpacityMask>
           </Rectangle>
          </Border>
         </Grid>
        </Border>
        <ControlTemplate.Triggers>
         <Trigger Property="IsMouseOver" Value="True">
          <Setter Property="Background" Value="{DynamicResource Brushes50HzOrange158}" />
          <Setter Property="Foreground" Value="White" />
         </Trigger>
         <Trigger Property="IsPressed" Value="True">
          <Setter Property="Background" Value="{DynamicResource Brushes50HzRotOrange179}" />
          <Setter Property="Foreground" Value="White" />
         </Trigger>
         <Trigger Property="IsEnabled" Value="False">
          <Setter Property="Foreground" Value="{DynamicResource Brushes50HzGrau430}"/>
         </Trigger>
        </ControlTemplate.Triggers>
       </ControlTemplate>
      </Setter.Value>
     </Setter>
    </Style>

This post was written by: