Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ImagesComboBox Control

0.00/5 (No votes)
29 May 2005 1  
A combobox holding pictures as the items themselves and not just drawing them on run-time.

Sample Image - ImagesComboBox.jpg

Introduction

I needed my client to select a picture from a list of pictures, only that it wasn�t the main thing on my form. The lists of pictures were part of a big complicated and loaded GUI with lots of controls. If it wasn�t a picture I wanted him to select, I would have given him a ComboBox with values. But it was. So the most logical thing was to have one that will hold pictures instead of values.

What's all the fuss? Regular ComboBox can do it, can�t it ?!

Yes, the regular ComboBox can show pictures. You can do it by adding the following code:

(I assume that you have a ComboBox named ComboBox1 and an ImageList called ImageList1 holding your pictures on your form.)

Private Sub Form1_Load(ByVal sender As System.Object, +
            ByVal e As System.EventArgs) _
            Handles MyBase.Load

    Dim items(Me.ImageList1.Images.Count - 1) As String

    For i As Int32 = 0 To Me.ImageList1.Images.Count - 1
        items(i) = "Item " & i.ToString
    Next

    Me.ComboBox1.Items.AddRange(items)
    Me.ComboBox1.DropDownStyle = ComboBoxStyle.DropDownList
    Me.ComboBox1.DrawMode = DrawMode.OwnerDrawVariable
    Me.ComboBox1.ItemHeight = Me.ImageList1.ImageSize.Height
    Me.ComboBox1.Width = Me.ImageList1.ImageSize.Width + 18
    Me.ComboBox1.MaxDropDownItems = Me.ImageList1.Images.Count
End Sub

Private Sub ComboBox1_DrawItem(ByVal sender As Object, ByVal e As _
      System.Windows.Forms.DrawItemEventArgs) _
      Handles ComboBox1.DrawItem

    If e.Index <> -1 Then
        e.Graphics.DrawImage(Me.ImageList1.Images(e.Index), _
                                 e.Bounds.Left, e.Bounds.Top)
    End If       
End Sub

Private Sub ComboBox1_MeasureItem(ByVal sender As Object, ByVal e As _
        System.Windows.Forms.MeasureItemEventArgs) _
        Handles ComboBox1.MeasureItem

    e.ItemHeight = Me.ImageList1.ImageSize.Height
    e.ItemWidth = Me.ImageList1.ImageSize.Width
End Sub

But it has some lacks:

  • Every time you'll dropdown your ComboBox and move your mouse on the list, the ComboBox1_DrawItem will be called numerous times.
  • You need to resize the ComboBox manually (in code or in the properties browser) according to the images' width and height, otherwise, your images will be resized and of course, the ComboBox won't capture its actual place on design time.
  • Need to add this pack of code every time you'll want to be able to choose images from a ComboBox. I'd like it to be integrated...
  • The DrawMode property is not implemented in the Compact Framework� and the project I was on is involving developing for PDA as well. Nevertheless in this article's example, I'll use some (main) features that also do not exist in the Compact Framework for the benefit of using a designer. At the end of this article, I will state some directions for implementing it under the Compact Framework and maybe even will publish a second article about implementing it under the Compact Framework environment.

Characterization or 'what do I want from my ImagesComboBox ?'

  • I want design time support that will enable me to see the real client size of the control.
  • I want to be able to select a default picture.
  • I want to override regular ComboBox properties and disable those I don�t need at all.
  • I want it to feel like a regular ComboBox.
  • And of course, I want it to work� :-)

So let's dive into my ImagesComboBox

I've decided to build the ImagesComboBox from scratch so I derived it from System.Windows.Forms.UserControl and stated Imports System.ComponentModel at the beginning of the file for design time support in the 'Properties Browser'. Importing the ComponentModel allows to add attributes to the properties (as a matter of fact, it allows you to add attributes to the class itself and other parts as well). For documentation, see Enhancing Design-Time Support from MSDN.

Imports System.ComponentModel
Imports System.Drawing
Imports System.Windows.Forms

Public Class ImagesComboBox
    Inherits System.Windows.Forms.UserControl
.
.
.
End Class

I added to the UserControl a Panel named PanelHeader, a Panel named PanelMain and an inherited button named btnComboHandle. The PanelHeader will be the ComboBox in a closed state. The btnComboHandle is an inherited class from System.Windows.Forms.Button which only overrides the OnPaint event for drawing the little black triangle appearing on the combobox's handle. The PanelMain will be the drop-down list itself. By default, when dragging a control to a container (form, UserControl, etc.), it gets a declaration as 'Friend'. We don�t need these controls to be friends of anyone so I changed their declaration to 'Private'.

