WPF Themes – Markup Extensions
This is the third post in a series about my experience developing a WPF Theme system. In my last post, I showed how I solved the problem of Theme Management and extensibility.
In this post, I’m going to talk about some Markup Extensions that I created in order to assist in making the final Theme brushes.
When I look at the brushes used in other people’s Themes, it seems clear that the people making them are at least moderately skilled graphic designers. The obviously have artistic talent. I, on the other hand, have none. The best I can do is say “I want this color/brush to be a little darker or lighter than that color/brush.”
I am also lazy (I have long held that a good engineer is a lazy engineer). While it would be possible to take the ARGB of a color and manually do math to figure out what another related color should be, there was no way I was going to spend that much time doing something so tedious.
Instead, I wrote a number of Markup Extensions to simplify this process.
The first is the ColoringExtension, which takes a base color, an opacity, and an optional background color, and calculates a new color.
public class ColoringExtension : MarkupExtension
{
private bool BackgroundColorSet { get; set; }
private Color _backgroundColor;
public Color BackgroundColor
{
get { return _backgroundColor; }
set { _backgroundColor = value; BackgroundColorSet = true; }
}
public Color BaseColor { get; set; }
public double Opacity { get; set; }
public ColoringExtension()
{
Opacity = 1.0;
}
public override object ProvideValue(IServiceProvider sp)
{
if (BackgroundColorSet == false)
{
return Color.FromArgb((byte)(Opacity * 255),
BaseColor.R,
BaseColor.G,
BaseColor.B);
}
else
{
return Color.FromArgb((byte)255,
(byte)((Opacity * BaseColor.R) +
((1 - Opacity) * BackgroundColor.R)),
(byte)((Opacity * BaseColor.G) +
((1 - Opacity) * BackgroundColor.G)),
(byte)((Opacity * BaseColor.B) +
((1 - Opacity) * BackgroundColor.B)));
}
}
}
With this, I can take a single base color and create a collection of related colors.
<SolidColorBrush x:Key="BackgroundBrush">
<SolidColorBrush.Color>
<st:Coloring BaseColor="{StaticResource BackgroundColor}"
BackgroundColor="White"
Opacity="{StaticResource WindowOpacity}" />
</SolidColorBrush.Color>
</SolidColorBrush>
<SolidColorBrush x:Key="FadedBackgroundBrush">
<SolidColorBrush.Color>
<st:Coloring BaseColor="{StaticResource BackgroundColor}"
BackgroundColor="White"
Opacity="0.4" />
</SolidColorBrush.Color>
</SolidColorBrush>
<SolidColorBrush x:Key="ControlBackgroundBrush">
<SolidColorBrush.Color>
<st:Coloring BaseColor="{StaticResource BackgroundColor}"
BackgroundColor="White"
Opacity="0.3" />
</SolidColorBrush.Color>
</SolidColorBrush>
What this really enables me to do is have a single list of brushes used by all Themes, with a simple list of Theme-specific colors – but I’ll get into that more in my next post.
The other extension is the TintingExtension, which takes a brush, a tint (percent lighter or darker) and an opacity, and modifies the brush that it is attached to.
public class TintingExtension : MarkupExtension
{
public Brush Brush { get; set; }
public double Tint { get; set; }
public double Opacity { get; set; }
public TintingExtension()
{
Brush = null;
Tint = 1.0;
Opacity = 1.0;
}
public override object ProvideValue(IServiceProvider sp)
{
return new TintInfo(Brush, Tint, Opacity);
}
}
public class TintInfo
{
public Brush Brush { get; set; }
public double Tint { get; set; }
public double Opacity { get; set; }
public TintInfo()
{
}
public TintInfo(Brush brush, double tint, double opacity)
{
Brush = brush;
Tint = tint;
Opacity = opacity;
}
}
public class Tint
{
public static TintInfo GetInfo(DependencyObject obj)
{
return (TintInfo)obj.GetValue(InfoProperty);
}
public static void SetInfo(DependencyObject obj, TintInfo value)
{
obj.SetValue(InfoProperty, value);
}
public static readonly DependencyProperty InfoProperty = null;
static Tint()
{
FrameworkPropertyMetadata metaData = new FrameworkPropertyMetadata(null,
new PropertyChangedCallback(OnInfoAttachedPropertyChanged));
InfoProperty = DependencyProperty.RegisterAttached("Info", typeof(TintInfo),
typeof(Tint), metaData);
}
public static void OnInfoAttachedPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
Brush brush = d as Brush;
TintInfo info = e.NewValue as TintInfo;
if (brush != null && info != null)
{
BrushHelper.CopyAndTintBrush(info.Brush, brush, info.Tint, info.Opacity);
}
brush.Freeze();
}
}
I mainly use this for creating a collection of related gradient brushes from a single base brush
<LinearGradientBrush x:Key="ActiveItemsGradientBrush" EndPoint="0.5,0" StartPoint="0.5,1">
<GradientStop Offset="0">
<GradientStop.Color>
<st:Coloring BaseColor="{StaticResource SelectedHighlightColor}"
Opacity="1.0" />
</GradientStop.Color>
</GradientStop>
<GradientStop Offset="0.01">
<GradientStop.Color>
<st:Coloring BaseColor="{StaticResource SelectedHighlightColor}"
Opacity="0.5" />
</GradientStop.Color>
</GradientStop>
<GradientStop Offset="0.45">
<GradientStop.Color>
<st:Coloring BaseColor="{StaticResource SelectedHighlightColor}"
Opacity="0.0" />
</GradientStop.Color>
</GradientStop>
<GradientStop Offset="0.55">
<GradientStop.Color>
<st:Coloring BaseColor="{StaticResource SelectedHighlightColor}"
Opacity="0.0" />
</GradientStop.Color>
</GradientStop>
<GradientStop Offset="0.99">
<GradientStop.Color>
<st:Coloring BaseColor="{StaticResource SelectedHighlightColor}"
Opacity="0.5" />
</GradientStop.Color>
</GradientStop>
<GradientStop Offset="1.0">
<GradientStop.Color>
<st:Coloring BaseColor="{StaticResource SelectedHighlightColor}"
Opacity="1.0" />
</GradientStop.Color>
</GradientStop>
</LinearGradientBrush>
<LinearGradientBrush x:Key="SelectedItemsBackgroundBrush"
st:Tint.Info="{st:Tinting Brush={StaticResource ActiveItemsGradientBrush},
Opacity={StaticResource ActiveOpacity}}" />
<LinearGradientBrush x:Key="MouseOverItemsBackgroundBrush"
st:Tint.Info="{st:Tinting Brush={StaticResource SelectedItemsBackgroundBrush},
Opacity=0.5}" />
The meat of the work is done in the BrushHelper class.
public static class BrushHelper
{
public static void CopyAndTintBrush(Brush sourceBrush, Brush targetBrush,
double tint, double opacity)
{
if (sourceBrush.GetType() != targetBrush.GetType())
{
throw new ArgumentException("Invlid targetBrush");
}
if (sourceBrush is SolidColorBrush)
{
SolidColorBrush solidSourceBrush = sourceBrush as SolidColorBrush;
SolidColorBrush solidTargetBrush = targetBrush as SolidColorBrush;
solidTargetBrush.Color = TintColor(solidSourceBrush.Color, tint, opacity);
}
else if (sourceBrush is LinearGradientBrush)
{
LinearGradientBrush gradientSourceBrush = sourceBrush as LinearGradientBrush;
LinearGradientBrush gradientTargetBrush = targetBrush as LinearGradientBrush;
// Empty the target brush's stops, gut in case
gradientTargetBrush.GradientStops.Clear();
gradientTargetBrush.StartPoint = gradientSourceBrush.StartPoint;
gradientTargetBrush.EndPoint = gradientSourceBrush.EndPoint;
foreach (GradientStop stop in gradientSourceBrush.GradientStops)
{
gradientTargetBrush.GradientStops.Add(new
GradientStop(TintColor(stop.Color, tint, opacity), stop.Offset));
}
}
}
public static Color TintColor(Color color, double tintShift, double opacity)
{
Color newColor = color;
newColor.A = (byte)Math.Max(Math.Min((double)color.A * opacity, 255.0), 0.0);
newColor.R = (byte)TintShade((double)color.R, tintShift);
newColor.G = (byte)TintShade((double)color.G, tintShift);
newColor.B = (byte)TintShade((double)color.B, tintShift);
return newColor;
}
private static double TintShade(double shade, double tint)
{
if (tint == 0)
{
return 0;
}
else
{
double remainder = 255 - shade;
double newRemainder = remainder / tint;
return Math.Max(Math.Min(255 - newRemainder, 255), 0);
}
}
}
In the next (and final) installment, I’ll tie it all together and show how I built the actual Themes.