In the past few posts, I have been exploring owner-drawing of vector images to create a more compelling user interface that recreates, to a high degree of fidelity, the intentions of the designers. However, as engineers, we are implementing the entirety of the user experience, and not simply the appearance. In addition to faithfully representing the visuals, our code must deliver on other promises for a compelling user experience. One of these components is performance.

As I mentioned in my first post on creating Gel Buttons, it is faster to move memory than it is to perform computations, which is why you can render a raster image (bitmap) faster than you can render a vector image (such as the GDI+ code that we created). In the Gel Buttons example, we were including animation, which made raster images a less tantalizing choice. However, what if we don't need animations, but we would like the scalability characteristics of vector images? We can pre-rasterize our vectors once we know the size that we are rasterizing to, and all subsequent calls to render our control can leverage the speed of rendering bitmaps (moving memory).

As with many performance-related engineering decisions, here we have a trade-off between size and performance. By consuming more memory to render in advance, we can improve runtime performance. To explore this, let's use a navigation control as an example. I have created a custom control to present a navigation UI, a common idiom in applications that users are probably most familiar with from Web applications. (This idiom receives first class support in Windows Presentation Foundation, with a NavigationWindow providing this navigation UI out of the box.)

WinForms Navigation Control

(Once again, I am not presenting this particular design as a model of good design, but I am instead interested in the engineering techniques that we can use to represent such a button.)

What I have created includes several layers of painting. There is a linear gradient background behind both buttons. Each button has a radial gradient, and there are several alpha-blended gradients on top of this button, designed both to (hopefully) make it look kind of cool and glassy.

For this example, unlike our gel buttons, I have implemented 4 distinct states: normal, mouse over, mouse down, and disabled. (For the time being, I am not addressing accessibility issues.) Because I do not have animations, there are only 16 possible states for the entire control (4 states for the left button times 4 states for the right button). Let's take a look at the rendering code for this button:

private void DrawBackground(Graphics g) {
  using (GraphicsPath p = new GraphicsPath()) {
    p.FillMode = FillMode.Winding;
    p.AddEllipse(0, 0, Width / 2, Height - 1);
    p.AddEllipse(Width / 2, 0, Width / 2 - 1, Height - 1);
    p.AddRectangle(new Rectangle(this.Width / 4, this.Height / 4,
      this.Width / 2, this.Height / 2));
    using (LinearGradientBrush b = new LinearGradientBrush(ClientRectangle,
      backgroundTop, backgroundBottom, LinearGradientMode.Vertical)) {
      g.FillPath(b, p);
    }
  }
}

