Suppose you have several objects in your WPF application filled with similar Brushes which differ in brightness only. Like in this image:
The nine rectangles use the same RadialGradienBrush but each rectangle is a little darker than previous. You can create these 9 different brushes in your favorite design tool (Expression Blend, etc.). But what if you need to change the base color later or you just need to make the brush user-configurable? Like in these samples:
The better approach would be to define the base brush once and then change it’s brightness in code. Unfortunately there’s no built-in way to change brightness of the Brush or Color in WPF. This is what we are going to address in this article.
System.Windows.Media.Color structure represents colors as 4 channels: alpha (opacity) channel and 3 channels for each core color (red, green and blue). aRGB color space is good for representing colors but it’s quite problematic to control such properties as brightness with these 4 channels. We will convert our colors to and from a HSB color space which is good at exactly what we need – controlling such perceptual color characteristics as hue, saturation and brightness. To do this we create a new class called HSBColor defined like this:
1: public class HSBColor
2: {
3: public double H { get; set; }
4: public double S { get; set; }
5: public double B { get; set; }
6: public byte A { get; set; }
7:
8: public static HSBColor FromColor(Color rgbColor)
9: {
10: HSBColor result = new HSBColor();
11:
12: // preserve alpha
13: result.A = rgbColor.A;
14:
15: // convert R, G, B to numbers from 0 to 1
16: double r = rgbColor.R / 255d;
17: double g = rgbColor.G / 255d;
18: double b = rgbColor.B / 255d;
19:
20: double max = Math.Max(r, Math.Max(g, b));
21: double min = Math.Min(r, Math.Min(g, b));
22:
23: // hue
24: if (max == min)
25: result.H = 0;
26: else if (max == r)
27: result.H = (60 * (g - b) / (max - min) + 360) % 360;
28: else if (max == g)
29: result.H = 60 * (b - r) / (max - min) + 120;
30: else
31: result.H = 60 * (r - g) / (max - min) + 240;
32:
33: // saturation
34: if (max == 0)
35: result.S = 0;
36: else
37: result.S = 1 - min / max;
38:
39: // brightness
40: result.B = max;
41:
42: return result;
43: }
44:
45: public Color ToColor()
46: {
47: Color result = new Color();
48:
49: result.A = this.A;
50:
51: int hi = (int)Math.Floor(this.H / 60) % 6;
52: double f = this.H / 60 - Math.Floor(this.H / 60);
53:
54: double p = this.B * (1 - this.S);
55: double q = this.B * (1 - f * this.S);
56: double t = this.B * (1 - (1 - f) * this.S);
57:
58: switch (hi)
59: {
60: case 0:
61: result.R = (byte)(this.B * 255);
62: result.G = (byte)(t * 255);
63: result.B = (byte)(p * 255);
64: break;
65: case 1:
66: result.R = (byte)(q * 255);
67: result.G = (byte)(this.B * 255);
68: result.B = (byte)(p * 255);
69: break;
70: case 2:
71: result.R = (byte)(p * 255);
72: result.G = (byte)(this.B * 255);
73: result.B = (byte)(t * 255);
74: break;
75: case 3:
76: result.R = (byte)(p * 255);
77: result.G = (byte)(q * 255);
78: result.B = (byte)(this.B * 255);
79: break;
80: case 4:
81: result.R = (byte)(t * 255);
82: result.G = (byte)(p * 255);
83: result.B = (byte)(this.B * 255);
84: break;
85: case 5:
86: result.R = (byte)(this.B * 255);
87: result.G = (byte)(p * 255);
88: result.B = (byte)(q * 255);
89: break;
90: }
91:
92: return result;
93: }
94: }
The algorithm behind these conversions could be found in this Wikipedia article.
Now all we need to do is modify brightness of every color used in our brushes.
We will create an extension method for Brush class which creates a new brush based on the original brush but with new brightness applied. Our method will support all Color based brushes such as SolidColorBrush, LinearGradientBrush and RadialGradientBrush. For all other brush types our method will just return the clone of the original brush.
With SolidColorBrush we are going to set brightness on the color set in it’s Color property and for GradientBrush(es) – on Color property of each GradientStop. So, enough talking – here’s the code:
1: public static Brush SetBrightness(this Brush original, double brightness)
2: {
3: if (brightness < 0 || brightness > 1)
4: throw new ArgumentOutOfRangeException("brightness", "brightness should be between 0 and 1");
5:
6: Brush result;
7:
8: if (original is SolidColorBrush)
9: {
10: HSBColor hsb = HSBColor.FromColor(((SolidColorBrush)original).Color);
11: hsb.B = brightness;
12: result = new SolidColorBrush(hsb.ToColor());
13: }
14: else if (original is GradientBrush)
15: {
16: result = original.Clone();
17: // change brightness of every gradient stop
18: foreach (GradientStop gs in ((GradientBrush)result).GradientStops)
19: {
20: HSBColor hsb = HSBColor.FromColor(gs.Color);
21: hsb.B = brightness;
22: gs.Color = hsb.ToColor();
23: }
24: }
25: else
26: {
27: result = original.Clone();
28: }
29:
30: return result;
31: }
Let’s look at a simple usage scenario. We will create those 9 rectangles pictured above. The window’s XAML looks like this:
1: <Window x:Class="Brightness.Window2"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: Title="Window2" Height="250" Width="300" Background="#333333"
5: Loaded="Window_Loaded">
6: <Window.Resources>
7: <Style x:Key="RectangleStyle" TargetType="Rectangle">
8: <Setter Property="RadiusX" Value="10" />
9: <Setter Property="RadiusY" Value="10" />
10: <Setter Property="Margin" Value="5" />
11: </Style>
12: </Window.Resources>
13: <UniformGrid x:Name="mainGrid" Margin="30">
14: <Rectangle Style="{StaticResource RectangleStyle}">
15: <Rectangle.Fill>
16: <RadialGradientBrush GradientOrigin="-0.3 -0.3">
17: <GradientStop Color="#ffffff" Offset="0" />
18: <GradientStop Color="#0000FF" Offset="1" />
19: </RadialGradientBrush>
20: </Rectangle.Fill>
21: </Rectangle>
22: <Rectangle Style="{StaticResource RectangleStyle}" />
23: <Rectangle Style="{StaticResource RectangleStyle}" />
24: <Rectangle Style="{StaticResource RectangleStyle}" />
25: <Rectangle Style="{StaticResource RectangleStyle}" />
26: <Rectangle Style="{StaticResource RectangleStyle}" />
27: <Rectangle Style="{StaticResource RectangleStyle}" />
28: <Rectangle Style="{StaticResource RectangleStyle}" />
29: <Rectangle Style="{StaticResource RectangleStyle}" />
30: </UniformGrid>
31: </Window>
The first rectangle has it’s Fill property set to a RadialGradientBrush and all other rectangles have no fill at all. In the Window_Loaded event handler we just take the brush of the first rectangle and apply it to all other rectangles with different brightness.
1: private void Window_Loaded(object sender, RoutedEventArgs e)
2: {
3: Brush mainBrush = (mainGrid.Children[0] as Rectangle).Fill;
4: for (int i = 1; i < 9; i++)
5: {
6: (mainGrid.Children[i] as Rectangle).Fill = mainBrush.SetBrightness(1 - i / 10d);
7: }
8: }
And that’s it.
Rather than creating an extension method it would be nice to create a custom Brush that would take another “standard” brush and apply specified brightness to it. This would allow animation of Brightness property resulting in nice fade-in and out effects (for example). Unfortunately Microsoft has implemented abstract Brush class in such a way that no 3rd party can inherit from it. Some of the abstract methods in the Brush class are also declared as internal so there’s no way we can implement them. I hope they had a good reason for doing this. Otherwise it doesn’t make much sense.
We pay for user submitted tutorials and articles that we publish. Anyone can send in a contribution
Learn More
Greg Said on Jul 24, 2009 :
You could make it slightly better by defining an attached property for brush named Brightness, and in the Changed event, use your extension method.
Code would look something similar to
DependencyProperty Brightness = new Depe..(,new PropertyMetatdata(1, OnBrightnessChanged);
public static void OnBrightnessChanged(DependencyObject o, EventArgs e)
{
((Brush)o).SetBrightness(e.NewValue); //Use you extension method.
}
This way animation would work, and you could set it from a designer.
Jardar Said on Jul 28, 2009 :
To have single parameter to adjust from white black this calculation could be used
if (brightness > 1.0)
{
hsb.S = hsb.S - hsb.S * (brightness - 1.0);
hsb.B = hsb.B + (1.0 - hsb.B) * (brightness - 1.0);
}
else
{
hsb.S = hsb.S + (1 - hsb.S) * (1.0 - brightness);
hsb.B = hsb.B - hsb.B * (1.0 - brightness);
}
Jardar Said on Jul 28, 2009 :
static public class BrushBehaviour
{
public static readonly DependencyProperty BrightnessProperty = DependencyProperty.RegisterAttached(
“Brightness”,
typeof(Double),
typeof(BrushBehaviour),
new FrameworkPropertyMetadata(1.0, new PropertyChangedCallback(OnBrightnessChanged)));
private static void OnBrightnessChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
Brush b = ((Brush)o).SetBrightness((Double)e.NewValue);
object ob = b.GetValue(SolidColorBrush.ColorProperty);
o.SetValue(SolidColorBrush.ColorProperty, ob);
}
}
Dave Said on Oct 28, 2009 :
I’m a silverlight (3) newbie, but where do you get the .Clone() method from in your extension method? After some googling I thought Silverlight didn’t have one - or have you written your own?
Alan Said on Nov 13, 2009 :
You are right. There’s no Clone() in Silverlight. As a workaround you can use a custom made Clone() method. A good one was posted by Justin Angel in Silverlight forums http://forums.silverlight.net/forums/p/1730/51694.aspx
Ryan Said on Apr 13, 2010 :
Very cool. Here’s my addition (brightness palette generator)…
public static Brush[] GenerateBrightnessPalette(this Brush BaseBrush, int ColorCount)
{
Brush[] palette = new Brush[ColorCount];
for (int index = 0; index < ColorCount; index++)
{
double brightnessFactor = (((double)index + 1.0d) / (double)ColorCount);
palette[index] = BaseBrush.SetBrightness(brightnessFactor);
}
return palette;
}