Дата публикации статьи: 29.12.2005 19:19

Как создать свой элемент управления в VB.NET (часть 2)

Как и было обещано, теперь будем делать ЭУ, который совместит в себе свойства полосы прокрутки, ползунка и окошка с текущим числовым значением. Мене всегда этого окошка не хватало, приходилось всегда приделывать «Label» к ползунку и полосе прокрутки. Кроме этого, мне не нравилось, что у ползунка надо устанавливать свойство «Orientation», он, что сам не может догадаться об этом, когда его создают. Так как мы хотим создать совершенно новый ЭУ, проще будет наследовать сам «Control». Так, что будем создавать ЭУ почти с нуля.
Сначала определимся со свойствами.

  1. Хотя мы и планируем автоматически определять ориентацию, все равно такое свойство «Orientation» придется завести, но будет оно только для чтения.
  2. Нужно еще задавать размер окошка для индикации числового значения. Если оно будет равно нулю, то его просто не показываем. «WinRazmer». Окошки можно расположить слева или справа (при горизонтальном варианте), вверху или внизу (при вертикальном варианте). Поэтому будет логическое свойство «WinLocate».
  3. Две кнопки на концах полосы тоже будут иметь задаваемый размер «ButRazmer». Кроме этого я хочу завести логическое свойство «ButMinMax», которое будем определять, где у полосы минимум и максимум. Это особенно важно при вертикальном варианте, ноль не всегда внизу, иногда требуется и наоборот.
  4. Ползунок тоже захочет иметь свой размер «PolzRazmer» и еще «PolzStyle». Как Вы уже догадались, это будет перечисляемое свойство, принимающее значения: прямоугольник, овал, ромб, треугольник, стрелка и так далее.
  5. Размеры окошка, кнопок и ползунка можно задавать в пикселях, а можно в процентах от второго размера. В первом случае, при изменении высоты горизонтальной прокрутки, кнопка будет просто вытягиваться. Во втором, она будет еще и расширяться, пытаясь сохранить свои пропорции. Чтобы реализовать оба варианта добавим логическое свойство «RazmerStyle».
  6. Разумеется три числовых значения: минимальное, максимальное и текущее. «Minimum», «Maximum», «Value». Кроме этого два свойства, отвечающие за шаг, при щелчке на кнопках или полях «SmallChange» и «LargeChange».
  7. Пять градиентных заливок: окно, кнопки, ползунок, поле не заполненное и роле заполненное. Ну, пусть еще и для текста, уговорили.
  8. Для улучшения внешнего вида добавим обводящую рамку вокруг всего ЭУ. Поэтому просто добавим свойство «FrameColor».
  9. Теперь события, которые придется отловить: нажатие на ползунок и процесс его перетаскивания, нажатия на кнопки (для увеличения или уменьшения с малым шагом), нажатия на поля (большой шаг) и, наконец, щелчок по окошку для сброса в минимальное состояние. Можно добавить, что повторный щелчок устанавливает в максимум.
  10. Есть еще события, которые мы сгенерируем для программиста, который будет пользоваться нашим ЭУ (например, событие, когда «Value» достигает максимума). Об этом поговорим после создания графической части.

Теперь можно приступить к созданию ЭУ. Жмем «Add Class…» и создаем новый класс «NewTrackBar» и копируем с нашей «NewPanel.vb» практически все (лично я копирую все строки, а потом переименовываю название класса и убираю лишнее). С иконкой тоже можно поступить аналогичным образом: правой кнопкой на «NewPanel.bmp», жмем «Copy», потом правой кнопкой на «NewControl» и «Paste». Теперь переименовываем «Copy of NewPanel.bmp» в «NewTrackBar.bmp». Осталось заняться рисованием иконки, что не составит большого труда. Для тех, кому лень, может воспользоваться вот таким атрибутом:

<ToolboxBitmap(GetType(TrackBar))> _
    Public Class NewTrackBar

Здесь мы в качестве иконки задаем стандартную иконку другого ЭУ. Можно вместо «TrackBar» поставить название класса любого другого ЭУ «Panel», «Label» и так далее. У этого атрибута есть еще два варианта написания, но я считаю, что они не стоят упоминания, незачем себе голову забивать тем, чем не придется пользоваться.
Написание свойств, несмотря на помощь самой студии, процесс все равно утомительный, поэтому я сел и написал свой макрос. Как только я познакомился с VS2003 и увидел, что можно писать макросы как в Worde, я сразу же нашлепал себе кнопок, которые вставляют часто встречающийся текст. Например:

    Sub Impor()
        DTE.ActiveDocument.Selection.Text = "Imports System."
    End Sub
    

К сожалению возможности не такие как в VBA, я так и не смог сделать вызов формы. Немного помучавшись (целый день угрохал) я, наконец, добился своего.

Sub FunZF()
        Dim s, w, t(7), pp, pt, pm As String
        Dim p() As String = {"Boolean", "Integer", "Long", "String", "Image", "Color"}
        t(0) = "'    Property 11111() As 22222"
        t(1) = "    Get"
        t(2) = "        Return m11111"
        t(3) = "    End Get"
        t(4) = "    Set(ByVal Value As 22222)"
        t(5) = "        m11111 = Value : MyBase.Invalidate()"
        t(6) = "    End Set"
        t(7) = "End Property"
        Dim i%
        For i = 0 To t.Length - 1 : w = w & t(i) & Chr(13) : Next i
        s = InputBox("", " Название свойства ?", "")
        w = w.Replace("11111", s)
        For i = 0 To p.Length - 1
            pt = pt & i & " - " & p(i) & vbCrLf
        Next
        pm = "    Private m" & s & " As "
        s = InputBox(pt, " Тип свойства ?", "") : pp = s
        If InStr("0123456789", s.Substring(0, 1)) > 0 Then
            i = System.Convert.ToInt16(s)
            If i < p.Length Then pp = p(i)
        Else : pm = pm & "New "
        End If
        ' вставляем наверх внутреннюю переменную
        Dim te As TextSelection = DTE.ActiveDocument.Selection
        te.ClearBookmark() : te.SetBookmark()
        te.StartOfDocument() : DTE.Find.FindWhat = "Private m" : DTE.Find.Execute()
        te.StartOfLine(0) : te.Text = pm & pp : te.NewLine()
        te.NextBookmark() : te.ClearBookmark()
        ' печатаем свойство
        w = w.Replace("22222", pp)
        DTE.ActiveDocument.Selection.Text = w : Call ss(8)
    End Sub
 
    Sub ss(ByVal ns As Integer)
        Dim te As TextSelection = DTE.ActiveDocument.Selection
        te.LineUp(False, ns) : te.StartOfLine(1)
        DTE.ExecuteCommand("Edit.UncommentSelection") : te.LineDown(False, ns)
    End Sub
 

Надеюсь, мне не надо объяснять, куда поместить данный код и как сделать кнопку для его вызова. Интересно, а среди Вас есть любители писать макросы для VS2003? Мне как-то не встречались на форуме вопросы на эту тему.
Для разминки добавим свойству «Gradient» еще пару «Sub»

    Public Sub New(ByVal c1 As Int32, ByVal c2 As Int32, ByVal g As Int16)
        Me.Color1 = Color.FromArgb(c1)
        Me.Color2 = Color.FromArgb(c2)
        Me.ColorN = g
    End Sub
    Public Sub Copy(ByVal kg As Gradient)
        Me.Color1 = kg.Color1
        Me.Color2 = kg.Color2
        Me.ColorN = kg.ColorN
    End Sub

Первая позволяет задавать градиент с помощью трех целых чисел, а вторая – делает копию с другого градиента. Позже вы оцените их полезность.
Добавим также еще одну функцию.

Overrides Function ToString() As String
        Return mColor1.ToArgb & "," & mColor2.ToArgb & "," & Val(mColorN)
    End Function

Это функция заменяет текст в странице свойств напротив имени градиента с совершенно не несущего никакой информации «NewControl.Gradient» на три числа записанных через запятую, которые и задают градиент.
Итак, перечисляемые типы созданы, внутренние переменные и свойства тоже. Для краткости приведу только самое начало.

Public Class NewTrackBar
    Inherits Control
    Enum Orientations
        Горизонтально = 0
        Вертикально = 1
    End Enum
    Enum PolzStyles
        Прямоугольник = 0
        Овал = 1
        Ромб = 2
        Треугольник = 3
        Стрелка = 4
    End Enum
    Private WithEvents mGrad0Fon As New Gradient(24)
    Private WithEvents mGrad1Fon As New Gradient(5)
    Private WithEvents mGrad2Fon As New Gradient(18)
    Private WithEvents mGrad3Fon As New Gradient(-7667573, -65281, 8)
    Private WithEvents mGrad4Fon As New Gradient(-5658199, -8355712, 8)
    Private WithEvents mGrad0Text As New Gradient(-16777216, -16777216, 0)
    Private mOrientation As New Orientations
    Private mLargeChange As Integer = 10
    Private mSmallChange As Integer = 1
    Private mValue As Integer = 25
    Private mMaximum As Integer = 100
    Private mMinimum As Integer = 0
    Private mRazmerStyle As Boolean = False
    Private mPolzStyle As New PolzStyles
    Private mPolzRazmer As Integer = 12
    Private mButMinMax As Boolean = False
    Private mButRazmer As Integer = 16
    Private mWinLocate As Boolean = False
    Private mWinRazmer As Integer = 30
    Private mGradSource As NewTrackBar = Nothing
    Private mGradSTyle As Boolean = False
    Private mFrameColor As Color = Color.DarkGray

Свойство «mGradSource» тоже скопировалось с «NewPanel.vb» и я не стал его убирать, пусть будет. Как Вы уже, наверное, заметили, я всем свойствам дал начальные значения. Должен же вновь создаваемый ЭУ как-то при рождении выглядеть. Откуда я взял такие хитрые числовые значения у градиента? Не угадали, не с потолка и не из пальца. Технология такая: на вновь созданном ЭУ я менял цвета его градиентов, а потом из окошка его свойства копировал в файл «NewTrackBar.vb». Теперь стало понятно, для чего были добавления в «Gradient.vb»? К сожалению, изменением начального внешнего вида Вы займетесь, когда ЭУ будет нарисован, а пока мы еще до этого не дошли.
Для начала определимся с ориентацией нашего ЭУ. Для этого переопределим событие «onResize». Для тех, кто забыл, повторим. В левом выпадающем списке выбираем «Overrides», а в правом событие «onResize». После чего в полученную заготовку вставляем следующий код. Я очень надеюсь, что все, кто читает данную статью, уже сами могут, без посторонней помощи его написать.

Protected Overrides Sub OnResize(ByVal e As System.EventArgs)
        mOrientation = Orientations.Горизонтально
        If Height > Width Then mOrientation = Orientations.Вертикально
        MyBase.Invalidate()
    End Sub
    

Так как наш новый ЭУ намного сложнее чем прежний, то возможны ситуации, когда задание некоторых значений свойствам будут, мягко говоря, бессмысленными. Нельзя же задавать размер окошка или кнопок больше размера самого ЭУ. Поэтому наложим ограничения на задание этим свойствам. Для примера возьмем ползунок. Минимальная его ширина (или высота при вертикальном расположении) не может быть меньше, скажем 5 точек. Иначе за что его мышкой таскать? С другой стороны ширина не должна быть слишком большой иначе ползунок будет неприлично полный (хотя о вкусах не спорят). Поэтому сначала определим возможные границы допустимых значений, а потом проверим, входит ли новое значение в них. Если нет, то: «К сожалению, господа, но свободных мест нет».

Property PolzRazmer() As Integer
        Get
            Return mPolzRazmer
        End Get
        Set(ByVal Value As Integer)
            Dim Rmin, Rmax As Integer
            If mRazmerStyle Then
                ' размер в процентах
                Rmax = 70 : Rmin = 500 / Height
                If mOrientation = Orientations.Вертикально Then Rmin = 500 / Width
            Else
                ' размер в пикселях
                Rmin = 5 : Rmax = 0.7 * Height
                If mOrientation = Orientations.Вертикально Then Rmax = 0.7 * Width
            End If
            If Rmax < Rmin Then Rmax = 3 * Rmin + 5
            If (Value >= Rmin And Value <= Rmax) Or Value = 0 Then
                mPolzRazmer = Value : MyBase.Invalidate()
            Else
                If DesignMode Then
                    MsgBox("Размер должен быть от " & Rmin & " до " & Rmax, _
                                    MsgBoxStyle.Critical, "Ошибка")
                End If
            End If
        End Set
    End Property

Как вы уже догадались, сообщение об ошибке выдается только в режиме конструктора. В приведенном коде есть пара возможных деления на нуль, а это влечет за собой необходимость переопределить свойства «Width» и «Height» или надеяться, что нулевые размеры нашему ЭУ никто не задаст. Ограничения на окошко и кнопки попробуйте осилить самостоятельно.
Кое-кто, наверное, подумал, что раз уже есть свойства «Width» и «Height», то их можно проконтролировать прямо в «onResize» и если их значения будут нулевыми, то изменить их. Рискните и увидите, что Вас ожидает. Отрицательный опыт тоже бывает полезным.
Ну а мы перейдем к самому интересному. Будем рисовать наш ЭУ. Это уже не простая панель, тут все гораздо сложнее, а главное вариантов изображения несколько и все их надо учесть, никакой не пропустить. Для начала заведем массив прямоугольников, который будет хранить размер и положение каждого подэлемента нашего ЭУ: окошка, двух кнопок, двух полей и ползунка. Итого 6 штук. Потом мы их просто закрасим и все. Кроме этого, когда будем обрабатывать нажатие мышкой, легко можно определить, какому из прямоугольников принадлежит координата мышки. Это требует, чтобы массив был общим для всех и находился в том же блоке, что и наши внутренние переменные.
Для тех, кто уже чувствует в себе силы самому написать «OnPaint», предлагая сравнить свой вариант с моим.

Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
        If Width < 2 Or Height < 2 Then Exit Sub
        Dim r0, r1, r2, r3, r4,i  As Integer
        Try
            For i = 0 To 6
                mRec(i) = New Rectangle(0, 0, Width - 1, Height - 1)
            Next
            Dim k, m As Single ' доля заполнения и масштаб для процентов
            k = (mValue - mMinimum) / (mMaximum - mMinimum)
            Dim nv As Integer = 0
            If mOrientation = Orientations.Вертикально Then nv = 1
            If mWinLocate Then nv = nv + 2
            If mButMinMax Then nv = nv + 4
            ' имеем 8 вариантов расположения 
            Select Case nv
                Case 0 ' горизонтально, окошко слева
                    m = 1 : If mRazmerStyle Then m = Height / 100
                    r0 = mWinRazmer * m : r3 = mPolzRazmer * m : r4 = mButRazmer * m
                    mRec(0).Width = r0 : mRec(3).Width = r3 : mRec(4).Width = r4 : mRec(5).Width = r4
                    mRec(4).X = r0 : mRec(5).X = Width - r4
                    r2 = (Width - r0 - 2 * r4) * k
                    r1 = Width - r0 - 2 * r4 - r2
                    mRec(1).Width = r1 : mRec(2).Width = r2
                    mRec(2).X = r0 + r4 : mRec(1).X = r0 + r4 + r2
                    mRec(3).X = mRec(1).X - r3 / 2
                Case 1 ' вертикально, окошко вверху
                    m = 1 : If mRazmerStyle Then m = Width / 100
                    r0 = mWinRazmer * m : r3 = mPolzRazmer * m : r4 = mButRazmer * m
                    mRec(0).Height = r0 : mRec(3).Height = r3 : mRec(4).Height = r4 : mRec(5).Height = r4
                    mRec(4).Y = Height - r4 : mRec(5).Y = r0
                    r2 = (Height - r0 - 2 * r4) * k
                    r1 = Height - r0 - 2 * r4 - r2
                    mRec(1).Height = r1 : mRec(2).Height = r2
                    mRec(1).Y = r0 + r4 : mRec(2).Y = r0 + r4 + r1
                    mRec(3).Y = mRec(2).Y - r3 / 2
                Case 2 ' горизонтально, окошко справа
                Case 3 ' вертикально, окошко внизу
                Case 4 ' тоже, что 0, но минимум справа
                Case 5 ' тоже, что 1, но минимум вверху
                Case 6 ' тоже, что 2, но минимум справа
                Case 7 ' тоже, что 3, но минимум вверху
                Case Else
            End Select
            Dim EU As NewTrackBar = Me
            If mGradSTyle = True Then EU = mGradSource
            If mWinRazmer > 0 Then
                FillGrad(e.Graphics, EU.mGrad0Fon, mRec(0))
                FillText(e.Graphics, EU.mGrad0Text, mRec(0), mValue, Font)
            End If
            FillGrad(e.Graphics, EU.mGrad1Fon, mRec(1))
            FillGrad(e.Graphics, EU.mGrad2Fon, mRec(2))
            FillGrad(e.Graphics, EU.mGrad4Fon, mRec(4))
            FillGrad(e.Graphics, EU.mGrad4Fon, mRec(5))
            If mPolzStyle = PolzStyles.Прямоугольник Then
                FillGrad(e.Graphics, EU.mGrad3Fon, mRec(3))
            Else : Dim nf As Integer = mPolzStyle
                FillFigure(e.Graphics, EU.mGrad3Fon, mRec(3), nf)
            End If
            DrawFrame(e.Graphics, mFrameColor, 1, mRec(6))
        Catch ex As Exception
        End Try
 


Здесь я описал только два основных варианта нашего ЭУ. Несмотря на казалось бы головоломный текст программы, все довольно просто, надо просто внимательно переделать уже имеющиеся варианты (эти два и сами то друг от друга почти не отличаются). Для тех, кто решил ограничиться только двумя приведенными вариантами, надо убрать свойства «mWinLocate» и «mButMinMax». Кроме этого появилось две новые графические функции, которые, разумеется, расположены в общем модуле «ModulControl.vb». «FillText» - для градиентного текста, хотя для данного ЭУ оно не принципиально. Здесь мы вынуждены, ограничится только четырьмя вариантами заливки текста, поэтому все остальные значения градиента приравниваются к вертикальной заливке.

