Сайт Алексея Муртазина (Star Cat) E-mail: starcat-rus@yandex.ru
Мои программы Новости сайта Мои идеи Мои стихи Форум Об авторе Мой ЖЖ
VB коды Статьи о VB6 API функции Самоучитель по VB.NET
Собрания сочинений Обмен ссылками Все работы с фото и видео
О моём деде Муртазине ГР Картинная галерея «Дыхание души»
Звёздный Кот

Самоучитель по VB.NET
Глава 13.

Приложения Windows

Первая неудачная попытка

«...Visual Basic .NET предоставляет в распоряжение программиста визуальные средства ускоренной разработки приложений, которые стали одной из главных причин успеха Visual Basic. Создание пользовательского интерфейса сводится к простому выбору нужных элементов из палитры и размещению их на форме. Значения свойств элементов либо вводятся в специальном окне, либо задаются на программном уровне, после чего программист пишет код для обработки различных событий.

Далее следует простое описание приложения „Hello World" в Visual Basic .NET...»

Многие авторы именно так знакомят читателя с приложениями Windows. У подобного подхода есть два недостатка. Во-первых, он сводится к пересказу документации, поэтому такие книги скучно читать (и писать, кстати, тоже). Во-вторых, если читатель еще не понимает концепций, представленных в таких «введениях», ему вообще слишком рано читать подобные книги.

Большинство программистов VB .NET освоится в Visual Studio IDE за полчаса, и при этом им не понадобится ни моя помощь, ни чтение документации.

Вторая неудачная попытка

Осваивая программирование приложений Windows в VB .NET, программист VB6 среднего или высокого уровня уже знает, что именно его интересует. Остается лишь выбрать из огромного набора свойств, методов и событий именно то, что необходимо для решения конкретной задачи. Формы и элементы VB .NET сохранили практически все возможности VB6 (а также приобрели множество новых), но не всегда понятно, как они соответствуют друг другу. Даже после чтения раздела «What's New in VB .NET» («Что нового в VB .NET») документации Microsoft вам придется в течение некоторого времени поэкспериментировать с новым пакетом форм, чтобы добиться нужного результата.

Итак, большая часть вашего времени будет потрачена на изучение новых свойств и событий объектов, но я даже не буду пытаться описывать их. Пространство имен System. Windows. Forms содержит столько классов, методов и событий, что ими можно заполнить отдельную книгу, не говоря уже об одной главе. Возникает резонный вопрос: о чем же тогда написана эта глава?

 

Новый пакет форм

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

Прежде всего следует запомнить, что пакет форм .NET не имеет ничего общего с тем пакетом, который использовался в VB6. Это совершенно другой пакет, написанный заново1.

На практике это в первую очередь означает невозможность каких-либо предположений, выходящих за рамки документации, книг по VB .NET или того, что вы узнаете опытным путем. Впрочем, я говорю не о принципиальных особенностях, ведь свойства и события работают примерно так же, как прежде. Скорее, речь идет о практических мелочах вроде правильных способов проверки полей, порядка инициирования событий или принципов связывания элементов с данными. Постепенно вы получите всю нужную информацию и необходимые практические навыки, но это потребует определенного времени, и эта глава лишь укажет нужное направление.

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

1Хотя такие классы, как Form, UserControl и т. д., действительно написаны заново, многие стандартные элементы (например, RichTextBox) представляют собой простые «обертки» для стандартных элементов Windows или их СОМ-аналогов. Это означает, что они подвержены проблемам обновления (пресловутый «кошмар DLL») в той же степени, как и все остальные компоненты, использующие DLL стандартных компонентов.

 

Повторное создание окон

Многие свойства элементов VB6 задаются только на стадии конструирования. Например, вам не удастся перевести список с одиночным выделением в режим множественного выделения во время работы программы. На первый взгляд это кажется досадным упущением, но для программиста, знакомого с механикой работы Windows, причина ясна: эти свойства соответствуют стилям Windows, которые не могут изменяться после создания окна.

В VB6 при создании формы или размещении на ней элемента для этой формы/элемента создается окно. Изменить стиль окна можно только одним способом: уничтожить его и создать заново, что приведет к потере текущих данных окна.

В VB .NET значения этих свойств могут изменяться на стадии выполнения, как показывает следующий фрагмент приложения Recreate.

Private Sub Forml_Load(ByVal sender As System.Object, _

ByVal e As System.EventArgs) Handles MyBase.Load 

Dim x As Integer For x = 1 To 20

listBoxlQ.Items.Add ("Entry # " & CStr(x))

 Next

IblWindowQ.Text = "hWnd = " & 11stBoxlQ.Handle.ToString

 End Sub

Private Sub chkMulti_CheckedChanged(ByVal sender As System.Object, _

 ByVal e As System.EventArgs) Handles chkMulti.CheckedChanged

If chkMulti().CheckState = CheckState.Checked Then

  IistBoxK).SelectionMode = SelectionMode.MultiExtended

Else listBoxlQ .SelectionMode = SelectionMode.One

End If

IblWindowO .Text = "hWnd = " & IistBoxK) .Handle.ToString

End Sub

Находящийся на форме флажок позволяет выбрать режим выделения для элемента ListBox (одиночное или множественное). Поэкспериментируйте с этим приложением и убедитесь в том, что поведение списка соответствует значению свойства SelectionMode.

А теперь проследите за изменением надписи при переключении режима. Вы увидите, что манипулятор окна изменяется.

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

Первый урок особенно важен для тех, кто использует функции Win32 API: манипуляторы окон в .NET могут изменяться.

Графические элементы

В Visual Basic 4 появилась новая разновидность элементов управления — «упрощенные» или «графические» элементы, не имеющие собственных окон. Их можно рассматривать как инструкции контейнеру о выполнении некоторых графических операций. Графические элементы были включены в OLE, поскольку они расходовали меньше ресурсов и обеспечивали лучшее быстродействие.

В .NET все элементы ассоциируются с окнами. Преимущество нового подхода заключается в том, что модель программирования становится более последовательной, поскольку вам не придется разбираться с ошибками, связанными с различиями в поведении оконных и графических элементов. Впрочем, есть и недостаток: теперь каждый элемент требует дополнительных затрат, связанных с ассоциированным окном.

Если вы привыкли использовать элементы Line и Shape (самые распространенные графические элементы), учтите, что в VB .NET они не поддерживаются. Вам придется воспользоваться другим, более рациональным решением — прорисовкой всех дополнительных изображений в обработчике события Paint формы.

 

Согласованное поведение контейнеров

Как опытный разработчик элементов ActiveX заявляю, что самый большой недостаток этой технологии — непредсказуемый характер работы элементов ActiveX в каждом конкретном контейнере. Мелкие различия между управляющими приложениями приводят к мелким (а то и крупным) различиям в поводении элементов ActiveX. Успешное тестирование элемента в VB6 не гарантирует, что он будет нормально работать в Visual C++, Microsoft Word или Excel. Подобная несовместимость объясняется тем, что каждое управляющее приложение содержит собственную реализацию контейнера. Дело в том, что каждый разработчик по-своему толкует спецификации СОМ (надо признать, местами весьма невразумительные), не говоря уже об ошибках, неизбежно присутствующих в любой сложной программе.

На практике это означает, что разработчики элементов создают и тестируют элементы в VB6 (или еще для одного-двух управляющих приложений), где существует определенный рынок, и фактически игнорируют другие платформы, поскольку они не оправдывают расходов на тестирование и сопровождение элементов. Отсутствие элементов для этих платформ отрицательно сказывается на развитии спроса и подавляет инициативу разработчиков. Возникает порочный круг. Аналогичные проблемы возникают и при создании специализированных элементов для внутреннего использования, хотя и в меньшей степени.

 

Архитектурные шаблоны и System.Windows.Forms

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

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

Я выбрал второй путь.

Прежде всего необходимо понять, что пакет форм строится на основе наследования. Каждый раз, когда вы создаете форму, вы в действительности определяете класс, производный от класса System.Windows .Forms . Form.

На рис. 13.1 показана часть иерархии классов форм .NET. На рисунке изображены не все классы, но и этого вполне достаточно, чтобы проиллюстрировать иерархический характер связей между классами.

Рассмотрим ключевые классы и пространства имен этой иерархии.

 

System. ComponentModel. Component

Пространство имен System.ComponentModel содержит объекты, управляющие работой компонентов и их взаимосвязями.

Рис. 13.1. Часть пакета Windows. Forms

 

System. Windows. Forms.Control

Чтобы лучше разобраться в иерархии форм, мы рассмотрим серию примеров, демонстрирующих функциональные возможности каждого класса в цепочке наследования. Начнем с проекта ControlOnly. Мы рассмотрим его особенно подробно, чтобы вы поняли, как работают формы.

Прежде всего обратите внимание на то, что класс формы объявляется производным от System.Windows.Forms.Form:

Public ont>

Inherits System.Windows.Forms.Form

При создании нового приложения в VB6 имя «Forml» имеет двойной смысл. Оно представляет тип формы и одновременно глобальное имя первой формы этого типа. Например, следующий фрагмент создает две формы типа Forml с идентификаторами Forml и f2:

Dim f2 As New Forml 

Forml.Caption = "First form 1"

 f2.Show 

f2.Caption = "Second form 1"

В VB .NET эта концепция не поддерживается. Здесь имя Forml относится к новому классу объектов, производному от типа Form. На этот объект можно ссылаться с ключевым словом Me.

Для форм VB .NET определен конструктор по умолчанию, который вызывает конструктор базового класса (как обычно), а затем вызывает InitializeComponent:

#Region " Windows Form Designer generated code "

 Public Sub New() 

MyBase.New()

' Необходимо для дизайнера форм Windows.

  InitializeComponent()

' Дальнейшая инициализация выполняется 

' после вызова InitializeComponent()> 

End Sub

Формы также реализуют интерфейс IDisposable. Форма, как и другие контейнеры, хранит список внутренних компонентов в объекте System. ComponentModel. Container, обладающем средствами для освобождения этих внутренних объектов:

' Необходимо для дизайнера форм Windows.

Private components As System.ComponentModel.Container

' Форма переопределяет Dispose для очистки списка компонентов.

 Public Overloads Overrides Sub Dispose()

MyBase.Dispose()

If Not (components Is Nothing) Then components.Dispose()

End If

 End Sub

На форме расположены три элемента. В данном примере наибольший интерес представляет первый элемент, относящийся к обобщенному типу Control. Вряд ли вам когда-нибудь придется создавать элементы типа Control на практике, но в данном примере это поможет разобраться в функциональности разных объектов, использованных при построении форм.

