Critical Development

Language design, framework development, UI design, robotics and more.

Compact Framework Controls (Part 3): Linear Gradients

Posted by Dan Vanderboom on May 5, 2008

[This article is part of a series that starts in this article and precedes this one here.]

Linear Gradients

Linear gradients are a nice, subtle effect that can turn a boring control into something more interesting and professional looking.  You can use a linear gradient for the entire background of your control, which I’ll demonstrate in this article, or you can paint one or more regions selectively to display a gradient.  A linear gradient is a gradual transition from one color to another, and while you can transition through multiple colors along an axis, going from blue to red to green to white to black if you wanted, I’m going to start simple and create a control that blends between only two colors.  You can also define the line of change to be any angle: vertical (as shown in the example below), horizontal, or some other angle.  I’ll show how to draw just the vertical and horizontal linear gradients.

Linear Gradient Example

I’ll be using the control from my previous article, and adding to it, to demonstrate linear gradients.  We’re going to need some new properties to support gradients, so first we need to add a couple enumerations.

public enum RegionFillStyle
{
    SolidColor,
    LinearGradient
}

public enum LinearGradientDirection
{
    Horizontal,
    Vertical
}

And now the new properties.

private RegionFillStyle _FillStyle = RegionFillStyle.SolidColor;
[DefaultValue(RegionFillStyle.SolidColor)]
public RegionFillStyle FillStyle
{
    get { return _FillStyle; }
    set { _FillStyle = value; Refresh(); }
}

private LinearGradientDirection _LinearGradientDirection = LinearGradientDirection.Vertical;
[DefaultValue(LinearGradientDirection.Vertical)]
public LinearGradientDirection LinearGradientDirection
{
    get { return _LinearGradientDirection; }
    set { _LinearGradientDirection = value; Refresh(); }
}

private Color _BackColor2 = Color.White;
public Color BackColor2
{
    get { return _BackColor2; }
    set { _BackColor2 = value; Refresh();  }
}

The goal is to draw a background for our control that is a linear gradient, fading from BackColor to BackColor2.  We’re going to use a technique called interpolation, which is the calculation of new data points based on the values of adjacent data points.  In our case, we’re going to be interpolating color values.  We know the color at the top and the bottom (in the case of a vertical gradient), so a pixel halfway between them spatially should have a color value that is halfway between the color values at both ends.  Because colors are manipulated in bitmaps with an RGB scheme (using red, blue, and green aspects), we actually have three component values that need to be interpolated.

Understanding the Math

If our control is 100 pixels tall, and the color at the bottom is 100 units less blue than at the top, the translation is very simple: as we move down each pixel from top to bottom, we’ll subtract 1 unit of color from the blue value.  The challenge lies in the fact that we can’t assume our height and our color values will line up so nicely.  Furthermore, we have two other colors to deal with, and they may need to change at different rates or in different directions: the color may become slightly more red while simultaneously becoming drastically less green.

So we’re going to need some way of finding the scaling factor between the height or width of the control and the distance in color values for red, green, and blue separately.  In our example of 100 pixels to 100 color units, we have a 1:1 scaling factor.  If we instead had to make a color change of 200 units, we’d have a scaling factor of 1:2, or 1 pixel for 2 units of color change.  Another way to think of this is to say that for every pixel we move along the line, we’re going to increase or decrease our color by 2 units.

double RedScale = (double)Height / (BackColor2.R - BackColor.R);

The RedScale variable divides our height by our gradient’s difference (or change) in redness, and we make the same scaling calculation with green and blue.  This scaling takes increasing and decreasing color changes into account based on whether the subtraction expression on the right results in a positive or negative number.  As we move along the y axis, we’ll create a color that uses BackColor as a base and adds RGB values to it that divide the current y position with this scaling factor.  Let’s take a look at that code:

Bitmap LinearGradient = null;

