Дата публикации статьи: 27.01.2006 14:28

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

В этой части рассмотрим создание ЭУ, который не размещается на форме имеет коллекцию свойств. Больше всего для этого подходит контекстное меню.
Пример создания своего меню, на базе существующего вы можете посмотреть у Темура Бобохидзе в его «ResourseDLL». Количество кода, который пришлось ему написать, для придания меню вида как в «Office 2003» так велико, что у меня возник вопрос: «А не проще сделать все с нуля, чем переделывать?». Дело в том, что меню это не какая-то там кнопка. Здесь замешано много разных компонентов: само меню, пункты меню, коллекция пунктов, редактор коллекции. И приходится все эти компоненты переделывать, а иначе кое-что работает не так как надо.
Разные ЭУ имеют разные коллекции, и пример построение своей коллекции даст больше понимания в этом вопросе, чем пример с какой-то конкретной коллекцией. Кроме этого можно всегда плюнуть на неподдающуюся переделке вредную коллекцию и создать свою собственную.
Для начала надо создать экземпляр коллекции. Это что-то вроде сложного свойства. Здесь есть два основных варианта – это экземпляр коллекции на базе объекта или компонента (контрола). Разница в том, что компонент или контрол получают собственные имена и их можно видеть в раскрывающемся списке на странице свойств. А объекты ведут себя поскромнее, собственных имен не имеют и, как правило, редактируются при помощи редактора коллекций или собственного редактора типа, который мы обязательно создадим (это я обещал еще в 1 части).
Стандартный экземпляр строки меню – это компонент. Если не верите, то просмотрите его происхождение в «Object Browser». Очень полезная вещь для понимания того, откуда взялся тот или иной компонент, кто его родитель. Наш экземпляр строки меню будет обычный объект, потому что я хочу иметь один обработчик события меню, а не целую кучу, как в стандартном меню. Кроме этого наше меню не будет иметь горячих клавиш, зато будет раскрываться или появляться разными способами, будет полупрозрачным. Если делать новый ЭУ, то не повторять уже имеющееся, а делать что-то действительно новое.
Итак, какие же у нашего пункта меню будут свойства?

  • Text, Checked, Enabled, Visible – без этого набора не обойтись.
  • PicN0 – номер картинке в ImageList (что за меню без картинок?).
  • Sost – состояние пункта меню (основное, с галочкой, с отметиной, разделитель и содержащий подменю).
  • Level – уровень меню. Я не хочу делать коллекцию, в которой элементы сами содержат коллекции, это слишком сложно. Кроме этого предложенный вариант отличается от стандартного и можно будет сравнить достоинства и недостатки разных подходов.
  • Name – может не совсем удачное название свойства. Это то, что будет возвращаться в обработчик события после клика мышкой. То есть, если есть разные пункты меню, но действия должны быть одинаковые, то и это свойство тоже должно совпадать.

Создаем новый класс «NmenuItem.vb». Текст привожу только до первого свойства, остальные Вы напишете сами. Обратите внимание на два новых атрибута:

  • Serializable – без него не будет сохраняться коллекция.
  • DefaultValue – значения по умолчанию (вообще-то нужны для сокращения текста в «Component Designer generated code»).
Imports System.ComponentModel
<Serializable()> _
Public Class NMenuItem
Enum Sostojnie As Integer
        Нормальное = 0
        Отмеченное = 1
        Выбранное = 2
        Разделитель = 3
        Родитель = 4
    End Enum
    Private mText As String = "Новый"   ' текст строки меню
    Private mSost As Sostojnie           ' состояние
    Private mChecked As Boolean = False ' галочка или отметка на строке
    Private mEnable As Boolean = True   ' пункт меню доступен
    Private mVisible As Boolean = True  ' пункт меню виден
    Private mPicN0 As Integer = -1      ' номер иконки
    Private mLevel As Integer = 0       ' номер уровня меню
    Private mName As String = "New"     ' возвращаемое имя нажатой строки 
					' (совпадения допускаются)

    Public Sub New()
        MyBase.New()
    End Sub

