Icons helfen Nutzern sich in einer Anwendungsoberfläche zurecht zu finden. Gut gemachte Icons sind schnell erfassbar und leisten damit einen wichtigen Beitrag zur intuitiven Bedienbarkeit. Sie gehören daher in jede Anwendung – können Entwickler aber auch vor unerwartete Probleme stellen.
Das Problem
Auf einem Button soll ein Icon erscheinen. Je nach Zustand (normal, hover, disabled) sollen sich Vorder- und Hintergrundfarbe des Controls ändern und damit auch die Farbe des Icons. In meinem Beispiel sorgt die relativ dunkle Hintergrundfarbe beim Hover dafür, dass die Farbe des Icons von schwarz zu weiß wechseln muss. Da die Hintergrundfarbe durch die CI des Kunden vorgegeben wird, lässt sie sich auch nicht ohne Weiteres ändern.
Üblicherweise braucht es jetzt verschiedene Icon-Sets und die Grafiken müssen zur Laufzeit ausgetauscht werden. Wäre es nicht toll, man könnte die Icons einfach zur Laufzeit beliebig einfärben?
So geht’s
Wir ersetzen das Control-Template des Buttons per Style. Der eigentliche Inhalt wird dabei hinter einem Border und einem Rectangle versteckt. Wichtig ist, dass beide genau so groß wie der originale Content sind. Die Hintergrundfarbe des Borders wird per Template-Binding auf die Hintergrundfarbe des Buttons gebunden. Die Füllfarbe des Rectangles binden wir auf die Vordergrundfarbe. In dieser Farbe wird dann der Inhalt des Buttons sichtbar.
Den jetzt verdeckten Content-Presenter des Buttons verwenden wir, mit Hilfe eines Visualbrush, als Opacity-Mask für das Rectangle. Das Rectangle wird nun “ausgestanzt”: An den Stellen, an denen der Content sichtbar ist, bleibt das Rectangle ebenfalls sichtbar. An allen anderen wird es transparent. Der Inhalt des Buttons wird also in der gesetzten Vordergrundfarbe sichtbar!
Hier eine gekürzte Version des Control-Templates (einige Properties fehlen):
<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>
Der Button kann weiterhin mit beliebigem Content befüllt werden:
<Button>
<StackPanel Orientation="Horizontal">
<Image Source="{StaticResource Icon}" Margin="2" />
<TextBlock VerticalAlignment="Center">los gehts!</TextBlock>
</StackPanel>
</Button>
(Am Ende des Artikels findet befindet sich der komplette Style)
Fazit
Mit diesem kleinen Trick ist es möglich, Icons zur Laufzeit praktisch beliebig einzufärben. Verschiedenfarbige Icon-Sets werden damit unnötig. Das Prinzip ist sehr leichtgewichtig, hat aber auch Einschränkungen: Graustufige oder Mehrfarbige Icons, lassen sich bspw. nicht verwenden.
Das Prinzip ist natürlich nicht nur für Buttons sinnvoll, sondern kann auch bei beliebigen anderen Controls Anwendung finden. Durch das Binding der Farbe wären letztendlich sogar Farbverläufe oder gar Animationen machbar.
Der komplette 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>