In our last episode, we went through the process of creating the static rendering of a gel button using Windows Forms. (The code I provided was developed using the .NET Framework 2.0, although at least one person has pointed out that it doesn't take much work to port this code to the 1.x version of the framework.) At the end of this, I mentioned that we could have achieved the same effect without having to write any code at all using a static image. I want to clarify this statement, as there could potentially be a difference.

What we have created is a vector drawing, that will smoothly scale to any size that you want to render a button. If you choose to implement this as a static image, not all image representations are vector representations. In fact, most of the most familiar image types are raster image types, such as bmp, jpg, gif, or png. If you needed to resize the image, you would be forced to resort to raster techniques, such as bicubic, bilinear, or nearest neighbor interpolation, in order to stretch the image to the target size - this pretty much always makes it look worse than the original image. In addition, your application would need to allocate space to house this raster image as a resource, increasing the size of your binary.

Not all image types are raster images, however. Windows Metafiles (wmf) and Enhanced Windows Metafiles (emf) are both vector image types. We could potentially have implemented this static image as an emf. In fact, if we had, our application would have virtually the same characteristics. You can think of emf files as being, at some level, a markup language for GDI+ drawing such as what we are doing here. If you enumerate through a metafile, you will see a series of records such as EMREXTCREATEPEN, EMRGRADIENTFILL, and EMRFILLRGN - each of these records maps directly to GDI+ commands, in exactly the same way that our System.Drawing code maps to GDI+ commands. So, from a general standpoint, it is possible to represent a vector image as a metafile rather than in code as we have done here and achieve fundamentally equivalent results in terms of both file size and drawing performance.

Of course, when I refer to metafiles as being like a markup language, there are some key differences between it and a true markup language, such as how you may use the XAML markup language to render an image in the Windows Presentation Foundation. First, metafiles are procedural - they execute from top to bottom, and do not include a notion of nesting other than by the order of commands executed. Second, they are not presented in a human readable form. They are specifically encoded to a binary format.

Some of you may be considering putting together a metafile generator to capture some of the great drawings that you may have implemented in code. A single file certainly is much easier to distribute than a block of code - and this is convenient packaging. This is not as straightforward as you might hope, as the .NET Framework documentation tells us.

When you use the Save method to save a graphic image as a Windows Metafile Format (WMF) or Enhanced Metafile Format (EMF) file, the resulting file is saved as a Portable Network Graphics (PNG) file instead. This behavior occurs because the component of the does not have an encoder that you can use to save files as .wmf or .emf files.

Of course, that does not mean that we can't do it, but the fact that it is not obvious and ubiquitous probably explains why more software doesn't provide metafile output today. (For example, you can create a vector drawing in using the January 2006 CTP of Microsoft Expression Graphic Designer, but you can't save your drawing as a metafile.) We may come back to this in a later entry.

Of course, now that we have talked around our static vector image for a while, I think it's about time that we brought our button to life. Static images of buttons are only good for screen shots, and are not that interesting in the real world! As we said before, we would like our button to be suitably pliant, as well as taking advantage of the smart client platform. This is the result we are aiming for:

Windows Forms Gel Buttons : Second Revision

First, let's consider how we want to convey our pliancy, and how we could implement that in code. What I have chosen to do here is to manipulate the highlight, rather than manipulating any of the colors on the button. This makes it easy to support any background color that my consumer may use. Regardless of whether the button is the default blue I am using, or if it has been manipulated to be red, green, or any other color, I am always using a white highlight.

What do we want to do with that highlight? When you mouse over the button, a common approach is to make it lighter. This is fairly straightforward, as we merely need to increase the alpha channel for the bottom portion of the highlight from 0 to a higher value, allowing more of the white to shine through. However, since we have complete control of the graphics platform (a major advantage of the smart client), why limit ourselves to merely rendering a second, slightly brighter, static image? We can instead use animation to make our button pulsate, conveying the sense of pliancy through more than a single image change.

Animation will become easier with the Windows Presentation Foundation, but we can achieve the same effect today. What is animation but a series of static images that change smoothly over time? This is how movies and TV work. But how do we add a third time dimension to our code? Timers are one way to do that, and this is what I have chosen to do here.

Note that there are several options to choose from for a Timer object, with instances to choose from in the System.Threading, System.Timers, and System.Windows.Forms namespaces. I won't go into the details of when to choose which timer here (there is an introduction in MSDN Magazine for those who are interested), but I have chosen the timer object in the System.Windows.Forms namespace. If the primary UI thread is busy doing something else (handling the remainder of the windows messages coming in to the application), then losing a beat in my animation is not going to be my primary concern. Since I am doing UI work, I would need to invoke onto the primary UI thread anyway. Of course, if you unnecessarily block the primary UI thread with synchronous IO or network calls, then I will have yet another visual representation of this design deficiency.

Using this timer, I can gradually pulsate my button - adjusting the alpha of the bottom of the highlight. You can modify the properties of the timer to either speed this up or slow it down according to your tastes.

I used a second timer to return to normal, so that moving away from a button smoothly animates back to normal, rather than instantly and jarringly returning to its normal rendering state. Once I have reached that state, then the timer tick event can turn the timer off itself. Fairly straightforward.

Finally, we handle the pressed state. If I have a light coming from above in a button sticking up, then it serves to reason that this light would strike the bottom portion of that button when it is depressed, although that light would be somewhat less strong. So, here I merely moved the highlight to the bottom of the screen, reversed the order of the alpha manipulation, and modified the final alpha values to be less bright overall.

That being said, let's take a look at the code:

namespace GelButtons {

  using System;
  using System.ComponentModel;
  using System.Drawing;
  using System.Drawing.Drawing2D;
  using System.Windows.Forms;

  class GelButton : Button {

    private Color gradientTop = Color.FromArgb(255, 44, 85, 177);
    private Color gradientBottom = Color.FromArgb(255, 153, 198, 241);
    private Rectangle buttonRect;
    private int highlightAlphaTop = 255;
    private int highlightAlphaBottom;
    private Rectangle highlightRect;
    private Timer animateMouseOverTimer = new Timer();
    private Timer animateResumeNormalTimer = new Timer();
    private bool increasingAlpha;

    [Category("Appearance"), 
Description("The color to use for the top portion of the gradient fill of the component.")]
    public Color GradientTop {
      get {
        return gradientTop;
      }
      set {
        gradientTop = value;
        Invalidate();
      }
    }

    [Category("Appearance"),
Description("The color to use for the bottom portion of the gradient fill of the component.")]
    public Color GradientBottom {
      get {
        return gradientBottom;
      }
      set {
        gradientBottom = value;
        Invalidate();
      }
    }

    protected override void OnCreateControl() {
      SuspendLayout();
      base.OnCreateControl();
      buttonRect = new Rectangle(ClientRectangle.X, ClientRectangle.Y, 
ClientRectangle.Width - 1, ClientRectangle.Height - 1);
      highlightRect = new Rectangle(ClientRectangle.X, ClientRectangle.Y, 
ClientRectangle.Width - 1, ClientRectangle.Height / 2 - 1);
      animateMouseOverTimer.Interval = 20;
      animateMouseOverTimer.Tick += new EventHandler(animateMouseOverTimer_Tick);
      animateResumeNormalTimer.Interval = 5;
      animateResumeNormalTimer.Tick += new EventHandler(animateResumeNormalTimer_Tick);
      ResumeLayout();
    }

    protected override void OnPaint(PaintEventArgs pevent) {
      Graphics g = pevent.Graphics;
      // Fill the background
      ButtonRenderer.DrawParentBackground(g, ClientRectangle, this);
      // Paint the outer rounded rectangle
      g.SmoothingMode = SmoothingMode.AntiAlias;
      using (GraphicsPath outerPath = RoundedRectangle(buttonRect, 5, 0)) {
        using (LinearGradientBrush outerBrush = 
new LinearGradientBrush(buttonRect, gradientTop, gradientBottom, LinearGradientMode.Vertical)) {
          g.FillPath(outerBrush, outerPath);
        }
        using (Pen outlinePen = new Pen(gradientTop)) {
          g.DrawPath(outlinePen, outerPath);
        }
      }
      // Paint the highlight rounded rectangle
      using (GraphicsPath innerPath = RoundedRectangle(highlightRect, 5, 2)) {
        using (LinearGradientBrush innerBrush = new LinearGradientBrush(highlightRect, 
Color.FromArgb(highlightAlphaTop, Color.White), Color.FromArgb(highlightAlphaBottom, Color.White), 
LinearGradientMode.Vertical)) {
          g.FillPath(innerBrush, innerPath);
        }
      }
      // Paint the text
      TextRenderer.DrawText(g, Text, Font, buttonRect, ForeColor, Color.Transparent, 
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis);
    }

    private static GraphicsPath RoundedRectangle(Rectangle boundingRect, int cornerRadius, int margin) {
      GraphicsPath roundedRect = new GraphicsPath();
      roundedRect.AddArc(boundingRect.X + margin, boundingRect.Y + margin, 
cornerRadius * 2, cornerRadius * 2, 180, 90);
      roundedRect.AddArc(boundingRect.X + boundingRect.Width - margin - cornerRadius * 2, 
boundingRect.Y + margin, cornerRadius * 2, cornerRadius * 2, 270, 90);
      roundedRect.AddArc(boundingRect.X + boundingRect.Width - margin - cornerRadius * 2, 
boundingRect.Y + boundingRect.Height - margin - cornerRadius * 2, cornerRadius * 2, cornerRadius * 2, 0, 90);
      roundedRect.AddArc(boundingRect.X + margin, boundingRect.Y + boundingRect.Height - 
margin - cornerRadius * 2, cornerRadius * 2, cornerRadius * 2, 90, 90);
      roundedRect.CloseFigure();
      return roundedRect;
    }

    protected override void OnMouseEnter(EventArgs e) {
      animateResumeNormalTimer.Stop();
      animateMouseOverTimer.Start();
      base.OnMouseEnter(e);
    }

    protected override void OnMouseLeave(EventArgs e) {
      animateMouseOverTimer.Stop();
      animateResumeNormalTimer.Start();
      base.OnMouseLeave(e);
    }

    protected override void OnMouseDown(MouseEventArgs mevent) {
      animateMouseOverTimer.Stop();
      animateResumeNormalTimer.Stop();
      highlightRect.Location = new Point(0, ClientRectangle.Height / 2);
      highlightAlphaTop = 0;
      highlightAlphaBottom = 200;
      Invalidate();
      base.OnMouseDown(mevent);
    }

    protected override void OnMouseUp(MouseEventArgs mevent) {
      highlightRect.Location = new Point(0, 0);
      highlightAlphaTop = 255;
      highlightAlphaBottom = 0;
      if (DisplayRectangle.Contains(mevent.Location)) {
        animateMouseOverTimer.Start();
      }
      base.OnMouseUp(mevent);
    }

    protected override void OnMouseMove(MouseEventArgs mevent) {
      if ((mevent.Button & MouseButtons.Left) == MouseButtons.Left && 
!ClientRectangle.Contains(mevent.Location)) {
        OnMouseUp(mevent);
      }
      base.OnMouseMove(mevent);
    }

    private void animateMouseOverTimer_Tick(object sender, EventArgs e) {
      if (increasingAlpha) {
        if (100 <= highlightAlphaBottom) {
          increasingAlpha = false;
        } else {
          highlightAlphaBottom += 5;
        }
      } else {
        if (0 >= highlightAlphaBottom) {
          increasingAlpha = true;
        } else {
          highlightAlphaBottom -= 5;
        }
      }
      Invalidate();
    }

    private void animateResumeNormalTimer_Tick(object sender, EventArgs e) {
      bool modified = false;
      if (highlightAlphaBottom > 0) {
        highlightAlphaBottom -= 5;
        modified = true;
      }
      if (highlightAlphaTop < 255) {
        highlightAlphaTop += 5;
        modified = true;
      }
      if (!modified) {
        animateResumeNormalTimer.Stop();
      }
      Invalidate();
    }

  }
}

So, in the end we have a gel button class that looks interesting (again, depending on your tastes), and actually exhibits pliancy to the user. It looks and behaves like a button - something that you can click.

Of course, you already have that behavior in the default buttons, so this example is really only useful if you want to create a button to specifically support a design theme for your application. Otherwise, visual styles in Windows XP and above will already provide you with this functionality, and you don't need to write any code to achieve it.

Windows, however, does not provide a control for every possible scenario. While you could use the base toolset to create just about any UI, you may very well want to optimize your UI by using a UI control that isn't provided in the base toolkit, and you don't want to have to compromise to fit your needs into this toolset. That's where custom controls become really interesting, and it is where we will go next.