<Description("Текст пункта меню"), DefaultValue("Новый")> _
    Public Property Text() As String
        Get
            Return mText
        End Get
        Set(ByVal Value As String)
            mText = Value
        End Set
    End Property

Теперь переходим к созданию коллекции. Это тоже будет класс, который можно создать, как отдельный файл, но я предлагаю дописать его в этот же. Пусть и экземпляр коллекции, сама коллекция и редактор коллекции находятся вместе.
Коллекция будет уже наследовать базовую коллекцию. Их вообще-то несколько, есть очень даже специфические, такие например как стек или очередь. В нашем варианте я считаю, больше всего подходит «ArrayList», представляющий собой динамический массив.
Для работы нашей коллекции надо переопределить основные функции. Для любых других коллекций этот класс будет отличаться только именем экземпляра, так что можете его смело в дальнейшем копировать и заменять только «NMenuItem» на другое имя.

Public Class NMenuCollection
    Inherits ArrayList
    Public Sub New()
        MyBase.New()
    End Sub
    Public Shadows Function Add(ByVal e As NMenuItem) As Integer
        Return MyBase.Add(e)
    End Function
    Public Shadows Sub AddRange(ByVal ee() As NMenuItem)
        MyBase.AddRange(ee)
    End Sub
    Public Shadows Function Contains(ByVal e As NMenuItem) As Boolean
        Return MyBase.Contains(e)
    End Function
    Public Shadows Function Item(ByVal i As Integer) As NMenuItem
        Return MyBase.Item(i)
    End Function
    Public Shadows Sub Remove(ByVal e As NMenuItem)
        MyBase.Remove(e)
    End Sub
    Public Shadows Sub RemoveAt(ByVal i As Integer)
        MyBase.RemoveAt(i)
    End Sub
    Overrides Function ToString() As String
        Return "Колекция"
    End Function
    

Теперь, наконец, дошла очередь до самого меню. Наследовать будем стандартное контекстное меню, но только ради его способности появляться в качестве сноски в свойствах других ЭУ и конечно ради его способности вызываться правой кнопкой мыши. Все остальное будет наше.
Какие же свойства у нас будут?
Во-первых, конечно сама коллекция пунктов меню. Фонт для текста и логическое свойство для выбора: автоматическое определение высоты строк меню или задаваемое. Высота строк меню или ширина боковой панели для картинок (пусть они будут совпадать, если это кого-то не устраивает, тот может завести себе два свойства). Три разных градиента для фона и текста основного, выделенного и недоступного состояния пунктов меню. Два градиента фона для боковой панели и общего фона. Цвет и стиль обводящей рамки для всего меню. Цвет обводящей рамки для выделенного пункта меню. Способ появления и исчезновения (два перечисляемых свойства, плюс временной параметр). Прозрачность в процентах и два размера в процентах для картинок (основного и выбранного состояния). Можно еще добавить звук при открытии и закрытии меню.
Ну и конечно событие, когда пользователь нажимает по пункту меню. И возвращаемое свойство «KlikName», чтобы по нему узнать пункт кликнутого меню.
Еще я хочу обговорить некоторые особенности поведения нашего меню. Например, когда пользователь ставит галочки напротив пунктов меню, я хочу, чтобы меню не закрывалось, а давало возможность поставить несколько галочек. Поэтому сделаем так, чтобы если пользователь кликнет по боковой панели, галочка ставится и снимается без закрытия меню, а если по самой строке, то меню закрывается. Аналогично и для выбора из нескольких вариантов как у «RadioButton». Будем считать, что у нас в одном развороте меню одна группа переключателей. Можно потом сделать и несколько таких групп, поставив условием, что они должны обязательно разделяться разделителем (сепаратором). Ну вот, пожалуй, и все. Переходим к созданию самого контекстного меню. Здесь нам придется описать все наши свойства, причем без «MyBase.Invalidate()», потому что никакой перерисовки во время проектирования меню нет. Обратите внимание на то, как определено свойство «Font». Использованы квадратные скобки, чтобы отличить переменную от точно такого же зарезервированного слова.

Public Class NewConMenu
    Inherits ContextMenu
    Enum AnimMenu
        none = 0
        Проявление = 1
        Из_угла = 2
        Сверху_плавно = 3
        Слева_плавно = 4
    End Enum
    Private CMItem As New NmenuCollection
    Private mFont As New Font("Times New Roman", 12.0!, FontStyle.Regular, GraphicsUnit.Point, 204)
    Private mAutoHeight As Boolean = True
    Private mItemHeight As Integer ' высота пунктов меню
    Private WithEvents mGrad0Fon As New Gradient ' основное состояние
    Private WithEvents mGrad0Text As New Gradient(-2293501, -808696, 1)
    Private WithEvents mGrad1Fon As New Gradient(14) ' выделенное
    Private WithEvents mGrad1Text As New Gradient(-2293501, -808696, 1)
    Private WithEvents mGrad2Fon As New Gradient(13) ' недоступное
    Private WithEvents mGrad2Text As New Gradient(-2293501, -808696, 1)
    Private WithEvents mGrad3Fon As New Gradient   ' фон боковой панели
    Private WithEvents mGrad4Fon As New Gradient   ' общий фон
    Private mFrameColor As Color = Color.LightGray ' цвет обводящей рамки
    Private mFrameStyle As NewPanel.FrameStyles = NewPanel.FrameStyles.Линия
    Private mBorderColor As Color = Color.Red      ' цвет рамки выделенного состояния
    Private mAnimOpen As AnimMenu = AnimMenu.none
    Private mAnimClose As AnimMenu = AnimMenu.none
    Private mAnimTimer As Integer = 100
    Private mOpacity As Integer = 100
    Private mProc As Integer = 80
    Private mProcV As Integer = 90
    Public Event Klik(ByVal sender As Object, ByVal e As EventArgs)
    Private mKlikName As String ' имя кликнуторо пункта меню
    Private mStyle As Boolean = False
    Private mSource As NewConMenu
    Private mPicList As System.Windows.Forms.ImageList
    Public Sub New()
        MyBase.New()
    End Sub
    Protected Overrides Sub Finalize()
        CMItem = Nothing : MyBase.Finalize()
    End Sub
    <Editor(GetType(MenuEditor), GetType(System.Drawing.Design.UITypeEditor)), _
    DesignerSerializationVisibility(DesignerSerializationVisibility.Content)> _
        Public Property MenItems() As NMenuCollection
        Get
            Return CMItem
        End Get
        Set(ByVal Value As NMenuCollection)
            CMItem = Value
        End Set
    End Property
    Public Property [Font]() As Font
        Get
            Return mFont
        End Get
        Set(ByVal Value As Font)
            mFont = Value
        End Set
    End Property
    

Имя нашей коллекции отличается от стандартной одной буквой, вместо «MenuItems» называем «MenItems». Старая коллекция никуда не делась, просто мы ее не используем. Поэтому при размещении нашего контекстного меню на форме, не надо использовать стандартный редактор меню, а то у нас будет две коллекции и два меню.
Перед определением коллекции у нас стоит ссылка на редактор свойства. Поэтому нам придется создать класс с именем «MenuEditor», который мы расположим там же где и сама коллекция.
Этот текст повторяется почти полностью во всех редакторах типа.

    Public Class MenuEditor
    Inherits System.Drawing.Design.UITypeEditor
    Public Sub New()
        MyBase.New()
    End Sub
Public Overloads Overrides Function GetEditStyle(ByVal context As _
	System.ComponentModel.ITypeDescriptorContext) As System.Drawing.Design.UITypeEditorEditStyle
        If Not (context Is Nothing) AndAlso Not context.Instance Is Nothing Then
            Return Drawing.Design.UITypeEditorEditStyle.Modal
        Else
            MyBase.GetEditStyle(context)
        End If
    End Function
Public Overloads Overrides Function EditValue(ByVal context As _ 
	System.ComponentModel.ITypeDescriptorContext, ByVal provider As System.IServiceProvider, _
	ByVal value As Object) As Object
        Dim FE As New FormMenuEditor
        FE.CM = CType(value, NMenuCollection) : FE.ShowDialog()
        Return FE.CM
    End Function
End Class

Отличие состоит в последней функции, где происходит вызов формы для редактирования коллекций и передачей им ссылки на нужную коллекцию.
Добавляем в проект форму, на которой разместим список «ListM», пару кнопок для добавления и удаления элемента коллекции (ButNew, ButDel) и четыре кнопки для навигации (ButUp, ButDown, ButLeft, ButRight). И самый интересный ЭУ – страницу свойств (PrGrid). Если его у Вас нет на панели инструментов, зайдите на закладку «Conponents» и добавьте из «.NET Framework Components» ссылку «PropertyGrid».
Все должно выглядеть так. Правда, похоже на стандартный редактор коллекция?
Я хотел, чтобы этот редактор меню выглядел почти как в VB6 и работал по тому же принципу.

Public CM As NMenuCollection
Dim nt As Integer ' Текущая строка
Dim ks As Integer ' количество строк
Private Sub FormEditor_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
    Call NovSp(0) : If ks > 0 Then Call NovEl()
End Sub
Private Sub ButNew_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
	Handles ButNew.Click
    Dim ne As New NMenuItem
    CM.Add(ne) : nt = ks : Call NovSp(nt) : Call NovEl()
End Sub
Private Sub ButDel_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ButDel.Click
    nt = ListM.SelectedIndex : If nt < 0 Then Exit Sub
    CM.RemoveAt(nt) : ks = ks - 1 : If nt >= ks Then nt = ks - 1
    Call NovSp(nt) : Call NovEl()
End Sub
Private Sub ListM_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) _
	Handles ListM.SelectedIndexChanged
    Call NovEl()
End Sub
Private Sub NovSp(ByVal n As Integer) ' Обновить список
    ListM.Items.Clear() : ks = CM.Count : If ks = 0 Then Exit Sub
    For i As Integer = 0 To ks - 1
        Dim s As String = CM.Item(i).Text
        ListM.Items.Add(s.PadLeft(Len(s) + 2 * CM.Item(i).Level, "."))
    Next : If n < ks Then ListM.SelectedIndex = n
End Sub
Private Sub NovEl() ' Обновить элемент в странице свойств
    ks = CM.Count
    If ks = 0 Then
        PrGrid.SelectedObject = Nothing
    Else : If ListM.SelectedIndex < 0 Then ListM.SelectedIndex = 0
        PrGrid.SelectedObject = CM.Item(ListM.SelectedIndex)
    End If
End Sub
Private Sub ButUp_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
	Handles ButUp.Click
    nt = ListM.SelectedIndex
    If nt > 0 Then CM.Reverse(nt - 1, 2) : NovSp(nt - 1)
End Sub
Private Sub ButDown_Click(ByVal sender As System.Object, ByVal e As System.EventArgs)  _	
	Handles ButDown.Click
    nt = ListM.SelectedIndex
    If nt < ks - 1 Then CM.Reverse(nt, 2) : NovSp(nt + 1)
End Sub
Private Sub ButLeft_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ButLeft.Click
    nt = ListM.SelectedIndex
    If CM.Item(nt).Level > 0 Then CM.Item(nt).Level -= 1 : NovSp(nt)
End Sub
Private Sub ButRight_Click(ByVal sender As System.Object, ByVal e As System.EventArgs)  _	
	Handles ButRight.Click
    nt = ListM.SelectedIndex
    CM.Item(nt).Level += 1 : NovSp(nt)
End Sub
Private Sub PrGrid_SelectedGridItemChanged(ByVal sender As Object, _
ByVal e As System.Windows.Forms.SelectedGridItemChangedEventArgs) Handles PrGrid.SelectedGridItemChanged
    If e.OldSelection.Label() = "Text" Then
        nt = ListM.SelectedIndex : NovSp(nt)
    End If
End Sub


Последний фрагмент нужен для того, чтобы внести изменения в список, когда редактируется свойство «Text» в странице свойств.
Еще конечно нужно сделать проверку корректности всего меню при закрытие формы, но это я оставляю для самостоятельной работы. Теперь у нас есть практически все, кроме самого изображения меню. Если раньше у нас всегда было «OnPain» для перерисовки, то у меню его нет. Зато есть «OnPopup». Это вызов нашего контекстного меню, которое запустит форму, предварительно передав ей ссылку на само контекстное меню, а сама форма все и сделает. То есть наше контекстное меню – это отдельная форма, которая все рисует на себе и как надо срабатывает. Еще форме надо задать номер в списке и уровень меню (это будет нужно для подменю). Конечно, без координат, где должно раскрыться меню тоже не обойтись.

    Protected Overrides Sub OnPopup(ByVal e As System.EventArgs)
        Dim fm As New FormCM
        fm.MM = Me : fm.M0 = 0 : fm.Ur = 0
        fm.Left = Cursor.Position.X() - 10 : fm.Top = Cursor.Position.Y() - 10
        fm.Show()
    End Sub
    

Остается только нарисовать эту самую форму «FormCM», которая не имеет заголовка (свойство «FormeBorderStyle» устанавливаем в «none») и будет закрываться, если мышь уйдет за границы формы. Форма не должна показываться на панели и поэтому свойство «ShowInTaskbar» должно быть «False».
Определим общие переменные.

    Public Ur, M0 As Integer ' Уровень и начальный пункт меню
    Public MM As NewConMenu  ' ссылка на первоначальное меню
    Dim PM() As Int16        ' массив номеров пунктов меню
    Dim rc() As Rectangle    ' прямоугольники для рисования пунктов меню
    Dim kp As Int16 ' всего пунктов меню
    Dim tm As Int16 ' текущий пункт меню
    Dim mx, my As Integer ' позиция мыши
    Dim i, j, k, n, x0, y0, h0, w0, hf, hs, mst, mw, mh As Int16
    Dim mew, meh, hmew, hmeh, shag As Integer ' размеры и шаги анимации
    Dim fm As FormCM      ' заготовка для подменю
    

Сначала форма загружается и необходимо определить ее размеры, которые зависят от количества строк меню и их типа (будем считать, что разделитель (сепаратор) имеет 10% высоты от обычной строки и закраску как у боковой панели или добавим еще один градиент и высоту разделителя).

    Private Sub FormCM_Load(ByVal sender As System.Object, ByVal e As System.EventArgs)  _
							Handles MyBase.Load
        ' определяем размеры формы
        Dim h As New IntPtr
        h = Me.Handle
        Dim g As Graphics = Graphics.FromHwnd(h)
        hf = MM.ItemHeight  ' высота пунктов меню = высоте шрифта
        If MM.AutoHeight Then hf = g.MeasureString("0", MM.Font).Height
        w0 = 10 + hf : h0 = 10 ' высота и ширина формы
        kp = 0 : hs = hf / 10 : If hs < 3 Then hs = 3 ' высота разделителя
        j = MM.MenItems.Count - 1 : mst = 0
        ReDim PM(j)
        ReDim rc(j)
        For i As Int16 = M0 To j
            Dim m1 As NMenuItem = MM.MenItems(i)
            If m1.Level = Ur Then
                If m1.Visible Then
                    PM(kp) = i : kp += 1
                    If m1.Sost <> 3 Then
                        h0 = h0 + hf ' обычный пункт меню
                        k = g.MeasureString(m1.Text, MM.Font).Width
                        If m1.Sost = 4 Then k = k + hf / 2
                        If k > mst Then mst = k ' вычисляем самую длинную строку
                    Else
                        h0 = h0 + hs ' разделитель
                    End If
                End If
            End If
        Next
        w0 = w0 + k + 10 ' 10 точек - это рамка меню
        g.Dispose() : shag = 20 ' шаг анимации
        tm = 0 : Me.Text = " " : Call OpenMenu()' запускаем появление меню
    End Sub
    

Надеюсь, что комментарии в тексте программы дают понимание того, что делается. Для анимации нам понадобятся два таймера на форме «Tim» при появлении меню и «TimС» при его закрытии.

   Private Sub OpenMenu()
        Select Case MM.AnimOpen
            Case NewConMenu.AnimMenu.none : Me.Width = w0 : Me.Height = h0
                Me.Opacity = MM.Opacity / 100
            Case NewConMenu.AnimMenu.Проявление : Me.Opacity = 0 : Me.Width = w0 : Me.Height = h0
                Tim.Interval = MM.AnimTimer / MM.Opacity : Tim.Enabled = True
            Case NewConMenu.AnimMenu.Из_угла : Me.Opacity = MM.Opacity / 100 
                Me.Width = 10 : Me.Height = 10
                mew = w0 / shag : meh = h0 / shag : hmew = w0 / shag : hmeh = h0 / shag
                Tim.Interval = MM.AnimTimer / shag : Tim.Enabled = True
            Case NewConMenu.AnimMenu.Сверху_плавно 
            Case NewConMenu.AnimMenu.Слева_плавно 
            Case Else
        End Select 
    End Sub
    Private Sub Tim_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) _
	 Handles Tim.Tick
        Select Case MM.AnimOpen
            Case NewConMenu.AnimMenu.Проявление
                Me.Opacity = Me.Opacity + 0.01
                If Me.Opacity * 100 >= MM.Opacity Then  Tim.Enabled = False
           Case NewConMenu.AnimMenu.Из_угла
                mew = mew + hmew : meh = meh + hmeh
                If mew >= w0 Or meh >= h0 Then
                    mew = w0 : meh = h0 : Tim.Enabled = False
                End If : Me.Width = mew : Me.Height = meh
            Case NewConMenu.AnimMenu.Сверху_плавно
            Case NewConMenu.AnimMenu.Слева_плавно
        End Select
    End Sub
    

Два других вида появления меню – это всего лишь вариация второго варианта.
Аналогично будет выглядеть и закрытие меню. Попробуйте написать сами.
Самый большой кусок кода – это конечно «OnPaint». Можно было бы использовать готовые элементы типа нашей панели и отлавливать перемещение мыши и ее нажатия, но я предлагаю все делать с нуля. Сначала мы рисуем общий фон и обводящую рамку, потом боковой бордюр и наконец сами пункты меню. Здесь код привожу полностью.

Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
        Try
            ' общий фон и рамка
            Dim r0 As New Rectangle(0, 0, Width, Height)
            FillGrad(e.Graphics, MM.Grad4Fon, r0)
            Dim rr As New Rectangle(0, 0, Width, Height)
            Dim nf As Integer = MM.FrameStyle
            If nf <> 3 Then
                rr.X += 3 : rr.Y += 3 : rr.Height -= 6 : rr.Width -= 6
            End If
            DrawFrame(e.Graphics, MM.FrameColor, nf, rr)
            ' рисуем бордюр
            Dim r As New Rectangle(5, 5, Width - 10, Height - 10)
            Dim rb As New Rectangle(5, 5, hf, Height - 10)
            FillGrad(e.Graphics, MM.Grad3Fon, rb)
            Dim rt As New Rectangle(5 + hf, 5, Width - 10 - hf, hf)
            Dim EU As NewConMenu = MM
            If MM.GradStyle = True Then EU = MM.GradSource
            Dim gr, gt As Gradient
            y0 = 5
            For i = 0 To kp - 1 : j = PM(i)
                Dim m1 As NMenuItem = MM.MenItems(j)
                Dim t As String = m1.Text
                Dim tip As Integer = m1.Sost
                Dim ng As Integer = 0
                ' ng=0 - обычное состояние
                ' ng=1 - выделенное состояние
                ' ng=2 - недоступное состояние
                ' ng=3 - разделитель
                ' ng=4 - недоступное выделенное состояние
                rt.Height = hf : rt.Y = y0
                Dim rf As New Rectangle(rt.X - hf, rt.Y, rt.Width + hf, rt.Height)
                rc(i) = New Rectangle(rf.X, rf.Y, rf.Width, rf.Height)
                If tip = 3 Then rt.Height = hs : ng = 3 : rc(i).Width = 1
                If rf.Contains(mx, my) And tip <> 3 Then ng = 1 : tm = j
                If m1.Enabled = False Then
                    If ng = 1 Then ng = 4 Else ng = 2
                End If
                Select Case ng
                    Case 0 : gr = EU.Grad0Fon : gt = EU.Grad0Text
                    Case 1 : gr = EU.Grad1Fon : gt = EU.Grad1Text
                    Case 2, 4 : gr = EU.Grad2Fon : gt = EU.Grad2Text
                    Case 3 : gr = EU.Grad3Fon : gt = EU.Grad3Fon
                    Case Else
                End Select
                FillGrad(e.Graphics, gr, rt)
                FillText(e.Graphics, gt, rt, t, MM.Font, 150)
                Dim hk As Integer = MM.Proc * hf / 100 ' высота картинки
                If ng = 1 Then hk = MM.ProcV * hf / 100
                Dim rk As New Rectangle(0, 0, hk, hk)
                ' центрируем картинку
                rk.X = 5 + (hf - hk) / 2 : rk.Y = rf.Y + (hf - hk) / 2
                Select Case tip
                    Case 0 ' обычная иконка
                        Try
                            Dim ka As Image = MM.PicList.Images(MM.MenItems(i).PicN0)
                            e.Graphics.DrawImage(ka, rk)
                        Catch ex As Exception
                        End Try
                    Case 1 ' галочка
                        Dim cv As Color = Color.FromArgb(50, gt.Color1.R, gt.Color1.G, _
				gt.Color1.B)
                        DrawFrame(e.Graphics, cv, 10, rk)
                        If tip = 1 And MM.MenItems(i).Checked Then FillFigure(e.Graphics, _
				gt, rk, 10)
                    Case 2 ' переключатель
                        Dim cv As Color = Color.FromArgb(50, gt.Color1.R, gt.Color1.G, _
				gt.Color1.B)
                        DrawFrame(e.Graphics, cv, 11, rk)
                        If tip = 2 And MM.MenItems(i).Checked Then FillFigure(e.Graphics, _
				gt, rk, 11)
                    Case 3 ' разделитель (сепаратор)
                        FillGrad(e.Graphics, MM.Grad3Fon, rt)
                    Case 4 ' родитель
                        Try
                            Dim ka As Image = MM.PicList.Images(MM.MenItems(i).PicN0)
                            e.Graphics.DrawImage(ka, rk)
                        Catch ex As Exception
                        End Try
                        ' треугольник для дочернего меню
                        Dim rd As New Rectangle(rt.X + rt.Width - hf / 2, rt.Y, hf / 2, hf)
                        FillFigure(e.Graphics, gt, rd, 12)
                    Case Else
                End Select
                ' рамка вокруг выделенного
                If ng = 1 Then DrawFrame(e.Graphics, MM.BorderColor, 1, rf)
                y0 = y0 + rt.Height
            Next
        Catch ex As Exception
        End Try
    End Sub

Теперь переходим к обработке событий на нашей форме. Тут сделаем небольшое отступление и поговорим о дочернем подменю, которое будет вызываться. Как связать все в кучу, чтобы правильно работало? Например, как узнать, что дочернее меню закрылось и надо закрыть родительское или наоборот не закрывать его, если мышка по-прежнему на нем? Я предлагаю грубый и простой способ – таймер «TimD». Когда дочернее меню открывается, запускаем таймер, которые каждые полсекунды спрашивает: «Дочерняя форма закрылась или нет?». Для этого, при загрузке формы мы присваиваем свойству «Me.Text» пробел, а при закрытие – пустую строку. И если дочерняя форма закрылась, то предпринимает соответствующие действия.
И так программируем события мыши: уход с формы, движение над формой и клик.

    Private Sub FormCM_MouseLeave(ByVal sender As Object, ByVal e As System.EventArgs) _
								Handles MyBase.MouseLeave
        If TimD.Enabled = False Then CloseMenu()
    End Sub
    Private Sub FormCM_MouseMove(ByVal sender As Object, ByVal e As _
	System.Windows.Forms.MouseEventArgs) Handles MyBase.MouseMove
        mx = e.X : my = e.Y
        If rc(tm).Contains(mx, my) = False Then
            TimD.Enabled = False : Me.Invalidate()
            Try
                fm.Close() : fm.Dispose()
            Catch ex As Exception
            End Try
        End If
    End Sub
    Private Sub FormCM_MouseDown(ByVal sender As Object, ByVal e As _
	System.Windows.Forms.MouseEventArgs) Handles MyBase.MouseDown
        mx = e.X : my = e.Y
        For i = 0 To kp - 1
            If rc(i).Contains(mx, my) Then
                Dim m1 As NMenuItem = MM.MenItems(PM(i))
                If m1.Enabled Then
                    Select Case m1.Sost
                        Case NMenuItem.Sostojnie.Нормальное
                            MM.KlikName = m1.Name : CloseMenu()
                        Case NMenuItem.Sostojnie.Отмеченное
                            m1.Checked = Not m1.Checked : Me.Invalidate() : MM.KlikName = m1.Name
                            If (hf - mx + rc(i).X) < 0 Then CloseMenu()
                        Case NMenuItem.Sostojnie.Выбранное
                            If m1.Checked = False Then
                                For l As Int16 = 0 To kp - 1
                                    If MM.MenItems(PM(l)).Sost = 2 Then
                                        MM.MenItems(PM(l)).Checked = False
                                    End If
                                Next : m1.Checked = True : Me.Invalidate() : MM.KlikName = m1.Name
                                If (hf - mx + rc(i).X) < 0 Then CloseMenu()
                            End If
                        Case NMenuItem.Sostojnie.Родитель ' открываем подменю
                            fm = New FormCM
                            TimD.Interval = 500 : TimD.Enabled = True
                            fm.MM = MM : fm.M0 = PM(i) : fm.Ur = m1.Level + 1
                            fm.Left = Me.Left + rc(i).X + rc(i).Width
                            fm.Top = Me.Top + rc(i).Y
                            fm.Show()
                        Case Else
                    End Select
                End If
            End If
        Next
    End Sub
    

Осталось совсем чуть-чуть, обработку таймера «TimD» и закрытие формы.

    Private Sub TimD_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles TimD.Tick
        Dim t$ = ""
        Try
            t = fm.Text
        Catch ex As Exception
        End Try
        If t = "" Then
            TimD.Enabled = False : Me.Invalidate()
            Dim mr As New Rectangle(Me.Left, Me.Top, Me.Width, Me.Height)
            If mr.Contains(Cursor.Position.X, Cursor.Position.Y) = False Then CloseMenu()
        End If
    End Sub
Private Sub FormCM_Closing(ByVal sender As Object, ByVal e As System.ComponentModel.CancelEventArgs) _
                                                                  Handles MyBase.Closing
    Me.Text = ""
End Sub


Единственным недостатком данного меню является его неумение определять, что он может выйти за пределы экрана. Но данное упущение можно подправить, если при загрузке, после определения размера сместить форму. Второй вариант – определить размер дочернего меню до его появления и дать начальные координаты так, что бы дочернее меню не вышло за пределы экрана.
Для редактирования градиентов добавьте дизайнер. Здесь придется наследовать уже не «System.Windows.Forms.Design.ParentControlDesigner» как у панели, а другой дизайнер «System.ComponentModel.Design.ComponentDesigner». Кроме этого придется заменить «Me.Control» на «Me.Component». Остальное без изменений.
Как некоторые уже догадались, сделать главное меню можно, используя наше контекстное. Основное видимое меню – это на самом деле панель с кнопками, при нажатии на которые, будет вызывать подменю.
Недостаток стандартного главного меню состоит в том, что нельзя увеличить высоту видимого меню. Мне этого пока не удалось. Поэтому и шрифт приходится использовать не слишком крупный.
Некоторые вопросы мне так и не удалось выяснить. Например, как программно добавить компонент на форму, что бы он появился в «Windows Form Designer generated code». Редакторы коллекций (напрмер: TabControl, Menu) могут их создавать. Весь вопрос, а как они это делают? Если кто знает, напишете мне, пожалуйста.
Немного позже я выложу библиотеку своих ЭУ. Там у меня есть и другие элементы, а не только те, которые описаны в статье. Осталось немного доработать ее, что бы не стыдно было показать.

Автор: Александр Воробьев