if (LinearGradientDirection == LinearGradientDirection.Vertical)
{
    double RedScale = (double)Height / (BackColor2.R - BackColor.R);
    double GreenScale = (double)Height / (BackColor2.G - BackColor.G);
    double BlueScale = (double)Height / (BackColor2.B - BackColor.B);

    LinearGradient = new Bitmap(1, Height);
    for (int y = 0; y < Height; y++)
    {
        int red = BackColor.R + (int)((double)y / RedScale;
        int green = BackColor.G + (int)((double)y / GreenScale;
        int blue = BackColor.B + (int)((double)y / BlueScale;

        Color color = Color.FromArgb(red, green, blue);
        LinearGradient.SetPixel(0, y, color);
    }
}

if (LinearGradient != null)
{
    FillBrush = new TextureBrush(LinearGradient);
}

After calculating our scaling factors, we define a bitmap that’s as tall as our control, but only one pixel wide.  You can see this bitmap being used to create a TextureBrush at the bottom of the code.  This brush will be used to fill the entire area from left to right, copying the bitmap across the entire surface, so there’s no need to make it any wider than a single pixel.  (For horizontal gradients, we do the opposite: create a bitmap as wide as our control but only one pixel tall.)

In our hypothetical 100-pixel-tall control, with a red value that decreases 200 units from top to bottom (and therefore has a scaling factor of -0.5), we calculate each pixel’s redness by dividing y by -0.5.  At pixel y=25, which is 25% of the way down, we get a value of -50, which is 25% of -200.  At pixel y=75, we get 75 / -0.5 = -150.  So we take our original BackColor.R, and add this negative number, which makes it decrease from the base color as desired.

The only thing we need to do now is to ensure that each of our three color values never go outside the range of 0 to 255, otherwise we’ll get an error thrown from the Bitmap class.  We can do this with the Math class’s methods Min and Max, like this:

int red = Math.Max(Math.Min(BackColor.R + (int)((double)y / RedScale), 255), 0);
int green = Math.Max(Math.Min(BackColor.G + (int)((double)y / GreenScale), 255), 0);
int blue = Math.Max(Math.Min(BackColor.B + (int)((double)y / BlueScale), 255), 0);

Min returns the smaller of two numbers, and since we pass in 255 along with our calculated color, if our calculation is over 255, then the value it will return is 255.  Max does the opposite, and the combination ensures we stay within the valid range.

Results

The code sample above only showed the code for a vertical gradient, so here is the complete listing of our control with the logic for horizontal gradients as well.

using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Drawing;
using System.Reflection;
using System.ComponentModel;

namespace CustomControlsDemo
{
    public class ClippingControl : Control
    {
        private RegionFillStyle _FillStyle = RegionFillStyle.SolidColor;
        [DefaultValue(RegionFillStyle.SolidColor)]
        public RegionFillStyle FillStyle
        {
            get { return _FillStyle; }
            set { _FillStyle = value; Refresh(); }
        }
        
        private LinearGradientDirection _LinearGradientDirection = LinearGradientDirection.Vertical;
        [DefaultValue(LinearGradientDirection.Vertical)]
        public LinearGradientDirection LinearGradientDirection
        {
            get { return _LinearGradientDirection; }
            set { _LinearGradientDirection = value; Refresh(); }
        }
        
        private Color _BackColor2 = Color.White;
        public Color BackColor2
        {
            get { return _BackColor2; }
            set { _BackColor2 = value; Refresh(); }
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            // define a canvas for the visual content of the control
            Bitmap MyBitmap = new Bitmap(Width, Height);
            Graphics g = Graphics.FromImage(MyBitmap);

            Brush FillBrush = null;
            if (FillStyle == RegionFillStyle.SolidColor)
            {
                FillBrush = new SolidBrush(BackColor);
            }
            else if (FillStyle == RegionFillStyle.LinearGradient)
            {
                Bitmap LinearGradient = null;

                if (LinearGradientDirection == LinearGradientDirection.Horizontal)
                {
                    double RedScale = (double)Width / (BackColor2.R - BackColor.R);
                    double GreenScale = (double)Width / (BackColor2.G - BackColor.G);
                    double BlueScale = (double)Width / (BackColor2.B - BackColor.B);

                    LinearGradient = new Bitmap(Width, 1);
                    for (int x = 0; x < Width; x++)
                    {
                        int red = Math.Max(Math.Min(BackColor.R + (int)((double)x / RedScale), 255), 0);
                        int green = Math.Max(Math.Min(BackColor.G + (int)((double)x / GreenScale), 255), 0);
                        int blue = Math.Max(Math.Min(BackColor.B + (int)((double)x / BlueScale), 255), 0);

                        Color color = Color.FromArgb(red, green, blue);
                        LinearGradient.SetPixel(x, 0, color);
                    }
                }
                else if (LinearGradientDirection == LinearGradientDirection.Vertical)
                {
                    double RedScale = (double)Height / (BackColor2.R - BackColor.R);
                    double GreenScale = (double)Height / (BackColor2.G - BackColor.G);
                    double BlueScale = (double)Height / (BackColor2.B - BackColor.B);

                    LinearGradient = new Bitmap(1, Height);
                    for (int y = 0; y < Height; y++)
                    {
                        int red = Math.Max(Math.Min(BackColor.R + (int)((double)y / RedScale), 255), 0);
                        int green = Math.Max(Math.Min(BackColor.G + (int)((double)y / GreenScale), 255), 0);
                        int blue = Math.Max(Math.Min(BackColor.B + (int)((double)y / BlueScale), 255), 0);

                        Color color = Color.FromArgb(red, green, blue);
                        LinearGradient.SetPixel(0, y, color);
                    }
                }

                if (LinearGradient != null)
                {
                    FillBrush = new TextureBrush(LinearGradient);
                }
            }

            if (FillBrush != null)
            {
                g.FillRectangle(FillBrush, ClientRectangle);
            }

            // draw graphics on our bitmap
            g.DrawLine(new Pen(Color.Black), 0, 0, Width - 1, Height - 1);
            g.DrawEllipse(new Pen(Color.Black), 0, 0, Width - 1, Height - 1);

            // dispose of the painting tools
            g.Dispose();

            // paint the background to match the Parent control so it blends in
            e.Graphics.FillRectangle(new SolidBrush(Parent.BackColor), ClientRectangle);

            // define the custom shape of the control: a trapezoid in this example
            List<Point> Points = new List<Point>();
            Points.AddRange(new Point[] { new Point(20, 0), new Point(Width - 21, 0), 
                new Point(Width - 1, Height - 1), new Point(0, Height - 1) });

            // draw that content inside our defined shape, clipping everything that falls outside of the region;
            // only if the image is much smaller than the control does it really get "tiled" and act like a textured painting brush
            // but our bitmap image is the same size as the control, so we're just taking advantage of clipping
            Brush ImageBrush = new TextureBrush(MyBitmap);

            e.Graphics.FillPolygon(ImageBrush, Points.ToArray());
        }
    }
}

Placing a couple of these controls on a form, I can set one of them to use a solid background (yellow), and the others to use vertical and horizontal linear gradients.

Linear Gradient Control Screenshot

Conclusion

Linear gradients are a great effect to have in your repertoire of techniques.  Compact Framework applications especially tend to be flat and dull, with an unimpressive array of built-in controls, and with more focus on user interfaces like the iPhone and some of the cool new HTC touch devices, the desire for fancier interfaces is growing.  As we start to mix operations like polygon clipping and quasi-transparency (presented in the previous article), linear gradients, and others, we can put together a bag of tricks for composing beautiful and interesting user experiences.

Advertisements

9 Responses to “Compact Framework Controls (Part 3): Linear Gradients”

  1. Malcolm Hall said

    If you need a large gradient and experience ugly banding you can p/invoke the GradientFill method just like in this

    This technique doesn’t apper to use linear interpolation and does some kind of dithering to give a perfectly smooth effect.

  2. Dan Vanderboom said

    The major disadvantage to p/invoking the GradientFill method is that it will only work on Windows Mobile devices above a certain version number. My code will run on any version of Windows Mobile, and also runs fine on the desktop framework, which is very useful because then you can see it render correctly in the Windows Forms designer in Visual Studio.

    I haven’t noticed any of the ugly banding you mentioned. Do you have example code to demonstrate this?

  3. Malcolm Hall said

    Banding happens if you make a single colour gradient on an area of larger than 255 pixels. E.g. 0,0,0 to 0,255,0. Basically there isn’t enough of a range of colours to fill the area when it is big. However, GradientFill appears to dither the gradient and make it look smooth in this case.

    Another problem is that you are using SetPixel which is very slow. People just need to be aware that if using this techique for animations they should buffer the gradient image to speed things up.

    But yeh this is a great gradient solution to make your controls work in the designer. Currently I am wrapping up this code and GradientFill with a Platform check to hopefully have the best of both worlds, we’ll see if it works.

  4. Malcolm Hall said

    Actually, I think since you used the Texture brush that might prevent banding because it does interpolation when it scales.

  5. Dan Vanderboom said

    SetPixel isn’t the fastest way to do this, true. In another article, I show how to use very fast, unsafe code manipulating pointers, which could certainly be done here. But because I’m only setting pixels for a single row or column of pixels, even for a large area, the time spent drawing it is negligable. Still, it’s an optimization worth making if you want to support as many simultaneous effects as possible.

    Many of the drawing operations in System.Drawing seem to be routed to the internal AGL component, which I’m guessing is a set of P/Invokes into the OS in some way, or into something else which has been optimized for speed. Animation of any computationally expensive graphical effects is going to be tough for any mobile processors right now, though (except for HTC, etc, who are releasing new devices with graphics processors).

    I have some basic alpha compositing techniques working, which also work just as well in a designer as on any version of Windows Mobile, which will probably be my next article in this series… but even using fast unsafe pointer manipulation, it’s still not super fast if you consider multiple large rectangles in overlapping layers, calculating transparency as they all move around.

  6. Nathan Stiles said

    Awesome info. This really helped me realize nicer controls for my mobile app. I’ve changed this a little to make it more efficient(I think). Also added a double gradient, and tried to prevent redrawing of gradients unless it really needs to be redrawn.


    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Text;
    using System.Windows.Forms;
    using System.Drawing;
    using System.Drawing.Drawing2D;
    using System.Drawing.Imaging;
    using System.Drawing.Text;
    using System.Reflection;
    using System.ComponentModel;

    namespace fbwmcontrols
    {
    public class ctrBase : Control
    {
    public enum RegionFillStyle { SolidColor, LinearGradient, LinearDoubleGradient }
    public enum LinearGradientDirections { Horizontal, Vertical }

    private RegionFillStyle _FillStyle = RegionFillStyle.LinearGradient;
    [DefaultValue(RegionFillStyle.LinearGradient)]
    public RegionFillStyle FillStyle
    {
    get { return _FillStyle; }
    set
    {
    _FillStyle = value;
    setDrawing();
    Refresh();
    }
    }

    private LinearGradientDirections _LinearGradientDirection = LinearGradientDirections.Vertical;
    [DefaultValue(LinearGradientDirections.Vertical)]
    public LinearGradientDirections LinearGradientDirection
    {
    get { return _LinearGradientDirection; }
    set
    {
    _LinearGradientDirection = value;
    setDrawing();
    Refresh();
    }
    }

    private Color _BackColor = Color.Black;
    public override Color BackColor
    {
    get { return _BackColor; }
    set
    {
    _BackColor = value;
    setDrawing();
    Refresh();
    }
    }

    private Color _BackColor2 = Color.White;
    public Color BackColor2
    {
    get { return _BackColor2; }
    set
    {
    _BackColor2 = value;
    setDrawing();
    Refresh();
    }
    }
    protected override void OnParentChanged(EventArgs e)
    {
    if (Parent != null)
    setDrawing();
    base.OnParentChanged(e);
    }
    private Bitmap buffer = null;
    private Brush FillBrush = null;
    private Bitmap LinearGradient = null;

    private void disposeDrawing()
    {
    try
    {
    if (buffer != null) { buffer.Dispose(); buffer = null; }
    if (FillBrush != null) { FillBrush.Dispose(); FillBrush = null; }
    if (LinearGradient != null) { LinearGradient.Dispose(); LinearGradient = null; }
    }
    catch { }
    }

    protected override void OnResize(EventArgs e)
    {
    setDrawing();
    Refresh();
    }

    private static void setHorizontalGradient(int istart, int istop, Color cstart, Color cstop, Bitmap b)
    {
    int width = istop - istart;
    double RedScale = (double)width / (cstart.R - cstop.R);
    double GreenScale = (double)width / (cstart.G - cstop.G);
    double BlueScale = (double)width / (cstart.B - cstop.B);

    for (int x = 0; x < width; x++)
    {
    int red = Math.Max(Math.Min(cstop.R + (int)((double)x / RedScale), 255), 0);
    int green = Math.Max(Math.Min(cstop.G + (int)((double)x / GreenScale), 255), 0);
    int blue = Math.Max(Math.Min(cstop.B + (int)((double)x / BlueScale), 255), 0);

    Color color = Color.FromArgb(red, green, blue);
    b.SetPixel(x + istart, 0, color);
    }
    }

    private static void setVerticalGradient(int istart, int istop, Color cstart, Color cstop, Bitmap b)
    {
    int height = istop - istart;
    double RedScale = (double)height / (cstart.R - cstop.R);
    double GreenScale = (double)height / (cstart.G - cstop.G);
    double BlueScale = (double)height / (cstart.B - cstop.B);

    for (int y = 0; y < height; y++)
    {
    int red = Math.Max(Math.Min(cstop.R + (int)((double)y / RedScale), 255), 0);
    int green = Math.Max(Math.Min(cstop.G + (int)((double)y / GreenScale), 255), 0);
    int blue = Math.Max(Math.Min(cstop.B + (int)((double)y / BlueScale), 255), 0);

    Color color = Color.FromArgb(red, green, blue);
    b.SetPixel(0, y + istart, color);
    }
    }

    int widthSet, heightSet;
    Color colorSet, color2Set;
    LinearGradientDirections gradDirSet;
    RegionFillStyle gradTypeSet;

    public int topPart
    {
    get
    {
    double h = Height;
    double r = h / 5.0d;
    r = r * 2.0d;
    return (int)r;
    }
    }
    public int leftPart
    {
    get
    {
    double h = Width;
    double r = h / 5.0d;
    r = r * 2.0d;
    return (int)r;
    }
    }

    private void setDrawing()
    {
    if (Parent == null)
    return;
    bool willset = false;
    if (widthSet != Width)
    willset = true;
    if (heightSet != Height)
    willset = true;
    if (color2Set != BackColor2)
    willset = true;
    if (gradDirSet != LinearGradientDirection)
    willset = true;
    if (gradTypeSet != FillStyle)
    willset = true;
    if (!willset) return;
    widthSet = Width;
    heightSet = Height;
    colorSet = BackColor;
    color2Set = BackColor2;
    gradDirSet = LinearGradientDirection;
    gradTypeSet = FillStyle;
    disposeDrawing();
    try
    {
    if (FillStyle == RegionFillStyle.SolidColor)
    {
    FillBrush = new SolidBrush(BackColor);
    }
    else if (FillStyle == RegionFillStyle.LinearDoubleGradient)
    {
    if (LinearGradientDirection == LinearGradientDirections.Horizontal)
    {
    LinearGradient = new Bitmap(Width, 1);
    setHorizontalGradient(0, leftPart, BackColor, BackColor2, LinearGradient);
    setHorizontalGradient(leftPart, Width, BackColor2, BackColor, LinearGradient);
    }
    else if (LinearGradientDirection == LinearGradientDirections.Vertical)
    {
    LinearGradient = new Bitmap(1, Height);
    setVerticalGradient(0, topPart, BackColor, BackColor2, LinearGradient);
    setVerticalGradient(topPart, Height, BackColor2, BackColor, LinearGradient);
    }
    }
    else if (FillStyle == RegionFillStyle.LinearGradient)
    {
    if (LinearGradientDirection == LinearGradientDirections.Horizontal)
    {
    LinearGradient = new Bitmap(Width, 1);
    setHorizontalGradient(0, Width, BackColor, BackColor2, LinearGradient);
    }
    else if (LinearGradientDirection == LinearGradientDirections.Vertical)
    {
    LinearGradient = new Bitmap(1, Height);
    setVerticalGradient(0, Height, BackColor, BackColor2, LinearGradient);
    }
    }
    if (LinearGradient != null)
    FillBrush = new TextureBrush(LinearGradient);

    buffer = new Bitmap(Width, Height);

    using (Graphics g = Graphics.FromImage(buffer))
    {
    g.FillRectangle(FillBrush, ClientRectangle);
    }
    }
    catch
    {
    }
    }

    protected override void OnPaintBackground(PaintEventArgs e) { }

    protected override void OnPaint(PaintEventArgs e)
    {
    try { e.Graphics.DrawImage(buffer, 0, 0); }
    catch { }

    }

    }
    }

  7. Nathan Stiles said

    oh and you need to add this in setDrawing

    if (colorSet != BackColor)
    widthSet = true;

  8. shilpa said

    I was using PInvoke Gradient fill native method for giving gradient effect which is a costly process in CF. Now i replaced that with your code. Thanks i worked for me!!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
%d bloggers like this: