#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
Вот как это работает: