Easily make owner-drawn ComboBoxes display images with text in C#

The example Easily make owner-drawn ComboBoxes display lists of colors or images in C# shows how to make an owner-drawn ComboBox that displays either a series of color samples or a list of images. This example shows how to make a ComboBox that displays a series of images plus associated text.

The previous example places Color or Image objects in the ComboBox's Items collection. This example needs to display both images and text so it can't simply put Images in the ComboBox's Items collection. Instead this example uses the ImageAndText class to hold an item's image and text.

To make it easier to lay out the items, the ImageAndText class also provides methods to measure and draw its item.

The following code shows the class's fields, declaration, and constructor.

public class ImageAndText
{
    // Margins around owner drawn ComboBoxes.
    private const int MarginWidth = 4;
    private const int MarginHeight = 4;

    public Image Picture;
    public string Text;
    public Font Font;

    public ImageAndText(Image picture, string text, Font font)
    {
        Picture = picture;
        Text = text;
        Font = font;
    }
    ...
}

The class stores the image and text it will draw in the Picture and Text fields. It stores the font it will use to draw the text in its Font field.

The constructor simply initializes the Picture, Text, and Font fields.

The following code shows the class's MeasureItem method.

// Set the size needed to display the image and text.
private int Width, Height;
private bool SizeCalculated = false;
public void MeasureItem(MeasureItemEventArgs e)
{
    // See if we've already calculated this.
    if (!SizeCalculated)
    {
        SizeCalculated = true;

        // See how much room the text needs.
        SizeF text_size = e.Graphics.MeasureString(Text, Font);

        // The height is the maximum of the image height and text height.
        Height =  2 * MarginHeight +
            (int)Math.Max(Picture.Height, text_size.Height);

        // The width is the sum of the image and text widths.
        Width = (int)(4 * MarginWidth + Picture.Width + text_size.Width);
    }

    e.ItemWidth = Width;
    e.ItemHeight = Height;
}

This method returns the amount of space needed to draw the ImageAndText object's item. The private Width and Height variables store the calculated width and height calculated so the object doesn't need to calculate those values more than once. The SizeCalculated variable keeps track of whether the width and height have been calculated.

The MeasureItem method calculates the width and height if it has not yet done so. It uses the e.Graphics object's MeasureString method to see how big the string will be when drawn. It sets the necessary height to be the larger of the picture's height and the text's height (plus top and bottom margins). It sets the necessary width to the sum of the picture's width and the text's width (plus 4 times the margin width, once for each side and twice for spacing between the picture and text).

The method then returns the calculated width and height.

The following code shows the class's DrawItem method.

// Draw the item.
public void DrawItem(DrawItemEventArgs e)
{
    // Clear the background appropriately.
    e.DrawBackground();

    // Draw the image.
    float hgt = e.Bounds.Height - 2 * MarginHeight;
    float scale = hgt / Picture.Height;
    float wid = Picture.Width * scale;
    RectangleF rect = new RectangleF(
        e.Bounds.X + MarginWidth,
        e.Bounds.Y + MarginHeight,
        wid, hgt);
    e.Graphics.InterpolationMode = InterpolationMode.HighQualityBilinear;
    e.Graphics.DrawImage(Picture, rect);

    // Draw the text.
    // If we're drawing on the control,
    // draw only the first line of text.
    string visible_text = Text;
    if (e.Bounds.Height < Picture.Height)
        visible_text = Text.Substring(0, Text.IndexOf('\n'));

    // Make a rectangle to hold the text.
    wid = e.Bounds.Width - rect.Right - 3 * MarginWidth;
    rect = new RectangleF(
        rect.Right + 2 * MarginWidth, rect.Y,
        wid, hgt);
    using (StringFormat sf = new StringFormat())
    {
        sf.Alignment = StringAlignment.Near;
        sf.LineAlignment = StringAlignment.Center;
        e.Graphics.TextRenderingHint = TextRenderingHint.AntiAliasGridFit;
        e.Graphics.DrawString(visible_text, Font, Brushes.Black, rect, sf);
    }
    e.Graphics.DrawRectangle(Pens.Blue, Rectangle.Round(rect));

    // Draw the focus rectangle if appropriate.
    e.DrawFocusRectangle();
}