Private WithEvents PanelHeader As System.Windows.Forms.Panel
Private WithEvents PanelMain As System.Windows.Forms.Panel
Private WithEvents btnComboHandle As _
        Quammy.F.Controls.ImagesComboBox.ComboHandleButton

I didn�t interfere with the InitializeComponent sub as it is being constructed automatically by the designer. So that part of code you can pick up from the attached project but, do watch for some fine tuning of the controls' properties like the sizes I've used � these are my default sizes (the +/-1 pixel differences are for viewing the inner controls in a good way. Play with the sizes in design time to understand my meaning...).

The trick to initialize the btnComboHandle in the InitializeComponent sub is to put on the designer a System.Windows.Forms.Button and then replace the definition with your inherited button. Don�t forget to remove unnecessary properties such as Text on this case. The reason that�s working is that it is really a button and the designer sees it as one...

One general offside remark though, about InitializeComponent sub: if you want to optimize your run-time, write your own InitializeComponent � it should be the copy of the original one with the following two changes:

  1. make sure that every container is first being added to its container and only then being added with its own child controls.
  2. change every two lines of "Control.Location = ..." and "Control.Size = ..." to one line "Control.Bounds = ...".

To still be able to work with the designer, wrap your original InitializeComponent like that:

#If DEBUG Then
    Private Sub InitializeComponent() 'Original

        ...
    End Sub
#Else
    Private Sub InitializeComponent() 'Optimized

        ...
    End Sub
#End If