Private WithEvents ctll As System.Windows.Forms.Control

Private WithEvents textBoxl As System.Windows.Forms.TextBox

Private WithEvents textBox2 As System.Windows.Forms.TextBox

На примере функции InitializeComponent наглядно проявляются принципиальные различия между VB6 и VB .NET. В VB6 чтение и запись свойств происходит «по волшебству», незаметно для программиста. В VB .NET значения свойств задаются программой на стадии выполнения, а окно свойств среды Visual Studio — всего лишь особая разновидность редактора программного кода.

Элемент ctl1 (тип Control) включается в коллекцию Control формы. Контейнером текстового поля textBoxl в действительности является ctll, а не форма. Из чего это следует? Из того, что этот элемент включается в коллекцию ctll.Control (коллекцию элементов элемента ctl1) вместо коллекции элементов формы. Процесс инициализации формы показан в листинге 13.1.

Листинг 13.1. Инициализация формы в проекте ControlOnly1

' ВНИМАНИЕ: следующий фрагмент необходим

' для дизайнера форм Windows.

' Для его модификации следует использовать дизайнер форм.

' Не изменяйте его в редакторе!

<System.Diagnostics.DebuggerStepThrough()> Private _

Sub InitializeComponentO

Me.ctll = New System.Windows.Forms.Control()

Me.textBoxl = New System.Windows.Forms.TextBox()

Me.textBox2 = New System.Windows . Forms .TextBox()

Me.ctll.SuspendLayout()

Me.SuspendLayout()

'ctll

Me.ctll.BackColor = System.Drawing.Color.Bisque

Me.ctll.Controls.AddRange(New System.Windows.Forms.Control() _

{Me.textBoxl})

Me.ctll.Name = "ctll"

Me.ctll.Size = New System.Drawing.Size(128, 112)

Me.ctll.Tablndex = 0

'textBoxl

Me.textBoxl.Location = New System.Drawing.Point(32 , 48)

Me.textBoxl.Name = "textBoxl"

Me.textBoxl.Size = New System.Drawing.Size(72, 20)

Me.textBoxl.Tablndex = 1

Me.textBoxl.Text = "textBoxl"

' textBox2

Me.textBox2.Location = New System.Drawing.Point(24 , 128)

Me.textBox2.Name = "textBox2"

Me.textBox2.Size = New System.Drawing.Size(96, 20)

Me.textBox2.Tablndex = 1

Me.textBox2.Text = "textBox2"

'Forml

Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13)

Me.ClientSize = New System.Drawing.Size(292, 273)

Me.Controls.AddRange(New System.Windows.Forms.Control() _

{Me.textBox2, Me.ctll})

Me.Name = "Forml"

Me.Text = "Forml"

Me.ctll.ResumeLayout (False)

Me.ResumeLayout (False)

End Sub 

1 Все исходные тексты можно найти на сайте издательства «Питер» www.piter.com. —Примеч. ред.

Тот факт, что элемент textBoxl принадлежит с til, приводит к весьма странным последствиям. Среда Visual Studio не позволяет непосредственно выбрать текстовое поле на форме. При перемещении между двумя текстовыми полями элемент с 111 тоже получает фокус. Все эти проблемы возникают из-за того, что объект Control не рассчитан на непосредственное включение внутренних элементов — по крайней мере, в среде Visual Studio.

Но несмотря на это, объект ctl1 типа Control содержит многие свойства, которые мы ожидаем увидеть в элементах управления: BackColor, ForeColor, Backgroundlmage, ClientRectangle, ContextMenu, Cursor, Font, Top, Left, Width, Height, Parent, Visible и т. д. Объект класса, производного от Control, автоматически наследует все эти свойства. Стандартные элементы (такие, как TextBox, Label и DataGrid) являются непосредственно производными от Control.

Естественно, одним из первых шагов при самостоятельном знакомстве с пространством имен Windows . Forms должно стать изучение свойств и методов класса Control.

Продолжим описание объектов, используемых при построении форм.

 

System. Windows. Forms.ScrollableControl

Проект ScrollableOnly аналогичен ControlOnly, однако объект ctl1 в нем объявляется производным от класса ScrollableControl (производного от Control). В следующем фрагменте несколько текстовых полей и надпись:

Private WithEvents ctll As System.Windows.Forms.ScrollableControl

Private WithEvents labell As System.Windows.Forms.Label

Private WithEvents textBox2 As System.Windows.Forms.TextBox

Private WithEvents textBoxl As System.Windows.Forms.TextBox

Private WithEvents textBoxS As System.Windows.Forms.TextBox

Функция InitializeComponent (листинг 13.2) задает значения свойств этих элементов (для экономии места процедура приводится с сокращениями).

Листинг 13.2. Инициализация формы в проекте ScrollableOnly

<System.Diagnostics.DebuggerStepThroughAttribute()> Private

 Sub InitializeComponent()

Me.textBox2 = New System.Windows . Forms .TextBox()

Me. textBoxl = New System.Windows . Forms.TextBox()

Me.ctll = New System.Windows.Forms.ScrollableControl()

Me. labell = New System.Windows . Forms. Label()

Me.textBoxB = New System.Windows . Forms.TextBox()

'ctll

Me.ctll.AutoScroll = True

Me.ctll.BackColor = System.Drawing.Color.Crimson

Me.ctll.Controls.AddRange(New System.W1ndows.Forms.Control() _

{Me.textBox2, Me.textBoxl, Me.labell})

Me.ctll.Location = New System.Drawing.Point(40, 16)

Me.ctll.Name = "ctll"

Me.ctll.Size = New System.Drawing.Size(200, 136)

Me.ctll.Tablndex = 0

'labell

Me. labell.BackCol.or = System. Drawing. Color. FromArgb(0, 192, 0)

 Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13)

 Me.ClientSize = New System.Drawing.Size(292, 273)

  Me.Controls.AddRange(New System.Windows.Forms.Control() _ 

{Me.textBox3, Me.ctll}) 

Me.Name = "Forml"

 Me.Text = "Forml"

 End Sub

Элементы textBox2, textBoxl и labell содержатся в элементе ctll. Элемент textBox3 содержится в самой форме. Свойству AutoScroll элемента ctll задается значение True. Это свойство, добавленное классом ScrollableControl, позволяет элементу включать другие элементы не только на логическом, но и на визуальном уровне. Когда внутренний элемент выходит за пределы видимых границ объекта ScrollableControl, появляются полосы прокрутки, при помощи которых его можно увидеть.

Поэкспериментируйте с этим приложением и понаблюдайте за тем, как действуют полосы прокрутки. Вы увидите, что в режиме конструирования можно выбрать внутренние элементы и разместить новые элементы на ctll.

Впрочем, при перемещении между элементами происходит нечто странное: элемент с 111 по-прежнему получает фокус.

 

Контейнеры, формы и класс UserControl

Проект Container-Control практически идентичен ScrollableOnly. Единственное изменение заключается в том, что объект ctll теперь принадлежит к классу ContainerControl. Класс ContainerControl, в свою очередь, является производным от ScrollableControl, но включает дополнительные средства управления передачей фокуса, необходимые для правильной работы внутренних элементов. При попытке передачи фокуса контейнеру он автоматически передает фокус первому внутреннему элементу. Запустите программу и поэкспериментируйте с передачей фокуса клавишей Tab.

Классы Form и UserControl являются производными от класса ContainerControl. В формах добавлены свойства и методы для работы с рамками и заголовками, поддержки MDI и других возможностей, присущих окнам верхнего уровня. Количество новых членов класса UserControl не велико, зато в этом классе имеются атрибуты, указывающие Visual Studio на необходимость использования особых средств конструирования UserControl.

 

Ориентация в пространстве имен System. Windows. Forms

Просматривая электронную документацию по классам пространства имен Forms, вы увидите, что для каждого класса в документации приведен полный список всех членов. В документации также сказано, какие члены реализуются (или перегружаются) классом, а какие наследуются от базовых классов. Хотя вы сможете легко получить список членов класса, с первого взгляда трудно понять, какие члены непосредственно реализуются или перегружаются классом — для этого вам придется просмотреть весь список.

Приложение DirectMembers представляет собой утилиту для ускоренного поиска членов, реализованных непосредственно в классе. Кроме того, программа демонстрирует некоторые возможности, общие для многих элементов.

На форме находится иерархический список (класс TreeView), свойство Sorted которого равно True. Во время инициализации формы вызывается функция LoadTreeview, подробно описанная ниже.

Private Sub LoadTreeview() 

Dim asm As Assembly 

Dim asmtypesQ As Type 

Dim ThisType As Type

asm = Reflection.Assembly.GetAssembly (_

  GetType(System.Windows.Forms.Form))

Получение данных сборки посредством рефлексии вам уже хорошо знакомо. В нашем примере запрашивается информация о сборке, содержащей пакет форм.

asmtypes = asm.GetTypes() 

For Each ThisType In asmtypes 

If ThisType.IsType.IsPublic Then

Переменная asmtypes заполняется массивом с описанием всех типов сборки. Нас интересуют только классы с атрибутом Public.

 Dim tn As New TreeNode(ThisType.Name)

Имена классов отображаются в корневых узлах иерархического дерева TreeView. Элемент TreeView содержит коллекцию TreeNodeCollection, состоящую из объектов TreeNode. Каждый объект TreeNode, в свою очередь, содержит собственную коллекцию TreeNodeCollection — так образуется иерархическая структура.

Подобное решение используется практически всюду, где элемент управления содержит список внутренних объектов. Например, элемент ListBox уже не поддерживает методов для непосредственного добавления и удаления элементов. Вместо этого необходимые операции выполняются со свойством Items объекта ListBox. Хотя по сравнению с VB6 ситуация заметно изменилась, особых проблем быть не должно, поскольку вы уже умеете работать с коллекциями. Но еще интереснее выглядит тот факт, что в список можно включать любые объекты! Элемент ListBox просто вызывает метод ToString и выводит строковое представление объекта, что снимает необходимость в управлении дополнительной информацией при помощи свойства ItemData. Таким образом, хранение сложных данных в списке организуется просто: вы определяете класс, загружаете в него все необходимые данные и переопределяете метод ToString, чтобы в списке выводилась нужная информация.

Программисты VB6, жалующиеся на несовместимость списков VB .NET и VB6, просто не разобрались в ситуации. Подход .NET гораздо более интуитивен, а поскольку он применяется всюду, где элементы управляют списками, программирование становится более последовательным. Это ускоряет освоение новых элементов, упрощает чтение и сопровождение программ.

Но я отклонился от темы1. Вернемся к проекту DirectMembers. Следующий фрагмент выполняется для каждого класса в пространстве имен System. Windows.Forms.

Dim members(), mi As Memberlnfo

treeViewl().Nodes.Add (tn)

members = ThisType.GetMembers(B1ndingFlags.De_

BindingFlags.Public Or BindingFlags.Instance Or _

BindingFlags.Static)

Массив Members заполняется данными обо всех открытых и общих членах, объявленных на этом уровне (к их числу относятся перегруженные реализации, но не те, которые были в неизменном виде унаследованы от базовых классов). Затем программа в цикле перебирает все элементы массива.

For Each mi In members

 Dim methinfo As Methodlnfo

 Select Case mi.MemberType 

Case MemberTypes.Method

 methinfo = CType(mi, Methodlnfo)

 If Not methinfo.IsSpecialName Then

tn.Nodes.Add (StripType(mi.ToString))

 End If

Case MemberTypes.Event

 tn.Nodes.Add (StripType(mi.ToString) &_

" event")

 Case Else

tn.Nodes.Add (StripType(mi.ToString))

 End Select

 Next End 

If Next 

End Sub

Метод ToString класса Methodlnfo создает полное строковое представление метода, включая специальные методы доступа (вида get_xxx для свойства ххх). В строку включается информация о возвращаемом значении (задается в отдельном пространстве имен). Если функция вызывается для метода, мы проверяем результат и заносим его в список лишь в том случае, если метод не является специальным. Если метод на самом деле оказывается событием, к строке для наглядности добавляется слово «event». Свойства добавляются без дополнительной обработки. Метод StripType удаляет информацию о возвращаемом значении, чтобы результат легче читался. Тип возвращаемого значения всегда можно узнать из документации.

Private Function StripType(ByVal s As String) As String

Dim spacepos As Integer

spacepos = InStr(s, " ")

If spacepos > 0 Then Return Mid$(s, spacepos + 1)

Return (s)

 End Function

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

При помощи этой утилиты вы сможете легко определить, какие члены были добавлены в класс. Рисунок 13.2 ясно показывает, что количество новых членов класса User-Control по отношению к базовому классу Container-Control относительно невелико.

Рис. 13.2. Приложение DirectMember

Дальнейшие исследования

Как я и обещал, мы не будем рассматривать все изменения в пакете форм, однако некоторые классы и изменения заслуживают особого внимания.

AutoRedraw, события и переопределения

Я не люблю свойство AutoRedraw VB6. В эпоху VB1 разработчики Visual Basic решили, что программирование, управляемое событиями, в области графического вывода порождает слишком много проблем. Программистам VB и так приходилось адаптировать свои DOS-приложения под обработку событий, и заставлять их обрабатывать событие Paint и перерисовывать графику было бы слишком жестоко.

Возможно, они были правы.

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

Руководствуясь этими соображениями, разработчики Microsoft создали фоновые растровые изображения для всех элементов Form и Picture. Все графические операции с этими объектами в действительности выполнялись с фоновыми растрами, содержимое которых затем автоматически копировалось в форму или элемент. Использование этого растра зависело от значения свойства AutoRedraw, по умолчанию равного True (использовать фоновый растр). Хотя такой подход упрощал задачу бывших DOS-программистов, он был связан с большими затратами ресурсов, особенно в 16-разрядных операционных системах того времени.

В последующих версиях Visual Basic это свойство по умолчанию было равно False, и перерисовка была основана на использовании события Paint.

Вероятно, в наши дни большинство программистов VB не использует свойство AutoRedraw = True, однако бывают и исключения, например, если элемент содержит сложное графическое изображение и экономия времени на его построение оправдывает затраты на хранение фонового растра (что в наши дни обходится гораздо дешевле).

В проекте AutoRedraw показано, как функциональные возможности «в стиле AutoRedraw» реализуются на уровне приложения.

В следующем фрагменте определяется объект фонового растра и переопределяется событие Paint. 

Dim backgroundbitmap As Bitmap

Protected Overrides Sub OnPaint(ByVal e As

  System.Windows.Forms.PaintEventArgs)

MyBase.OnPaint (e) ' Что произойдет, если убрать эту команду?

If backgroundbitmap Is Nothing Then

' Событие может быть получено до получения Resize -

' особенно при первой перерисовке.

backgroundbitmap = New Bitmap(Me.ClientSize.Width, _

Me.ClientSize.Height)

End If

e.Graphics.Drawlmage(backgroundbitmap, 0, 0)

 End Sub

Если фоновый растр не существует, он создается программой, а если существует — его содержимое просто копируется на форму.

У приведенного фрагмента есть одна довольно странная особенность. В отличие от VB6, он не является обработчиком события с формальной точки зрения!

Подумайте — что такое событие? Механизм, при помощи которого объект сообщает своим клиентам о выполнении некоторого условия. Например, форма инициирует событие, чтобы передать нужную информацию программному коду, находящемуся за пределами формы.

Еще раз: при помощи событий объект передает информацию внешнему коду.

Однако в приведенном примере мы неиспользуем форму, а реализуем ее. В VB6 вы создавали клиентский код, использующий стандартный объект Form. В VB .NET вы создаете собственный уникальный класс, производный от стандартного класса формы. Вместо того чтобы ограничиваться обработкой событий в базовом классе, вы обычно переопределяете реализации базового класса и наделяете свой класс уникальными возможностями.

Но каким образом ваш класс инициирует события Paint?

Приложение AutoRedraw содержит метод cmdOtherForm_Click, создающий новый экземпляр класса For ml. Другими словами, первая форма действительно становится клиентом по отношению к другому экземпляру класса. В качестве клиента она может обнаруживать события этого объекта, как показывает следующий фрагмент:

Private Sub OtherFormPaint(ByVal sender As Object, _

ByVal args As PaintEventArgs)

Debug.WriteLine("Other paint arrived")

 End Sub

Private Sub cmdOtherForm_Click(ByVal sender As System.Object,

ByVal e As System.EventArgs) Handles cmdOtherForm.Click

Dim f As New Forml()

f.Visible = True

AddHandler f.Paint, AddressOf Me.OtherFormPaint

 End Sub

Попытайтесь запустить приложение и щелкнуть на кнопке Other Form два раза: с командой MyBase.OnPaint (приведена выше) и без нее. Вы увидите, что событие Paint инициируется только при вызове метода MyBase.OnPaint. Из этого можно сделать вывод, что событие инициируется базовым классом (производным от класса Control).

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

Предположим, вы хотите, чтобы ваша форма обладала собственным событием Paint — возможно, с уникальной сигнатурой. Нет проблем. Просто определите собственное событие, маскирующее событие базового класса:

Shadows Event Paint(ByVal sender As Object, ByVal args AS PaintEventArgs)

При желании вы можете инициировать новое событие командой RaiseEvent.

Различия между реализацией и использованием формы/элемента — вопрос весьма тонкий, и большинству программистов VB6 придется к ним привыкать. К счастью, это один из тех случаев, когда задача решается несколькими способами, и ни одному из них нельзя отдать однозначного предпочтения.

Вернемся к проекту. На форме находятся еще две кнопки: первая кнопка объявляет текущее содержимое формы недействительным (что приводит к форсированной перерисовке и инициированию события Paint), а вторая рисует на форме косой крест и объявляет форму недействительной.

Private Sub cmdInvalidate_Click(ByVal sender As System.Object,_

ByVal e As System.EventArgs) Handles cmdlnvalidate.Click 

Me.Invalidate()

 End Sub

Private Sub cmdDrawStuff_Click(ByVal sender As System.Object,

 _ ByVal e As System.EventArgs) Handles cmdDrawStuff.Click

Dim g As Graphics = graphics.Fromlmage(backgroundbitmap)

g.DrawLine(pens.Black, 0, 0, 200, 200)

g.Drawl_ine(pens.Red, 0, 200, 200, 0)

cmd!nvalidate_Click(He, Nothing)

 End Sub

В реализации фонового растра особый интерес представляет обработка события Resize:

Protected Overrides,Sub OnResize(ByVal e As System.EventArgs)

 If Not Me.ClientRectangle.IsEmpty Then

If Not Me.ClientSize.Equals(New Size(backgroundbitmap.Width._

  backgroundbitmap.Height)) Then

 Dim newbitmap As Bitmap

newbitmap = New Bitmap(Me.ClientSize.Width, _

  Me.ClientSize.Height) 

If Not backgroundbitmap Is Nothing Then

 Dim g As Graphics = graphics.Fromlmage(newbitmap)

  g..DrawImage(backgroundbi tmap, 0, 0)

End If

backgroundbitmap = newbitmap 

End If

 End If 

End Sub

При изменении размеров окна функция сначала убеждается в том, что форма не была свернута (в этом случае структура Client Rectangle описывает пустой прямоугольник). Если новые размеры формы соответствуют текущим размерам растра (что происходит при восстановлении свернутой формы), делать ничего не нужно. В противном случае создается новый растр нужного размера и в него копируется текущее содержимое растра.

Поэкспериментируйте с изменением размеров формы. Уменьшите форму так, чтобы крест на ней не помещался, и затем снова увеличьте ее.

Также обратите внимание на то, что во всех вычислениях координаты задаются в пикселах. Пакет графического вывода позволяет работать в других единицах, однако во всех базовых операциях управления окнами используются пикселы. Честно говоря, я никогда не понимал, почему в Visual Basic 6 и более ранних версиях использовались твипы. Этими единицами почти никто не пользуется, к тому же они приводят к ошибкам округления при выводе и вычислении прямоугольников.

Главный аргумент в пользу пикселов — их эффективность. Пикселы и целые числа используются в работе внутренних механизмов Windows. Твипы и вещественные вычисления лишь замедляют работу Visual Basic, не предоставляя программисту никаких новых возможностей.

 

Формы MDI и принадлежность окон

При попытках организовать связи между формами программисты VB6 постоянно сталкиваются с трудностями. Для передачи элементов между формами или другими контейнерами они используют функцию API SetParent. Проблемы возникают и с отношениями принадлежности, поскольку все дочерние окна сворачиваются вместе с родительским окном. Это особенно важно в приложениях, создающих несколько форм верхнего уровня одновременно, — обнаруживать свертывание одной формы, чтобы вручную сворачивать остальные формы, по меньшей мере неудобно.

Наконец, программисты VB6 испытывают трудности с созданием надежно работающих иерархий MDI, в которых дочерние формы могут реализовываться в виде отдельных библиотек DLL. В одной из моих самых популярных статей описывалась динамическая иерархия MDI и было показано, как решить эту проблему при помощи динамически создаваемых пользовательских элементов VB6, однако попытки использования в этой схеме коммерческих элементов сопровождались появлением нетривиальных Ошибок и лицензионных проблем.

А теперь посмотрите, как это делается в VB .NET.

Чтобы установить иерархическую связь между окнами, создайте приложение с двумя формами и объявите в первой форме экземпляр второй формы: 

Dim f2 As New Form2()

В дальнейшем связь между формами создается простым включением второй формы в коллекцию OwnedForm первой формы:

 Me.AddOwnedForm(f2)

Ну как, впечатляет? Еще бы! Ведь изменить принадлежность формы после ее создания невозможно не только в VB6, но и в Windows! Принадлежность формы должна определяться при создании окна. Как же .NET Framework добивается такого эффекта? Точно так же, как при изменении стилей окон: существующее окно уничтожается и воссоздается заново с новым владельцем.

Но это еще не все!

Чтобы объявить главную форму приложения формой MDI, достаточно задать значение свойства IsMdiContainer

Me.IsMdiContainer = True

Отдельный класс MDI Form не нужен. Допустим, у вас имеется другая форма (возможно, даже реализованная в отдельной сборке). Стоит задать ее свойству MdiParent нужное значение, и она мгновенно превращается в дочернее окно MDI:

Me.MdiParent = parent

Проще не бывает. Примеры практического использования этих приемов показаны в приложении FormlnForm.

А заодно рассмотрите приложение Parenting и посмотрите, как кнопка перемещается с одной формы на другую всего одной строкой программного кода.

В VB6 элементы обычно были статическими и создавались на стадии конструирования. Создавать элементы динамически можно было только с применением массивов элементов.

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

В VB .NET все элементы и формы создаются динамически, а статических элементов «стадии конструирования» не существует в принципе. При размещении элемента на форме на стадии конструирования мастер Design Wizard генерирует код динамического создания этого элемента.

А если вам все же не хватает массивов элементов, обратитесь к проекту EventExample главы 10 — в нем показано, как аналогичные возможности реализуются в VB .NET.

 

Субклассирование и объект Application

Термин «субклассирование» (sub>) означает перехват сообщений Windows, получаемых формой или элементом (как правило, для реализации возможностей, не поддерживаемых стандартными событиями Visual Basic).

Моя компания Desaware завоевала свою репутацию за создание лучшего компонента для субклассирования до появления Visual Basic 5, когда субклассирование в VB вообще не поддерживалось1

1Это был наш первый пакет из семейства SpyWorks. В последующих версиях возможности SpyWorks уже не ограничивались субклассированием.

Когда в VB5 появились средства субклассирования, мы создали компонент VB, который демонстрировал их правильное применение, и включили его исходный текст в поставку SpyWorks.

Приложение Messages показывает, как субклассирование выполняется в .NET.

Private Sub buttonl_Click(ByVal sender As System.Object,

ByVal e As System.EventArgs) Handles buttonl.Click

listBoxK).Items.Clear()

 End Sub

Protected Overrides Sub wndproc(ByRef m As Message)

 Select Case m.Msg

Case &H20, &H84

' Игнорировать многочисленные

' сообщения WM_SETCURSOR, WM_NCHITTEST Case 

Else

listBoxlO.I terns.Add (m.ToString)

End Select MyBase.WndProc (m)

 End Sub

На форме находится список всех сообщений, полученных формой, кроме WM_SETCURSOR и WM_NCHITTEST (их слишком много). Все, что от нас требуется, — переопределить функцию wndproc базового класса Control и обработать интересующие сообщения. Не забудьте вызвать MyBase.WndProc, иначе вы рискуете серьезно нарушить работу формы. Метод MyBase.WndProc выполняет стандартную обработку сообщений, благодаря чему все окна сохраняют свое характерное поведение.

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

Переопределяя функцию PreProcessMessage, можно установить фильтр предварительной обработки сообщений. В частности, это позволяет организовать специальную обработку некоторых клавиш (например, Tab и клавиш управления курсором).

Кроме того, объект System.Windows.Forms.Application позволяет установить фильтр сообщений для программного потока методом Application . Add-MessageFilter. При этом устанавливается перехватчик уровня потока, который обнаруживает сообщения, выбранные из очереди для конкретного потока. Действуйте осмотрительно, поскольку установка фильтра может отрицательно повлиять на быстродействие программы.

Я рекомендую продолжить самостоятельные исследования класса Application. Этот класс содержит ряд полезных свойств и методов, относящихся к работе приложений, включая пресловутый метод DoEvents (которым пользоваться не рекомендуется, особенно учитывая поддержку многопоточности в VB .NET).

Если вы хотите выполнять межпроцессное субклассирование, перехватывать сообщения других процессов или всей системы или обнаруживать нажатия клавиш на системном уровне, VB .NET не предложит вам ничего нового по сравнению с VB6. Впрочем, пакет SpyWorks 6.5 (вероятно, он уже будет продаваться к моменту выхода книги) после доработки позволяет решать все эти задачи в приложениях Visual Basic .NET. За подробностями обращайтесь по адресу http:// www.desaware.com1.

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

 

Формы и потоки

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

Шутка.

Но вы должны очень хорошо понять, как работать с формами и элементами в многопоточном приложении.

Проект Threading показывает, как создать форму в отдельном потоке. Как обычно, большая часть автоматически сгенерированного кода пропускается. В процессе обработки события Forml_Load в элементе Label выводится идентификатор текущего потока, которому принадлежит форма.

Private Sub Forml_Load(ByVal sender As System.Object, _ 

ByVal e As System.EventArgs) Handles MyBase.Load

 labell.Text = "Thread ID: " & CStr(AppDomain.GetCurrentThreadld)

End Sub

Переменная OtherThread определяет объект класса Thread, связанный с процедурой OtherThreadEntryPoint. При запуске этого потока (нажатием кнопки buttonl) создается новый экземпляр формы2. Процедура вызывает метод Application.Run и передает ему форму в качестве параметра. При вызове Арplication.Run поток входит в цикл обработки сообщений. Заданная форма отображается на экране, а сообщения обрабатываются вплоть до ее закрытия (CLR автоматически выполняет указанные действия для стартовой формы приложения). Ссылка на новую форму хранится в переменной OtherForml.

 Dim OtherThread As New Thread(AddressOf OtherThreadEntryPoint)

Module UsedByThread

Public OtherForml As Forml 

Public Sub OtherThreadEntryPoint()

 OtherForml = New Forml()

 OtherForml.DisableButtons()

 Application.Run (OtherForml) 

End Sub 

End Module

Private Sub buttonl_Click(ByVal sender As System.Object, _

ByVal e As System.EventArgs) Handles buttonl.Click

Other-Thread.Start()

 End Sub

2Не нажимайте кнопку во второй раз! Это простой демонстрационный пример и проверка ошибок в нем не предусмотрена.

Метод ThreadIdInLabel2 формы выводит идентификатор текущего потока в элементе-надписи label2.

Public Sub ThreadIdInLabel2()

label2.Text = "Current Thread " & CStr(AppDomain.GetCurrentThreadld)

 End Sub

При нажатии кнопки button2 вызывается метод ThreadIdInLabel2 формы OtherForml:

Private Sub button2_Click(ByVal sender As System.Object, _

ByVal e As System.EventArgs) Handles button2.Click

If Not OtherForml Is Nothing Then

 OtherForml.ThreadIdInLabel2()

End If 

End Sub

Как показывает эксперимент, на новой форме выводится идентификатор потока исходной, а не новой формы.

Конечно, это плохо.

Дело в том, что в документации ничего не сказано о потоковой безопасности пакета форм. Более того, Microsoft специально оговаривает, что все методы формы или элемента должны вызываться в том потоке, где они создавались.

К счастью, у этой задачи имеется простое решение. Метод Invoke класса Control (который, как было сказано выше, является базовым классом всех форм и элементов) правильно осуществляет маршалинг вызовов методов и процедур в поток элемента. Приведенная ниже процедура button3_Click показывает, как следует вызывать метод Invoke. Если воспользоваться этим приемом, на новой форме будет выводиться правильный идентификатор программного потока. 

Delegate Sub NoParams()

Private Sub button3_Click(ByVal sender As System.Object,_

 ByVal e As System.EventArgs) Handles buttons.Click

 If Not OtherForml Is Nothing Then

Dim usedel As NoParams = AddressOf OtherForml.ThreadIdInLabel2

 OtherForml.Invoke (usedel)

 End If 

End Sub

Инициирование событий объектов в других потоках безопасно. При этом используется механизм вызова с применением делегатов, а вызовы методов передаются нужному потоку.

Избегайте вызовов Sync Lock и других функций ожидания для потоков, работающих с элементами или другими составляющими пользовательского интерфейса. Мало того, что это может нарушить нормальную работу пользовательского интерфейса — возникает риск взаимной блокировки, поскольку эти объекты часто реентерабельны (например, вызов методов часто приводит к вызовам других методов или инициированию событий).

Но, прежде всего, старайтесь избегать создания форм в разных потоках. Приложения, выигрывающие от многопоточного пользовательского интерфейса, встречаются очень редко — только в ситуациях, когда каждая форма ведет себя как отдельное приложение (например, окна web-браузеров обычно принадлежат к разным потокам, а окна документов — нет). Тем не менее представленные принципы применимы и к более общему случаю — созданию в фоновом потоке компонента или класса, взаимодействующего с пользовательским интерфейсом (как правило, посредством событий).

 

Итоги

Эта глава не сводится к простому сопоставлению возможностей VB6 и VB .NET — в ней рассматривается архитектура, и я даже рискну сказать — философия нового пакета форм .NET Framework. Мы познакомились с иерархией основных объектов пространства имен System.Windows. Forms и выяснили, что все элементы и формы .NET основаны на базовом классе Control. Простое приложение Direct-Members поможет вам быстро определить, какие члены определяются непосредственно самими классами пространства имен.

Далее мы рассмотрели некоторые модификации, обусловленные не столько расширенной функциональностью, сколько изменениями в архитектуре. Вы узнали, как имитировать свойство AutoRedraw VB6, причем этот пример был использован для объяснения различий между использованием формы (VB6) и реализацией нового типа Form (VB .NET). Были приведены примеры связей между формами и элементами, которые являются естественным следствием того, что все элементы в .NET создаются динамически. Также вы узнали, как в VB .NET организовать обработку сообщений и как обеспечить потоковую безопасность при работе с формами.

Назад   Вперёд

 


Инфо
Сайт создан: 20 июня 2015 г.
Рейтинг@Mail.ru
Главная страница