颜色组合框自定义控件设计器问题

Color ComboBox Custom Control designer problem

我有一个自定义控件,可以在下拉菜单中显示颜色选择,效果很好。
我发现同一个表单上有多个控件时性能很差,所以我将其更改为将颜色索引存储在 Items 集合中。
这很好用,但 Designer 会填充大量值,这会导致控件中出现空项。

如何阻止设计器存储项目?

这是我不想要的设计器代码:

Me.cboCWarcColor.Items.AddRange(New Object() 
    {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 
     19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 
     36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 
     53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 
     70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 
     87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 
     103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 
     116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 
     129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140}
)

这是自定义控件代码:

Imports System.Collections.Generic

Public Class ColorCombo
    Inherits System.Windows.Forms.ComboBox
    Private mSelectedColor As Color = Nothing
    Private Shared myColors As New List(Of Color)
    Private Shared myColorsIndices As New List(Of Object)

    Private Sub ColorCombo_DrawItem(ByVal sender As Object, ByVal e As System.Windows.Forms.DrawItemEventArgs) Handles Me.DrawItem
        Try
            If e.Index < 0 Or e.Index >= myColors.Count Then
                e.DrawBackground()
                e.DrawFocusRectangle()
                Exit Try
            End If
            ' Get the Color object from the Items list
            Dim aColor As Color = myColors.Item(e.Index) 'myColors.Item(e.Index)

            ' get a square using the bounds height
            Dim rect As Rectangle = New Rectangle(4, e.Bounds.Top + 2, CInt(e.Bounds.Height * 1.5), e.Bounds.Height - 4)


            ' call these methods first
            e.DrawBackground()
            e.DrawFocusRectangle()

            Dim textBrush As Brush
            ' change brush color if item is selected
            If e.State = DrawItemState.Selected Then
                textBrush = Brushes.White
            Else
                textBrush = Brushes.Black
            End If

            ' draw a rectangle and fill it
            Dim p As New Pen(aColor)
            Dim br As New SolidBrush(aColor)
            e.Graphics.DrawRectangle(p, rect)
            e.Graphics.FillRectangle(br, rect)

            ' draw a border
            rect.Inflate(1, 1)
            e.Graphics.DrawRectangle(Pens.Black, rect)
            ' draw the Color name
            e.Graphics.TextRenderingHint = Drawing.Text.TextRenderingHint.ClearTypeGridFit
            e.Graphics.DrawString(aColor.Name, Me.Font, textBrush, rect.Width + 5, ((e.Bounds.Height - Me.Font.Height) \ 2) + e.Bounds.Top)

            p.Dispose()
            br.Dispose()

        Catch ex As Exception
            e.DrawBackground()
            e.DrawFocusRectangle()
        End Try
    End Sub

    Public Sub New()
        ' This call is required by the Windows Form Designer.
        InitializeComponent()
        Try
            Dim aColorName As String
            Me.BeginUpdate()
            Items.Clear()
            SelectedItem = Nothing
            If myColors.Count = 0 Then
                Dim names() As String = System.Enum.GetNames(GetType(System.Drawing.KnownColor))
                For Each aColorName In names
                    If aColorName.StartsWith("Active") _
                    Or aColorName.StartsWith("Button") _
                    Or aColorName.StartsWith("Window") _
                    Or aColorName.StartsWith("Inactive") _
                    Or aColorName.StartsWith("HighlightText") _
                    Or aColorName.StartsWith("Control") _
                    Or aColorName.StartsWith("Scroll") _
                    Or aColorName.StartsWith("Menu") _
                    Or aColorName.StartsWith("Gradient") _
                    Or aColorName.StartsWith("App") _
                    Or aColorName.StartsWith("Desktop") _
                    Or aColorName.StartsWith("GrayText") _
                    Or aColorName.StartsWith("HotTrack") _
                    Or aColorName.StartsWith("Transparent") _
                    Or aColorName.StartsWith("Info") Then
                    Else
                        AddColor(Color.FromName(aColorName))
                    End If
                Next

            Else
                Me.Items.AddRange(myColorsIndices.ToArray)
            End If

        Catch
        Finally
            Me.EndUpdate()
        End Try
        ' Add any initialization after the InitializeComponent() call.

    End Sub

    Public Function AddColor(clr As Color) As Integer
        myColors.Add(clr)
        Dim idx As Integer = myColors.Count - 1
        myColorsIndices.Add(idx)
        Me.Items.Add(idx)
        Return idx
    End Function

    ''' <summary>
    ''' Returns a named color if one matches else it returns the passed color
    ''' </summary>
    Public Function GetKnownColor(ByVal c As Color, Optional ByVal tolerance As Double = 0) As Color
        For Each clr As Color In myColors
            If ColorDistance(c, clr) <= tolerance Then
                Return clr
            End If
        Next
        Return c
    End Function

    ''' <summary>
    ''' Returns index if one matches
    ''' </summary>
    Public Function ContainsColor(ByVal c As Color) As Integer
        Dim idx As Integer = 0
        For Each clr As Color In myColors
            If c.ToArgb = clr.ToArgb Then
                Return idx
            End If
            idx += 1
        Next
        Return -1
    End Function

    Sub ColorCombo_SelectedIndexChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.SelectedIndexChanged
        If SelectedIndex >= 0 Then
            mSelectedColor = myColors.Item(SelectedIndex)
        End If
    End Sub
    Public Property SelectedColor() As Color
        Get
            'If mSelectedColor.Name = "Transparent" Then
            '    Return Color.Black
            'End If
            Return mSelectedColor
        End Get

        Set(ByVal value As Color)
            Try
                Dim smallestDist As Double = 255
                Dim currentDist As Double = 0
                Dim bestMatch As Integer = 0
                Dim idx As Integer = -1
                For Each c As Color In myColors
                    idx += 1
                    currentDist = ColorDistance(c, value)
                    If currentDist < smallestDist Then
                        smallestDist = currentDist
                        bestMatch = idx
                    End If
                Next
                If Me.Items.Count >= bestMatch Then
                    Me.SelectedIndex = bestMatch
                End If
            Catch ex As Exception
                Debug.Print(ex.Message)
            End Try
        End Set
    End Property

    Private Function ColorDistance(ByRef clrA As Color, ByRef clrB As Color) As Double
        Dim r As Long, g As Long, b As Long
        r = CShort(clrA.R) - CShort(clrB.R)
        g = CShort(clrA.G) - CShort(clrB.G)
        b = CShort(clrA.B) - CShort(clrB.B)
        Return Math.Sqrt(r * r + g * g + b * b)
    End Function
End Class

由于您要将颜色选择添加到 ComboBox.Items 集合中,表单设计器会序列化此集合,将所有项目添加到 Form.Designer.vb 文件中。当您使用设计器中的“属性”窗格将项目添加到 ComboBox 时也会发生这种情况:相同 effect.

您可以改为设置 ComboBox 的数据源:速度更快,并且您添加的对象不会被序列化。我还建议不要在 Control Constructor 中添加这些值,而是在 OnHandleCreated() override 中:这些值仅在创建 Control Handle 时加载,位于 run-time,因此您不加载(不是很有用)设计器中的项目集合。
由于可以在 run-time 重新创建句柄不止一次,因此需要对其进行检查(以避免多次构建集合)。

这里,我使用的是 ColorConverter's GetStandardValues() method to build a collection of known colors, excluding from the enumeration colors that have the IsSystemColor 属性 集。
该集合存储在一个 Color 对象数组中,这里命名为 supportedColors.

你也可以过滤[Enum].GetValues()返回的集合得到相同的结果,例如:

Dim colors As Color() = [Enum].GetValues(GetType(KnownColor)).OfType(Of KnownColor)().
    Where(Function(kc) kc > 26 AndAlso kc < 168).
    Select(function(kc) Color.FromKnownColor(kc)).ToArray()

SystemColors 的索引 < 27 和 > 167(我建议不要依赖这些值)。

我对自定义控件做了一些更改:

  • 当控件派生自现有 class 时,我们不订阅事件(例如,DrawItem),我们覆盖引发事件的方法(例如,OnDrawItem()), 然后调用 base (MyBase) 来触发事件(最终,如果需要的话,我们也可以不这样做)。我们总是领先一步。
  • 绘图部分需要一些重构:
    • 项目的背景实际上被绘制了 3 次
    • 一次性对象应使用 Using 语句声明,因此我们不会忘记 处理它们:对于图形对象而言非常重要。
    • Graphics.DrawString() 替换为 TextRenderer.DrawText,以尊重原图。
    • 简化了计算:在这里尽可能快是很重要的。
    • 因此也删除所有 Try/Catch 块:成本高且不是真正需要的(绘图时不要使用 Try/Catch 块,一些 If 条件和一些约束 - 例如,Math.Min(Math.Max()) - 更好)。
    • 也覆盖 OnMeasureItem() 以更改项目的高度,设置为 Font.Height + 4(相当标准)。
    • 您可以在源代码中看到的其他内容。

我已经将 SelectedColor 自定义 属性 更改为更 可靠 并使其工作OnSelectedIndexChanged() and OnSelectionChangeCommitted().
所有项目都代表一种颜色,因此您可以将颜色选择为,例如:

Private Sub ColorCombo1_SelectionChangeCommitted(sender As Object, e As EventArgs) Handles ColorCombo1.SelectionChangeCommitted
    SomeControl.BackColor = DirectCast(ColorCombo1.SelectedItem, Color)
    ' Or
    SomeControl.BackColor = ColorCombo1.SelectedColor
End Sub