Public Sub FillText(ByVal pov As Graphics, ByVal mg As Gradient, ByVal r As Rectangle, _
                                ByVal t As String, ByVal ft As Font)
        Dim n As LinearGradientMode = LinearGradientMode.Vertical
        If mg.ColorN < 4 Then n = mg.ColorN
    Try
            Dim rt As New Rectangle
            rt.Width = pov.MeasureString(t, ft).Width
            rt.Height = pov.MeasureString(t, ft).Height
            rt.X = r.X + (r.Width - rt.Width) / 2
            rt.Y = r.Y + (r.Height - rt.Height) / 2
            pov.DrawString(t, ft, New LinearGradientBrush(rt, mg.Color1, mg.Color2, n), rt.X, rt.Y)
    Catch ex As Exception
    End Try
End Sub

Обратите внимание на то, как вычисляется размер будущего текста и его центровка в заданном прямоугольнике.
Рисование фигур, дело намного более хлопотное и разных способов куча.

Public Sub FillFigure(ByVal pov As Graphics, ByVal mg As Gradient, ByVal r As Rectangle, ByVal nf As Integer)
        pov.SmoothingMode = SmoothingMode.HighQuality ' качаство линий
        Try
            Select Case nf
                Case 1 ' овал
                    Dim pat As New GraphicsPath
                    pat.AddEllipse(r)
                    Dim b As New PathGradientBrush(pat)
                    b.CenterPoint = New PointF(r.X + r.Width / 2, r.Y + r.Height / 2)
                    b.CenterColor = mg.Color2
                    Dim mc() As Color = {mg.Color1}
                    b.SurroundColors = mc
                    pov.FillPath(b, pat) : pov.DrawEllipse(New Pen(mg.Color1, 1), r)
                    b.Dispose() : pat.Dispose()
                Case 2 ' ромб
                    Dim xy() As Single = {0.0, 0.5, 0.5, 0.0, 1.0, 0.5, 0.5, 1.0}
                    Figure(pov, mg, r, xy)
                Case 3 ' треугольник
                    Dim xy() As Single = {0.0, 1.0, 0.5, 0.0, 1.0, 1.0}
                    If r.Width > r.Height Then
                        xy(2) = 0 : xy(5) = 0.5
                    End If
                    Figure(pov, mg, r, xy)
                Case 4 ' стрелка
                    Dim xy() As Single = {0.5, 0.0, 1.0, 0.5, 0.7, 0.45, 0.7, 1.0, 0.3, 1.0, 0.3, 0.45, 0.0, 0.5}
                    If r.Width > r.Height Then
                        xy(0) = 1.0 : xy(1) = 0.5 : xy(2) = 0.5 : xy(3) = 1.0
                        xy(4) = 0.55 : xy(5) = 0.7 : xy(6) = 0.0 : xy(7) = 0.7
                        xy(8) = 0.0 : xy(9) = 0.3 : xy(10) = 0.55 : xy(11) = 0.3
                        xy(12) = 0.5 : xy(13) = 0.0
                    End If : Figure(pov, mg, r, xy)
                Case Else
            End Select
        Catch ex As Exception
        End Try
    End Sub
    Private Sub Figure(ByVal pov As Graphics, ByVal mg As Gradient, ByVal r As Rectangle, ByVal rr() As Single)
        Try
            Dim k As Int16 = rr.Length / 2 - 1
            Dim mt(k) As Point
            For i As Int16 = 0 To k
                mt(i).X = r.X + r.Width * rr(i * 2)
                mt(i).Y = r.Y + r.Height * rr(i * 2 + 1)
            Next
            Dim b As New PathGradientBrush(mt)
            Dim mc() As Color = {mg.Color1, mg.Color2, mg.Color1}
            Dim pos() As Single = {0.0F, 0.5F, 1.0F}
            Dim cb As New ColorBlend
            cb.Colors = mc : cb.Positions = pos
            b.InterpolationColors = cb
            pov.FillPolygon(b, mt) : b.Dispose()
            pov.DrawPolygon(New Pen(mg.Color1, 1), mt)
        Catch ex As Exception
        End Try
    End Sub
    

По использованию GDI+ можно писать отдельную статью. И не одну даже. Сам я еще плаваю в GDI+ по-собачьи. Заранее извиняюсь за возможную тяжеловесность кода. Например, поворот стрелки или треугольника можно было осуществить при помощи афинских преобразований. Теперь приступим к механизму, который будет управлять нашим ЭУ. Для начала определим событие, которое будет происходить при изменении значения «Value» и заведем три внутренние переменные, отвечающие за номер прямоугольника: 0 – окошко, 1 – заполненная область, 2 – не заполненная область, 3 – ползунок, 4 и 5 кнопки. Две другие – это координаты мышки (понадобятся, когда будем тащить мышкой ползунок).

    Public Event ValueChanged(ByVal sender As Object, ByVal e As EventArgs)
    Private mNom, xm, ym As Int16
    

Теперь переопределяем события для мышки: мышка нажимается, отжимается, движется и кликает. Первые два события выглядят совсем безобидно:

Protected Overrides Sub OnMouseDown(ByVal e As System.Windows.Forms.MouseEventArgs)
        For i As Int16 = 0 To 5
            If mRec(i).Contains(e.X, e.Y) Then mNom = i
        Next
        If mNom = 3 Then xm = e.X : ym = e.Y
    End Sub
Protected Overrides Sub OnMouseUp(ByVal e As System.Windows.Forms.MouseEventArgs)
        Mnom = 6
    End Sub

А вот движение мышки – совсем другой коленкор. Приходится учитывать ориентацию нашего ЭУ. А вот учет того, где находится минимум и максимум (свойство «ButMinMax») я специально опустил, что бы Вы самостоятельно могли дописать код.

    Protected Overrides Sub OnMouseMove(ByVal e As System.Windows.Forms.MouseEventArgs)
        If mNom = 3 And e.Button = MouseButtons.Left Then
            Try
                If mOrientation = Orientations.Горизонтально Then
                    Dim x0, x1, dx
                    x0 = Math.Min(mRec(1).X, mRec(2).X)
                    x1 = Math.Max(mRec(1).X + mRec(1).Width, mRec(2).X + mRec(2).Width)
                    dx = e.X : If dx < x0 Then dx = x0
                    If dx > x1 Then dx = x1
                    mValue = (dx - x0) / (x1 - x0) * (mMaximum - mMinimum) + mMinimum
                Else
                    ' вертикально
                    Dim y0, y1, dy As Int16
                    y0 = Math.Min(mRec(1).Y, mRec(2).Y)
                    y1 = Math.Max(mRec(1).Y + mRec(1).Height, mRec(2).Y + mRec(2).Height)
                    dy = e.Y : If dy < y0 Then dy = y0
                    If dy > y1 Then dy = y1
                    mValue = (y1 - dy) / (y1 - y0) * (mMaximum - mMinimum) + mMinimum
                End If
                RaiseEvent ValueChanged(Me, New System.EventArgs)
                Me.Invalidate()
            Catch ex As Exception
            End Try
        End If
    End Sub
    

Наконец последнее событие. В зависимости по какому прямоугольнику произошло нажатие мышкой, происходит изменение значения ползунка. Тут происходит контроль за тем, чтобы не выйти за возможные границы значений и проверка того, что новое значение отличается от предыдущего. То есть при нажатии кнопки «уменьшить», когда значение и так минимально событие «ValueChanged» не будет происходить. Логически это справедливо.

    Protected Overrides Sub OnClick(ByVal e As System.EventArgs)
        Dim nz As Integer = mValue ' начальное значение
        Select Case mNom
            Case 0
                If mValue > mMinimum Then
                    mValue = mMinimum
                Else : mValue = mMaximum
                End If
            Case 1 : mValue = mValue + mLargeChange
            Case 2 : mValue = mValue - mLargeChange
            Case 4 : mValue = mValue - mSmallChange
            Case 5 : mValue = mValue + mSmallChange
            Case Else
        End Select
        If mValue < mMinimum Then mValue = mMinimum
        If mValue > mMaximum Then mValue = mMaximum
        If nz <> mValue Then RaiseEvent ValueChanged(Me, New System.EventArgs)
        MyBase.Invalidate()
    End Sub
    

Теперь сделаем небольшую паузу и поработаем с нашим новым ЭУ на тестовой форме. Градиентных свойств много, редактировать их, даже если подключить форму, как это сделано в «NewPanel» тоже не выход. Сделаем такой редактор свойств, который бы мог редактировать у любого ЭУ сразу все градиентные свойства, включая и текстовый.
Для этого нам необходим еще один новый ЭУ типа «Label», чтобы был градиентный текст. Создадим его (я специально засек время и уложился за 6 минут).
Добавляем новый класс «NewLabel», копируем все полностью из «NewPanel». Затем с помощью поиска и замены заменяем «NewPanel» на «NewLabel». Здесь будьте внимательны, замена должна быть не во всем проекте, а только в файле «NewLabel.vb». Надо заменить наследование, то есть вместо «Panel» надо написать «Control». В дизайнере тоже надо изменить наследование с «ParentControlDesigner» на просто «ControlDesigner», так как надпись не является контейнером для других ЭУ в отличие от панели. Это касается и нашего ползунка. Добавляем новое свойство «Grad0Text», полный аналог уже имеющегося «Grad0Fon». И изменяем немного «OnPaint», который будет выглядеть так:

Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
        Try
            Dim r As New Rectangle(0, 0, Width, Height)
            Dim EU As NewLabel = Me
            If mGradSTyle = True Then EU = mGradSource
            FillGrad(e.Graphics, EU.Grad0Fon, r)
            FillText(e.Graphics, EU.Grad0Text, r, Me.Text, Me.Font)
            Dim ns As Integer = mFrameStyle
            DrawFrame(e.Graphics, mFrameColor, ns, r)
        Catch ex As Exception
        End Try
End Sub

Осталось только задать начальное значение «Grad0Text», если только Вы не скопировали строку из «NewTrackBar.vb» и расширить обработку имеющегося события «mGrad0Fon_GradChanged», добавив еще одно событие через запятую (Handles mGrad0Fon.GradChanged, mGrad0Text.GradChanged). Вот и все!!!

Оставим без изменения нашу старую форму «GradEdit.vb» и сделаем более совершенную «GradEditM.vb». Кое-что со старой формы скопируем: кнопку и список. Вместо шести счетчиков создадим с теми же именами шесть наших «NewTrackBar». Вместо «NewPanel» будет «NewLabel» с тем же именем. Добавляем еще два списка: для различных градиентов «ListF» и «ListFT» список-переключатель «Фон-Текст».
Здесь я сразу привожу уже работающий редактор, с помощью которого редактируется наш ЭУ.
Для этого надо передать список всех градиентов, их названий, которые поместим в список и название ЭУ для полного комфорта. Лучше всего заякорить все элементы к краям формы, чтобы при ее растягивании увеличивалась только надпись-образец «NP».
Теперь рассмотрим код. В общем модуле заведем целый массив градиентов:

Public Mgr(19) As Gradient

Это значит, я рассчитываю, что у меня когда-нибудь будет ЭУ с десятью парами фон-текст. Далее при загрузке формы, через свойство «Tag» передается название ЭУ, список названий градиентов и список текстовых образцов. Он нужен для того, чтобы форма могла отличить просто фон от фона с текстом.

    Dim kf As Integer ' количество различных фонов
    Dim nt As Integer ' номер текущего фона в списке
    Dim n2 As Integer ' номер в списке фон-текст
    Dim tm(9) As String ' образцы текста
 
Private Sub GradEditM_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) _
                                                            Handles MyBase.Load
        Dim t0 As String = Convert.ToString(Me.Tag)
        Dim t1() As String = t0.Split(";")
        Me.Text = "Редактируем - " & t1(0)
        Dim t2() As String = t1(1).Split(",")
        Dim t3() As String = t1(2).Split(",")
        For i As Int16 = 0 To t3.Length - 1 : tm(i) = t3(i) : Next
        ListF.Items.Clear() : ListF.Items.AddRange(t2) : kf = t2.Length
        nt = 0 : ListF.SelectedIndex = nt
        n2 = 0 : ListFT.SelectedIndex = n2 : NovGrad()
    End Sub
 

Следующая «Sub NovGrad» нужна при выборе нового градиента и задании ползункам значений цветов, а списку «ListN» – направления градиента. Кроме этого, если фон существует без текста, то автоматически происходит переход на редактирование фона. Ну, нельзя редактировать градиент текста, если его нет. Редактируемые градиенты в массиве расположены так: сначала все градиенты фона, потом соответствующие им градиенты текста. Надеюсь, у нас не будет ЭУ, у которого на одном фоне будет два разных градиента текста, иначе придется редактор переделывать.

Sub NovGrad()
        NP.Grad0Fon.Copy(Mgr(nt))
        NP.Grad0Text.Copy(Mgr(nt + kf))
        NP.Text = tm(nt) : NP.Refresh()
        If NP.Text = «» Then n2 = 0 : ListFT.SelectedIndex = 0
        NR1.Value = Mgr(nt + kf * n2).Color1.R
        NG1.Value = Mgr(nt + kf * n2).Color1.G
        NB1.Value = Mgr(nt + kf * n2).Color1.B
        NR2.Value = Mgr(nt + kf * n2).Color2.R
        NG2.Value = Mgr(nt + kf * n2).Color2.G
        NB2.Value = Mgr(nt + kf * n2).Color2.B
        ListN.SelectedIndex = Mgr(nt + kf * n2).ColorN
