Как создать свой элемент
управления в VB.NET?
Намучившись с этим вопросом, потеряв массу времени и нервов, перерыв не одну книгу и посетив не один форум, я решил написать статью, которая помогла бы другим программистам в этом вопросе.
Мне самому очень помог Тимур
Бобохидзе и его библиотека «ResourseDLL», которая лежит на форуме VBStreets. Много полезного
удалось прочитать в книге Джона Коннелла «Разработка элементов управления
Microsoft® .NET на Microsoft Visual Basic® .NET».
Заранее извиняюсь за вольность изложения и терминологию. Объясняю все так, как я сам понимаю.
С чего начать? А с того, что сначала определиться с тем, какая стоит задача. То ли надо сделать один ЭУ (элемент управления), то ли целую группу. Поэтому берем лист бумаги, ручку и составляем список ЭУ: придумываем им названия, новые свойства. Я в начале делал все на ходу, а потом вынужден был все переделывать. Например, делал панель с градиентной заливкой фона и дал свойству имя FonGrad, а у надписи наоборот GradFon. Не дело, что одно и тоже свойство по-разному называется. Будут потом пользователи вспоминать неласковым словом. Да и самому будет легче работать, создавая следующий ЭУ, знай, копируй одинаковые свойства и добавляй новые.
Для начала сделаем что-нибудь попроще. Панель с градиентной заливкой фона. Потом посложнее - элемент типа прокрутки с окошком, где показывается значение.
Возьмем стандартный ЭУ «Panel» добавим ему свойств, а лишние уберем (BackColor и BackgroundImage). Почему беру именно «Panel»? А для того, чтобы наследовать у него свойство «контейнер», то есть у нового ЭУ оно тоже будет. Еще пару месяцев назад, это слово (наследовать) было без смысла. Теперь я, наконец, понял, что оно означает. Наследовать – это значит получить все, все, все, что предок имел и умел. Поэтому очень важно определить, а кто похож больше всего на новый ЭУ. Лично я предпочитаю чаще всего брать в качестве прародителя «Control», то есть делать свой элемент почти с нуля.
Начали! Запустили Visual Studio (у меня 2003). Выбрали «New Project» и «Class Library». Дали проекту имя «NewControl» и получили совершенно пустую заготовку с классом, который называется «Class1». Мы создаем библиотеку ЭУ, в которой будет несколько новых ЭУ, каждый из которых будет представлен как самостоятельный класс. Кроме этого будут еще классы, которые будут общими для всех ЭУ. Нет смысла делать для каждого ЭУ свой отдельный проект.
Меняем название класса на «NewPanel» и имя файла тоже, хотя это и не обязательно, но для удобства лучше, когда имя файла и имя ЭУ совпадают. Теперь укажем, кто же является родителем нашего нового ЭУ.
Public Class
NewPanel
Inherits Panel End Class |
Не хочет работать, ругается, потому что не понимает нас. Что ему надо? А надо ему ссылки на библиотеки, чтобы нормально работать. Открываем в проекте References, а там всего три ссылки, поэтому добавляем «System.Windows.Forms» для родителя и «System.Drawing» для возможности рисовать.
Все равно ругается. Вообще-то эти три строчки можно не писать, но тогда придется все имена указывать полностью. Вместо «Panel» - «System.Windows.Forms.Panel», а вместо «Color» - «System.Drawing.Color». Потому для лучшей читабельности добавляем на самый верх эти самые «Imports».
Imports System.Windows.Forms Imports System.Drawing Imports System.Drawing.Drawing2D Public Class NewPanel
Inherits Panel End Class |
Можно сказать, что все уже готово. Мы создали свой ЭУ, который полностью совпадает со стандартной панелью.
Как же его протестировать? Для этого добавим в проект новый проект. Жмем «File», «Add Project», «New Project», выбираем «Windows Application» и задаем имя «TestNewControl». Потом правой кнопкой по нему и выбрать «Set as StartUp Project». То есть мы в одном окне будем иметь и разрабатываемые ЭУ и проект для тестирования. Можно открыть VS второй раз и создать проект для тестирования там, но это не так удобно. Для того чтобы на панели инструментов появился значок с новым ЭУ надо во-первых создать DLL (жмем правой кнопкой на NewControl и выбираем Build), во-вторых сделать ссылку на DLL (переходим на «TestNewControl», на панели инструментов выбираем закладку «My User Control», правой кнопкой вызываем контекстное меню, выбираем «Add/Remote Items…», жмем «Browse…» и находим в папке «NewControl\bin» свою «NewControl.dll». У нас появилась шестеренка с именем «NewPanel». Разместим ее на форме а рядом обычную стандартную панель, как видим никакой разницы между ними нет. Свойства у них одинаковые и ведут они себя тоже одинаково. Пока еще одинаково. Сейчас мы их сделаем разными!
Во-первых иконка в виде шестеренки это ни куда не годится. Делаем свою иконку. На проекте «NewControl» правой кнопкой, выбираем «Add», далее «Add New Item…», отмечаем «Bitmap File» и задаем имя точно такое же как и у нашего ЭУ, то есть «NewPanel.bmp». Совпадение должно быть точным, вплоть до регистра всех букв! Иначе иконка к ЭУ не приклеится. Жмем «Open» и создаем свою картинку с помощью открывшегося редактора. Тут опять есть пара подводных камней. Первый – это размер иконки 16 х 16. Для этого надо перетащить белый квадратик (нижний справа) до нужного размера. Второй – при рисовании нельзя закрашивать самый нижний слева пиксель (надо оставить его белым, он задает прозрачный цвет). После закрываем окно редактора изображения, подтверждаем сохранение и последний штрих, выделяем в проекте нашу картинку, в окне свойств меняем свойство «Build Action» на «Embedded Resourse». Теперь надо опять «Build». Это надо делать каждый раз, когда делаем изменение в проекте нашей DLL. Теперь удаляем с панели инструментов шестеренку (это тоже обязательно надо делать, когда меняется картинка или добавляется новый ЭУ в нашей DLL). Снова добавляем на панель инструментов нашу DLL («Add/Remote Items…» и так далее).
После всех этих мытарств теперь можно спокойно редактировать наш ЭУ, а в тестовом проекте все будет автоматически отражаться после «Build». Правда, при серьезных изменениях в ЭУ рекомендую удалить с формы наш ЭУ, а потом поместить на форму новый. В старом добром VB6 с иконкой ЭУ было намного проще.
Создадим три новых свойства: два цвета и направление градиента. Назовем их например «Color1», «Color2», «ColorN». Для работы этих свойств задаем три внутренние переменные (я просто добавляю к имени букву «m») «mColor1», «mColor2», «mColorN». Это прием очень облегчает работу с программой. В книге Джон Коннелл использует такой знак-букву «_».
Private mColor1 As Color
Private mColor2 As Color
Private mColorN As LinearGradientMode |
Создание самих свойств довольно просто: набираем «Property Color1 as Color» и жмем Enter.
После чего в программе появляется следующий текст:
Property Color1() As
Color Get End Get Set(ByVal Value As
Color) End
Set End Property |
Молодец VS! Теперь добавляем в пустые строки довольно банальные вещи:
Property Color1() As Color Get Return
mColor1 End Get Set(ByVal Value As
Color) mColor1 = Value : MyBase.Invalidate() End Set
End Property |
Смысл первой строки – это возврат значения свойства, а второй – это задание свойству нового значения и перерисовка ЭУ после этого изменения.
Затем копируем данный фрагмент два раза и немного изменяем, чтобы получить два остальных свойства.
Property Color2() As
Color Get Return
mColor2 End Get Set(ByVal Value As
Color) mColor2 = Value : MyBase.Invalidate() End Set
End Property
Property ColorN() As LinearGradientMode Get Return
mColorN End Get Set(ByVal Value As
LinearGradientMode) mColorN = Value : MyBase.Invalidate() End Set End Property |
Теперь самое интересное – переопределение свойства OnPaint. Для этого выбираем в левом выпадающем списке (Overrides), потом в правом OnPaint. После чего появляется текст:
Protected Overrides
Sub OnPaint(ByVal
e As System.Windows.Forms.PaintEventArgs) End Sub |
Теперь пишем фрагмент программы, который закрашивает наш ЭУ:
Protected Overrides Sub
OnPaint(ByVal e As
System.Windows.Forms.PaintEventArgs) Dim r
As New
Rectangle(0, 0, Width - 1, Height - 1) e.Graphics.FillRectangle(New LinearGradientBrush(r, mColor1, mColor2,
mColorN), r) End Sub |
Первая строка – это задание прямоугольника равного нашему ЭУ. Вторая заполнением этого прямоугольника градиентной кистью.
Для полного смака остается задать начальный градиент при создании ЭУ, а то он какой-то серый. Для этого существует стандартный прием.
Public Sub New() Me.Color1
= Color.FromArgb(255, 221, 236, 254) Me.Color2
= Color.FromArgb(255, 129, 169, 226) Me.ColorN = LinearGradientMode.Vertical
End Sub |
То есть, когда создается новый ЭУ, то задаются начальные значения. Но есть и другой способ – это задание внутренним переменным начальных значений. Поэтому дописываем соответствующие строки. Какой вариант лучше? Мне оба нравятся.
Private mColor1 As Color = Color.FromArgb(255, 221, 236, 254)
Private mColor2 As Color = Color.FromArgb(255, 129, 169, 226)
Private mColorN As LinearGradientMode = LinearGradientMode.Vertical |
После всего жмем «Build», переходим на тестовую форму и создаем наш новый ЭУ.
Мы добавили три свойства, теперь уберем пару из страницы свойств. Для этого сначала необходимо переопределить эти свойства. Это делается аналогично «OnPaint». Выбираем в левом выпадающем списке (Overrides), потом в правом «BackColor». В результате появляется текст:
Public Overrides
Property BackColor() As
System.Drawing.Color Get End Get Set(ByVal Value As
System.Drawing.Color) End
Set End Property |
Теперь вставляем вот такой тект <Browsable(False)>
< Browsable(False)
> Public Overrides
Property BackColor() As
System.Drawing.Color Get End Get Set(ByVal Value As
System.Drawing.Color) End
Set End Property |
Ругается! Забыли добавить на
самый верх еще один Imports.
Imports System.ComponentModel |
Теперь мы можем пользоваться АТРИБУТАМИ без лишних проблем. Если бы не эта строчка, то пришлось бы писать < System.ComponentModel.Browsable(False)>.
Не забыли, что с «BackgroundImage» надо поступить аналогичным образом.
Что еще можно сделать при помощи атрибутов? Очень, очень многое. Это одна из мощнейших новых возможностей языка программирования. Все атрибуты пишутся внутри скобок <Attribut1(), Attribut2(), Attribut3()> через запятую перед определением свойства или класса.
Часто встречающиеся атрибуты:
Description("Первый цвет градиентной заливки") – описание свойства, которое появляется внизу страницы свойств.
Category("Gradient") – группирование свойств по группам в странице свойств. Когда вы выбираете, сортировка свойств по Categorized, а не по алфавиту, то свойства, принадлежащие к одной категории, располагаются рядом. Этим атрибутом создается новая категория и принадлежность свойства к этой категории.
Другие интересные атрибуты рассмотрим по мере усложнения нашего ЭУ.
Теперь поговорим немного о свойстве «градиентная заливка». У панели есть только одна градиентная заливка, но у других ЭУ может быть еще несколько таких заливок. Например, у кнопки может быть одна заливка, когда кнопка не нажата, вторая – мышка над кнопкой, третья – кнопка недоступна. Кроме трех заливок фона, можно иметь еще три градиента для текста. Итого 6х3=18 новых свойств. Можно и запутаться в них. Какой же выход? А что если сделать градиентное свойство сложным (например, как Font или Size) с плюсиком для раскрытия. Выгода очевидно: количество свойств в три раза меньше, а это и программа короче и редактировать свойства легче. Еще бы свой редактор для визуальной настройки этого свойства, чтобы сразу видеть результат.
Все это возможно!!!
Сначала создаем новый класс для нового свойства. Кажется, слышу недовольный возглас: «Делали, старались, а теперь все переделывай?». Ничего переделывать не придется, мы вырежем часть кода из нашего класса и вставим в другой.
В одном файле «*.vb» можно иметь несколько определений классов, но я предлагаю создать отдельный файл для нового сложного, составного свойства. Поэтому добавляем в проект «NewControl» новый класс и называем его «Gradient». Так как у нас одновременно присутствуют два проекта, будьте внимательны, когда добавляете новые элементы. Во избежание ошибок лучше пользоваться контекстным меню, а не главным меню наверху.
Теперь копируем все «Imports», которые стоят перед определением класса. Потом вырезаем определения трех внутренних переменных и вставляем их в новый класс. То же самое делаем и с тремя свойствами. Теперь уберем из наших свойств команды «MyBase.Invalidate()» и класс «Gradient» в принципе готов к использованию.
Теперь создаем в «NewPanel» внутреннюю переменную «mGrad0Fon» :
Private mGrad0Fon As Gradient |
Далее новое свойство:
Property Grad0Fon() As Gradient Get Return
mGrad0Fon End Get Set(ByVal Value As
Gradient) mGrad0Fon = Value : MyBase.Invalidate() End Set
End Property |
Немного подправляем «OnPaint». Старые переменные типа «mColor1» теперь выглядят так «mGrad0Fon.Color1», то есть надо между буквой «m» и именем переменной вставить имя новой переменной с точкой. Теперь «Build» и смотрим результат на тестовой форме. Думаю внесенные в наш ЭУ оказались слишком принципиальными и ЭУ исчез с панели инструментов. Придется его снова добавить. Добавили, поместили на форму, а он ни в какую не хочет работать, рисует красную рамку и чего-то там пишет. Если растянуть рамку элемента, то можно увидеть, что его не устраивает строка в которой мы пытались красить наш ЭУ. А если заглянуть в «Windows Form Designer generated code», там, где автоматически записываются свойства, то увидим вот что!
Me.NewPanel1.Grad0Fon
= Nothing |
Теперь кажется понятно. Мы пытались делать градиентную заливку не имея никаких данных о цвете и направлении. Короче послали нас «нафик». Сейчас мы это исправим. А надо-то всего – поставить ключевое слово «New» в строке с определение переменной.
Private mGrad0Fon As New Gradient |
Эти составные свойства такие привереды, подавай им все только новое. Есть и другая неприятность – нет в странице свойств нашего нового свойства.
За это отвечает атрибут, который пишется так:
TypeConverter(GetType(ExpandableObjectConverter))
Вставляем его и в странице свойств, появилось новое свойство с плюсиком, который раскрывает свойство и можно редактировать внутренние свойства. Но вот беда – не сохраняется наше свойство при старте проекта и сбрасывается в начальное состояние.
Тут необходим атрибут с очень длинным название:
DesignerSerializationVisibility(DesignerSerializationVisibility.Content)
Короче, заголовок и текст нового свойства будет выглядеть так:
<DesignerSerializationVisibility(DesignerSerializationVisibility.Content),
_
TypeConverter(GetType(ExpandableObjectConverter))>
_
Property Grad0Fon() As Gradient Get Return
mGrad0Fon End Get Set(ByVal Value As
Gradient) mGrad0Fon = Value : MyBase.Invalidate() End Set End Property |
Все вроде работает, но есть одна маленькая, но вредная неприятность. Когда меняем любое свойство ЭУ, то изменения отражаются сразу же, а вот с нашим новым свойством почему-то нет. Такое чувство, что строка «mGrad0Fon = Value : MyBase.Invalidate()» не работает. И действительно если ее убрать, то будет все, то же самое. По-видимому, тут нужен еще какой-то атрибут. Но, к сожалению, я не знаю. Может из Вас кто-то раскопает. Напишите мне обязательно. Но отчаиваться рано. Как говорят в армии: «Не хочешь, заставим!» Нужно, чтобы, когда изменяется одно из свойств градиента генерировалось событие, а наш ЭУ на это событие реагировал.
Первый шаг. Добавляем в файл «Gradient.vb» строчку перед определением свойств.
Public Event GradChanged(ByVal
sender As Object,
ByVal e As EventArgs) |
Второй шаг. Теперь надо сгенерировать событие. Это, значит, придется три раза вставить вызов события в конструкцию Set, то есть, на то место, где раньше стояло «MyBase.Invalidate()».
mColor1 =
Value : RaiseEvent GradChanged(Me, New System.EventArgs) |
Третий шаг. Наше свойство теперь не простое, а свойство с событием. Поэтому вставим ему «WithEvents».
Private WithEvents
mGrad0Fon As New
Gradient |
Вы не поверите, но теперь наш «mGrad0Fon» появился в левом выпадающем списке, как будто бы это какая-то кнопка на форме. Поэтому четвертый шаг будет очевидным: выбираем слева «mGrad0Fon», а справа единственное событие «GradChanged». Ну и разумеется «Invalidate».
Private Sub mGrad0Fon_GradChanged(ByVal sender As
Object, ByVal e As _
System.EventArgs) Handles
mGrad0Fon.GradChanged MyBase.Invalidate() End Sub |
Теперь любое изменение составного свойства сразу же приводит к перекраске. Темур Бобохидзе предложил другой оригинальный способ: при создании свойства в «New» передать создаваемому свойству имя его родителя, то есть:
mGrad0Fon
As New Gradient(Me) |
А потом, когда меняется одно из трех свойств, делать родителя «Invalidate». Нет, наш ЭУ не уголовник, никого он инвалидом не делает, просто заставляет ЭУ перерисовываться.
Поработав с новым свойством градиентной заливки, я понял, что работать с ним очень неудобно. Поясняю. Сделал я одну панель, подобрал заливку, последующие копировал с предыдущей. Вроде все нормально. Но потом мне захотелось поменять стиль панелей, захотелось другую цветовую гамму. Это же, сколько надо внести изменений!!! Нет, так жить нельзя, с этим надо что-то делать. Так появилось на свет новое свойство, которое я назвал «GradSource», то есть источник градиента. А к нему «GradStyle» для включения/отключения этого свойства. Сначала вставим эти два свойства и понаблюдаем, как они работают. Задаем внутренние переменные:
Private
mGradSource As NewPanel = Nothing Private
mGradSTyle As Boolean = False |
Создаем свойства:
Property GradSource() As NewPanel Get Return
mGradSource End Get Set(ByVal Value As
NewPanel) mGradSource = Value End Set
End Property
Property GradStyle() As Boolean Get Return
mGradSTyle End Get Set(ByVal Value As Boolean) mGradSTyle
= Value End Set End Property |
И немного переписываем «OnPaint»
Protected
Overrides Sub
OnPaint(ByVal e As
System.Windows.Forms.PaintEventArgs) Try Dim r As New Rectangle(0, 0, Width - 1, Height - 1) Dim mg As Gradient
= Me.mGrad0Fon If mGradSTyle = True
Then mg = mGradSource.Grad0Fon
e.Graphics.FillRectangle(New
LinearGradientBrush(r, mg.Color1, mg.Color2, mg.ColorN), r) Catch
ex As Exception End
Try End Sub |
Здесь стоит обратить внимание на оператор «Try», он избавит нас от любых неприятностей при перерисовке (например, если вдруг ширина или высота панели будут равны нулю или при включенном «mGradStyle» не укажут «mGradSourse» или элемент на который идет ссылка будет удален.
Делаем «Build», переходим на тестовую форму и создаем две новые панели, у второй в свойстве «GradSourse» выбираем из списка первую и свойство «GradStyle» ставим в True. То есть мы во второй панели при перерисовке будем брать не свой градиент, а из первой панели. Проверим. Изменим градиент в первой панели, обновим форму (клацните на любой верхней закладке, а потом вернитесь назад). «Заработало!!!» - крикнул кот Матроскин.
Что же мы имеем от этого нового свойства? Огромную помощь при создании программ, изменяем градиент одной панели, и мы получаем изменения во всех связанных с ней. Можно легко менять цветовую гамму всего приложения и во время выполнения. Тут главное не забыть сделать «Me.Refresh()». Попробуйте и убедитесь.
Почему я дал градиентному свойству такое название «Grad0Fon»? Я смотрел чуть дальше чем просто новая панель. Например у флажка таких свойств будет 6 штук: «Grad0Fon», «Grad0Text» для фона и текста основного состояния, «Grad1Fon», «Grad1Text» для фона и текста отмеченного состояния, «Grad2Fon» и «Grad2Text» для фона и текста заблокированного состояния. Аналогично для кнопки и переключателя.
Давайте, усовершенствуем наше свойство градиент. Например, мне хотелось бы иметь небольшой набор стандартных градиентов, которые легко было бы задать. Посмотрим на тот градиент, который задается по умолчанию.
Color.FromArgb(255, 221, 236, 254) – первый цвет
Color.FromArgb(255, 129,
169, 226) – второй цвет
А что если перетасовать хотя бы эти три значения интенсивности во всевозможных комбинациях. Добавим в класс нашего градиента такой код:
Public Sub
New()
End Sub
Public Sub
New(ByVal n As Int16) Dim a
As Int16 = n Dim
r, g, b As Byte Dim
c0() As Byte
= {221, 236, 254} Dim
c1() As Byte
= {129, 169, 226} b = a Mod
3 : a = a \ 3 : g = a Mod 3 : a = a \ 3 : r =
a Mod 3 Me.Color1
= Color.FromArgb(255, c0(r), c0(g), c0(b)) Me.Color2
= Color.FromArgb(255, c1(r), c1(g), c1(b)) Me.ColorN
= LinearGradientMode.Vertical End Sub |
А в тестовый проект (надеюсь ЭУ с именем NewPanel1 там есть)
Private Sub NewPanel1_Click(ByVal sender As Object, ByVal e As System.EventArgs) _ Handles
NewPanel1.Click Static
n As Short NewPanel1.Grad0Fon = New NewControl.Gradient(n) Me.Text
= n : n = n + 1 : Me.Refresh()
End Sub |
Кликая по панели получаем 27 различных заливок. По умолчанию имеет номер 5.
Попробуем сделать собственный редактор градиентного свойства. Для этого добавим в проект «NewControl» форму с именем GradEdit.
На ней разместим 6 счетчиков с именами NR1, NR2, NG1, NG2, NB1, NB2.
В центре наша панель NP.
Список ListN, который содержит четыре
строки с названиями направлений заливки.
И кнопку ButSave для записи изменений в редактируемую панель.
Как все должно работать? Форма должна при запуске получить значение градиента от редактируемой панели и присвоить его панели NP. Потом с панели NP отобразить значения на счетчиках и выделит строку в списке.
Изменение значений на счетчиках и в списке отображаются на панели NP. При нажатии на кнопку градиент с NP присваивается редактируемой панели.
Для передачи самого градиента лучше всего использовать общую переменную, поэтому создаем модуль «ModulControl.vb» и объявляем в нем общую переменную «gr». Вообще весь код, кот наплакал:
Module ModulControl
Public gr As
Gradient End Module |
Затем в форме создаем следующий код:
Private Sub
GradEdit_Load(ByVal sender As System.Object, ByVal
e As System.EventArgs) _ Handles MyBase.Load NP.Grad0Fon.Color1 = gr.Color1 NP.Grad0Fon.Color2 = gr.Color2 NP.Grad0Fon.ColorN = gr.ColorN ListN.SelectedIndex = gr.ColorN
NR1.Value = gr.Color1.R :
NR2.Value = gr.Color2.R NG1.Value = gr.Color1.G : NG2.Value =
gr.Color2.G NB1.Value = gr.Color1.B : NB2.Value =
gr.Color2.B End Sub |
|
|
Private Sub
NB1_ValueChanged(ByVal sender As Object, ByVal e As
System.EventArgs) _ Handles
NB1.ValueChanged, NB2.ValueChanged, NG1.ValueChanged, _
NG2.ValueChanged, NR1.ValueChanged, NR2.ValueChanged NP.Grad0Fon.Color1 =
Color.FromArgb(NR1.Value, NG1.Value, NB1.Value)
NP.Grad0Fon.Color2 =
Color.FromArgb(NR2.Value, NG2.Value, NB2.Value) End Sub |
|
Private Sub ListN_SelectedIndexChanged(ByVal sender As
System.Object, ByVal e _
As System.EventArgs)
Handles ListN.SelectedIndexChanged
NP.Grad0Fon.ColorN = ListN.SelectedIndex End Sub |
|
Private Sub
ButSave_Click(ByVal sender As System.Object, ByVal
e As System.EventArgs) _ Handles ButSave.Click gr.Color1 = NP.Grad0Fon.Color1 gr.Color2 = NP.Grad0Fon.Color2 gr.ColorN = NP.Grad0Fon.ColorN Me.Close() End Sub |
Надеюсь, что здесь все понятно из самого текста программы. Можно было сделать еще короче, без панели NP. Сразу внося изменения в переменную «gr». Некоторые скажут, что это даже и лучший вариант. Но я хотел одновременно видеть старый вариант закраски ЭУ и новый. Если новый меня устраивает больше, то я делаю обновление.
Как же вызвать эту форму? Здесь есть два стандартных варианта. Первый - создать свой редактор типа (наследуем класс «System.Drawing.Design.UITypeEditor»). Этот вариант обязательно будет рассмотрен, но на другом свойстве, позже объясню, почему для градиента он мало подходит. Второй – создать собственный дизайнера для элемента управления (наследуем класс «System.Windows.Forms.Design.ParentControlDesigner»). Мне этот вариант нравится больше всего, здесь можно вызвать форму через контекстное меню, этот случай мы тоже рассмотрим позже, когда будем создавать собственный «Designer» для своих ЭУ.
Но еще есть один хитрый фокус с логической переменной. Когда в окне свойств по ней делают двойной клик, она меняет значение на противоположное. Это может и не совсем профессионально, зато дешево и сердито.
Остается объявить в «NewPanel» переменную «GradEdit» и вместо изменения свойства вызывается форма в модальном режиме.
Property GradEdit() As
Boolean Get End Get Set(ByVal Value As Boolean) If
Value And DesignMode Then Dim
fe As New
GradEdit
gr = Me.mGrad0Fon : fe.ShowDialog() : MyBase.Invalidate() End
If End Set End Property |
Здесь используется свойство «DesignMode», которое имеет значение True, когда мы находимся в режиме конструирования и False, когда программа выполняется. Таким образом мы не даем возможности вызвать эту форму, когда программа будет выполняться.
Поиграв с новой панелью, зададим себе вопрос: «Почему у нас только 4 варианта закраски? ». Неужели только потому, что «LinearGradientMode» может принимать только четыре значения, а я хочу больше вариантов. От центра, например. Кто сказал: «Ну, раскатал губы!»? Вот и возвращаемся мы опять к самому началу этой статьи, где я писал, что сначала надо четко поставить перед собой задачу, что за ЭУ я хочу сделать.
Вообще-то изменения, которые я хочу сделать не такие уж и большие. Надо создать собственный перечисляемый тип «MyLinearGradientMode» или покороче «MyLGM» и собственную функцию закраски в зависимости от градиента, которую будем вызывать в «OnPaint» любого нашего ЭУ. В результате не надо будет переделывать ЭУ, а только один общий модуль. И создавать новые ЭУ будет проще, не надо будет больше обращать внимание на заливку, а сконцентрироваться на поведении своих ЭУ.
Самые серьезные изменения нас ожидают в «Gradient.vb». Здесь мы объявляем новый перечисляемый тип, в котором кроме уже известных 4 вариантов заливки добавим еще 4 варианта, которые представляют собой противоположное направления (это заливки где «Color1» и «Color2» поменялись местами). Кроме этого добавим заливки, в которых участвует центр закрашиваемого прямоугольника, заливка идет как бы в два этапа: от края к центру и от центра к другому краю (это будет еще 2 штуки плюс 2 реверса). Две заливки чисто от центра, две похожие на крест и еще две фигуры. На первый раз пока хватит, если понадобится, еще добавим. Может, кто придумает какую-нибудь интересную заливку. Присылайте идеи, возможности GDI огромны.
Перед такими серьезными изменениями рекомендую убрать с тестовой формы все наши панели, а то ругани потом будет…
Итак, начало нашего класса теперь имеет вот такой устрашающий вид:
Public Class Gradient Public Enum MyLGM Горизонтальный = 0 Вертикальный = 1 Диагональ_вверх = 2 Диагональ_вниз = 3 Горизонтальный_реверс = 4 Вертикальный_реверс = 5 Диагональ_вверх_реверс = 6 Диагональ_вниз_реверс = 7 Горизонтальный_центр = 8 Вертикальный_центр = 9 Горизонтальный_центр_реверс = 10 Вертикальный_центр_реверс = 11 К_центру = 12 От_центра = 13 Крест = 14 Крест_реверс = 15 Фигура = 16 Фигура_реверс = 17 End Enum |
Вообще-то присваивать числовые значения не обязательно, по умолчанию они сами получат значения с нарастанием. Первые четыре получили старые значения, которые были у «LinearGradientMode».
Перепишем переменную «mColorN».
Private mColorN As MyLGM = MyLGM.Вертикальный |
Не забудьте и в самом «Property» поменять «LinearGradientMode» на «MyLGM».
Аналогичную вещь придется
сделать в форме в " Windows Form Designer generated code ". И
разумеется добавить в список ListN
14 строк. Справитесь ?
Me.NP.Grad0Fon.ColorN = Gradient.MyLGM.Вертикальный |
Добавляем в «ModulControl.vb» возможность рисовать, а то он какой-то совсем маленький (только место в списке занимает).
Imports System.Drawing Imports System.Drawing.Drawing2D Module ModulControl Public gr As Gradient Public Sub FillGrad(ByVal pov
As Graphics, ByVal mg As
Gradient, ByVal r As Rectangle) Dim n As LinearGradientMode, d As Integer Select Case mg.ColorN Case 0 To 3 : n = mg.ColorN pov.FillRectangle(New LinearGradientBrush(r, mg.Color1, mg.Color2, n), r) Case 4 To 7 : n = mg.ColorN - 4 pov.FillRectangle(New LinearGradientBrush(r, mg.Color2, mg.Color1, n), r) Case Else End Select End Sub End Module |
Градиенты с номерами от 8 до 17 напишем немного позже, а сейчас надо проверить, как наша панель будет рисоваться.
Для этого в «OnPaint» заменяем строку отвечающую за заливку.
'e.Graphics.FillRectangle(New
LinearGradientBrush(r, mg.Color1, mg.Color2, mg.ColorN), r) FillGrad(e.Graphics,
mg, r) |
То есть передаем «FillGrad» три параметра: поверхность, где рисуем; градиент и прямоугольник. Проверяем как все работает. Если без проблем то добавляем еще графики.
Case 8 : n = 0 : d = r.Width
/ 2 Dim
r1 As New
Rectangle(r.X, r.Y, d +
1, r.Height) Dim
r2 As New
Rectangle(r.X + d, r.Y, r.Width - d, r.Height) pov.FillRectangle(New LinearGradientBrush(r2,
mg.Color1, mg.Color2, n), r2) pov.FillRectangle(New LinearGradientBrush(r1,
mg.Color2, mg.Color1, n), r1) Case
9 : n = 1 : d = r.Height / 2 Dim r1 As New Rectangle(r.X, r.Y, r.Width,
d + 1) Dim
r2 As New
Rectangle(r.X, r.Y + d, r.Width, r.Height - d) pov.FillRectangle(New LinearGradientBrush(r2,
mg.Color1, mg.Color2, n), r2) pov.FillRectangle(New LinearGradientBrush(r1,
mg.Color2, mg.Color1, n), r1) Case
10 : n = 0 : d = r.Width / 2 Dim
r1 As New
Rectangle(r.X, r.Y, d +
1, r.Height) Dim
r2 As New
Rectangle(r.X + d, r.Y, r.Width - d, r.Height) pov.FillRectangle(New LinearGradientBrush(r2,
mg.Color2, mg.Color1, n), r2) pov.FillRectangle(New LinearGradientBrush(r1,
mg.Color1, mg.Color2, n), r1) Case
11 : n = 1 : d = r.Height / 2 Dim
r1 As New Rectangle(r.X, r.Y, r.Width,
d + 1) Dim
r2 As New
Rectangle(r.X, r.Y + d, r.Width, r.Height - d) pov.FillRectangle(New LinearGradientBrush(r2,
mg.Color2, mg.Color1, n), r2) pov.FillRectangle(New LinearGradientBrush(r1,
mg.Color1, mg.Color2, n), r1) Case
12, 13 : Dim pat As
New GraphicsPath Dim
r1 As New
Rectangle(r.X - r.Width *
0.25, r.Y - r.Height *
0.25, r.Width * 1.5, r.Height
* 1.5) pat.AddEllipse(r1) Dim b As New PathGradientBrush(pat) b.CenterPoint
= New PointF(r.X + r.Width / 2, r.Y + r.Height / 2) b.CenterColor
= mg.Color1 Dim
mc() As Color = {mg.Color2} If
mg.ColorN = 13 Then
b.CenterColor = mg.Color2 : mc(0) = mg.Color1 b.SurroundColors
= mc pov.FillRectangle(b,
r) : b.Dispose() : pat.Dispose() Case
14, 15 : Dim mt() As Point = {New
Point(r.X, r.Y), New Point(r.X + r.Width, r.Y), New Point(r.X + r.Width, r.Y + r.Height), New Point(r.X, r.Y + r.Height)} Dim
b As New PathGradientBrush(mt) Dim
mc() As Color = {mg.Color1, mg.Color2} If
mg.ColorN = 15 Then
mc(0) = mg.Color2 : mc(1) = mg.Color1 Dim
pos() As Single
= {0.0F, 1.0F} Dim
cb As New ColorBlend cb.Colors
= mc : cb.Positions = pos b.InterpolationColors
= cb pov.FillPolygon(b,
mt) : b.Dispose() Case
16, 17 : Dim mt() As Point = {New
Point(r.X, r.Y), New Point(r.X + r.Width, r.Y), New Point(r.X + r.Width, r.Y + r.Height), New Point(r.X, r.Y + r.Height)} Dim
b As New PathGradientBrush(mt) Dim
mc() As Color = {mg.Color1, mg.Color2,
mg.Color1} If
mg.ColorN = 17 Then
mc(0) = mg.Color2 : mc(1) = mg.Color1 : mc(2) = mg.Color2 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() |
Наверное, можно где-то и покороче. Есть много очень похожих фрагментов. Взял примеры из книги Андрея Гарнаева (глава 12. Графические возможности GDI+).
Теперь, наконец, рассмотрим создание собственного дизайнера для нашего ЭУ.
Первый шаг - надо добавить «Referens», а именно «System.Design»
Второй шаг - надо добавить еще один «Imports System.ComponentModel.Design» в файл «NewPanel.vb».
Третий шаг - добавляем процедуру «GradEditor» очень похожую на свойство «GradEdit», только гораздо короче.
Sub GradEditor () If DesignMode Then Dim fe As New GradEdit gr = Me.mGrad0Fon : fe.ShowDialog() : MyBase.Invalidate() End If End Sub |
Четвертый шаг. В самом-самом конце файла создаем еще один класс, который и является собственно нашим собственным дизайнером.
Public Class NewPanelDesigner Inherits System.Windows.Forms.Design.ParentControlDesigner Private dVerbs As New DesignerVerbCollection Sub New() With dVerbs .Add(New
DesignerVerb("Редактор градиента", New EventHandler(AddressOf
onVerb))) '.Add(New
DesignerVerb("Об элементе", New
EventHandler(AddressOf onVerb))) End With End Sub Public Overrides ReadOnly Property
verbs() As DesignerVerbCollection Get Return
dVerbs End Get End Property Protected
Sub onVerb(ByVal sender As Object, ByVal e As EventArgs) Dim sString As String = CType(sender, DesignerVerb).Text Select
Case sString Case
"Редактор градиента" : CType(Me.Control, NewPanel).GradEditor() 'Case
"Об элементе" : CType(Me.Control, NewPanel).About() Case
Else End Select End Sub End Class |
И наконец, подключаем наш ЭУ к дизайнеру с помощью атрибута.
<Designer(GetType(NewPanelDesigner))>
Public Class NewPanel |
Результатом
всех этих манипуляций, мы имеем строку в контекстном меню и ссылку на странице
свойств. Щелчок по любой вызывает форму редактора градиента!
Для наиболее любознательных могу дать самостоятельное задание: добавить нашей панели еще пару свойств «FrameColor» и «FrameStyle», то есть цвет рамки и стиль рамки. Стиль рамки – это перечисляемый тип переменной: (none) или нет рамки, одинарная линия, двойная линия, фигура и т.д. (можно конкурс объявить).
Разумеется рисование рамки должно быть вынесено из класса самой панели в общий файл (вдруг аналогичные рамки понадобятся и для других ЭУ, не мучится же с их рисованием каждый раз).
Продолжение следует…