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 PictureBox
es 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... :).