End Sub
 

Далее у нас идет обработка ползунков и списка направлений градиента.

    Private Sub NR1_ValueChanged(ByVal sender As Object, ByVal e As System.EventArgs) _
                        Handles NR1.ValueChanged, NG1.ValueChanged, NB1.ValueChanged, _
                        NR2.ValueChanged, NG2.ValueChanged, NB2.ValueChanged
        Dim c1 = Color.FromArgb(NR1.Value, NG1.Value, NB1.Value)
        Dim c2 = Color.FromArgb(NR2.Value, NG2.Value, NB2.Value)
        If n2 = 0 Then
            NP.Grad0Fon.Color1 = c1 : NP.Grad0Fon.Color2 = c2
        Else
            NP.Grad0Text.Color1 = c1 : NP.Grad0Text.Color2 = c2
        End If : NP.Refresh()
    End Sub

    Private Sub ListN_SelectedIndexChanged(ByVal sender As System.Object, _ 
                                       ByVal e As System.EventArgs) Handles ListN.SelectedIndexChanged
        If n2 = 0 Then
            NP.Grad0Fon.ColorN = ListN.SelectedIndex
        Else
            NP.Grad0Text.ColorN = ListN.SelectedIndex
        End If : NP.Refresh()
    End Sub

Тут все дело в том, что в данный момент редактируется, фон или текст. Следующие два элемента осуществляют переход от одного редактируемого градиента к другому.

    Private Sub ListF_SelectedIndexChanged(ByVal sender As System.Object, _ 
		ByVal e As System.EventArgs) Handles ListF.SelectedIndexChanged
        nt = ListF.SelectedIndex : NovGrad()
    End Sub
 
    Private Sub ListFT_SelectedIndexChanged(ByVal sender As System.Object, _ 
		ByVal e As System.EventArgs) Handles ListFT.SelectedIndexChanged
        n2 = ListFT.SelectedIndex : NovGrad()
    End Sub
 

Наконец внесение изменений в сам ЭУ.

    Private Sub ButSave_Click(ByVal sender As System.Object, _
		ByVal e As System.EventArgs) Handles ButSave.Click
        Mgr(nt).Copy(NP.Grad0Fon)
        If NP.Text <> "" Then Mgr(nt + kf).Copy(NP.Grad0Text)
    End Sub
 

Здесь у нас в отличие от предыдущего редактора градиента, каждая пара градиентов фон-текст записывается в ЭУ отдельно и форма не закрывается. Теперь рассмотрим вызов формы из ЭУ. Разумеется, у нас уже есть свой дизайнер для нашего «NewTrackBar». Текст их практически не меняется и надо будет сделать один на целую группу разных ЭУ. Различие заключается в том, является ЭУ контейнером или нет. В самом же ЭУ добавляем такой код.

    Sub GradEditorM()
        If DesignMode Then
            Dim fe As New GradEditM
            Mgr(0) = Me.mGrad0Fon : Mgr(5) = Me.mGrad0Text
            Mgr(1) = Me.mGrad1Fon : Mgr(6) = Me.mGrad0Text
            Mgr(2) = Me.mGrad2Fon : Mgr(7) = Me.mGrad0Text
            Mgr(3) = Me.mGrad3Fon : Mgr(8) = Me.mGrad0Text
            Mgr(4) = Me.mGrad4Fon : Mgr(9) = Me.mGrad0Text
            fe.Tag = "NewTrackBar;Окошко,Не заполненное поле, _
			Заполненное поле,Ползунок,Кнопки;199,,,,,,"
            fe.ShowDialog() : MyBase.Invalidate()
        End If
    End Sub

Казалось бы, элементам массива с номерами больше 5 можно ничего и не присваивать. Но я это делаю, чтобы обновить ссылки на всякий пожарный. Дело в том, что в NET это принципиальное отличие от VB6. И очень удобное, если конечно умеешь им пользоваться.
Свое обещание показать, как сделать свой редактор свойства я обязательно выполню на следующем ЭУ. Вообще-то это очень похоже на то, что делалось выше, только вызывается через кнопочку в странице свойств, расположенную рядом с самим свойством, как у фонта или цвета.
Впереди у нас еще другие интересные ЭУ: те, которые не располагаются на форме (типа таймер, ToolTip), ЭУ содержащие коллекции (наборы страниц, различные списки) и такие хитрющие бяки как меню. (Александр Воробьев mailto: Alexandr_post3@Rambler.ru) Продолжение следует…