修改了组合框自定义控件:

  • 删除 Public Sub NewInitializeComponent() 中的内容,不再需要了。
Imports System.Collections.Generic
Imports System.ComponentModel
Imports System.Drawing
Imports System.Windows.Forms

Public Class ColorCombo
    Inherits ComboBox

    Private mSelectedColor As Color = Color.Empty
    Private supportedColors As Color() = Nothing

    Public Sub New()
        DropDownStyle = ComboBoxStyle.DropDownList
        DrawMode = DrawMode.OwnerDrawVariable
        FlatStyle = FlatStyle.Flat
        FormattingEnabled = False
        ' Set these just to show that the background color is important here
        ForeColor = Color.White
        BackColor = Color.FromArgb(32, 32, 32)
    End Sub

    Protected Overrides Sub OnHandleCreated(e As EventArgs)
        MyBase.OnHandleCreated(e)
        If DesignMode OrElse Me.Items.Count > 0 Then Return

        supportedColors = New ColorConverter().GetStandardValues().OfType(Of Color)().
            Where(Function(c) Not c.IsSystemColor).ToArray()

        ' Preserves a previous selection if any
        Dim tmpCurrentColor = mSelectedColor
        Me.DisplayMember = "Name"
        Me.DataSource = supportedColors
        If Not tmpCurrentColor.Equals(Color.Empty) Then
            mSelectedColor = tmpCurrentColor
            SelectedColor = mSelectedColor
        End If
    End Sub

    Private flags As TextFormatFlags = TextFormatFlags.NoPadding Or TextFormatFlags.VerticalCenter
    Protected Overrides Sub OnDrawItem(e As DrawItemEventArgs)
        e.DrawBackground()
        If e.Index < 0 Then Return

        Dim itemColor = supportedColors(e.Index)
        Dim colorRect = New Rectangle(e.Bounds.X + 1, e.Bounds.Y + 1, e.Bounds.Height - 2, e.Bounds.Height - 2)

        Using colorBrush As New SolidBrush(itemColor)
            e.Graphics.FillRectangle(colorBrush, colorRect)

            Dim textRect = New Rectangle(New Point(colorRect.Right + 6, e.Bounds.Y), e.Bounds.Size)
            TextRenderer.DrawText(e.Graphics, itemColor.Name, e.Font, textRect, e.ForeColor, Color.Transparent, flags)
        End Using

        e.DrawFocusRectangle()
        MyBase.OnDrawItem(e)
    End Sub

    Protected Overrides Sub OnMeasureItem(e As MeasureItemEventArgs)
        e.ItemHeight = Font.Height + 4
        MyBase.OnMeasureItem(e)
    End Sub

    Protected Overrides Sub OnSelectedIndexChanged(e As EventArgs)
        If SelectedIndex >= 0 Then mSelectedColor = supportedColors(SelectedIndex)
        MyBase.OnSelectedIndexChanged(e)
    End Sub

    Protected Overrides Sub OnSelectionChangeCommitted(e As EventArgs)
        mSelectedColor = supportedColors(SelectedIndex)
        MyBase.OnSelectionChangeCommitted(e)
    End Sub

    Public Property SelectedColor As Color
        Get
            Return mSelectedColor
        End Get
        Set
            mSelectedColor = Value
            If Not IsHandleCreated Then Return

            If mSelectedColor.IsKnownColor Then
                SelectedItem = mSelectedColor
            Else
                If supportedColors Is Nothing Then Return
                Dim smallestDist As Double = 255
                Dim currentDist As Double = 0
                Dim bestMatch As Integer = 0
                Dim idx As Integer = -1

                For Each c As Color In supportedColors
                    idx += 1
                    currentDist = ColorDistance(c, Value)
                    If currentDist < smallestDist Then
                        smallestDist = currentDist
                        bestMatch = idx
                    End If
                Next
                If supportedColors.Count >= bestMatch Then
                    mSelectedColor = supportedColors(bestMatch)
                    SelectedItem = mSelectedColor
                End If
            End If
        End Set
    End Property

    Private Function ColorDistance(clrA As Color, clrB As Color) As Double
        Dim r As Integer = CInt(clrA.R) - clrB.R
        Dim g As Integer = CInt(clrA.G) - clrB.G
        Dim b As Integer = CInt(clrA.B) - clrB.B
        Return Math.Sqrt(r * r + g * g + b * b)
    End Function

    Public Function GetKnownColor(c As Color, Optional ByVal tolerance As Double = 0) As Color
        For Each clr As Color In supportedColors
            If ColorDistance(c, clr) <= tolerance Then Return clr
        Next
        Return c
    End Function

    Public Function ContainsColor(c As Color) As Integer
        Dim idx As Integer = 0
        For Each clr As Color In Me.Items
            If c.ToArgb = clr.ToArgb Then Return idx
            idx += 1
        Next
        Return -1
    End Function
End Class

这是它的工作原理: