Subclassing в VB.NET

Введение

Прежде всего необходимо сказать несколько слов для начинающих программистов, возможно впервые сталкивающихся со словом subclassing (читается как "сабклассинг"). По-простому можно объяснить этот термин, как создание некоторой "обертки", расширяющей уже имеющуюся функциональность, для уже существующего класса. Допустим, уже имеется такой контрол как TreeView, и нет особого смысла заново создавать этот элемент управления, гораздо проще расширить уже имеющийся, например добавив возможность подсветки разными цветами, либо включением анимированных gif картинок и т.п.

Ранее, при использовании Visual Basic вплоть до шестой версии, приходилось идти на различные ухищрения для реализации subclassing'а. Порой, напарываясь на множество подводных камней, разработчик отказывался от идеи использования функций "чужого" контрола и писал свой. Для реализации subclassing'а в VB требовалось достаточно много не всегда понятного начинающим программистам кода. Примером могут служить популярные библиотеки, используемые для реализации subclassing: SSubTmr.dll, автор: Steve McMahon (доступна на http://www.vbaccelerator.com/ с исходным кодом), либо SubTimer.dll, автор: Bruce McKinney (на основе этой библиотеки создан SSubTmr.dll).

В Visual Basic .NET эта проблема решена! Теперь subclassing это достаточно простой и удобный метод создания контролов с расширенной функциональностью. (Примером может служить мой контрол DiskTree, опубликованный на этом сайте).

В этой статье я постараюсь достаточно подробно и понятно рассказать о использовании subclassing'а. Статья рассчитана как на начинающих программистов, так и на опытных программистов VB6 переходящих на VB.NET.

Принципы subclassing'а

Основной принцип subclassing'a - переопределение методов и свойств имеющихся объектов. Переопределение реакции на различные события, происходящие для данного объекта. Допустим, существует некоторый элемент управления, который по клику мышки на нем записывает в файл некоторую информацию. Нам же требуется, чтобы эта информация не сохранялась в файле, а записывалась в системный журнал Windows. Для этого мы переопределяем событие OnClick таким образом, чтобы выполнялись именно требуемые действия.

Таким образом можно создать новый элемент управления, как бы содержащий старый, но иначе реагирующий на событие щелчка мышкой.

Также это может быть необходимо для переопределения некоторых методов "отрисовки" контрола, примером может служить cPopMenu с вышеуказанного сайта http://www.vbacelerator.com/. На основе стандартного меню создается новый контрол, позволяющий использовать иконки напротив пунктов меню и рисовать меню в стиле Office 2000.

В VB.NET эти действия являются естественными - все элементы, формы и даже типы данных - это классы. И, в соответствии с концепциями ООП, допускают наследование и переопределение методов и свойств.

Subclassing формы

Начнем с наиболее простого и наиболее понятного. Создайте новый проект (либо, если не хотите делать все своими руками, посмотрите прилагаемый к статье пример). По умолчанию в него будет включена одна форма. Перейдите к просмотру кода (Code View) и посмотрите на два выпадающих списка вверху окна редактирования. К моему большому удивлению, немногие знают или догадываются, что код для обработки событий и переопределения методов можно не набирать руками, а использовать автоматически генерируемый средой Visual Studio. В левом списке выберите Overrides, а в правом WndProc. Вы получите следующий код.

 Protected Overrides Sub  WndProc( ByRef  m  As  System.Windows.Forms.Message)

End Sub

Все, таким образом мы получаем контроль над всеми событиями формы. Так или иначе события обрабатываются данной процедурой. Ключевое слово Overrides означает, что эта функция переопределяет уже существующую. На этом простом примере я покажу основные принципы реализации subclassing'а.
Допустим, надо перехватить событие щелчка левой кнопкой мыши по форме, для этого напишем следующий код:

 Protected Overrides Sub  WndProc( ByRef  m  As  System.Windows.Forms.Message)
	 If  m.Msg = WM_LBUTTONUP  Then 
		MsgBox("Щелкнули левой кнопкой!")
	 End If 
	 MyBase .WndProc(m)
 End Sub 

Поясню этот код. Для начала происходит проверка, какое событие "попало в нашу функцию". В данном случае нас интересует только "отжатие" левой кнопки. Константа WM_LBUTTONUP - обозначает именно сообщение, получаемое формой, при "отжатии" левой кнопки мыши. Полный список констант вы можете посмотреть в модуле WindowsMessages.vb, входящем в проект, прилагаемый к статье. В модуле я собрал наиболее часто используемые сообщения Windows для легкого и быстрого использования в проектах.
Теперь еще чуть-чуть проиграемся с этим кодом. Напишем следующий код:

 Protected Overrides Sub  WndProc( ByRef  m  As  System.Windows.Forms.Message)
	 If  m.Msg = WM_LBUTTONUP  Then 
		MsgBox("Щелкнули левой кнопкой!")
	 End If 
	 MyBase .WndProc(m)
 End Sub 

Private Sub frmMain_Click( ByVal sender As Object, ByVal e As System.EventArgs) _ Handles MyBase.Click MsgBox("По форме кликнули!") End Sub

Запустив проект мы обнаружим, что при щелчке по форме отображаются последовательно два MsgBox. То есть происходят оба события. И, как и должно быть, сначала происходит "отжатие кнопки", а потом событие щелчка. Теперь добавим еще одну строку кода:

 Protected Overrides Sub  WndProc( ByRef  m  As  System.Windows.Forms.Message)
	 If  m.Msg = WM_LBUTTONUP  Then 
		MsgBox("Щелкнули левой кнопкой!")
		 Return  ' Если щелкнули, то дальше не передаем это событие
	 End If 
	 MyBase .WndProc(m)
 End Sub 

Private Sub frmMain_Click( ByVal sender As Object, ByVal e As System.EventArgs) _ Handles MyBase.Click MsgBox("По форме кликнули!") End Sub

Теперь второй MsgBox не будет появляться, что служит свидетельством того, что форма не получила предназначенного ей события. Таким образом я показал, как можно перехватывать события формы до того, как она их получит, и выполнять какие-либо действия, что иногда бывает очень удобно.

Помимо методов, можно переопределять и свойства. Для этого добавьте следующий код:

 Public Overrides Property  Text()  As String 
	 Get 
		 Return  Replace(MyBase.Text, "Пробел", "32")
	 End Get
	Set ( ByVal  Value  As String )
		MyBase.Text = Replace(Value, "32", "Пробел")
	 End Set
End Property 

Private Sub frmMain_KeyDown( ByVal sender As Object, ByVal e As _ System.Windows.Forms.KeyEventArgs) Handles MyBase.KeyDown Me.Text = "Кнопка: " & e.KeyValue End Sub

По нажатию кнопки, текст заголовка формы будет изменен на "Кнопка:" плюс код нажатой клавиши, но переопределенное свойство Text проводит некоторую операцию перед выводом текста на экран. Таким образом, при нажатии пробела будет отображен заголовок "Кнопка: Пробел", при этом получая значение свойства, мы получим "Кнопка: 32".
Этот пример достаточно бессмысленный, но он иллюстрирует способ изменения свойств объекта.

Subclassing элемента управления

Теперь самое время перейти к более интересному варианту - расширим свойства стандартного элемента управления TextBox. Для этого снова создайте новый проект (либо, опять-таки возьмите мой из архива). Прежде всего определимся, чего бы нам хотелось получить от этого нового-старого контрола. На этот раз не будем проявлять особую оригинальность и создадим компонент позволяющий вводить только числа, такой контрол нам всегда пригодится. При этом этот контрол не позволяет производить копирование и вставку текста и не отображает ниспадающего меню по щелчку правой кнопкой мыши. Также, в качестве тренировки, добавим запрет на присвоение значения Text непосредственно из кода.
Добавьте в проект новый UserControl, назовите его NumbersBox . И переходите сразу к редактированию кода. По умолчанию новый класс наследуется от System.Windows.Forms.UserControl, но нам это не подходит, мы же не хотим добавить TextBox на наш контрол и создавать элемент управления содержащий TextBox. Наша задача - расширить функциональность TextBox непосредственно. Поэтому смело заменяем строку:

 Inherits  System.Windows.Forms.UserControl

На:

 Inherits  System.Windows.Forms.TextBox

Так мы получили контрол, который уже включает в себя все функции, методы и свойства стандартного элемента TextBox. Теперь в Toolbox на вкладке My User Controls отображается наш NumbersBox. Налюбовавшись на это чудо дивное, перейдем непосредственно к реализации задуманного. Опять-же, основная часть кода будет сосредоточена в WndProc.

 Protected Overrides Sub  WndProc( ByRef  m  As  System.Windows.Forms.Message)
 Select Case  m.Msg
	 Case  WM_CHAR
		' Если не число или Backspace, то не надо нам этого вообще
		 If  (m.WParam.ToInt32 > 57  Or  m.WParam.ToInt32 < 48)  And  _
		m.WParam.ToInt32 <> 8  Then Return 
		' Не пускаем Ctrl+C, Ctrl+V или Ctrl+X
		 If   Me .ModifierKeys.Control  And  (m.WParam.ToInt32 = 3  Or  _
		m.WParam.ToInt32 = 22  Or  m.WParam.ToInt32 = 24)  Then Return 
	 Case  WM_RBUTTONDOWN ' Не надо нам никаких менюшек при правом клике
		 Return 
 End Select 
 MyBase .WndProc(m) ' Передаем события дальше
 End Sub 

А для того, чтобы запретить присвоение значения Text из кода (то есть текст может быть введен только непосредственно с клавиатуры), нужно переопределить свойство Text базового контрола TextBox. Заметьте, мы не можем сделать это свойство ReadOnly, т.к. базовое свойство таковым не является, но это легко обходится с помощью небольшой хитрости.

 Public Overrides Property  Text()  As String 
	 Get 
		 Return MyBase .Text.ToString
	 End Get
	Set ( ByVal  Value  As String )
		' А тут ничего не делаем
	 End Set
End Property 

Теперь вы можете просто перетащить контрол на форму, запустить проект на выполнение и порадоваться этому "гениальному" творению. Этот пример фактических повторяет предыдущий, но несколько в ином контексте. Дополнительным примером к этому разделу статьи может служить контрол DiskTree, опубликованный на этом же сайте.

Создание класса для subclassing'a

Теперь перейдем к созданию класса для закрепления сведений, изложенных в статье. Для этого создадим новое приложение (эх, неужели так лениво? Ну открывайте мой пример). И добавим новый класс, который унаследуем от System.Windows.Forms.NativeWindow. NativeWindow - специальный класс созданный для поддержки Subclassing'а для Windows Forms, класс уже содержит все необходимые методы и свойства для реализации subclassing'а, поэтому мы и используем его.
Код класса:

 Public Class  Subclasser
	 Inherits  System.Windows.Forms.NativeWindow

#Region "Constants" ' Здесь сокрыты все константы

Public Event EventHappened( ByRef m As System.Windows.Forms.Message)

Protected Overloads Overrides Sub WndProc( ByRef m As System.Windows.Forms.Message) RaiseEvent EventHappened(m) MyBase .WndProc(m) End Sub End Class

Как видите - идея до гениальности проста - класс просто вызывает событие EventHappend, как только получает какое-либо сообщение. Далее мы привязываем объект к кнопке, предварительно помещенной на форму.

 Private Sub  frmMain_Load( ByVal  sender  As  System.Object,_
  ByVal  e  As  System.EventArgs)  Handles  MyBase.Load
	' Подключаем объект к кнопке
	sc.AssignHandle(Me.Button1.Handle)
 End Sub 

Private Sub sc_EventHappened( ByRef m As System.Windows.Forms.Message) _ Handles sc.EventHappened Select Case m.Msg Case sc.WM_KEYDOWN MsgBox("Нажата клавиша!") End Select End Sub

При нажатии клавиши программа будет отображать MsgBox. Таким образом subclassing любой формы или контрола осуществляется очень легко - для этого достаточно сопоставить описатель элемента (Handle) с объектом Subclasser и написать код обработки сообщений по событию EventHappened.

Заключение

В этой статье рассматривались основы subclassing'а в Visual Basic .NET. Как могли убедится читатели, работавшие ранее с VB6 и пробовавшие реализовать subclassing, это стало намного проще и понятнее. При этом большую часть работы берет на себя среда .NET, а программисту остается только лишь пользоваться "благами цивилизации".

Исходник к статье можно скачать здесь.

 


Страница сайта http://silicontaiga.ru
Оригинал находится по адресу http://silicontaiga.ru/home.asp?artId=4795