Login


Owner Drawn Controls

By Rod Stephens on 7/14/2011
Language: C#
Technology: .NETWinForms
Platform: Windows
License: CPOL
Views: 12,642
Desktop Development » Controls » User Controls » Owner Drawn Controls

Download the OwnerDrawListBox Project Download the OwnerDrawListBox Project

Download the OwnerDrawComboBox Project Download the OwnerDrawComboBox Project

Download the OwnerDrawMenu Project Download the OwnerDrawMenu Project

Introduction

Typically the ListBox, ComboBox, and MainMenu controls display a series of text items. But with a little extra work you can make many of them display whatever you want. By making these controls owner-drawn, you can use your own code to draw the items in any way you like. They can display images, text, and other graphics drawn by your code, such as ellipses and polygons.

Making Owner Drawn Controls

There are only three steps to making an owner-drawn control. First, specify that the control is owner drawn. Exactly how you do this varies slightly depending on the type of control. Second, handle the control's MeasureItem event to indicate the size needed by the items displayed in the control. Third, handle the control's DrawItem event to draw the items displayed by the control.

The following sections explain how the example programs display an owner drawn ListBox, ComboBox, and MainMenu. All three examples use the PlanetInfo class described in this section.

The PlanetInfo class shown in Listing 1 holds information about a planet. It provides a simple constructor to make initializing its fields easier.

The class provides two methods that return text. First, it overrides the ToString method to return the planet's name. This is generally useful because it helps IntelliSense tell you what value an object contains. For example, if you hover the mouse over the variable mars, which contains information about the planet Mars, then IntelliSense uses the object's ToString method show the value "Mars" in a tooltip.

The second method that returns text is TextData. This method returns all of the object's text data concatenated across multiple lines.

The class's most interesting method is DrawItem, which draws the object's picture and text data inside a rectangle. It takes as parameters the Graphics object on which to draw, a Rectangle indicating where to draw, the Font to use while drawing the text, and a Boolean indicating whether to draw only the planet's name (as opposed to all of the object's text).

The DrawItem method calculates a five percent margin around the picture and draws it in the target Rectangle. It then draws the appropriate text to the right of the picture.

This code has total control over how the object is drawn so you can do anything you want here. For example, you could draw the picture on the right side of the text, draw the planet's name in bold, or draw a box around the entry.

Listing 1: The PlanetInfo Class

public class PlanetInfo
{
    public Image Picture;
    public string Name;
    public double Mass, YearLength;

    // Initialize properties.
    public PlanetInfo(Image picture, string name, double mass, double yearLength)
    {
        Picture = picture;
        Name = name;
        Mass = mass;
        YearLength = yearLength;
    }

    // Return the planet's Name.
    public override string ToString()
    {
        return Name;
    }

    // Return a string representation of all fields.
    public string TextData()
    {
        return Name + '\n' +
            "Mass: " + Mass.ToString("0.00") + " Earths" + '\n' +
            "Year: " + YearLength.ToString("0.00") + " Earth days";
    }

    // Draw the planet's information in the rectangle.
    public void DrawItem(Graphics gr, Rectangle bounds, Font font, bool showNameOnly)
    {
        // Calculate a reasonable margin.
        int margin = (int)(bounds.Height * 0.05);
        int height = bounds.Height - 2 * margin;

        // Draw the picture.
        Rectangle srcRect = new Rectangle(0, 0,
            Picture.Width, Picture.Height);
        Rectangle destRect = new Rectangle(
            bounds.Left + margin, bounds.Top + margin,
            height, height);
        gr.DrawImage(Picture, destRect, srcRect,
            GraphicsUnit.Pixel);

        // Draw the name, mass, and year length.
        int textLeft = destRect.Right + margin;
        int width = bounds.Width - textLeft;
        Rectangle layoutRect = new Rectangle(
            textLeft, bounds.Top,
            width, bounds.Height);
        using (StringFormat stringFormat = new StringFormat())
        {
            stringFormat.LineAlignment = StringAlignment.Center;
            if (showNameOnly)
            {
                gr.DrawString(ToString(), font, Brushes.Black,
                    layoutRect, stringFormat);
            }
            else
            {
                gr.DrawString(TextData(), font, Brushes.Black,
                    layoutRect, stringFormat);
            }
        }
    }
}

Owner Drawn ListBoxes

Figure 1: An Owner Drawn ListBox

Owner Drawn ListBox

The OwnerDrawListBox example program shown in Figure 1 displays an owner drawn ListBox. It uses the following code to create several PlanetInfo objects and add them to its ListBox.

