Monday, May 17, 2010

Themed Image Button in .NETCF

In this article I'd like to demonstrate how to create a themed image button control. I'll be using the same techniques in my previous article, How to draw a textured rounded rectangle and Semi-Transparent Controls in .NETCF.

First, we need to define our theme. We get the start and end gradient colors from alpha blending the SystemColors.Highlight color and Color.White

class Theme
{
    public static Color AlphaBlend(Color value1, Color value2, int alpha)
    {
        int ialpha = 256 - alpha;
        return Color.FromArgb((value1.R * alpha) + (value2.R * ialpha) >> 8,
                              (value1.G * alpha) + (value2.G * ialpha) >> 8,
                              (value1.B * alpha) + (value2.B * ialpha) >> 8);
    }
 
    public static Color GradientLight
    {
        get
        {
            var color = AlphaBlend(SystemColors.Highlight, Color.White, 100);
            return AlphaBlend(Color.White, color, 50); ;
        }
    }
 
    public static Color GradientDark
    {
        get
        {
            var color = AlphaBlend(SystemColors.Highlight, Color.Black, 256);
            return AlphaBlend(Color.White, color, 50); ;
        }
    }
}


We'll be using the GradientFill method as well in this example. For this we need to define 2 structures, TRIVERTEX and GRADIENT_RECT

struct TRIVERTEX
{
    private int x;
    private int y;
    private ushort Red;
    private ushort Green;
    private ushort Blue;
    private ushort Alpha;
 
    public TRIVERTEX(int x, int y, Color color)
        : this(x, y, color.R, color.G, color.B, color.A)
    {
    }
 
    public TRIVERTEX(
        int x, int y,
        ushort red, ushort green, ushort blue,
        ushort alpha)
    {
        this.x = x;
        this.y = y;
        Red = (ushort)(red << 8);
        Green = (ushort)(green << 8);
        Blue = (ushort)(blue << 8);
        Alpha = (ushort)(alpha << 8);
    }
}
 
struct GRADIENT_RECT
{
    private uint UpperLeft;
    private uint LowerRight;
 
    public GRADIENT_RECT(uint ul, uint lr)
    {
        UpperLeft = ul;
        LowerRight = lr;
    }
}


Using the 2 structures above we can now define our P/Invoke for GradientFill

[DllImport("coredll.dll")]
static extern bool GradientFill(
    IntPtr hdc, 
    TRIVERTEX[] pVertex, 
    int dwNumVertex, 
    GRADIENT_RECT[] pMesh, 
    int dwNumMesh, 
    int dwMode);


Let's wrap the P/Invoke call to GradientFill in a nice method

const int GRADIENT_FILL_RECT_V = 0x00000001;
 
public static void GradientFill(
    this Graphics graphics,
    Rectangle rect,
    Color startColor,
    Color endColor)
{
    var tva = new TRIVERTEX[2];
    tva[0] = new TRIVERTEX(rect.X, rect.Y, startColor);
    tva[1] = new TRIVERTEX(rect.Right, rect.Bottom, endColor);
    var gra = new GRADIENT_RECT[] { new GRADIENT_RECT(0, 1) };
 
    var hdc = graphics.GetHdc();
    GradientFill(hdc, tva, tva.Length, gra, gra.Length, GRADIENT_FILL_RECT_V);
    graphics.ReleaseHdc(hdc);
}


In order to use the function we need to create a few GDI objects: a Pen to draw the border, and a Brush to fill the rectangle. We will mostly use P/Invoke for creating and releasing GDI objects

const int PS_SOLID = 0;
const int PS_DASH = 1;
 
[DllImport("coredll.dll")]
static extern IntPtr CreatePen(int fnPenStyle, int nWidth, uint crColor);
 
[DllImport("coredll.dll")]
static extern int SetBrushOrgEx(IntPtr hdc, int nXOrg, int nYOrg, ref Point lppt);
 
[DllImport("coredll.dll")]
static extern IntPtr CreateSolidBrush(uint color);
 
[DllImport("coredll.dll")]
static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobject);
 
[DllImport("coredll.dll")]
static extern bool DeleteObject(IntPtr hgdiobject);
 
[DllImport("coredll.dll")]
static extern IntPtr CreatePatternBrush(IntPtr HBITMAP);
 
[DllImport("coredll.dll")]
static extern bool RoundRect(
    IntPtr hdc, 
    int nLeftRect, 
    int nTopRect, 
    int nRightRect, 
    int nBottomRect, 
    int nWidth, 
    int nHeight);


We'll draw a textured rounded rectangle with a gradient fill that uses the current theme's Highlight color as the base color.

public static void DrawThemedGradientRectangle(
    this Graphics graphics,
    Pen border,
    Rectangle area,
    Size ellipseSize)
{
    using (var texture = new Bitmap(area.Right, area.Bottom))
    {
        using (var g = Graphics.FromImage(texture))
            GradientFill(g, area, Theme.GradientLight, Theme.GradientDark);
 
        FillRoundedTexturedRectangle(graphics, border, texture, area, ellipseSize);
    }
}
 
static IntPtr CreateGdiPen(Pen pen)
{
    var style = pen.DashStyle == DashStyle.Solid ? PS_SOLID : PS_DASH;
    return CreatePen(style, (int)pen.Width, GetColorRef(pen.Color));
}
 
public static void FillRoundedTexturedRectangle(
    this Graphics graphics,
    Pen border,
    Bitmap texture,
    Rectangle rect,
    Size ellipseSize)
{
    Point old = new Point();
 
    var hdc = graphics.GetHdc();
    var hpen = CreateGdiPen(border);
    var hbitmap = texture.GetHbitmap();
    var hbrush = CreatePatternBrush(hbitmap);
 
    SetBrushOrgEx(hdc, rect.Left, rect.Top, ref old);
    SelectObject(hdc, hpen);
    SelectObject(hdc, hbrush);
 
    RoundRect(hdc, rect.Left, rect.Top, rect.Right, rect.Bottom, ellipseSize.Width, ellipseSize.Height);
 
    SetBrushOrgEx(hdc, old.Y, old.X, ref old);
    DeleteObject(hpen);
    DeleteObject(hbrush);
 
    graphics.ReleaseHdc(hdc);
}


Let's wrap all the code above as extension methods to the Graphics class and use them in our owner drawn button control. A button control is one of the easiest owner drawn controls to create. Let's keep it as simple as possible and only have 2 states for our button: pressed and not pressed.

class ThemedImageButton : Control
{
    bool pushed = false;
    private Bitmap image;
    private Bitmap offScreen;
 
    public Bitmap Image
    {
        get { return image; }
        set
        {
            image = value;
            Invalidate();
        }
    }
 
    protected override void OnResize(EventArgs e)
    {
        base.OnResize(e);
 
        if (offScreen != null)
        {
            offScreen.Dispose();
            offScreen = null;
        }
        offScreen = new Bitmap(ClientSize.Width, ClientSize.Height);
    }
 
    protected override void OnPaint(PaintEventArgs e)
    {
        if (offScreen == null)
            offScreen = new Bitmap(ClientSize.Width, ClientSize.Height);
 
        using (var attributes = new ImageAttributes())
        using (var g = Graphics.FromImage(offScreen))
        {
            if (pushed)
            {
                using (var pen = new Pen(SystemColors.Highlight))
                    g.DrawThemedGradientRectangle(pen, ClientRectangle, new Size(4, 4));
            }
            else
                g.Clear(Parent.BackColor);
 
            var textSize = g.MeasureString(Text, Font);
            var textArea = new RectangleF(
                (ClientSize.Width - textSize.Width) / 2,
                (ClientSize.Height - textSize.Height),
                textSize.Width,
                textSize.Height);
 
            if (Image != null)
            {
                var imageArea = new Rectangle(
                    (ClientSize.Width - Image.Width) / 2,
                    (ClientSize.Height - Image.Height) / 2,
                    Image.Width,
                    Image.Height);
 
                var key = Image.GetPixel(0, 0);
                attributes.SetColorKey(key, key);
 
                g.DrawImage(
                    Image,
                    imageArea,
                    0, 0, Image.Width, Image.Height,
                    GraphicsUnit.Pixel,
                    attributes);
            }
 
            using (var brush = new SolidBrush(ForeColor))
                g.DrawString(Text, Font, brush, textArea);
 
            if (pushed)
            {
                var key = offScreen.GetPixel(0, 0);
                attributes.SetColorKey(key, key);
            }
            else
                attributes.ClearColorKey();
 
            e.Graphics.DrawImage(
                offScreen,
                ClientRectangle,
                0, 0, offScreen.Width, offScreen.Height,
                GraphicsUnit.Pixel,
                attributes);
        }
    }
 
    protected override void OnPaintBackground(PaintEventArgs e)
    {
    }
 
    protected override void OnMouseDown(MouseEventArgs e)
    {
        base.OnMouseDown(e);
        pushed = true;
        Invalidate();
    }
 
    protected override void OnMouseUp(MouseEventArgs e)
    {
        base.OnMouseUp(e);
        pushed = false;
        Invalidate();
    }
 
    protected override void OnParentChanged(EventArgs e)
    {
        base.OnParentChanged(e);
        Invalidate();
    }
 
    protected override void OnTextChanged(EventArgs e)
    {
        base.OnTextChanged(e);
        Invalidate();
    }
}


I hope you found this useful. If you're interested in the full source code then you can grab it here

4 comments:

Anonymous said...

Looks great, would be nice to have an image of what the button looks like (in up/down mode) as part of the article. Thanks for making the code available!

Christian Resma Helle said...

I'll update the article and add a screenshot when I find the time

Leandro Passarelli said...

I'm trying to create a custom listbox and I would just modify the SystemColor.Highlight to a gradient.
How to get the properties of the rectangle of the item selected to replace it with a gradient rectangle?

Thanks

Christian Resma Helle said...

Hi Leandro,

I have an old article on ListView custom drawing here:
http://christian-helle.blogspot.com/2009/10/listview-custom-drawing-in-netcf.html.

I'm not entirely sure if ListBox supports custom drawing, but if it does then the same techniques described in the link above should apply.

Otherwise, creating an owner drawn control might be an easier solution. There should be a few articles on the internet on Owner Drawn Controls and Lists in .NET Compact Framework