Проблема с конструктором пользовательских элементов управления со списком цветов

#vb.net #winforms #graphics #combobox #user-controls

#vb.net #winforms #графика #combobox #пользовательские элементы управления

Вопрос:

У меня есть пользовательский элемент управления, который отображает выбор цвета в раскрывающемся списке, и он работает хорошо.
Я обнаружил, что производительность была низкой при использовании нескольких элементов управления в одной форме, поэтому я изменил ее, чтобы сохранить индекс цвета в коллекции элементов.
Это работает хорошо, но конструктор заполняется большим массивом значений, и это приводит к появлению пустых элементов в элементе управления.

Как мне запретить дизайнеру сохранять элементы?

Вот код дизайнера, который мне не нужен:

 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
  

Ответ №1:

Поскольку вы добавляете выбор цвета в поле со списком.Коллекция элементов, конструктор форм сериализует эту коллекцию, добавляя все элементы в файл Form.Designer.vb. Это также происходит, когда вы добавляете элементы в поле со списком, используя панель свойств в конструкторе: тот же эффект.

Вместо этого вы можете установить источник данных для ComboBox: это быстрее, и добавляемые вами объекты не сериализуются. Я также предлагаю добавлять эти значения не в конструктор элементов управления, а в переопределение OnHandleCreated(): значения загружаются только при создании дескриптора элемента управления во время выполнения, поэтому вы не загружаете (не очень полезные) коллекции элементов в конструкторе.
Поскольку дескриптор может быть воссоздан во время выполнения более одного раза, для этого есть проверка (чтобы избежать создания коллекции более одного раза).

Здесь я использую метод GetStandardValues() от ColorConverter для создания коллекции известных цветов, исключая из перечисления цвета, для которых установлено свойство IsSystemColor.
Коллекция хранится в массиве цветных объектов, названных здесь 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()
  

Системные цвета имеют индексы < 27 и> 167 (я предлагаю не полагаться на эти значения).

Я внес несколько изменений в пользовательский элемент управления:

  • Когда элемент управления является производным от существующего класса, мы не подписываемся на события (например, DrawItem ), мы переопределяем методы, которые вызывают события (например, OnDrawItem() ), затем вызываем base ( MyBase ), чтобы вызвать событие (в конце концов, мы также можем этого не делать, если это необходимо). Таким образом, мы всегда на шаг впереди.
  • Часть рисования нуждалась в некотором рефакторинге:
    • Фон элемента фактически был нарисован 3 раза
    • Одноразовый объект должен быть объявлен с Using помощью инструкции, поэтому мы не забываем их утилизировать: очень важно, когда речь идет о графических объектах.
    • Заменено Graphics.DrawString() на TextRenderer.Нарисуйте текст, чтобы сохранить исходный чертеж.
    • Упростил вычисления: здесь важно действовать как можно быстрее.
    • Таким образом, также удалите все Try/Catch блоки: дорогостоящие и не очень нужные (не используйте Try/Catch блоки при рисовании, несколько If условий и некоторые ограничения — например, Math.Min(Math.Max() ) — лучше).
    • Также переопределен OnMeasureItem() для изменения высоты элементов, установлен в Font.Height 4 (довольно стандартный).
    • Другие вещи, которые вы можете увидеть в исходном коде.

Я изменил SelectedColor пользовательское свойство, чтобы оно было более надежным и работало как с OnSelectedIndexChanged(), так и с 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 New и InitializeComponent() оно больше не понадобится.
 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
  

Вот как это работает:

Пользовательский элемент управления цветным списком