private void DrawButton(Graphics g, Rectangle boundingRectangle, 
  ArrowDirection direction, ButtonState state) {

  // Draw the button
  using (GraphicsPath buttonPath = new GraphicsPath()) {
    buttonPath.AddEllipse(boundingRectangle);
    using (PathGradientBrush buttonBrush = new PathGradientBrush(buttonPath)) {
      switch (state) {
        case ButtonState.Normal:
          buttonBrush.CenterColor = buttonInnerColorNormal;
          buttonBrush.SurroundColors = new Color[] { buttonOuterColorNormal };
          break;
        case ButtonState.MouseOver:
          buttonBrush.CenterColor = buttonInnerColorMouseOver;
          buttonBrush.SurroundColors = new Color[] { buttonOuterColorMouseOver };
          break;
        case ButtonState.MouseDown:
          buttonBrush.CenterColor = buttonInnerColorMouseDown;
          buttonBrush.SurroundColors = new Color[] { buttonOuterColorMouseDown };
          break;
        case ButtonState.Disabled:
          buttonBrush.CenterColor = buttonInnerColorDisabled;
          buttonBrush.SurroundColors = new Color[] { buttonOuterColorDisabled };
          break;
      }
      g.FillPath(buttonBrush, buttonPath);
    }
  }

  // Draw the halo
  using (GraphicsPath haloPath = new GraphicsPath()) {
    haloPath.AddEllipse(boundingRectangle);
    haloRectangle.Offset(boundingRectangle.Location);
    haloPath.AddEllipse(haloRectangle);
    haloRectangle.Offset(-boundingRectangle.X, -boundingRectangle.Y);
    using (PathGradientBrush haloBrush = new PathGradientBrush(haloPath)) {
      haloBrush.CenterColor = Color.FromArgb(200, Color.White);
      haloBrush.SurroundColors = new Color[] { Color.FromArgb(0, Color.White) };
      g.FillPath(haloBrush, haloPath);
    }
  }

  if (ButtonState.MouseDown != state) {

    // Draw the moon
    using (GraphicsPath moonBottomPath = new GraphicsPath()) {
      moonRectangle.Offset(boundingRectangle.Location);
      moonBottomPath.AddEllipse(moonRectangle);
      moonRectangle.Offset(-boundingRectangle.X, -boundingRectangle.Y);
      using (Region moonRegion = new Region(moonBottomPath)) {
        using (GraphicsPath moonTopPath = new GraphicsPath()) {
          moonTopPath.AddEllipse(boundingRectangle);
          moonRegion.Complement(moonTopPath);
          using (SolidBrush moonBrush = new SolidBrush(
            Color.FromArgb(30, Color.White))) {
            g.FillRegion(moonBrush, moonRegion);
          }
        }
      }
    }

      // Draw the top reflection
    using (GraphicsPath topReflectionPath = new GraphicsPath()) {
      topReflectionRectangle.Offset(boundingRectangle.Location);
      topReflectionPath.AddEllipse(topReflectionRectangle);
      topReflectionRectangle.Offset(-boundingRectangle.X, -boundingRectangle.Y);
      using (LinearGradientBrush topReflectionBrush = new
        LinearGradientBrush(topReflectionPath.GetBounds(),
        Color.FromArgb(200, Color.White), Color.FromArgb(0, Color.White),
        LinearGradientMode.Vertical)) {
        g.FillPath(topReflectionBrush, topReflectionPath);
      }
    }

    // Draw the bottom reflection
    using (GraphicsPath bottomReflectionPath = new GraphicsPath()) {
      bottomReflectionRectangle.Offset(boundingRectangle.Location);
      bottomReflectionPath.AddEllipse(bottomReflectionRectangle);
      bottomReflectionRectangle.Offset(-boundingRectangle.X, -boundingRectangle.Y);
      using (LinearGradientBrush bottomReflectionBrush = new
        LinearGradientBrush(bottomReflectionPath.GetBounds(),
        Color.FromArgb(0, Color.White), Color.FromArgb(50, Color.White),
        LinearGradientMode.Vertical)) {
        g.FillPath(bottomReflectionBrush, bottomReflectionPath);
      }
    }

  }

  // Draw the arrow
  arrowTop.Offset(boundingRectangle.Location);
  arrowBottom.Offset(boundingRectangle.Location);
  arrowLeft.Offset(boundingRectangle.Location);
  arrowRight.Offset(boundingRectangle.Location);
  Point[] arrowHeadPoints;
  if (ArrowDirection.Left == direction) {
    arrowHeadPoints = new Point[] { arrowTop , arrowLeft, arrowBottom };
  } else {
    arrowHeadPoints = new Point[] { arrowTop, arrowRight, arrowBottom };
  }
  Point[] arrowLinePoints = new Point[] { arrowLeft, arrowRight };
  Color arrowColor = ButtonState.MouseDown == state ? Color.Gray : Color.White;
  using (SolidBrush arrowBrush = new SolidBrush(arrowColor)) {
    using (Pen arrowPen = new Pen(arrowBrush, Height / 10)) {
      arrowPen.LineJoin = LineJoin.Round;
      arrowPen.StartCap = LineCap.Round;
      arrowPen.EndCap = LineCap.Round;
      g.DrawLines(arrowPen, arrowHeadPoints);
      g.DrawLines(arrowPen, arrowLinePoints);
    }
  }
  arrowTop.Offset(-boundingRectangle.X, -boundingRectangle.Y);
  arrowBottom.Offset(-boundingRectangle.X, -boundingRectangle.Y);
  arrowLeft.Offset(-boundingRectangle.X, -boundingRectangle.Y);
  arrowRight.Offset(-boundingRectangle.X, -boundingRectangle.Y);

}