The method starts by calling e.DrawBackground to draw the appropriate background. It then gets the height available to draw the picture and calculates the width it should use to make the picture fit without stretching it. When the program is drawing the ComboBox's dropdown list, this width and height will match the space available because that was specified by the MeasureItem method. Then the program is drawing the item on the control's surface, however, the available size will be the size of the control, which is probably smaller than the space desired. In that case this code scales the image to fit nicely.

Having calculated the size needed for the image, the code makes a RectangleF to position the picture and then draws it. (Setting InterpolationMode to HighQualityBilinear makes the image scale smoothly if it is scaled.)

If the available height is smaller than the picture, then the code assumes it is drawing the item on the control and not in the dropped-down list. (Let me know if you know another way to determine this.) In that case the method sets visible_text to just the first line of the text so it will fit nicely on the control.

Next the method makes a RectangleF representing the area to the right of the picture (minus appropriate margins). It then draws visible_text inside that area, centering it vertically and aligning it on the left horizontally.

The method then draws a box around the text to provide separation between the items if none is highlighted. It finishes by calling e.DrawFocusRectangle to draw a focus rectangle around the item if it has the focus.

As in the earlier example, this one adds an extension method to the ComboBox class to make it easy to display images with text. The following code shows the DisplayImagesAndText method that sets up the control.

// Set up the ComboBox to display images with text.
public static void DisplayImagesAndText(this ComboBox cbo, ImageAndText[] values)
{
    // Make the ComboBox owner-drawn.
    cbo.DrawMode = DrawMode.OwnerDrawVariable;

    // Add the images to the ComboBox's items.
    cbo.Items.Clear();
    cbo.Items.AddRange(values);

    // Subscribe to the DrawItem event.
    cbo.MeasureItem += cboDrawImageAndText_MeasureItem;
    cbo.DrawItem += cboDrawImageAndText_DrawItem;
}

This method sets the ComboBox's DrawMode to OwnerDrawVariable. It clears the items list and adds the ImageAndText objects to it. It then subscribes event handlers for the control's MeasureItem and DrawItem methods.

The following code shows the MeasureItem event handler.

// Measure a ComboBox item that is displaying an image.
private static void cboDrawImageAndText_MeasureItem(object sender, MeasureItemEventArgs e)
{
    if (e.Index < 0) return;

    // Get the item.
    ComboBox cbo = sender as ComboBox;
    ImageAndText item = (ImageAndText)cbo.Items[e.Index];

    // Make the item measure itself.
    item.MeasureItem(e);
}

This event handler gets the appropriate ImageAndText item and calls its MeasureItem method so it can do all of the interesting work.

The following code shows the DrawItem event handler.

// Draw a ComboBox item that is displaying an image.
private static void cboDrawImageAndText_DrawItem(object sender, DrawItemEventArgs e)
{
    if (e.Index < 0) return;

    // Get the item.
    ComboBox cbo = sender as ComboBox;
    ImageAndText item = (ImageAndText)cbo.Items[e.Index];

    // Make the item draw itself.
    item.DrawItem(e);
}

This event handler gets the appropriate ImageAndText item and calls its DrawItem method so it can do all of the interesting work.

This example draws the items' pictures at their full sizes. The result looks best when the pictures in all items have the same width. If you run the example and scroll down the list, you'll see that Saturn has a wider picture than the other planets so the items don't line up.

You could resize the images so they all have the same width. Another approach would be to change the code that draws the images so it resizes them at run time. For example, you could allow 100 pixels for the pictures and resize them as needed to fit, drawing the text to the right of the 100 pixel area.

(Another idea would be to make an image that includes both the picture and text and then use the previous example to display the results as images. That doesn't let the appropriate background color show through the text if the item is highlighted so the result isn't nearly as nice as the one given by this example.)

   

 

What did you think of this article?




Trackbacks
  • 1/18/2013 2:17 PM The Microsoft MVP Award Program Blog wrote:
    1. Easily make owner-drawn ComboBoxes display with images and text in C# By Visual Basic MVP Rod Stephens
Comments
  • No comments exist for this post.
Leave a comment

Submitted comments are subject to moderation before being displayed.

 Name

 Email (will not be published)

 Website

Your comment is 0 characters limited to 3000 characters.