65.9K
CodeProject 正在变化。 阅读更多。
Home

ImagesComboBox 控件

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.89/5 (10投票s)

2005年5月30日

10分钟阅读

viewsIcon

55346

downloadIcon

680

一个组合框,其项本身就是图片,而不是在运行时才绘制图片。

Sample Image - ImagesComboBox.jpg

引言

我需要我的客户从图片列表中选择一张图片,但这并不是我表单上的主要功能。图片列表是复杂且加载了大量控件的庞大 GUI 的一部分。如果我不是想让他选择图片,我会给他一个带有值的 ComboBox。但它确实是图片。所以最合乎逻辑的做法是使用一个可以容纳图片而不是值的组合框。

有什么大惊小怪的?普通的 ComboBox 也能做到,不是吗?!

是的,普通的 ComboBox 可以显示图片。你可以通过添加以下代码来做到这一点

(我假设你有一个名为 ComboBox1ComboBox 和一个名为 ImageList1ImageList,其中包含你表单上的图片。)

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 添加了一个名为 PanelHeaderPanel,一个名为 PanelMainPanel 和一个继承的按钮 btnComboHandlePanelHeader 将是关闭状态的 ComboBoxbtnComboHandle 是从 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——它应该是原始副本,但有以下两个更改

  1. 确保每个容器首先被添加到其容器中,然后才添加其自己的子控件。
  2. 将每两行“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 时,它会获得焦点,这意味着 PanelHeaderPanelMain 和控件本身将失去焦点,这会引发 Lost Focus 事件并关闭 ImageComboBox。然后 btnComboHandleClick 事件触发并发现 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
  • 随心所欲... :)。
© . All rights reserved.