ImagesComboBox 控件






2.89/5 (10投票s)
2005年5月30日
10分钟阅读

55346

680
一个组合框,其项本身就是图片,而不是在运行时才绘制图片。
引言
我需要我的客户从图片列表中选择一张图片,但这并不是我表单上的主要功能。图片列表是复杂且加载了大量控件的庞大 GUI 的一部分。如果我不是想让他选择图片,我会给他一个带有值的 ComboBox
。但它确实是图片。所以最合乎逻辑的做法是使用一个可以容纳图片而不是值的组合框。
有什么大惊小怪的?普通的 ComboBox 也能做到,不是吗?!
是的,普通的 ComboBox
可以显示图片。你可以通过添加以下代码来做到这一点
(我假设你有一个名为 ComboBox1
的 ComboBox
和一个名为 ImageList1
的 ImageList
,其中包含你表单上的图片。)
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
但它有一些不足
- 每次你下拉
ComboBox
并在列表上移动鼠标时,ComboBox1_DrawItem
都会被多次调用。 - 你需要根据图像的宽度和高度手动(在代码中或在属性浏览器中)调整
ComboBox
的大小,否则,你的图像将被调整大小,并且ComboBox
当然不会在设计时捕获其实际位置。 - 每次你想从
ComboBox
中选择图像时,都需要添加这堆代码。我希望它能集成在一起... DrawMode
属性未在 Compact Framework 中实现……而且我正在从事的项目也涉及到 PDA 的开发。尽管如此,在本文章的示例中,我将使用一些(主要)在 Compact Framework 中也不存在的特性,以便使用设计器。在本文的最后,我将说明在 Compact Framework 下实现它的一些方向,甚至可能会发表第二篇关于在 Compact Framework 环境下实现它的文章。
特征化或“我对我的 ImagesComboBox 有什么要求?”
- 我希望有设计时支持,使我能够看到控件的实际客户端大小。
- 我希望能够选择一张默认图片。
- 我希望能够覆盖常规
ComboBox
属性并禁用我根本不需要的属性。 - 我希望它感觉像一个常规的
ComboBox
。 - 当然,我希望它能工作…… :-)
那么,让我们深入了解我的 ImagesComboBox
我决定从头开始构建 ImagesComboBox
,所以我从 System.Windows.Forms.UserControl
派生它,并在文件开头声明 Imports System.ComponentModel
,以支持“属性浏览器”中的设计时。导入 ComponentModel
允许向属性添加特性(实际上,它允许向类本身和其他部分添加特性)。有关文档,请参阅 MSDN 中的 增强设计时支持。
Imports System.ComponentModel
Imports System.Drawing
Imports System.Windows.Forms
Public Class ImagesComboBox
Inherits System.Windows.Forms.UserControl
.
.
.
End Class
我向 UserControl
添加了一个名为 PanelHeader
的 Panel
,一个名为 PanelMain
的 Panel
和一个继承的按钮 btnComboHandle
。PanelHeader
将是关闭状态的 ComboBox
。btnComboHandle
是从 System.Windows.Forms.Button
继承的类,它只覆盖 OnPaint
事件以绘制组合框手柄上出现的小黑色三角形。PanelMain
将是下拉列表本身。默认情况下,将控件拖到容器(窗体、UserControl 等)时,它会获得“Friend
”声明。我们不需要这些控件成为任何人的朋友,所以我将其声明更改为“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
我没有干扰 InitializeComponent
子例程,因为它是由设计器自动构建的。因此,你可以从附加的项目中获取这部分代码,但请注意控件属性的一些微调,例如我使用的尺寸——这些是我的默认尺寸(+/-1 像素的差异是为了更好地查看内部控件。在设计时玩转尺寸以理解我的意思……)。
在 InitializeComponent
子例程中初始化 btnComboHandle
的技巧是在设计器上放置一个 System.Windows.Forms.Button
,然后用你继承的按钮替换其定义。不要忘记删除不必要的属性,例如此情况下的 Text
。之所以有效,是因为它确实是一个按钮,并且设计器将其视为一个按钮……
不过,关于 InitializeComponent
子例程,有一个一般的题外话:如果你想优化运行时,请编写自己的 InitializeComponent
——它应该是原始副本,但有以下两个更改
- 确保每个容器首先被添加到其容器中,然后才添加其自己的子控件。
- 将每两行“
Control.Location = ...
”和“Control.Size = ...
”更改为一行“Control.Bounds = ...
”。
为了仍然能够与设计器一起工作,将你的原始 InitializeComponent
包装起来,像这样
#If DEBUG Then
Private Sub InitializeComponent() 'Original
...
End Sub
#Else
Private Sub InitializeComponent() 'Optimized
...
End Sub
#End If
(如果你在 Intellisense 中看不到“Debug
”条件编译常量,你需要进入项目的属性 -> 配置属性 -> 生成,并勾选“定义 DEBUG 常量”复选框。)
现在,回到 ImagesComboBox
。
我为我的 ImagesComboBox
添加了几个属性。为了方便查看我添加的设计时属性,我将它们全部集中在“ImagesComboBox 功能”类别下,每个属性都有自己的描述。
ComboHandleWidth
是一个私有只读属性,它返回btnComboHandle
的宽度 + 3 像素,以避免覆盖按钮边缘。Private ReadOnly Property ComboHandleWidth() As Integer Get Return Me.btnComboHandle.Width + 3 End Get End Property
PicImageList
是内部ImageList
的公共属性,包含ImagesComboBox
的图像。设置此属性后,我们需要刷新(在属性浏览器中)一些其他属性,因此,我在属性的属性中声明了
RefreshProperties(RefreshProperties.All)
。当为此属性设置值时,我将图像填充到
PanelMain
中(CreatePicturesFromImageList
方法),将控件调整大小(InnerResize
方法)到列表中图像的大小(但保持最小高度为 20,最小宽度为ComboHandleWidth
),加载默认值,例如MaxDropDownItems
= 8,DefaultPictureIndex
= -1(SetValuesToDefaults
方法)。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
是一个公共只读属性,它根据给定的索引从PicImageList
返回特定图像。Default Public ReadOnly Property Images(ByVal index As Int32) As Image Get Return Me.PicImageList.Images(index) End Get End Property
DefaultPictureIndex
是一个公共属性,用于获取/设置要在ImagesComboBox
中显示为默认值的图像索引。如果给定的索引超出PicImageList
的范围,则会在设计时(消息框)和运行时(异常)显示错误消息。默认值为 '-1',表示未选择任何图片作为默认值。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
是一个公共属性,用于根据给定索引获取/设置要在ImagesComboBox
中选择的图像。如果给定索引超出PicImageList
的范围,将发生异常。默认值为 '-1',表示未选择任何图片。属性
Browsable(False)
禁用该属性在属性浏览器中显示。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
是一个公共只读属性,用于获取ImagesComboBox
当前显示的图像。<Browsable(False)> _ Public ReadOnly Property SelectedImage() As Image Get Return Me.PanelHeader.BackgroundImage End Get End Property
MaxDropDownItems
是一个公共属性,用于获取/设置ImagesComboBox
下拉时要显示的最大图像数。默认值为 8 张图像。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
是一个公共只读属性,用于获取ImagesComboBox
下拉列表的高度。此属性根据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
System.Windows.Forms.UserControl
的一些属性不适用于我的 ImagesComboBox
,所以我“禁止”了它们
#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
“大小”属性可能会在运行时“损害”我的控件(在设计时,通过 ImagesComboBox_Resize
事件进行处理),所以我禁用了它们
#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
我不能直接将这些属性设置为“只读”,因为 InitializeComponent
需要使用它们。
好的,外面很漂亮,里面发生了什么?
我有一些用于设置默认值的方法
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
一个用于调整(或阻止调整)控件大小的方法
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
ImagesComboBox_Resize
处理控件 Resize
事件,并且仅在设计时运行(用于防止用户更改 ImagesComboBox
大小)。只有通过设置 ImageList
,大小才能更改。
InitLayout
事件的重写版本在控件初始化结束后重新加载 PicImageList
。当 InitializeComponent
在初始化 ImageList
之前初始化 ImageComboBox
时,需要此重写。在这种情况下,ImageComboBox
将有一个有效的 PicImageList
,但其中不会有任何图像。因此,我需要在 PicImageList
填充图像后重新加载它。
Protected Overrides Sub InitLayout()
Me.PicImageList = Me._PicList
End Sub
一些方法负责从下拉列表 (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
在添加新创建的 PictureBox
以保存图像时,我挂钩了 PictureBox_Click
子程序来处理它们的 Click
事件。
这是 PictureBox_Click
子例程(它反过来会引发 PictureSelected
公共事件)
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
最后要处理的是 ImagesComboBox
的下拉。我希望我的 ImagesComboBox
在点击其手柄后下拉,如果它已经打开,我希望它关闭
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
如果 ImageComboBox
为空(PicImageList
中没有图像),那么我希望下拉一个空白部分(ImageComboBox
的高度)。
关闭 ImagesComboBox
相当简单
Private Sub CloseImagesComboBox()
Mybase.Height = Me.PanelHeader.Height
End Sub
打开它也很简单,但我希望它“动画化”。所以我在这里使用了小小的递归。每个循环都会添加一个延迟,然后将下拉高度扩大一小部分(10 像素)。结果是一个缓慢而漂亮的下拉
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
为了模仿另一个常规 ComboBox
的行为,我使用 LostFocus
事件在失去焦点时关闭 ImagesComboBox
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
我希望它只在 btnComboHandle
没有焦点时发生,否则我将无法通过单击 btnComboHandle
关闭 ImagComboBox
。(当单击 btnComboHandle
时,它会获得焦点,这意味着 PanelHeader
、PanelMain
和控件本身将失去焦点,这会引发 Lost Focus 事件并关闭 ImageComboBox
。然后 btnComboHandle
的 Click
事件触发并发现 ImagComboBox
处于关闭状态,因此它会再次打开它......)
就是这样!尽情享受吧。
Compact Framework 备注
当前版本的 Compact Framework 环境下的普通 ComboBox
没有 DrawMode
属性(以及一些其他属性)。如果你真的想在你的 Pocket PC 应用程序中使用普通 ComboBox
显示图片,你将不得不使用 OpenNetCF 库。OpenNETCF 的 ComboBoxEx
实现了完整框架 ComboBox
的大部分(如果不是全部)属性。ComboBoxEx
有一个设计时界面。请注意,使用 ComboBoxEx
,你可能会遇到我之前提到的普通 ComboBox
相同的不足之处。顺便说一下,ComboBoxEx
是 OpenNETCF 1.3 版本中引入的新控件,该版本刚刚发布(2005 年 5 月 12 日)。
在这一点上,我想要(必须!!!)对所有致力于这个庞大 OpenNETCF 项目的人表示衷心的感谢,你们的工作非常出色。
如果你想在 CF 下实现我的 ImagesComboBox
,你需要做一些基本的更改。目前,Visual Studio 2003 不支持设计时控件的实现(尽管有一个技巧可以做到,但只能使用 C#)。这会让你进行以下更改
- 从
System.Windows.Forms.Control
继承(或者更好地从System.Windows.Forms.Panel
继承,这样你就不需要使用PanelMain
了)。 - 删除属性中的所有属性(不支持设计时...)。
- 通过代码将
ImagesComboBox
控件放置在你的表单上(同样,不支持设计时...)。 ComboHandleButton
类需要稍微修改(目前,在 CF 中,只有Form
控件可以使用其Graphics
类)。- 还有其他一些小的更改。
未来发展可能性
- 添加键盘支持。
- 为图像添加
Text
属性(如图像名称),这样ImagesComboBox
也能返回此属性。 - 能够添加单个图像,而不仅仅是通过
ImageList
。 - 随心所欲... :)。