(If you don�t see the 'Debug' conditional compilation constant on the intellisense, you need to go to the project's properties -> Configuration Properties -> Build, and mark the 'Define DEBUG constant' checkbox.)

Now, back to ImagesComboBox.

I've added several properties to my ImagesComboBox. For the ease of viewing my added design-time properties, I concentrate them all under "ImagesComboBox Features" category, each with its own description.

  • ComboHandleWidth is a private read only property that returns the width of the btnComboHandle + 3 pixels for not overriding the button's edges.
    Private ReadOnly Property ComboHandleWidth() As Integer
        Get
            Return Me.btnComboHandle.Width + 3
        End Get
    End Property
  • PicImageList is a public property to the inner ImageList, holding the images of the ImagesComboBox.

    After this property is set, we need to refresh (in the properties browser) some other properties, hence, I stated RefreshProperties(RefreshProperties.All) in the property's attribute.

    When setting a value to this property, I populate the images into PanelMain (CreatePicturesFromImageList method), resize (InnerResize method) the control to the size of an image in the list (but keeping a minimum height of 20 and minimum width of ComboHandleWidth), load defaults like MaxDropDownItems = 8, DefaultPictureIndex = -1 (SetValuesToDefaults method).

    Private _PicList As ImageList
    <Category("ImagesComboBox Features"), _
     Description("ImageList holding" & _ 
          " the images to load into the ImagesComboBox"), _
     RefreshProperties(RefreshProperties.All)> _
    Public Property PicImageList() As ImageList
        Get
            Return Me._PicList
        End Get
    
        Set(ByVal Value As ImageList)
            Me._PicList = Value
    
            If Not Value Is Nothing Then
                CreatePicturesFromImageList()
            Else
                Me.PanelHeader.BackgroundImage = Nothing
            End If
    
            InnerResize()
            SetValuesToDefaults()
        End Set
    End Property
  • Images is a public read only property that returns a certain image from PicImageList according to a given index.
    Default Public ReadOnly Property Images(ByVal index As Int32) As Image
        Get
            Return Me.PicImageList.Images(index)
        End Get
    End Property
  • DefaultPictureIndex is a public property that gets/sets the index of the image to show as a default value in ImagesComboBox. An error message will appear at design-time (message box) and at run-time (exception) if the given index is out of range of PicImageList. Default value is '-1' meaning no picture is selected as default.
    Private _DefaultPictureIndex As Integer = -1
    <Category("ImagesComboBox Features"), _
     Description("Default Picture Index must be between" & _
        " 0 and number of images in the ImageList - 1."), _
     DefaultValue(-1)> _
    Public Property DefaultPictureIndex() As Integer
    
        Get
            Return _DefaultPictureIndex
        End Get
    
        Set(ByVal Value As Integer)
    
            If Me.PicImageList Is Nothing Then
                Me._DefaultPictureIndex = -1
                Me.PanelHeader.BackgroundImage = Nothing
                Exit Property
            End If
    
            If Value <= -1 Then
                Me._DefaultPictureIndex = -1
                Me.PanelHeader.BackgroundImage = Nothing
            ElseIf Value > Me.PicImageList.Images.Count - 1 Then
                Dim msg As String = _
                    "Default Picture Index must be between 0" & _
                    " and number of images in the ImageList - 1 " & _
                    vbCrLf & "Currently, there are " & _
                    (Me.PicImageList.Images.Count - 1).ToString & _
                    " images in the ImageList." & vbCrLf & vbCrLf & _
                    "To disable 'Default Picture'" & _ 
                    " - change the index to '-1'."
                Throw New _
                  ArgumentOutOfRangeException("DefaultPictureIndex", _
                                                           Value, msg)
            Else
                Me._DefaultPictureIndex = Value
                Me.PanelHeader.BackgroundImage = _
                               Me.PicImageList.Images(Value)
            End If
        End Set
    End Property
  • SelectedImageIndex is a public property that gets/sets the image to select in ImagesComboBox according to a given index. An exception will occur if the given index is out of range of PicImageList. Default value is '-1' meaning no picture is selected.

    The attribute Browsable(False) is disables the property from showing on the properties browser.

    Private _SelectedImageIndex As Integer = -1
    <Browsable(False), DefaultValue(-1)> _
    Public Property SelectedImageIndex() As Integer
    
        Get
            Return Me._SelectedImageIndex
        End Get
    
        Set(ByVal Value As Integer)
            If Not Me.PicImageList Is Nothing Then
              If Value = -1 Then
                Exit Property
              ElseIf Value < 0 OrElse Value > _
                     Me.PicImageList.Images.Count - 1 Then
                Throw New ArgumentOutOfRangeException("SelectedImageIndex", _
                                                Value, "Index out of range.")
              Else
                Me._SelectedImageIndex = Value
                Me.PanelHeader.BackgroundImage = _
                   Me.PicImageList.Images(Me._SelectedImageIndex)
              End If
            End If
        End Set
    End Property
  • SelectedImage is a public read only property that gets the image currently shown by ImagesComboBox.
    <Browsable(False)> _
    Public ReadOnly Property SelectedImage() As Image
        Get
            Return Me.PanelHeader.BackgroundImage
        End Get
    End Property
  • MaxDropDownItems is a public property that gets/sets the maximum images to show in ImagesComboBox while dropping down. Default value is 8 images.
    Private _MaxDropDownItems As Integer = 8
    <Category("ImagesComboBox Features"), _
     Description("Maximum items t0 reveal" & _
         " when dropping down the ImagesComboBox"), _
     DefaultValue(8)> _
    Public Property MaxDropDownItems() As Integer
    
        Get
            Return Me._MaxDropDownItems
        End Get
    
        Set(ByVal Value As Integer)
            If Not Me.PicImageList Is Nothing Then
                If Value > Me.PicImageList.Images.Count Then
                    Value = Me.PicImageList.Images.Count
                ElseIf Value < 1 Then
                    Value = 1
                End If
            End If
            Me._MaxDropDownItems = Value
        End Set
    End Property
  • MaxDropDownHeight is a public read only property that gets the height of the dropdown list of the ImagesComboBox. This property is updated according to the value of MaxDropDownItems.
    <Category("ImagesComboBox Features")> _
    Public ReadOnly Property MaxDropDownHeight() As Integer
    
        Get
            If Me.PicImageList Is Nothing Then Return 0
            With Me.PicImageList
                If .Images.Count < Me.MaxDropDownItems Then
                    Return .Images.Count * .ImageSize.Height
                Else
                    Return Me.MaxDropDownItems * .ImageSize.Height
                End If
            End With
        End Get
    End Property

Some properties of System.Windows.Forms.UserControl are not applicable to my ImagesComboBox so I 'outlawed' them:

#Region " Disable Properties "
    <Browsable(False)> _
Public Shadows ReadOnly Property BackgroundImage() As Image
    Get
        Throw New Exception("Property not supported.")
    End Get
End Property

<Browsable(False)> _
Public Shadows ReadOnly Property Font() As System.Drawing.Font

    Get
        Throw New Exception("Property not supported.")
    End Get
End Property

<Browsable(False)> _
Public Shadows ReadOnly Property ForeColor() As System.Drawing.Color

    Get
        Throw New Exception("Property not supported.")
    End Get

End Property

#End Region

The 'size' properties can 'harm' my control at run time (at design-time, there's a treatment through the ImagesComboBox_Resize event), so I disabled them:

#Region " Shadowed Properties "
    Public Shadows Property Size() As System.Drawing.Size
        Get
            Return MyBase.Size
        End Get

        Set(ByVal Value As System.Drawing.Size)
            If Me.PicImageList Is Nothing Then
                MyBase.Size = Value
            End If
        End Set
    End Property

    Public Shadows Property Width() As Integer
        Get
            Return MyBase.Size.Width
        End Get

        Set(ByVal Value As Integer)
            If Me.PicImageList Is Nothing Then
                MyBase.Width = Value
            End If
        End Set
    End Property

    Public Shadows Property Height() As Integer

        Get
            Return MyBase.Size.Height
        End Get

        Set(ByVal Value As Integer)
            If Me.PicImageList Is Nothing Then
                MyBase.Height = Value
            End If
        End Set
    End Property
#End Region

I couldn�t just transfer these properties to 'Read only' because the InitializeComponent needed to use them.

OK, outside is nice but what is going on inside?

I have a couple of methods for setting defaults:

   Private Sub SetSizesToDefaults()
        Me.Width = 100 + Me.ComboHandleWidth
        Me.Height = 20
        Me.btnComboHandle.Height = 16
    End Sub

    Private Sub SetValuesToDefaults()
        Me.DefaultPictureIndex = -1
        Me.MaxDropDownItems = 8
    End Sub

A method for resizing (or preventing from resizing) the control:

    Private Sub InnerResize()
            If Me.PicImageList Is Nothing Then
                SetSizesToDefaults()
            Else
                With Me.PicImageList
                    Mybase.Width = .ImageSize.Width + Me.ComboHandleWidth

                    If .ImageSize.Height < 20 Then
                       Mybase.Height = 20
                       Me.btnComboHandle.Height = 16
                    Else
                        Mybase.Height = .ImageSize.Height
                        Me.btnComboHandle.Height = .ImageSize.Height - 4
                    End If
                End With
            End If
            Me.PanelHeader.Height = Me.Height
    End Sub

    Private Sub ImagesComboBox_Resize(ByVal sender As Object, _
            ByVal e As System.EventArgs) Handles MyBase.Resize
        If Me.DesignMode Then InnerResize()
    End Sub

The ImagesComboBox_Resize handles the control Resize event and is operating only during design-time (for preventing the user from changing the ImagesComboBox size). Only by setting an ImageList, the size can be changed.

An overridden version of InitLayout event reloads the PicImageList after the initialize of the control has ended. This override is required in case the InitializeComponent initializes the ImageComboBox before it initializes the ImageList. In that case, the ImageComboBox will have a valid PicImageList but it won't have any images in it. So I need to reload the PicImageList after it has been populated with images.

    Protected Overrides Sub InitLayout()
        Me.PicImageList = Me._PicList
    End Sub

Couple of methods taking care of adding and removing pictures from/to the dropdown list (PanelMain):

    Private Sub CreatePicturesFromImageList()

            RemovePictureBoxesFromControl()

            Dim picb As PictureBox, img As Image

            For i As Integer = 0 To Me.PicImageList.Images.Count - 1
                img = Me.PicImageList.Images(i)
                picb = New PictureBox
                picb.Name = i.ToString 'Creating an index...

                picb.Image = img
                picb.Bounds = New Rectangle(New Point(0, _
                                       i * img.Height), img.Size)
                AddHandler picb.Click, AddressOf PictureBox_Click
                Me.PanelMain.Controls.Add(picb)
            Next
    End Sub

    Private Sub RemovePictureBoxesFromControl()
            For i As Integer = Me.PanelMain.Controls.Count - 1 To 0 Step -1
                If TypeOf Me.PanelMain.Controls(i) Is PictureBox Then
                    Me.PanelMain.Controls(i).Dispose()
                End If
            Next
    End Sub

While adding the newly created PictureBoxes to hold the images, I hooked the PictureBox_Click sub to handle their Click event.

Here is the PictureBox_Click sub (which in its turn raises the PictureSelected public event):

    Private Sub PictureBox_Click(ByVal sender As Object, _
                                           ByVal e As System.EventArgs)
            Me.PanelHeader.BackgroundImage = CType(sender, PictureBox).Image
            Me._SelectedImageIndex = _
                Convert.ToInt32(CType(sender, PictureBox).Name)
            CloseImagesComboBox()
            RaiseEvent PictureSelected(Me)
    End Sub

Last thing to deal with is the dropping down of the ImagesComboBox. I want my ImagesComboBox to drop down after its handle has been clicked, and if it is already open I want it to close:

    Private Sub btnComboHandle_Click(ByVal sender As Object, _
                      ByVal e As System.EventArgs) _
                      Handles btnComboHandle.Click

        If Me.Height = Me.PanelHeader.Height Then 'The comboBox is closed

           Me.BringToFront()
           If Not Me.PicImageList Is Nothing Then
               OpenImagesComboBox(Me.MaxDropDownHeight)
           Else
               OpenImagesComboBox(Me.Height)
           End If
        Else 'The comboBox is already opened

            CloseImagesComboBox()
        End If
    End Sub

If the ImageComboBox is empty (no images in PicImageList), then I want an empty portion (the height of the ImageComboBox) to dropdown.

Closing the ImagesComboBox is rather simple:

    Private Sub CloseImagesComboBox()
            Mybase.Height = Me.PanelHeader.Height
    End Sub

And opening it is simple as well but I wanted it to be 'animated'. So I used here a small recursion. Each loop adds a delay and then enlarges the dropdown height in a small fraction (10 pixels). The result is a slow and nice dropdown:

    Private Sub OpenImagesComboBox(ByVal DropDownHeight As Integer)
            Dim t As Integer = Environment.TickCount
            While t + 1 > Environment.TickCount
                Application.DoEvents()
            End While

            Mybase.Height = 10 + Me.Height
            Me.PanelMain.Refresh()

            If Me.Height >= Me.PanelHeader.Height + DropDownHeight Then
                Mybase.Height = Me.PanelHeader.Height + DropDownHeight
                Me.Focus()
                Exit Sub
            End If

            OpenImagesComboBox(DropDownHeight)
    End Sub

To imitate another regular ComboBox behavior, I used the LostFocus event to close the ImagesComboBox when losing focus:

    Private Sub Control_LostFocus(ByVal sender As Object, _
           ByVal e As System.EventArgs) _
           Handles MyBase.LostFocus, PanelMain.LostFocus, PanelHeader.LostFocus
        If Not Me.btnComboHandle.Focused Then CloseImagesComboBox()
    End Sub

I want it only to happen if the btnComboHandle is not focused, because otherwise I won't be able to close the ImagComboBox by clicking the btnComboHandle. (When clicking the btnComboHandle, it gets focused, which means that PanelHeader, PanelMain and the control itself will lose their focus, which raises the Lost Focus event and closes the ImageComboBox. Then the btnComboHandle Click event fires and finds the ImagComboBox in a close state so it opens it again...)

That's it! Enjoy.

Compact Framework remarks

Normal ComboBox under the current version of the Compact Framework environment does not have the DrawMode property (as well as some other properties). If you really want to use the regular ComboBox to show pictures in your Pocket PC application, you'll have to use the OpenNetCF libraries. The ComboBoxEx of OpenNETCF implements most (if not all) of the full framework ComboBox properties. The ComboBoxEx has a design-time interface. Consider that with the ComboBoxEx, you'll probably have the same lacks as I stated earlier about the regular ComboBox. By the way, the ComboBoxEx is a new control introduced in OpenNETCF version 1.3 which has been just published (May 12th, 2005).

On this point I want to (have to!!!) say a big thanks to all the guys working behind this huge OpenNETCF project, you are doing a remarkable work.

If you want to implement my ImagesComboBox under CF, you'll have to do some basic changes. Currently, Visual Studio 2003 does not support the implementation of design-time controls (although there is a trick to do it but only with C#). That will make you do the following changes:

  • Inherit from System.Windows.Forms.Control (or better from System.Windows.Forms.Panel and then, you wont need to use the PanelMain).
  • Remove all attributes from properties (no design-time support...).
  • Place the ImagesComboBox control on your form by code (again, no design-time support...).
  • ComboHandleButton class will need to be changed a bit (currently, in CF, only a Form control can use its Graphics class).
  • And several other small changes.

Future development possibilities

  • Add keyboard support.
  • Add a Text property to the images (like image name), so the ImagesComboBox will be able to return this property as well.
  • Ability to add single image and not only by ImageList.
  • Whatever is on your mind... :).

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here