Directory Freebies VS CheatSheet Forum

RSS

Email

Translate

Home About Archive Privacy Contact Advertise Write for Dev102
Posted by ailon on Jul 23rd, 2009 | Filed under .Net, C#, Silverlight, WPF |

Suppose you have several objects in your WPF application filled with similar Brushes which differ in brightness only. Like in this image:

red_rect

 

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:

 

 green_rect

 

blue_rect

 

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.

 

Hue, Saturation, Brightness

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.

 

SetBrightness() Extension Method

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: }

 

Using the Method

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.

 

Final thoughts

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.

Tags: , ,

7 Responses to “Changing Brush Brightness in WPF/Silverlight”


  1. 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.

  2. 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);
    }

  3. 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);
    }
    }

  4. 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?

3 Trackback(s)

  1. Jul 24, 2009: DotNetBurner - Silverlight
  2. Jul 27, 2009: Silverlight Cream for July 24, 2009 -- #654
  3. Sep 6, 2009: WPF: 90+ Miejsc które warto zna? « Dawid Po?li?ski

Post a Comment

Write Article for Dev102

Write for Dev102!

We pay for user submitted tutorials and articles that we publish. Anyone can send in a contribution

Learn More