Listing 2: Creating Several PlanetInfo Objects in the Load Event Handler

// Initialize PlanetInfo objects.
private void Form1_Load(object sender, EventArgs e)
{
    PlanetInfo mercury = new PlanetInfo(
        Properties.Resources.Mercury, "Mercury", 0.055, 87.9691);
    PlanetInfo venus = new PlanetInfo(
        Properties.Resources.Venus, "Venus", 0.815, 243);
    PlanetInfo earth = new PlanetInfo(
        Properties.Resources.Earth, "Earth", 1.0, 365.256);
    PlanetInfo mars = new PlanetInfo(
        Properties.Resources.Mars, "Mars", 0.107, 686.971);

    // ListBox items.
    planetListBox.Items.Add(mercury);
    planetListBox.Items.Add(venus);
    planetListBox.Items.Add(earth);
    planetListBox.Items.Add(mars);
}

At design time, I set the control's DrawMode property to OwnerDrawVariable to indicate that this is an owner drawn control and that each item sets its own height.

The program uses the following MeasureItem event handler. This code simply indicates that every item in the ListBox should have the same height: 50 pixels. You could give every item in the list a different height if you wanted.

Listing 3: Setting a List Item's Height

// Indicate the amount of space needed for a ListBox item.
private const int ItemHeight = 50;
private void planetListBox_MeasureItem(object sender, MeasureItemEventArgs e)
{
    e.ItemHeight = ItemHeight;
}

The program uses the following DrawItem event handler. This code converts the sender into the ListBox that raised the event and then gets the PlanetInfo object corresponding to the ListBox item that it should draw.

The code then uses the e.DrawBackground method to draw the item's background appropriately. This automatically gives the object a highlighted background if it is selected.

Finally the event handler calls the PlanetInfo object's DrawItem method to draw the item, passing it the e.Graphics object on which to draw and the e.Bounds rectangle to indicate where to draw. It also passes the method the form's font and the value false to indicate that the method should draw all of the object's text data not just its name.

Listing 4: The DrawItem Event Handler

// Draw a ListBox item.
private void planetListBox_DrawItem(object sender, DrawItemEventArgs e)
{
    // Get the item's PlanetInfo.
    ListBox listBox = sender as ListBox;
    PlanetInfo planetInfo = listBox.Items[e.Index] as PlanetInfo;

    // Draw the background.
    e.DrawBackground();

    // Make the PlanetInfo object draw itself.
    planetInfo.DrawItem(e.Graphics, e.Bounds, this.Font, false);
}

Owner Drawn ComboBoxes

Figure 2: An Owner Drawn ComboBox

Owner Drawn ComboBox

ComboBoxes are very similar to ListBoxes so it should come as no surprise that making an owner drawn ComboBox is similar to making an owner drawn ListBox.

The OwnerDrawComboBox program shown in Figure 2 uses code similar to the code used by the OwnerDrawListBox example to add PlanetInfo objects in its ComboBox. The form's Load event handler also includes the following code to set some important ComboBox properties. The code sets the control's DropDownStyle and indicates that it is an owner drawn control. It also sets the width and height of the dropdown area to constants defined elsewhere in the code.

Listing 5: Initializing the ComboBox to be Owner-Draw

// Set some ComboBox properties.
planetComboBox.DropDownStyle = ComboBoxStyle.DropDownList;
planetComboBox.DrawMode = System.Windows.Forms.DrawMode.OwnerDrawVariable;
planetComboBox.DropDownHeight = 3 * ItemHeight;
planetComboBox.DropDownWidth = ItemWidth;

The following code shows the program's MeasureItem event handler. It simply sets the size of the item. (As is the case for owner drawn ListBoxes, each item could have a different size.)

Listing 6: The ComboBox MeasureItem Event Handler

// Indicate the amount of space needed for a ComboBox item.
private const int ItemHeight = 75;
private const int ItemWidth = 300;
private void planetComboBox_MeasureItem(object sender, MeasureItemEventArgs e)
{
    e.ItemHeight = ItemHeight;
    e.ItemWidth = ItemWidth;
}

The following code shows the program's DrawItem event handler. The only real change from the previous version is that this code checks the height allowed for the item. If the height is smaller than the height requested for an item, then the control is trying to draw the selected item shown on the control's surface and not an item in the dropdown area. In that case, the code draws only the planet's name and not all of its data. In Figure 2 the control is displaying a tiny picture of Venus with its name at the top of the form.

Listing 7: The ComboBox DrawItem Event Handler