To improve performance somewhat, I have tried to factor out a great deal of the mathematical computations used to describe the vectors we are drawing, although there is still more work that could be done to leverage this particular performance optimization technique. (I opted for simplicity of a single method to render a button, which means that I require some additional offsets.) However, the point is that I only need to perform these mathematical calculations if the control is resized - the results will be the same with every subsequent call to the OnPaint method.

Following the technique I used previously, we can simply call these drawing methods when our OnPaint method is called, like so:

protected override void OnPaint(PaintEventArgs e) {
  Graphics g = e.Graphics;
  g.SmoothingMode = SmoothingMode.HighQuality;
  g.PixelOffsetMode = PixelOffsetMode.HighQuality;
  ButtonRenderer.DrawParentBackground(g, ClientRectangle, this);
  DrawBackground(g);
  DrawButton(g, leftButtonRectangle, ArrowDirection.Left, leftButtonState);
  DrawButton(g, rightButtonRectangle, ArrowDirection.Right, rightButtonState);
}

Now, if I instrument this code, I can manipulate the control and get a sense of how well it is performing as currently implemented. On my Tablet PC, the average time spent in OnPaint while using the real-time rasterization is 8.18ms. Since the rods in your eye have a period of integration of 100ms, and the cones have a period of integration of 10 to 15ms, this does not seem unreasonable. However, we would be wise to account for the fact that we are very unlikely to have a form where the only object we needed to paint was a navigation control - our control should provide some additional headroom to render the rest of the objects on the page, Furthermore, we may be implementing something more complex than a gel button with 11 distinct objects in order to faithfully represent the designers' intentions. Finally, keep in mind that those 8 ms represent time that your application isn't doing any meaningful business processing.

Knowing that there are only 16 distinct states which we will ever need to render, we can easily rasterize these states in advance, and use the much faster approach of moving memory to render these images. Here is some code to render these states in advance (which we can call when we create the control, and also in response to a resize event):

private void CreateImages() {
  DisposeImages();
  for (int row = 0; row < 4; row++) {
    for (int col = 0; col < 4; col++) {
      buttonImages[row, col] = new Bitmap(Width, Height, 
        System.Drawing.Imaging.PixelFormat.Format32bppArgb);
      using (Graphics g = Graphics.FromImage(buttonImages[row, col])) {
        g.SmoothingMode = SmoothingMode.HighQuality;
        g.PixelOffsetMode = PixelOffsetMode.HighQuality;
        ButtonRenderer.DrawParentBackground(g, ClientRectangle, this);
        DrawBackground(g);
        DrawButton(g, leftButtonRectangle, ArrowDirection.Left, (ButtonState)row);
        DrawButton(g, rightButtonRectangle, ArrowDirection.Right, (ButtonState)col);
      }
    }
  }
}

Now that we have the images pre-rendered, the OnPaint method can be used to simply paint our pre-rendered images:

protected override void OnPaint(PaintEventArgs e) {
  Graphics g = e.Graphics;
  g.DrawImage(buttonImages[(int)leftButtonState, (int)rightButtonState], ClientRectangle);
}

Again, if we instrument this code, we can determine how well this method performs. On my Tablet PC, the average time spent in the OnPaint method was reduced to 0.43ms. We were able to improve the performance of our rendering code by an order of magnitude! (An 1,802% increase - not bad.)

Of course, this method is not without its shortcomings. Every time that you create or resize the control, you have to wait for all 16 permutations to render before you can show anything with this naive implementation. A better approach might be to render the image for a state only when that state exists. You also have to set aside enough memory to hold all of these rendered images, which you did not have to do while rendering real-time. However, if you are implementing custom drawing and looking to make a significant increase in run-time performance, this is a technique worth considering.