
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:
- make sure that every container is first being added to its container and only then being added with its own child controls.
- 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()
...
End Sub
#Else
Private Sub InitializeComponent()
...
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
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
Me.BringToFront()
If Not Me.PicImageList Is Nothing Then
OpenImagesComboBox(Me.MaxDropDownHeight)
Else
OpenImagesComboBox(Me.Height)
End If
Else
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... :).