Click here to Skip to main content
Click here to Skip to main content

Drawing Images With Sizing Paddings

, 22 Jul 2014 CPOL
Rate this:
Please Sign up or sign in to vote.
Visual Style-like image stretching for custom skinning

Introduction

In this tutorial, I will show you how to stretch images like it is done in Windows for drawing visual styles. It is completely done in GDI+ and C#, so no interop has to be done (I also never found an interop which could do this common skinning task for me).

Background

Composition of Visual Style Elements

The visual styles in Windows (and in most other operating systems too) use bitmaps to draw their elements. Such bitmaps have of course a fixed size, but still have to be very flexible - almost every control can have any size and thus the bitmaps have to be stretched in a way that the result still looks good.

Simply stretching an image to the new size of the control doesn't look good - imagine the typical button design, which has a border and then a gradient in the middle:

This is the official Windows 7 style button. Yeah, it is pretty small actually! But how does Windows manage to draw buttons which are much bigger than this, for example, a button with a width of 75 pixels and a height of 50 pixels?

Stretching the image with the typical method, DrawImage, would also resize the border, and the output will be blurry and really, really ugly:

private void _panel_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.DrawImage(Resources.Button, _panel.ClientRectangle);
} 

Ew, that is not what we want. The borders have simply been stretched as any other part of the image. Windows can't do it like this.

Sizing Paddings

This is where so called "Sizing Paddings" come in. They are also often referred to as "Sizing Margins", but I stick to paddings. You'll see shortly why.

So, we do not want the border areas to be stretched. We have to tell Windows how large this border is and which size the inner content (the gray gradient part) has. We can divide the bitmap image into the following sections:

Things are getting smaller, eh? If you look closely, you see we now have 9 parts of the button:

  • It looks like there are 4 corner parts, which should not get stretched into any direction when drawing the resized version of the button.
  • Then there are 2 high parts on the middle left and middle right. These should only get stretched vertically, but should stay in the same width.
  • There are 2 wide parts at the top middle and bottom middle. These have to be stretched horizontally but keep their height.
  • Last but not least, there is the very middle part, which gets stretched into both directions, horizontally and vertically.

To make things clearer, here is an upscaled version with colored areas according to the bulleted list above:

To define these areas, 4 values are already enough! You can imagine them as a padding into the bitmap, where the Left value determines how wide the left parts are, the Top value how high the top parts are, and so on. In the image above, if you look close again, you'll notice the paddings here are (3, 3, 3, 3) because all parts are either 3 pixels wide or 3 pixels high (except the middle part of course which gets stretched into all directions).

That's also the reason why I don't call them sizing margins (as in WindowBlinds and its SkinStudio for example) because these are areas inside the bitmap, not outside of it.

Using the Code

Writing a method to draw the image according to those paddings can be tedious. GDI+ eventually needs rectangles of the source and destination parts (calculating these can be really nasty), and much code will be duplicated at first if the method is not optimized enough. Also, drawing GUI elements should be fast, so we need a light-weight method.

That's why I want to share the method I came up with after some time of debugging and testing. Since it is an extension method for the Graphics object, you can call it like you would call DrawImage(). It also supports a clipped source, more about that after this. Here is the complete class:

public static class GraphicsExtensions
{
    /// <summary>
    /// Draws the image into the specified destination rectangle with the specified sizing
    /// padding for stretched drawing.
    /// </summary>
    /// <param name="gr">The extended Graphics object.</param>
    /// <param name="image">The image which will be drawn.</param>
    /// <param name="destination">
    /// The destination rectangle in which the image will be drawn.</param>
    /// <param name="padding">
    /// The padding specifying the image parts which won't be stretched.</param>
    public static void DrawImageWithPadding(this Graphics gr, Image image,
        Rectangle destination, Padding padding)
    {
        if (image == null)
        {
            throw new ArgumentNullException("image");
        }

        DrawImageWithPadding(gr, image, destination, new Rectangle(Point.Empty, image.Size),
            padding);
    }

    /// <summary>
    /// Draws the part of the image defined by the source rectangle into the specified
    /// destination rectangle with the specified sizing padding for stretched drawing.
    /// </summary>
    /// <param name="gr">The extended Graphics object.</param>
    /// <param name="image">The image which will be drawn.</param>
    /// <param name="destination">
    /// The destination rectangle in which the image will be drawn.</param>
    /// <param name="source">
    /// The source rectangle in the image used for clipping parts of it.</param>
    /// <param name="padding">
    /// The padding specifying the image parts which won't be stretched.</param>
    public static void DrawImageWithPadding(this Graphics gr, Image image,
        Rectangle destination, Rectangle source, Padding padding)
    {
        if (gr == null)
        {
            throw new ArgumentNullException("gr");
        }
        if (image == null)
        {
            throw new ArgumentNullException("image");
        }

        Rectangle[] destinations = GetSizingRectangles(destination, padding);
        Rectangle[] sources = GetSizingRectangles(source, padding);

        for (int i = 0; i < 9; i++)
        {
            gr.DrawImage(image, destinations[i], sources[i], GraphicsUnit.Pixel);
        }
    }

    private static Rectangle[] GetSizingRectangles(Rectangle rectangle, Padding padding)
    {
        int leftV   = rectangle.X + padding.Left;
        int rightV  = rectangle.X + rectangle.Width - padding.Right;
        int topH    = rectangle.Y + padding.Top;
        int bottomH = rectangle.Y + rectangle.Height - padding.Bottom;
        int innerW  = rectangle.Width - padding.Horizontal;
        int innerH  = rectangle.Height - padding.Vertical;

        // Set parts in descending order to draw upper left tiles over bottom right ones
        Rectangle[] rectangles = new Rectangle[9];
        rectangles[8] = new Rectangle(rectangle.X, rectangle.Y, padding.Left, padding.Top);
        rectangles[7] = new Rectangle(leftV, rectangle.Y, innerW, padding.Top);
        rectangles[6] = new Rectangle(rightV, rectangle.Y, padding.Right, padding.Top);
        rectangles[5] = new Rectangle(rectangle.X, topH, padding.Left, innerH);
        rectangles[4] = new Rectangle(leftV, topH, innerW, innerH);
        rectangles[3] = new Rectangle(rightV, topH, padding.Right, innerH);
        rectangles[2] = new Rectangle(rectangle.X, bottomH, padding.Left, padding.Bottom);
        rectangles[1] = new Rectangle(leftV, bottomH, innerW, padding.Bottom);
        rectangles[0] = new Rectangle(rightV, bottomH, padding.Right, padding.Bottom);
        return rectangles;
    }
}

The most important method for you is DrawImageWithPadding. The simpler overload of it requires the image to be drawn, the destination rectangle in which the image will be drawn (correctly scaled!) and - of course - the sizing paddings about which we just spoken.

The results are pretty... pretty:

That's exactly how we want it1!

Clipped Areas and Different Button States

There's a slightly more complex version of DrawImageWithPadding, which actually does the real job. It has an additional source parameter.

The idea behind it is that most visual style bitmaps have more than one button state inside it. The Windows 7 button bitmap actually looks like this2:

You can recognize the following states: Normal, Hot (Hover), Pressed, Disabled, Focused, and Focused Hot.

You cannot use this bitmap with the simple overload of DrawImageWithPadding, but you can with the more complex one - it has the required parameter to extract only one of those states! For that, you need to define the area the state is in. In this example, the hot state would be the rectangle (0, 21, 11, 21):

 private void _panel_Paint(object sender, PaintEventArgs e)
 {
     e.Graphics.DrawImageWithPadding(Resources.Button, _panel.ClientRectangle,
         new Rectangle(0, 21, 11, 21), new Padding(3, 3, 3, 3));
 } 

Inside GetSizingRectangles

The method GetSizingRectangles seems to do all the calculations needed to retrieve the rectangles GDI+ requires to draw the image. And you are right!

First, it calculates the position of the black lines in the colored image above, since these are needed more than once in the following rectangles. It also retrieves the width and height of the inner part which gets stretched into both directions.

Then, all 9 areas are calculated. They are assigned backwards to the rectangle array. What seems to be weird actually has a sense: If the real control gets smaller than the actual bitmap used, parts which are more at the top left get drawn above the parts near the bottom right. Even though the bottom right borders start to disappear, this looks much better than the top left borders disappearing. And to get this result, the rectangles are assigned backwards to the array, so the bottom right part gets drawn first (of course, you could also invert the for-loop in DrawImageWithPadding, but I hate backwards-loops and also think they are a little slower?).

Points of Interest

1: People with great eyes will notice that the inner gray gradient is a bit blurry. Windows does not use any interpolation or bilinear filtering on the output image which keeps the sharp edges of the gradient. However, GDI+ does this by default and for simplicity I did not want to change this. You have to turn off the Graphics object interpolation properties. It also speeds up drawing the images a little, but may result in pixelated drawings at specific images (as it does in Windows - very big buttons are really ugly):

gr.InterpolationMode = InterpolationMode.NearestNeighbor;
gr.PixelOffsetMode = PixelOffsetMode.Half;

The PixelOffsetMode must also be set. If you do not set this, the more the images get stretched, the more completely transparent pixels appear at the bottom or right side of the image. It would look like that the images are not stretched wide or tall enough.

2: If you have worked with the Windows 7 style before, you will see that I have cheated a little bit. In the original bitmap, there are transparent pixels around each state. The Windows buttons are actually one pixel larger into each direction than what is actually seen on screen. However, the bitmap is just completely transparent there, so the button appears smaller. Try clicking on a button one pixels outside its visible border, and you will see that fact in action! I removed these transparent areas to simplify the article and avoid confusing the reader.

Source Code and Sample Program

I have attached a sample program with which you can play around with the parameters passed to the functions. Two sample images are included, including a Windows XP styled button. Resize the window to see the stretching in real time!

You also find the complete source code of the program attached to this article. It includes the GraphicsExtensions.cs shown above, which is all you will need in your own programs to start using these functions. 

History

  • 22.07.2014 - Updated sample application
  • 31.01.2014 - Added sample program and its source code 
  • 27.10.2013 - First release 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

DebugErr

United States United States
No Biography provided

Comments and Discussions

 
GeneralMy vote of 5 Pinmemberquinn.harly15-Jul-14 12:38 
QuestionSource Code PinmemberLaurent Muller31-Jan-14 0:16 
AnswerMessage Removed PinmemberPacMani31-Jan-14 1:33 
GeneralRe: Source Code PinmemberLaurent Muller31-Jan-14 3:35 
QuestionReally nifty Pinmemberbombersa30-Jan-14 19:51 
QuestionReally nifty..!! Pinmemberbombersa30-Jan-14 6:06 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.150129.1 | Last Updated 22 Jul 2014
Article Copyright 2013 by DebugErr
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid