Изучаем Visual Studio .NET. Часть 3. Компоненты Windows-приложений

Алексей Федоров, Наталия Елманова

Часть 2

В предыдущей статье рассказывалось, как с помощью Microsoft Visual Studio .NET создавать Windows-приложения. Мы рассмотрели основные элементы среды разработки, используемые в этом процессе, ознакомились с типами интерфейсных элементов, а также узнали, как создать простое однооконное и MDI-приложение. Кроме того, мы рассмотрели файлы, из которых состоит проект, и получили представление об их содержимом. В данной статье мы завершаем знакомство с Microsoft Visual Studio .NET, расскажем о базовых принципах организации Windows-приложений и приведем ряд примеров использования компонентов, реализующих интерфейсные элементы.

Оглавление

Генерация кода Windows-приложений

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

Выделим в окне Solution Explorer элемент Form1.vb и из контекстного меню этого элемента выберем пункт View code.

Мы увидим, что в редакторе кода содержатся сведения о том, что класс Form1 является наследником класса System.Windows.Forms.Form.

Заглянув в блок "Windows Form Designer generated code", мы обнаружим там следующий инициализационный код:

Public Class Form1
Inherits System.Windows.Forms.Form

#Region "Windows Form Designer generated code"

Public Sub New()
MyBase.New()

'This call is required by the Windows Form Designer.
InitializeComponent()

'Add any initialization after the InitializeComponent() call

End Sub

'Form overrides dispose to clean up the component list.
Protected Overloads Overrides Sub Dispose(ByVal disposing _
As Boolean)
If disposing Then
If Not (components Is Nothing) Then
components.Dispose()
End If
End If
MyBase.Dispose(disposing)
End Sub

'Required by the Windows Form Designer
Private components As System.ComponentModel.IContainer

'NOTE: The following procedure is required by the Windows Form
'Designer. It can be modified using the Windows Form Designer.
'Do not modify it using the code editor.
<System.Diagnostics.DebuggerStepThrough()>
Private Sub InitializeComponent()
components = New System.ComponentModel.Container()
Me.Text = "Form1"
End Sub

#End Region

End Class

Что означает код, приведенный выше? Описание класса формы Form1 начинается с конструктора New(), в котором вызываются конструктор базового класса и процедура InitializeComponent - в ней инициализируются компоненты, располагаемые на форме (на данный момент отсутствующие). Изначально процедура InitializeComponent выполняет одно-единственное действие - устанавливает значение свойства формы Text (надпись на ее заголовке). Далее переписывается метод Dispose класса формы, отвечающий за освобождение ресурсов, потребляемых формой и содержащихся на ней компонентов. Отметим, что в этом методе неуправляемые ресурсы должны быть обязательно освобождены, а управляемые ресурсы должны стать доступны сборщику мусора (подробнее об управляемых и неуправляемых ресурсах вы можете прочитать в цикле статей, посвященных Microsoft .NET Framework).

Теперь добавим к форме кнопку (компонент Button) и выясним, что изменилось при этом в сгенерированном Visual Studio .NET коде.

Мы обнаружим, что среди полей класса Form1 теперь имеется поле Button1 - наследник класса System. Windows.Forms.Button, а в процедуре InitializeComponent теперь появился код создания экземпляра этого класса и присвоения значений его свойствам Location, Size, Name, Text, TabIndex, а также появился код, отвечающий за размещение и отображение кнопки на форме:

Friend WithEvents Button1 As System.Windows.Forms.Button
<System.Diagnostics.DebuggerStepThrough()>
Private Sub InitializeComponent()
Me.Button1 = New System.Windows.Forms.Button()
Me.SuspendLayout()
'
'Button1
'
Me.Button1.Location = New System.Drawing.Point(16, 32)
Me.Button1.Name = "Button1"
Me.Button1.Size = New System.Drawing.Size(104, 32)
Me.Button1.TabIndex = 0
Me.Button1.Text = "Button1"
'
'Form1
'
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.Button1})
Me.Name = "Form1"
Me.Text = "Form1"
Me.ResumeLayout(False)

End Sub

Иными словами, когда мы имеем дело с контейнерами визуальных компонентов (каковым является, например, форма), последние становятся полями классов первых, и это находит отражение в сгенерированном коде.

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

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

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As _ System.EventArgs) Handles Button1.Click
MessageBox.Show("Button1 was clicked")
End Sub

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

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

Интерфейсные элементы Windows-приложений

Общие свойства, события и методы

Базовым классом всех интерфейсных элементов Windows-приложений в .NET Framework является класс System. Windows.Forms.Control, содержащийся в пространстве имен System. Windows.Forms. Именно в этом классе определены общие для всех интерфейсных элементов свойства, события и методы. Перечислим наиболее важные из них:

  • Cursor, Font, BackColor, ForeColor (так называемые Ambient properties) - свойства, значения которых элемент управления наследует от содержащего его контейнера, если значение этого свойства в явном виде не установлено и не определено в родительском классе;
  • Top, Left, Width, Height, Size, Location - свойства, отвечающие за размер и местоположение элемента относительно контейнера (для формы контейнером в этом случае является экран);
  • Anchor и Dock - свойства, определяющие, согласно каким принципам перемещается и меняет размеры интерфейсный элемент при изменении размеров контейнера;
  • Text, ImeMode, RightToLeft - свойства, определяющие надпись или текст в элементе управления, а также направление текста и способ его редактирования (последние два свойства, впрочем, не слишком актуальны для европейских языков, в том числе и для русского);
  • Enabled, Visible - свойства, определяющие, доступен ли пользователю интерфейсный элемент и отображается ли он;
  • Parent - свойство, указывающее, какой из интерфейсных элементов является контейнером для данного элемента.

Из наиболее важных методов этого класса следует отметить методы BringToFront, SendToBack, Show, Hide, Contains, Refresh, Update, а из событий - события, связанные с перемещением мыши и нажатием на клавиши MouseDown, MouseMove, MouseUp, MouseLeave, MouseHover, MouseEnter, MouseWheel, KeyDown, KeyPress, KeyUp, Click, с применением операции drag-and-drop: DragDrop, DragEnter, DragLeave, DragOver, а также связанные с изменением размера элемента - Resize - и его перерисовкой - Paint.

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

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

Применение компонентов DataSet, DataGrid и DateTimePicker

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

Для этого создадим новый проект и разместим на форме нашего приложения две кнопки и по одному элементу управления TextBox, DateTimePicker и DataGrid. Изменим надписи на кнопках (свойство Text) на «Добавить событие» и «Скрыть форму». Установим свойство FormBorderStyle формы равным FixedSingle - это предотвратит изменение пользователем ее размеров.

С помощью компонента DateTimePicker пользователь может ввести дату и время события, о котором ему следует напомнить. В элементе управления Text1 он может ввести текст напоминания. Однако список событий, содержащий эти данные, нужно где-то сохранить, и разумным представляется сохранить его не просто в памяти, а в файле на жестком диске - в этом случае сведения о будущих событиях не будут потеряны после перезапуска приложения. Пусть это будет XML-документ.

Поскольку список событий - это, по существу, набор данных, воспользуемся компонентом DataSet с вкладки Data в палитре компонентов. Так как этот компонент невизуальный, при перетаскивании его на форму он окажется в нижней части окна дизайнера форм. Компонент DataSet представляет интерфейс к кэшу в памяти, в котором могут содержаться одна или несколько таблиц (in-memory tables), которые могут быть результатом запроса к базе данных, чтения XML-документа, а могут быть созданы в самом приложении «из ничего». В данный момент этот набор данных не содержит ни одной таблицы, и сейчас мы займемся ее созданием.

Для создания таблицы со списком событий выберем только что созданный компонент DataSet1 и щелкнем мышью возле свойства Tables в окне Properties.

В результате получим окно Tables Collection Editor, в котором мы можем нажать клавишу Add и добавить в коллекцию Tables класса DataSet1 новую таблицу, которой присвоим имя Schedule.

Далее следует определить, что представляют собой поля этой таблицы. Для этого в редакторе Tables Collection Editor щелкнем мышью возле свойства Columns, после чего на экране появится окно Columns Collection Editor редактора коллекции полей созданной таблицы. С помощью кнопки Add добавим в нее три поля - EventID, DateTime и Message и установим их типы данных равными System.Int32, System.DateTime и System.String. Установим значение свойства AutoIncrement поля DateTime равным True - это позволит не заботиться о присвоении значений этому полю в коде приложения.

Нажимаем кнопку Close в редакторе Columns Collection Editor. В редакторе Tables Collection Editor, который по-прежнему открыт, установим значение PrimaryKey таблицы Schedule равным имени первой колонки и закроем этот редактор. Структура таблицы готова. Установим свойство DataSource элемента управления DataGrid1 равным DataTable1.

Теперь изменим внешний вид элемента управления DataGrid - как минимум, нам нужно, чтобы ширина колонок соответствовала отображаемым данным. Для этой цели нам следует создать коллекцию стилей отображения таблиц - DataGridTableStyles. Щелкнем по кнопке возле свойства TableStyles элемента управления DataGrid1 и в появившейся диалоговой панели создадим один из элементов такой коллекции. Установим его свойство MappingName равным имени отображаемой таблицы - Schedule; теперь эта таблица будет иметь такой вид, который описан в данном стиле. В этой же диалоговой панели можно выбрать и другие параметры отображения данных (например, цвет текста, фона, обрамления, линий и т.д.). В общем случае элемент управления DataGrid может быть связан с компонентом DataSet, содержащим несколько таблиц, - тогда можно отображать разные таблицы различными стилями.

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

Создадим три элемента этой коллекции - по одному на каждое поле таблицы - и установим их свойства Width (ширина) и HeaderText (заголовок) в соответствии с данными, содержащимися в данной таблице. Установим также значение свойства Format второй колонки равным «f» - в этом случае данные типа DateTime отображаются полностью и в соответствии с региональными настройками данного компьютера.

В результате элемент управления DataGrid1 приобретет вид, изображенный на следующем рисунке:

Теперь нам следует позаботиться о сохранении данных в файле (пусть он называется c:\Schedule.xml) и считывании их из файла. Представляется вполне разумным считывать файл при загрузке формы, поэтому мы создадим обработчик события Load формы:

Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As _
System.EventArgs) Handles MyBase.Load

Try
DataSet1.ReadXml("c:\Schedule.xml")
Catch
DataSet1.WriteXml("c:\Schedule.xml")
MessageBox.Show("Файл с расписанием не найден. " & _
" Создан новый")
End Try

End Sub

В этом фрагменте кода мы считываем файл c:\Schedule.xml, если он существует, и создаем новый (пустой), если он не найден.

Имеет смысл также сохранять файл с данными о событиях при добавлении в него новых записей. Поэтому реализуем соответствующий код в обработчике события Click кнопки с надписью «Добавить событие»:

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As _
System.EventArgs) Handles Button1.Click

Dim DR As DataRow = DataSet1.Tables(0).NewRow()
DR(1) = DateTimePicker1.Value
DR(2) = TextBox1.Text
DataSet1.Tables(0).Rows.Add(DR)
DataSet1.WriteXml("c:\Schedule.xml")
DataGrid1.Refresh()

End Sub

В этом фрагменте кода мы создаем новый объект DataRow и присваиваем его второму и третьему полю значения, считанные из элементов управления DateTimePicker1 и TextBox1 (значения же первого поля будут присваиваться автоматически, поскольку мы указали, что оно автоинкрементное). Затем добавляем этот объект DataRow в коллекцию строк созданной ранее таблицы, сохраняем ее в XML-файле и обновляем содержимое элемента управления DataGrid.

Далее изменим внешний вид и поведение элемента управления DateTimePicker1, позволив пользователю выбирать отдельно год, месяц, день, час и минуты. Для этого установим свойство ShowUpDown этого элемента управления равным True и в уже созданный обработчик события Load формы добавим фрагмент кода (он выделен жирным шрифтом), в котором значение свойства Format этого элемента управления устанавливается равным Custom, а значение свойства CustomFormat - равным строке "MMMM dd, yyyy - dddd, hh:mm", что позволит вводить в этом элементе год, месяц, день, час и минуты и отображать день недели. Заодно запретим вводить значения времени, относящиеся к прошлому:

Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As _
System.EventArgs) Handles MyBase.Load

DateTimePicker1.MinDate() = Now()
DateTimePicker1.Format = DateTimePickerFormat.Custom
DateTimePicker1.CustomFormat = "MMMM dd, yyyy - dddd, hh:mm"
Try
DataSet1.ReadXml("c:\Schedule.xml")
Catch
DataSet1.WriteXml("c:\Schedule.xml")
MessageBox.Show("Файл с расписанием не найден. Создан новый")
End Try

End Sub

Вот как примерно выглядит XML-файл со списком событий после их ввода с помощью созданного приложения:

<?xml version="1.0" standalone="yes"?>
<ScheduleDataSet>
<Schedule>
<EventID>1</EventID>
<DateTime>2002-04-21T16:15:00.0000000+04:00</DateTime>
<Message>Тестовое событие 1</Message>
</Schedule>
<Schedule>
<EventID>2</EventID>
<DateTime>2002-04-21T18:42:24.0000000+04:00</DateTime>
<Message>Тестовое событие 2</Message>
</Schedule>
<Schedule>
<EventID>3</EventID>
<DateTime>2002-04-21T18:47:03.0000000+04:00</DateTime>
<Message>Выключить сервер на ночь</Message>
</Schedule>
<Schedule>
<EventID>4</EventID>
<DateTime>2002-04-21T18:49:01.2592912+04:00</DateTime>
<Message>Позвонить Иванову по поводу завтрашнего собрания</Message>
</Schedule>
<Schedule>
<EventID>5</EventID>
<DateTime>2002-04-21T19:02:34.4686288+04:00</DateTime>
<Message>Тестовое событие 3</Message>
</Schedule>
</ScheduleDataSet>

Итак, мы научились вводить описания событий, отображать их в DataGrid и сохранять в XML-файле. Наша следующая задача - добавить к приложению возможность оповещать о них пользователя. Этим мы займемся в следующем разделе.

Применение компонентов Timer и NotifyIcon

Самый простой (хотя и не самый эффективный) способ оповещения пользователя о событии - это периодический просмотр списка событий с целью выяснения, не наступило ли время оповестить о каком-либо из них. Для выполнения некоторых периодических действий можно использовать компонент Timer - это невизуальный компонент, обладающий регулярно повторяющимся событием Tick и свойством Interval, равным числу миллисекунд, через которые наступает это событие.

Добавим компонент Timer в наше приложение и установим его свойство Interval равным 60 000, что соответствует одной минуте, а свойство Enabled равным True.

Создадим обработчик события Tick:

Private Sub Timer1_Tick_1(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Timer1.Tick

Dim DR As DataRow
Dim DT As New DateTime()
For Each DR In DataSet1.Tables(0).Rows
DT = CType(DR(1), DateTime)
If (Now() > DT And Now() < DT.AddMinutes(1)) Then
MessageBox.Show(DR(2))
End If
Next

End Sub

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

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

Для этой цели мы можем использовать компонент NotifyIcon, позволяющий создать такую пиктограмму. Это невизуальный компонент, основными свойствами которого являются Icon (графический файл с пиктограммой - в нашем примере это изображение будильника), Text (надпись на ярлычке с подсказкой) и ContextMenu.

Прежде чем установить значение свойства ContextMenu этого элемента, следует перенести в приложение компонент ContextMenu (он также невизуальный). Выбрав из контекстного меню созданного нами компонента ContextMenu1 пункт Edit Menu, мы можем отредактировать его, добавив к нему необходимые пункты, например «Список событий» и «Закрыть».

Создадим обработчики этих событий:

Private Sub MenuItem4_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MenuItem4.Click
Application.Exit()
End Sub

Private Sub MenuItem1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MenuItem1.Click
Me.Show()
End Sub

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

Private Sub Button2_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button2.Click
Me.Hide()
End Sub

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

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

Заключение

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


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