// Draw a ComboBox item.
private void planetComboBox_DrawItem(object sender, DrawItemEventArgs e)
{
    // Get the item's PlanetInfo.
    ComboBox comboBox = sender as ComboBox;
    PlanetInfo planetInfo = comboBox.Items[e.Index] as PlanetInfo;

    // Draw the background.
    e.DrawBackground();

    // Make the PlanetInfo object draw itself.
    if (e.Bounds.Height < ItemHeight)
    {
        planetInfo.DrawItem(e.Graphics, e.Bounds, this.Font, true);
    }
    else
    {
        planetInfo.DrawItem(e.Graphics, e.Bounds, this.Font, false);
    }
}

Owner Drawn MainMenus

Figure 3: An Owner Drawn MainMenu

Owner Drawn MainMenu

The .NET Framework 2.0 replaced the MainMenu control with the MenuStrip control. Unfortunately MenuStrip doesn't support owner drawn items. If you have the MainMenu control installed on your system, however, you can still use it to make owner drawn items if you don't mind its slightly different appearance. (You might need to right-click on your Toolbox and select Choose Items to make the control available.)

Instead of making the MainMenu itself owner drawn, the program can make the MenuItems that it contains owner drawn. To do so, set the items' OwnerDraw properties to true and handle the MeasureItem and DrawItem events as usual.

In the previous examples, the ListBox and ComboBox controls contained PlanetInfo objects. However a MenuItem can hold only text, not an arbitrary object. Fortunately its Tag property can hold an arbitrary object.

The OwnerDrawMenu program shown in Figure 3 includes the following code in its form's Load event handler to store PlanetInfo objects in its menu items' Tag properties.

Listing 8: Initializing the PlanetInfo Object's Menu Items' Tag Properties

// Menu items.
mercuryMenuItem.Tag = mercury;
venusMenuItem.Tag = venus;
earthMenuItem.Tag = earth;
marsMenuItem.Tag = mars;

The following code shows the program's MeasureItem event handler. The code converts the sender parameter into the MenuItem that raised the event and gets the PlanetInfo object stored in its Tag property. It then sets the item's size to a fixed height and enough width to display the picture and the planet's name.

Listing 9: MainMenu's MeasureItem Event Handler

// Indicate how much room this item needs.
private const int ItemHeight = 50;
private void planetMenuItem_MeasureItem(object sender, MeasureItemEventArgs e)
{
    // Get the item's PlanetInfo.
    MenuItem menuItem = sender as MenuItem;
    PlanetInfo planetInfo = menuItem.Tag as PlanetInfo;

    e.ItemHeight = ItemHeight;
    e.ItemWidth = ItemHeight +
        (int)e.Graphics.MeasureString(planetInfo.Name, this.Font).Width;
}

The following code shows the program's DrawItem event handler. This code recovers the appropriate PlanetInfo object as before, draws the item's background, and then uses the object's DrawItem method to draw the menu item. In this example, the program draws only the planet's name, not all of its text data.

Listing 10: MainMenu's DrawItem Event Handler

// Draw this menu item.
private void planetMenuItem_DrawItem(object sender, DrawItemEventArgs e)
{
    // Get the item's PlanetInfo.
    MenuItem menuItem = sender as MenuItem;
    PlanetInfo planetInfo = menuItem.Tag as PlanetInfo;

    // Draw the background.
    e.DrawBackground();

    // Make the PlanetInfo object draw itself.
    planetInfo.DrawItem(e.Graphics, e.Bounds, this.Font, true);
}

Conclusion

Usually the ListBox, ComboBox, and MainMenu controls display lists of text but by making them owner drawn you can make them display just about anything. All you need to do is mark the control as owner drawn and handle the MeasureItem and DrawItem events. With a little work, you can use owner drawn controls to make your programs more informative, distinctive, and appealing.

Unfortunately the CheckListBox doesn't support owner draw. The ListView does but it's a bit different from the controls described here. Perhaps I'll explain how to make owner drawn ListView controls in another article.

End-User License

Use of this article and any related source code or other files is governed by the terms and conditions of The Code Project Open License.

Author Information

Rod Stephens

Rod is a Microsoft MVP who has written more than 20 books that have been translated into languages from all over the world, and more than 250 magazine articles covering C#, Visual Basic, Visual Basic for Applications, Delphi, and Java. His C# Helper and VB Helper websites contains tips, tricks, and example programs for C# and Visual Basic programmers. You can contact Rod at RodStephens@CSharpHelper.com.