Размер
Размер – это структура типа Size или SizeF, принадлежащего пространству имен System.Drawing. Размер предназначен для задания ширины и высоты прямоугольной области.
Структура Размер аналогична структуре Точка, только вместо свойств X и Y у нее имеются свойства Width и Height.
Прямоугольник
Прямоугольник – это структура типа Rectangle, принадлежащего пространству имен System.Drawing. Прямоугольник определяется парой координат своего левого верхнего угла (X и Y), шириной (Width) и высотой (Height).
Dim Прямоугольник As Rectangle
Прямоугольник.X = 20
Прямоугольник.Width = 80
Debug.WriteLine(Прямоугольник)
Этот фрагмент напечатает:
{X=20,Y=0,Width=80,Height=0}
Кроме этих основных свойств у Прямоугольника есть еще несколько приятных дополнительных, с которыми вас может познакомить система помощи.
Также у Прямоугольника есть несколько интересных методов, с одним из которых (Inflate) мы познакомимся.
Кроме Прямоугольника типа Rectangle в VB имеется Прямоугольник типа RectangleF, который отличается от первого тем, что его координаты и размеры имеют тип не Integer, а Single.
Использование Точки и Прямоугольника в графических методах
Просмотрите еще раз варианты графических методов для рисования. Вы найдете много таких, в которых параметры обязаны иметь типы Point, PointF, Rectangle, RectangleF.
Вот пример рисования линии, прямоугольника и эллипса вариантами графических методов с использованием Точек и Прямоугольников:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim Гр As Graphics = Me.CreateGraphics
'Объявляем точки и прямоугольники:
Dim Т1, Т2 As Point
Dim П1, П2 As Rectangle
'Задаем координаты и размеры:
Т1.X = 50 : Т1.Y = 20
Т2.X = 200 : Т2.Y = 80
П1.X = 120 : П1.Y = 100 : П1.Width = 150 : П1.Height = 30
П2.X = 220 : П2.Y = 200 : П2.Width = 180 : П2.Height = 40
'Рисуем с использованием Точек и Прямоугольников:
Гр.DrawLine(Pens.Black, Т1, Т2)
Гр.DrawRectangle(Pens.Black, П1)
Гр.DrawEllipse(Pens.Black, П2)
End Sub
Пояснения: Как видите, заранее необходимо создать все нужные Точки и Прямоугольники и придать им нужные вам координаты и размеры, а затем уже можно рисовать с использованием подходящих вариантов графических методов. Здесь в методе DrawLine Точки Т1 и Т2 – начальная и конечная точки отрезка. Метод DrawRectangle рисует Прямоугольник П1. А метод DrawEllipse рисует эллипс, вписанный в Прямоугольник П2.
Inflate. Познакомимся с методом Прямоугольника Inflate. Этот метод расширяет (сужает) Прямоугольник во все стороны на заданные размеры. Так, оператор
П1.Inflate(-20, 10)
сузит Прямоугольник П1 на 20 пикселей в обе стороны по горизонтали и расширит на 10 пикселей в обе стороны по вертикали.
Разберитесь в программе:
Dim Гр As Graphics
Dim П1, П2 As Rectangle
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Гр = Me.CreateGraphics
П1.X = 200 : П1.Y = 150 : П1.Width = 150 : П1.Height = 30
П2.X = 550 : П2.Y = 150 : П2.Width = 200 : П2.Height = 10
End Sub
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
П1.Inflate(30, 10) 'Изменяем размеры Прямоугольника в памяти
Гр.DrawRectangle(Pens.Black, П1) 'Рисуем измененный Прямоугольник
End Sub
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
П2.Inflate(-10, 10) 'Изменяем размеры Прямоугольника в памяти
Гр.DrawRectangle(Pens.Black, П2) 'Рисуем измененный Прямоугольник
End Sub
Пояснения: Оператор П1.Inflate(30, 10) расширяет Прямоугольник П1 на 30 пикселей в обе стороны по горизонтали и на 10 пикселей в обе стороны по вертикали. Оператор П2.Inflate(-10, 10) сужает Прямоугольник П2 на 10 пикселей в обе стороны по горизонтали и расширяет на 10 пикселей в обе стороны по вертикали.
В результате нескольких нажатий на кнопку Button1 вы увидите фигуру в левой части Рис. 12.1, а в результате нескольких нажатий на кнопку Button2 – в правой.
Рис. 12.1
Задание 66.
«Круги на воде или радиоволны». Нарисуйте в цикле десяток концентрических окружностей, то есть окружностей разного радиуса, но имеющих общий центр.
Задание 67.
«Компакт-диск» и «Летающая тарелка». Если диаметр самого маленького «круга на воде» будет порядка 100, а самого большого – во весь экран, и если диаметры соседних окружностей будут различаться на 2-4 пикселя, то на экране вы увидите привлекательный «компакт-диск». Сделайте его белым или золотым на черном фоне. Если получилось, то сделайте ему внутренний и наружный ободки другого цвета. А теперь «положите» диск, то есть нарисуйте его не из окружностей, а из эллипсов, сжатых по вертикали. Получится «летающая тарелка».
Задание 68.
Меняя вместе с диаметром еще и вертикальную координату, вы получите «коническую башню».
Точки и прямоугольники
Многие графические методы требуют в качестве параметров так называемые «Точки», «Размеры» и «Прямоугольники». Эти элементы VB похожи по смыслу на одноименные зрительные образы, но это не одно и то же. Я буду писать их с заглавной буквы. Точка – это структура типа System.Drawing.Point. Размер – это структура типа System.Drawing.Size. Прямоугольник – это структура типа System.Drawing.Rectangle. Не путайте его с прямоугольником, нарисованным на экране, который никакой структурой не является, а представляет собой просто светящиеся пиксели. Наши Точка, Размер и Прямоугольник – это структуры, которые, подобно объекту класса Graphics, живут в памяти компьютера невидимо.
Что такое структура? Пока нам достаточно знать, что структура – это один из видов объектов (в широком смысле) VB, который наряду с классами, модулями и перечислениями входит в состав пространств имен. У структуры, также как и у класса, могут быть свойства и методы. Структуры похожи на классы, но чуть-чуть «не дотягивают» до них. Со структурами мы будем знакомиться постепенно.
Создаем собственные перья. Конструктор
Создаем перо. До этого момента для рисования линий мы пользовались стандартными перьями из класса Pens. Там можно было выбирать цвет пера и больше ничего. Мы же хотим управлять также толщиной, стилем и другими свойствами линий. Для этого существует специальный класс Pen, входящий в пространство имен System.Drawing (не путать с классом Pens). Но пользоваться им напрямую нельзя. Нам нужно сначала создать из класса Pen объекты-перья подобно тому, как из класса Button в 6.1.2 мы создавали объекты-кнопки.
Конструктор. В классах для создания из них экземпляров-объектов существует специальная процедура с именем New. Называется она конструктором. Вы можете объявить и создать объект тремя способами записи. Самый длинный:
Dim Перо As Pen 'Объявляем объект
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
Перо = New Pen(Color.Red, 20) 'Создаем объект
End Sub
При создании пера при помощи слова New необходимо в скобках указывать параметры. Я выбрал вариант с указанием двух параметров: цвета пера и толщины. В приведенном фрагменте создано красное перо толщины 20.
Откуда мы знаем, какие нужно указывать параметры, и нужно ли их указывать вообще? Об этом вам скажет подсказка по параметрам, которая возникнет, как только вы откроете скобку после слова Pen. Вы выберете вариант по вкусу и укажете параметры.
О процедуре New и вариантах ее параметров вы можете также узнать при помощи клавиши F1 или Object Browser.
Как видите, обращение к конструктору грамматически несколько отличается от общепринятого обращения к процедуре.
В приведенном варианте при запуске проекта объект только объявляется, а создается позже, когда мы нажмем на кнопку. Этот способ хорош тогда, когда вы хотите по ходу работы проекта то создавать, то уничтожать объект.
Вот способ покороче:
Dim Перо As Pen = New Pen(Color.Red, 20) 'Объявляем и создаем объект
Здесь объект и объявляется, и создается одновременно.
Рис. 12.3
Для этого создайте проект с тремя кнопками и меткой и введите следующую программу:
Dim Граф_формы, Граф_метки As Graphics
Dim Перо1, Перо2 As Pen
'Создаем графические объекты и перья:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Граф_формы = Me.CreateGraphics
Граф_метки = Label1.CreateGraphics
'Создаем перо 1:
Перо1 = New Pen(Color.Blue, 10)
Перо1.DashStyle = Drawing.Drawing2D.DashStyle.Dash
'Создаем перо 2:
Перо2 = New Pen(Color.Red, 20)
Перо2.StartCap = Drawing.Drawing2D.LineCap.RoundAnchor
Перо2.EndCap = Drawing.Drawing2D.LineCap.ArrowAnchor
End Sub
'Рисуем пером 1:
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
Граф_формы.DrawEllipse(Перо1, 100, 50, 200, 100) 'Рисуем пером 1 эллипс на форме
Граф_метки.DrawPie(Перо1, 10, 10, 150, 150, 0, 270) 'Рисуем пером 1 сектор на метке
End Sub
'Рисуем пером 2:
Private Sub Button3_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button3.Click
Граф_формы.DrawLine(Перо2, 50, 50, 330, 150) 'Рисуем пером 2 стрелку на форме
Граф_метки.DrawRectangle(Перо2, 50, 50, 70, 100) 'Рисуем пером 2 прямоугольник на метке
End Sub
Пояснения. Здесь мы объявили оба пера вне процедур, чтобы ими можно было пользоваться из каждой процедуры.
При создании пера 1 мы задали его свойство DashStyle (стиль штриховки). Как только в окне кода в операторе
Перо1.DashStyle = Drawing.Drawing2D.DashStyle.Dash
вы введете с клавиатуры знак равенства, перед вами развернется список возможных стилей штриховки (Рис. 12.4), из которых вы можете выбирать.
Рис. 12.4
Вот их узоры:
Custom |
Определяется программистом |
Dash |
- - - - - - - - |
DashDot |
- . - . - . - . |
DashDotDot |
- . . - . . - . . - . . |
Dot |
. . . . . . . . |
Solid |
________ |
Каждый из стилей является одним из 6 значений перечисления DashStyle, входящего в пространство имен Drawing.Drawing2D. Мы выбрали штриховку Dash и таким штрихованным пером начертили эллипс и сектор.
При создании пера 2 мы задали его свойства StartCap («Колпак» на начало линии) и EndCap («Колпак» на конец линии). Процесс их выбора аналогичен процессу выбора свойства DashStyle. В качестве колпаков может выступить стрелка (ArrowAnchor), кружок (RoundAnchor) и ряд других фигур и очертаний. Эти фигуры и очертания являются значениями перечисления LineCap, входящего в пространство имен Drawing.Drawing2D. Пером 2 мы начертили стрелку и прямоугольник. Обратите внимание, что прямоугольник получился без «колпаков». Наверное потому, что у линии прямоугольника нет ни начала, ни конца.
У пера есть и другие любопытные свойства, но объем книги не позволяет мне на них останавливаться. Если вам интересно, найдите в Object Browser список свойств класса Pen. Выделите интересующее вас свойство и нажмите клавишу F1. Если вы знаете английский, то попробуйте разобраться в возникшем окне помощи. Правда, разобраться как следует даже при знании английского начинающему будет трудновато.
Создаем собственные кисти
До этого момента для рисования закрашенных фигур мы пользовались стандартными кистями из класса Brushes. Однако, собственные кисти гораздо богаче возможностями, чем стандартные. Собственные объекты-кисти, как мы уже предвидим, нужно будет создавать из специального класса, подобно тому, как мы создавали из класса Pen объекты-перья. Что же это за класс? Он не один, их несколько, точнее – 5. Каждый из них обеспечивает создание кистей определенного вида. Наряду с термином «кисть» мы будем употреблять термин заливка с тем же смыслом, поэтому можно сказать, что каждый класс обеспечивает фигурам заливку (закраску) определенного вида.
Вот эти классы с указанием пространств имен:
System.Drawing.SolidBrush | Обычная сплошная простая заливка | ||
System.Drawing.Drawing2D.LinearGradientBrush | Линейный градиент, то есть плавный переход между двумя цветами | ||
System.Drawing.Drawing2D.PathGradientBrush | Нелинейный градиент | ||
System.Drawing.TextureBrush | Текстурная заливка | ||
System.Drawing.Drawing2D.HatchBrush | Штрихованная заливка |
Существует также специальный класс Brush (не путать с Brushes), но от него объекты-кисти образовывать нельзя, у него другая роль.
Сплошная кисть – SolidBrush. Собственная сплошная кисть нам не очень интересна, так как не богаче стандартной из класса Brushes. Покажу все же, как ее создавать:
Dim Кисть As SolidBrush
= New SolidBrush(Color.Blue)
Теперь все равно, что писать в программе: Кисть или Brushes.Blue.
Создав однажды Кисть, вы затем свободно можете менять ее цвет, задавая ее свойство Color:
Кисть.Color = Color.Brown
Градиентная кисть – LinearGradientBrush. Поставим задачу нарисовать маленький эллипс, такой, как в правой части Рис. 12.5.
Рис. 12.5
Вы видите, что его заливка не однородная, а меняется от красного к желтому. Для этого используется так называемая градиентная заливка (кисть) или точнее – линейная градиентная заливка (LinearGradientBrush). Вот как она работает. Чтобы было удобнее объяснять, я нарисовал еще и большой полосатый эллипс, нарисованный той же кистью.
Зададим на поверхности произвольную точку, где цвет должен быть абсолютно красным. Назовем ее Т1. Зададим другую произвольную точку, где цвет должен быть абсолютно желтым. Назовем ее Т2. Мысленно соединим две эти точки отрезком прямой (на рисунке этот отрезок показан черной линией). Вдоль этого отрезка цвет постепенно меняется от абсолютно красного к абсолютно желтому. Вся заливаемая поверхность покрыта этим изменяющимся цветом так, что любая воображаемая прямая, перпендикулярная данному отрезку, состоит из точек одного цвета. Способ заливки хорошо виден на рисунке: как только кисть дойдет до абсолютно желтого цвета, она вновь начинает с красного. Получается матрас.
Произвольно задавая положение точек Т1 и Т2, мы можем как угодно регулировать направление градиента (изменения) цвета и расстояние между полосами матраса.
Вот какая программа понадобилась для этого рисунка:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim Гр As Graphics = Me.CreateGraphics
Dim Т1 As New Point(220, 50)
Dim Т2 As New Point(350, 150)
Dim П As New Rectangle(10, 30, 530, 200)
'Создаем градиентную кисть:
Dim Кисть_град As New System.Drawing.Drawing2D.LinearGradientBrush(Т1, Т2, Color.Red, Color.Yellow)
Гр.FillEllipse(Кисть_град, П) 'Рисуем большой эллипс
Гр.DrawLine(Pens.Black, Т1, Т2) 'Рисуем отрезок
Гр.FillEllipse(Кисть_град, 450, 10, 180, 50) 'Рисуем маленький эллипс
End Sub
Пояснения. Здесь я использовал простейший вариант создания градиентной кисти с 4 параметрами: первая точка, вторая точка, один цвет, другой цвет. Есть и другие, например, с указанием угла.
Штрихованная кисть – HatchBrush. Фигуры можно заливать не только цветом, но и разными штриховками, например, такой, как на Рис. 12.6.
Рис. 12.6
Для этого используется так называемая штрихованная кисть (HatchBrush).
Прежде чем писать программу, позаботимся об экономии чернил. Поскольку HatchBrush принадлежит к пространству имен с очень длинным именем System.Drawing.Drawing2D, сделаем так, чтобы VB не утомлял нас им. Для этого (вспомните 2.2.3) самой первой строкой в окне кода напишем
Imports System.Drawing.Drawing2D
С учетом этой строки вот какой фрагмент понадобился для этого рисунка:
Dim Кисть_штрих As New HatchBrush(HatchStyle.BackwardDiagonal, Color.Blue, Color.Yellow)
Гр.FillEllipse(Кисть_штрих, 10, 50, 530, 200)
Пояснения. Самый первый параметр конструктора штрихованной кисти – HatchStyle – тип штриховки. На выбор вам будет предложено несколько десятков типов. Второй параметр – цвет штриха, третий (в 1 варианте конструктора он не указывается) – цвет фона.
Рассмотрение текстурной кисти отложим до 12.6. Кисть с нелинейным градиентом рассматривать не будем.
Шрифты
До сих пор мы писали на поверхности формы и элементов управления только теми шрифтами, что задавали в режиме проектирования (3.2.2 и 6.2.6). Покажем, как создавать собственные шрифты программным путем, в коде. Делается это аналогично созданию собственных перьев и кистей. Шрифт – это объект класса Font, входящего в пространство имен System.Drawing. Создадим один какой-нибудь шрифт:
Dim Строгий_шрифт As New Font("Arial", 20, FontStyle.Bold)
Пояснения. Из десятка вариантов конструктора я выбрал вариант с 3 параметрами:
Первый параметр – взятое в кавычки название (гарнитура) одного из установленных на вашем компьютере шрифтов (Arial). Гарнитура определяет рисунок букв. Гарнитуры можно посмотреть, зайдя в режиме проектирования в свойство Font любого элемента управления. На вашем компьютере установлены шрифты гарнитур «Arial», «Times», «Courier» и как минимум нескольких других.
Второй параметр – размер шрифта (20). На единицах измерения размера шрифта я не останавливаюсь.
Третий параметр – начертание (стиль) шрифта (FontStyle). VB предложит вам на выбор несколько начертаний:
Regular | обычный | ||
Bold | полужирный | ||
Italic | курсив | ||
Underline | подчеркнутый | ||
Strikeout | перечеркнутый |
Создадим проект с кнопками и меткой. Пусть при нажатии на первую кнопку пишется слово «Всегда!» на форме, а при нажатии на вторую – слова «Привет!» и «издалека» на метке (см. Рис. 12.7).
Рис. 12.7
Для этого понадобится такая программа:
Dim Строгий_шрифт As New Font("Arial", 20, FontStyle.Bold)
Dim Красивый_шрифт As New Font("Times", 80, FontStyle.Bold Or FontStyle.Italic)
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim Граф As Graphics = Me.CreateGraphics
Граф.DrawString("Всегда!", Строгий_шрифт, Brushes.Black, 0, 50)
End Sub
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
Dim Гр As Graphics = Label1.CreateGraphics
Dim Т1 As New Point(40, 20)
Dim Т2 As New Point(20, 40)
Dim Кисть As New Drawing2D.LinearGradientBrush(Т1, Т2, Color.Blue, Color.Yellow)
Гр.DrawString("Привет!", Красивый_шрифт, Кисть, 0, 0)
Гр.DrawString("издалека", Строгий_шрифт, Brushes.Black, 300, 100)
End Sub
Пояснения. Стиль для красивого шрифта мы указали так:
FontStyle.Bold Or FontStyle.Italic
Это значит, что мы хотим, чтобы он был одновременно и полужирным, и курсивом. Если вы помните, то при помощи логической операции Or мы точно так же в 7.8 задавали вид и поведение окна MsgBox. Несколько непривычное для нас применение логической операции Or.
Чтобы слово «Привет!» получилось полосатым, как на рисунке, я пишу это слово градиентной кистью, градиент для которой я определяю при помощи Точек Т1 и Т2.
Точки я задаю немного по-другому, чем делал это раньше. Я использую конструктор. У структур, как и у классов, есть конструкторы. Два параметра у конструктора Точки – это ее главные свойства X и Y. Их я и указал.
Задание 69.
Начертите график функции y = (sin x)/x так, как он выглядит на Рис. 12.8. Координатные оси должны быть со стрелками. Указания: Максимальное значение Y равно 1, максимальное значение X на рисунке примерно равно 30. Задание выполняется по тому же принципу, что и Задание 47. Подробные пояснения смотрите в ответе.
Рис. 12.8
Собственные перья, кисти и шрифты
Собственные перья, кисти и шрифты получатся у вас гораздо богаче, чем стандартные, которыми вы пользовались до этого.
Картинка, как свойство Image элемента управления
Картинки можно размещать на многих элементах управления. Рассмотрим PictureBox. Для размещения картинки в PictureBox мы должны установить его свойство Image. В режиме проектирования это делается совершенно так же, как мы это делали в 3.6.
У элемента PictureBox имеется свойство SizeMode, которое управляет положением картинки на элементе. Вот его значения:
Normal | Левый верхний угол картинки совпадает с левым верхним углом PictureBox. Если картинка больше PictureBox, то выступающие ее части не видны. | ||
CenterImage | Картинка размещается в центре PictureBox. Если картинка больше PictureBox, то выступающие ее части не видны. | ||
AutoSize | PictureBox автоматически изменяет свои размеры так, чтобы они равнялись размерам картинки. | ||
StretchImage | Картинка автоматически изменяет свои размеры так, чтобы они равнялись размерам PictureBox. |
У PictureBox есть также свойство BackgroundImage. Это картинка, которая мозаикой покрывает поверхность элемента на заднем плане. Она становится видна, если Image покрывает не всю поверхность элемента.
Свойство Image элементу управления можно устанавливать и в коде. Для этого быстрее всего использовать метод FromFile, принадлежащий классу Image. (Пусть вас не путает то, что свойство и класс имеют одинаковые имена. Для VB это в порядке вещей.) Если вы хотите, чтобы картинка оказалась на элементе управления, необходимо, чтобы файл с этой картинкой уже хранился на дисках вашего компьютера. Если вы не можете найти ни одного файла с картинкой, то подскажу, что в папке Windows вы всегда отыщете картинки – обои рабочего стола или в крайнем случае найдете что-нибудь по адресу
Program Files\Microsoft Visual Studio .NET\Common7\Graphics
Рекомендую предварительно скопировать файлы картинок в папку bin вашего проекта. Тогда обращаться к ним можно будет просто по имени, без необходимости указания полного адреса. Кстати, и картинки всегда будут при вас.
Вот оператор, устанавливающий свойство Image элементу PictureBox:
PictureBox1.Image
= Image.FromFile("Earth.JPG")
Здесь слева от знака равенства – свойство Image, справа – класс Image. Обратите внимание, что в кавычках я указал только имя файла Earth.JPG. Это стало возможным потому, что я скопировал файл в папку bin проекта. Иначе пришлось бы указать полный адрес: "D:\Фотографии\ Earth.JPG".
Элементы управления вполне могут заимствовать друг у друга картинки:
Button2.Image = PictureBox1.Image
Если мы хотим, чтобы на элементе управления картинки больше не было, мы пишем:
Button2.Image = Nothing
Слово Nothing означает «Ничто», то есть картинки никакой нет.
Задание 70.
Вы профессиональный продавец автомобилей. Вы приезжаете к покупателю, достаете портативный компьютер, на экране – несколько десятков кнопок, на каждой – маленькая фотография одного из продаваемых автомобилей. Покупатель говорит: «Вот этот покажите, пожалуйста». Вы нажимаете эту кнопку и на экране возникает та же фотография, но увеличенная.
Помощь: Если вы собираетесь в качестве кнопок использовать элементы управления Button, то вам придется предварительно позаботиться об уменьшении фотографий до размеров кнопок. Но можно и избежать такой потери времени. Ведь кнопками могут служить объекты PictureBox! Потому что у объекта PictureBox тоже есть событие Click! Создайте на форме несколько маленьких PictureBox и один большой и в маленькие впишите фото. По щелчку мыши по маленькому PictureBox большой PictureBox пусть копирует в себя его картинку.
Необязательное усложнение для тех, кто не боится системы координат: Если у вас фотографии имеют одинаковые размеры и пропорции, то все хорошо и ничего дополнительно делать не нужно. Проблемы возникают тогда, когда размеры и пропорции исходных фото разные: одни фото большие и продолговатые по горизонтали, другие – по вертикали, третьи маленькие квадратные. Во-первых, проблемы возникают уже с кнопками, потому что, чтобы фото на них не были искажены или обрезаны, они сами должны иметь разную продолговатость. Но Бог с ними, с кнопками, нам хочется, чтобы хоть большие-то картинки располагались на экране симметрично как по горизонтали, так и по вертикали, и имели максимально возможный размер. Вот этой цели я и хочу достигнуть. Для этого придется использовать оператор ветвления, а также свойства, задающие размер и местоположение объектов. Проверьте, чтобы форма была распахнута на весь экран. Как это сделать, я объяснял в 3.4.3. После щелчка мыши по кнопке компьютер должен сделать следующее:
Настроить большой PictureBox, чтобы он подстраивался под размеры картинки, установив его сами знаете какое свойство.
Пусть большой PictureBox копирует в себя картинку из маленького. На форме появится фото реальных размеров и неискаженных пропорций, но не по центру. Все это у вас уже давно готово. Задача – увеличить фото еще больше и чтобы оно располагалось по центру.
Мы предвидим, что размеры PictureBox придется изменять. Надо теперь заставить картинку принимать размеры PictureBox, установив по-другому то же самое свойство.
Поделить ширину формы на ее высоту, чтобы узнать ее «продолговатость». (Это надо бы пораньше, да ладно.)
Поделить ширину PictureBox на его высоту, чтобы узнать «продолговатость» картинки.
Если продолговатость картинки больше, чем продолговатость формы, то в идеале при максимальном увеличении картинка должна почти упереться левым и правым краем в края формы, а сверху и снизу должно остаться одинаковое пространство. Для этого нужно выполнить несколько операторов присваивания, увеличивающих размер и изменяющих местоположение PictureBox, да так, чтобы продолговатость PictureBox равнялась продолговатости картинки.
Если продолговатость картинки меньше, чем продолговатость формы, то в идеале при максимальном увеличении картинка упрется верхним и нижним краем в края формы, а слева и справа должно остаться одинаковое пространство. Здесь тоже нужно выполнить несколько аналогичных операторов.
Таким образом, при щелчке по кнопке на форме возникает фото максимальных размеров и по центру.
Растровая и векторная графика
В компьютерной графике все картинки можно разделить на растровые
и векторные. Соответственно говорят о двух типах графики – растровой
и векторной, и о двух типах форматов графических файлов – растровом
и векторном. К растровым форматам относятся форматы BMP, JPEG, GIF, TIFF, PNG, Icon и др., к векторным форматам относятся форматы WMF, EMF и др.
Упомянутые два вида графики различаются своим поведением, поэтому неплохо бы знать их различия. Посмотрите на Рис. 12.9.
Рис. 12.9
Слева – фотография (формат BMP), справа – рисунок из коллекции Microsoft (формат WMF). Фотография – типичный представитель растровой графики, рисунок из коллекции Microsoft – типичный представитель векторной графики. Ниже вы видите увеличенные фрагменты фотографии и рисунка соответственно.
Чем больше мы увеличиваем фотографию, тем больше становятся зернистыми и грубыми ее мелкие элементы. Это вполне естественно, то же самое мы видим, рассматривая в мощную лупу фотографию на бумаге. При дальнейшем увеличении на экране нам будет казаться, что фотография состоит из цветных квадратов. Эти квадраты – не что иное, как бывшие пиксели.
С рисунком же ничего подобного не происходит. Сколько бы мы его ни увеличивали, все линии рисунка остаются ясными и четкими. Причина вот в чем. Обратите внимание, что в отличие от фотографии, где цвет плавно изменяется от одного места фотографии к другому, векторный рисунок состоит из одноцветных областей разной формы с резким переходом между областями. Если точно задать линии контура всех областей и цвет заливки каждой области, то весь рисунок будет задан.
В файлах векторной графики хранится не собственно рисунок, сделанный из пикселей, как в случае с фотографией, а точное математическое описание рисунка: описание координат всех линий, из которых сделаны контуры областей, и цветов заливки этих областей. Видимого рисунка, как такового, нет, одни описания. Это как если бы вы попросили свою знакомую прислать вам свою фотографию, а она вместо этого прислала письмо: «Глаза у меня серые, ушки маленькие» и так далее. Но для компьютера такого описания достаточно. Если фотографию компьютер направляет на экран прямо так, как она есть, пиксель за пикселем, то рисунок на экране компьютер по описанию рисует. И если вы, работая с векторным рисунком, сместили его на экране, увеличили или как-то по-другому изменили, то компьютер его заново мгновенно перерисовывает. Никто ему не помешает при любом увеличении рисовать четко, ведь описания математически точные.
Рисуем картинки
С этого момента мы будем говорить о работе с картинками в коде.
Устанавливать элементам управления свойство Image быстро и просто. Но для осуществления всего богатства работы с картинками, о которых я говорил, нужны и другие способы.
Объекты. В VB имеются специальные объекты для работы с картинками. Для нас главный из них – объект класса Bitmap. Он может работать со всеми упомянутыми форматами файлов. Имеются еще класс Metafile, учитывающий специфику работы с файлами векторной графики, и класс Icon, учитывающий специфику работы с иконками. Существует еще упомянутый выше класс Image, для которого классы Bitmap и Metafile являются «наследниками». Поэтому считается, что объекты классов Bitmap и Metafile принадлежат не только своим классам, но и типу Image (о наследовании читайте в 22.7.).
Объект класса Bitmap находится в оперативной памяти и невидим для нас. Его главное дело – получить картинку из файла и хранить ее. Картинка, естественно, тоже будет невидима. В памяти она может быть по нашему желанию подвергнута разнообразной обработке, а затем, когда нам нужно, она из объекта Bitmap попадает на поверхность формы или элемента управления, где мы ее и увидим.
Способ 1. Вот самый простой способ поместить картинку из файла в Bitmap, а оттуда безо всяких преобразований – на элемент управления:
Private Sub Button5_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button5.Click
Dim Картинка As New Bitmap("Spacescape.JPG")
PictureBox1.Image = Картинка
End Sub
Пояснения. Первый оператор создает объект Картинка класса Bitmap и тут же при помощи параметра конструктора помещает в него картинку из файла Spacescape.JPG. Следующий оператор просто присваивает эту картинку свойству Image элемента управления PictureBox1. О свойстве Image мы уже говорили.
Способ 2:
Private Sub Button3_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button3.Click
Dim Картинка As New Bitmap("Spacescape.JPG")
Dim Гр As Graphics = PictureBox2.CreateGraphics
Гр.DrawImage(Картинка, 0, 0)
End Sub
Здесь картинка не присваивается свойству Image элемента управления PictureBox2, а рисуется
на этом элементе. Метод DrawImage объекта Гр рисует содержимое объекта Картинка на элементе, в результате чего мы видим на нем картинку. Левый верхний угол картинки имеет на элементе управления координаты 0, 0.
Аналогично картинки можно рисовать и на форме, и на других элементах управления. Оба способа на первый взгляд дают один и тот же результат, но на самом деле между ними есть существенные различия, которые вы постепенно поймете. Самое бросающееся в глаза различие – нарисованная картинка стирается, как только элемент управления, на котором она нарисована, оказывается загорожен другими окнами или задвинут вами за край экрана. Картинка же, являющаяся свойством Image – не стирается.
У конструктора Bitmap есть много вариантов. Мы рассмотрим их позже, а сейчас поговорим поподробнее о методе DrawImage.
Несколько картинок на форме. Вот программа, рисующая на форме три картинки разных форматов:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim Гр As Graphics = Me.CreateGraphics
'Создаем объекты Bitmap из графических файлов:
Dim Фото As New Bitmap("Spacescape.JPG")
Dim Векторный_рисунок As New Bitmap("DIME.WMF")
Dim Иконка As New Bitmap("FACE.ICO")
'Переносим картинки с объектов Bitmap на форму:
Гр.DrawImage(Фото, 0, 0)
Гр.DrawImage(Векторный_рисунок, 300, 50)
Гр.DrawImage(Иконка, 480, 50)
End Sub
Вот результат работы этой программы (Рис. 12.10).
Рис. 12.10
Левый верхний угол космического пейзажа имеет на форме координаты 0, 0. Обратите внимание, что рисунки векторной графики и иконки вовсе не обязаны иметь прямоугольную форму. Тем не менее, мы можем вообразить прямоугольники, описанные вокруг них. Поэтому можно сказать, что левый верхний угол векторного рисунка монеты имеет на форме координаты 300, 50. Маленькая иконка в виде круглой рожицы видна наверху монеты. Ее координаты – 480, 50.
Метафайлы и иконки. Вот для сведения программка, делающая то же самое с монетой и рожицей, но уже с использованием объектов не класса Bitmap, а классов Metafile и Icon:
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
Dim Гр As Graphics = Me.CreateGraphics
'Работаем с метафайлом:
Dim Векторный_рисунок As New System.Drawing.Imaging.Metafile("DIME.WMF")
Гр.DrawImage(Векторный_рисунок, 300, 50)
'Работаем с иконкой:
Dim Иконка As New Icon("FACE.ICO")
Гр.DrawIcon(Иконка, 480, 50)
End Sub
Как видите, если классы Image, Bitmap и Icon принадлежат пространству имен System.Drawing, то класс Metafile принадлежит пространству имен System.Drawing.Imaging. Для рисования иконок есть свой метод – DrawIcon. В дальнейшем я не буду рассматривать специфику классов Metafile и Icon:
Размер и разрешение картинок
Принцип получения изображения на экране монитора. Этот принцип иллюстрируется Рис. 12.11 и подробно рассмотрен в Приложении 1 (Устройства вывода – Монитор).
Рис. 12.11
Если он вам незнаком, то обязательно изучите его, иначе дальнейший материал вам будет непонятен.
Размер и разрешение. Как попадают фотографии на компьютерный диск? Со сканера, с цифрового фотоаппарата, из Интернета, с телевизора или видеомагнитофона и некоторыми другими путями. В любом случае изображение сохраняется в графическом файле одного из растровых форматов (BMP, JPEG и др.). Это означает, что изображение представляется в файле в виде мозаики мельчайших пикселей. В файле указывается ширина картинки в пикселях – это то количество пикселей, на которые разбита картинка по горизонтали. Аналогично указывается высота.
А откуда файл узнал это количество? Оно определяется аппаратурой, получившей фото для компьютера. Например, обычный сканер может различать 300 точек (пикселей) на один дюйм (около двух с половиной сантиметров) ширины. Это число называется разрешением (Resolution) по горизонтали. Раз так, то ширина фото в пикселях для сканера получается умножением разрешения сканера на ширину бумажной фотографии в дюймах. То же относится и к высоте. В цифровых фотоаппаратах свои цифры. Там заранее известны ширина и высота в пикселях.
Итак, в файлах растровых форматов кроме ширины и высоты картинки в пикселях указываются также разрешение по горизонтали и вертикали.
Такой вопрос: Современные мониторы не способны обеспечить такой маленький размер пикселя, чтобы на дюйме их умещалось целых 300. Зачем же тогда сканерам работать с таким разрешением? Ну, во-первых существует еще печать на бумаге, где такое разрешение уже достигнуто и превышено. Во-вторых, на экране мы можем изображение и увеличивать и тогда лишние пиксели пригодятся.
Тогда еще один вопрос: Пусть бумажная фотография имела ширину 5 дюймов. Следовательно, после сканера с разрешением 300 ширина картинки составит 1500 пикселей. Если я захочу увидеть ее на экране, то она не уместится, так как мой экран настроен на ширину 1280 пикселей. Что делать? На это я могу сказать, что многие программы показывают на экране картинки, исходя из их размеров не в пикселях, а в дюймах, и часто предоставляют пользователю выбор желаемого разрешения экрана. Кстати, программы этого раздела показывали картинки на экране именно исходя из их размеров в дюймах.
Размер и разрешение в объекте Bitmap. Свойства Width и Height объекта Bitmap – это его ширина и высота, то есть ширина и высота картинки, хранящейся в нем, в пикселях. Чтобы узнать, например, ширину объекта Фото, достаточно написать оператор
MsgBox (Фото.Width)
У меня ширина объекта Фото равнялась 1528. Разрешение равнялось 300, отсюда и размер на экране составлял около 5 дюймов.
Откуда взялись эти цифры – 1528 и 300? Объект Фото взял их из файла Spacescape.JPG, из которого он был создан оператором
Dim Фото As New Bitmap("Spacescape.JPG")
Управляем размерами и разрешением. Рассмотрим следующую программу:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim Гр As Graphics = Me.CreateGraphics
'Работаем с оригинальным фото:
Dim Фото As New Bitmap("Spacescape.JPG")
Debug.WriteLine(Фото.Size) : Debug.WriteLine(Фото.HorizontalResolution)
Гр.DrawImage(Фото, 0, 0)
'Работаем с уменьшенным фото:
Dim Фото1 As New Bitmap(Фото, 50, 40)
Debug.WriteLine(Фото1.Size) : Debug.WriteLine(Фото1.HorizontalResolution)
Гр.DrawImage(Фото1, 500, 0)
'Увеличиваем видимые размеры фото за счет уменьшения разрешения:
Фото1.SetResolution(10, 10)
Debug.WriteLine(Фото1.Size) : Debug.WriteLine(Фото1.HorizontalResolution)
Гр.DrawImage(Фото1, 570, 0)
End Sub
Пояснения. Программа состоит из трех последовательных частей. Первая часть рисует левую из трех фотографий на Рис. 12.12. Кроме этого, она печатает две строчки в окне Output:
{Width=1528, Height=1212}
300
Рис. 12.12
Здесь первая строка – это размеры (Size) объекта Фото. Вторая строка – разрешение по горизонтали (HorizontalResolution) объекта Фото.
Умножим для интереса 1528 на 1212. Получается, что наше фото состоит почти из двух миллионов пикселей. Многовато. А ведь каждый пиксель требует со стороны компьютера внимания и времени! Не удивительно, что при активной работе с такими качественными фотографиями компьютер начинает подтормаживать.
Давайте, чтобы не напрягать компьютер, прикажем разбить картинку на гораздо меньшее число пикселей. Скажем, 50 на 40. Я специально взял слишком мало, чтобы разница бросалась в глаза. К сожалению, объект класса Bitmap не позволяет после своего рождения менять свои размеры. Поэтому мы из старого объекта Фото создадим новый объект Фото1, причем воспользуемся тем, что при рождении объекта класса Bitmap его конструктор позволяет задавать размеры. Это я и сделал при помощи строки
Dim Фото1 As New Bitmap(Фото, 50, 40)
В данном варианте конструктора объект получает свое разрешение (Resolution) не от старого объекта, а приобретает стандартное в компьютерном мире разрешение для экрана монитора, равное 96. Это значит, что картинка на экране будет показана «пиксель в пиксель», то есть займет на экране 50 пикселей по горизонтали и 40 пикселей по вертикали. Что мы и видим на рисунке (маленькая картинка между двумя большими).
Посмотрим, что напечатала вторая часть нашей программы:
{Width=50, Height=40}
96
Нам такой размер не нравится. Нам хочется побольше. Пожалуйста. Для этого достаточно изменить разрешение объекта. У нас в одном дюйме умещается 96 пикселей. Сделаем, чтобы умещалось 10 по горизонтали и 10 по вертикали:
Фото1.SetResolution(10, 10)
Вот что печатает третья часть нашей программы:
{Width=50, Height=40}
10
Рисует она правую картинку из трех.
Вы видите, что пиксели (квадратики) получились настолько большие, что их легко заметить. Вся картинка получилась из-за этого очень грубой и зернистой. За что боролись! Вариант 500 на 400 был бы наилучшим выходом. И число пикселей уменьшилось бы на порядок, и потери качества мы бы не заметили.
Метод DrawImage и его варианты
Вот программа:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim Гр As Graphics = Me.CreateGraphics
Dim Фото As New Bitmap("Spacescape.JPG")
Dim Т As New Point(30, 10)
Dim П1 As New Rectangle(550, 10, 150, 130)
Dim П2 As New Rectangle(550, 160, 200, 70)
Dim П3 As New Rectangle(550, 250, 200, 150)
Dim П4 As New Rectangle(800, 170, 400, 300)
Гр.DrawImage(Фото, Т) 'Точка Т задает левый верхний угол картинки
Гр.DrawImage(Фото, П1) 'Втискиваем картинку в прямоугольник П1
Гр.DrawImage(Фото, П2) 'Втискиваем картинку в прямоугольник П2
'Вырезаем из картинки прямоугольник П4 и втискиваем его в прямоугольник П3:
Гр.DrawImage(Фото, П3, П4, GraphicsUnit.Pixel)
End Sub
Вот результат ее работы (Рис. 12.13).
Рис. 12.13
Пояснения. Я уже говорил, что у структур, как и у классов, есть конструкторы. Четыре параметра у конструктора прямоугольника – это его главные свойства X, Y, Width и Height.
Из трех десятков вариантов DrawImage я использовал три. Оператор
Гр.DrawImage(Фото, Т) 'Точка Т задает левый верхний угол картинки
рисует ту картинку, что на рисунке слева, с левым верхним углом в указанной точке.
Операторы
Гр.DrawImage(Фото, П1) 'Втискиваем картинку в прямоугольник П1
Гр.DrawImage(Фото, П2) 'Втискиваем картинку в прямоугольник П2
рисуют две картинки справа сверху. Они позволяют как угодно увеличивать, уменьшать, растягивать и сплющивать картинку, потому что картинка обязана уместиться в указанный вами прямоугольник.
Оператор
Гр.DrawImage(Фото, П3, П4, GraphicsUnit.Pixel)
вырезает из картинки прямоугольник П4 и умещает его в прямоугольник П3 на форме. Будьте внимательны насчет единиц измерения размеров в прямоугольнике П4. Прежде всего, четвертым параметром метода мы указали GraphicsUnit.Pixel. Это значит, что единицей измерения для прямоугольника П4 мы выбрали пиксель в объекте Фото. Не путайте пиксели на форме и экране с пикселями в невидимых объектах Bitmap. Не удивительно, что прямоугольник П4 я задаю с очень большими величинами параметров, ведь измеряются они в этих самых невидимых пикселях.
Метод RotateFlip объекта Bitmap
Метод RotateFlip позволяет поворачивать (Rotate) и зеркально отражать (Flip) картинку в памяти.
Вот программа:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim Гр As Graphics = Me.CreateGraphics
Dim Фото As New Bitmap("Spacescape.JPG")
Фото.RotateFlip(RotateFlipType.Rotate90FlipNone) 'Повернули по часовой на 90 град, не отражали
Гр.DrawImage(Фото, 0, 0)
Фото.RotateFlip(RotateFlipType.RotateNoneFlipY) 'Отразили по вертикали, не поворачивали
Гр.DrawImage(Фото, 400, 0)
End Sub
Вот результат ее работы (Рис. 12.14).
Рис. 12.14
Пояснения. У метода RotateFlip всего один параметр. Как только вы раскроете скобку, VB предложит вам все 16 возможных значений этого параметра. Они являются значениями перечисления RotateFlipType. В смысле всех 16 легко разобраться на 4 нижеприведенных примерах. Нужно только знать, что Rotate переводится «вращай по часовой стрелке», Flip переводится «зеркально отражай», None переводится «не надо»:
RotateNoneFlipX | Вращать не надо, отражай по горизонтали | ||
Rotate180FlipNone | Вращай на 180 градусов, отражать не надо | ||
Rotate90FlipY | Вращай на 90 градусов, после чего отрази по вертикали | ||
Rotate270FlipXY | Вращай на 270 градусов, после чего отрази по горизонтали и вертикали |
Посмотрим на программу. Оператор
Фото.RotateFlip(RotateFlipType.Rotate90FlipNone) 'Повернули по часовой на 90 град, не отражали
поворачивает картинку в объекте Фото. Она так и остается там повернутой. Но мы этого пока не видим. Чтобы увидеть ее на форме, применяем следующий оператор:
Гр.DrawImage(Фото, 0, 0)
Это и есть картинка в левой части Рис. 12.14. Она действительно повернута по сравнению с исходной ориентацией, которую мы можем видеть на Рис. 12.13. Далее оператор
Фото.RotateFlip(RotateFlipType.RotateNoneFlipY) 'Отразили по вертикали, не поворачивали
отражает по вертикали уже повернутую картинку в объекте Фото, после чего следующий оператор показывает ее нам в правой части рисунка..
Метод Save объекта Bitmap
Сохраняем картинку в файл. Метод Save сохраняет картинку из объекта Bitmap в файл. Это может понадобиться тогда, когда картинка как-то изменена. Вот программа, которая загружает картинку из файла Spacescape.JPG, поворачивает ее и сохраняет в повернутом виде в файле Spacescape-1.JPG.
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
Dim Фото As New Bitmap("Spacescape.JPG")
Фото.RotateFlip(RotateFlipType.Rotate90FlipNone) 'Повернули по часовой на 90 град, не отражали
Фото.Save("Spacescape-1.JPG") 'Сохранили измененную картинку в файле
End Sub
Преобразовываем формат графического файла. У метода Save – несколько вариантов. Один из них позволяет указывать формат сохраняемого файла. Поэтому в VB есть возможность преобразовать формат графического файла. Вот, например, как просто преобразовать файл из формата WMF в формат BMP:
Dim Картинка As New Bitmap("DIME.WMF")
Картинка.Save("DIME.BMP", System.Drawing.Imaging.ImageFormat.Bmp)
Первая строка загружает файл DIME.WMF в объект Картинка класса Bitmap. Вторая строка сохраняет эту картинку в файле DIME.BMP. Однако не расширение BMP, указанное нами, приказало компьютеру изменить формат файла. Это приказал сделать второй параметр метода – свойство Bmp класса ImageFormat, принадлежащего пространству имен System.Drawing.Imaging. Кроме свойства Bmp у этого класса есть еще десяток свойств, соответствующих разным форматам графических файлов. Однако не для всех из них преобразование этим методом осуществимо или же осуществимо так просто.
Работа с картинками
VB может не только сам рисовать линии, кружочки и другие фигуры. Он может работать и с готовыми картинками, доставшимися со стороны, то есть с рисунками, нарисованными в графических редакторах, и с фотографиями. Чтобы VB мог с ними работать, они должны храниться на диске в виде файлов следующих популярных графических форматов:
BMP | JPEG | GIF | TIFF | PNG | Icon | WMF | EMF | Exif |
Кроме этого, в VS есть собственный простенький графический редактор, позволяющий рисовать иконки (пиктограммы, значки), курсоры и обычные рисунки.
Что значит, что VB работает с фотографиями и рисунками? Это не значит, что он сам «фотографирует» или рисует, как художник. Это значит, что он может уже готовые картинки из файлов показывать в любом месте экрана и всячески их преобразовывать, например, увеличивать, уменьшать, растягивать, сплющивать во все стороны, обрезать, двигать по экрану, быстро сменять разные картинки, добиваясь эффекта анимации, изменять цвета и т.п.
Однако этого вполне достаточно, чтобы обеспечить вашему проекту привлекательный внешний вид или, скажем, использовать его для создания игр.
Рисуем в памяти
Программисты знают, что в оперативной памяти те или иные действия происходят быстрее, чем на поверхности экрана. Так, увеличить на единицу переменную в памяти быстрее, чем увеличить на единицу число в текстовом поле. То же относится и к графике. Рисование фигур и преобразование картинок непосредственно на экране происходит медленнее, чем те же действия в памяти. Поэтому, когда дело касается рисования большого числа фигур и сложных манипуляций с картинками, целесообразно все эти действия проделать в памяти, а человеку показать только конечный результат.
Но что значит «рисовать в памяти»? Пока мы умеем в памяти только хранить картинку в объекте Bitmap и там ее немножко преобразовывать. А как в памяти нарисовать, например, кружочек? Давайте посмотрим.
VB позволяет рассматривать объект Bitmap как некую невидимую поверхность, для которой можно создать объект класса Graphics точно так же, как мы создаем объект класса Graphics для формы или, скажем, метки. Как только объект класса Graphics для объекта Bitmap создан, вы получаете возможность пользоваться всеми его методами и, значит, рисовать на невидимой поверхности невидимыми перьями и кистями все, что вашей душе угодно, и помещать туда любые картинки. После того, как вы будете удовлетворены этим «нарядом голого короля», вы можете одним оператором поместить его на всеобщее обозрение на экран.
Вот программа:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim Картинка As New Bitmap(600, 400) 'Создаем пустую поверхность 600 на 400
Dim Гр As Graphics = Graphics.FromImage(Картинка) 'Создаем над ней объект класса Graphics
'Рисуем на поверхности:
Dim Векторный_рисунок As New Bitmap("DIME.WMF")
Гр.DrawImage(Векторный_рисунок, 10, 10)
Dim П As New RectangleF(300, 250, 160, 40)
Гр.FillRectangle(Brushes.White, П)
Гр.DrawString("Это монета", Me.Font, Brushes.Black, П)
'Переносим на форму то, что нарисовано на поверхности:
Dim Граф As Graphics = Me.CreateGraphics
Граф.DrawImage(Картинка, 0, 0)
End Sub
Пояснения. Результат работы этой программы – Рис. 12.15, содержащий картинку, фигуру (белый прямоугольник) и текст.
Рис. 12.15
Все эти элементы были сначала нарисованы в памяти в объекте Bitmap, потом одним оператором содержимое объекта Bitmap было помещено на форму. Рассмотрим программу поподробнее, оператор за оператором.
Dim Картинка As New Bitmap(600, 400) 'Создаем пустую поверхность 600 на 400
Здесь мы сталкиваемся с еще одним вариантом конструктора объекта Bitmap. Объект создается пустой, без картинки. Можно представить его, как чистую поверхность размером 600 на 400, готовую к тому, чтобы на ней что-нибудь нарисовали.
Dim Гр As Graphics = Graphics.FromImage(Картинка) 'Создаем над ней объект класса Graphics
Эта строка создает для объекта Картинка объект класса Graphics. Но, как видите, способ создания здесь не такой, как, скажем, для формы. Там мы использовали функцию CreateGraphics формы, здесь же мы используем функцию FromImage класса Graphics. Именно класса, а не объекта. Различие в смысле для вас прояснится позже.
Объект класса Graphics создан и вы получаете возможность пользоваться всеми его методами так, как если бы вы рисовали на форме или элементе управления. Следующие пять строк процедуры наносят на невидимую поверхность объекта Bitmap по очереди картинку монеты, белый прямоугольник и текст.
Последние две строки процедуры озабочены переносом содержимого объекта Bitmap на форму. Для этого создается объект класса Graphics для формы и перенос привычно осуществляется методом DrawImage этого объекта.
Перерисовка картинок, фигур и текста
Вы, наверное, давно уже обратили внимание на такой факт. Если форма с нарисованными фигурами, картинками или написанным текстом скрывается из глаз, закрытая другими окнами, то когда мы вновь ее добываем из-под других окон, то видим, что нарисованные картинки, фигуры и текст стерлись. Если мы часть формы затащили за край экрана, то стирается все, что было нарисовано и написано на этой скрывшейся за край экрана части. Если мы мышью уменьшили размеры формы, а потом снова увеличили, то на поверхности, обнажившейся после увеличения, все стерто. В общем, стирается все то, что пропадает с глаз.
Но если картинка находится на элементе управления в качестве его свойства Image, то она не стирается. Стирается лишь то, что нарисовано.
Чтобы стирания нарисованных вещей не происходило, в Visual Basic 6.0 мы просто в режиме проектирования устанавливали у формы свойство AutoRedraw, и могли больше об этом не думать. Однако VB слишком сложен и мощен для такого «детского» решения.
Посмотрим, что нужно делать в VB.
Событие Paint. В каком случае мы можем заметить, что на форме что-то стерто? Наверное, тогда и только тогда, когда в поле нашего зрения на экране появляется участок формы (пусть даже самый маленький), который был до этого по той или иной причине скрыт (и поэтому с него все было стерто).
Задача VB – создать у нас иллюзию, что ничего не стирается. Для этого путь один – как только такой участок со стертой информацией возникает в поле зрения, тут же его снова зарисовывать «как было», чтобы человек не успевал ничего заметить. Однако, сам VB зарисовывать ничего не собирается, он предоставляет позаботиться об этом программисту. Для чего в помощь ему VB отряжает событие Paint. Это событие наступает как раз тогда, когда в поле зрения на экране появляется участок формы (пусть самый ничтожный), который был до этого скрыт. Если вы, например, медленно вытаскиваете форму из-за края экрана или медленно увеличиваете ее высоту или ширину, то каждую ничтожную долю секунды в поле зрения появляются «стертые» участки, а значит, наступает событие Paint. Получается, что в данном случае событие Paint наступает много раз в секунду.
Раз есть событие, значит есть и процедура-обработчик этого события. Все, что должен сделать программист, это получить в окне кода обычным способом заготовку этой процедуры и записать в нее операторы рисования всех фигур и всего текста на форме, которые он хочет предохранить от стирания. По принципу: если забор в одном месте запачкался – перекрашивай весь забор.
Создайте проект с кнопкой. Введите в окно кода такой текст:
Dim Гр As Graphics = Me.CreateGraphics
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Гр.DrawEllipse(Pens.Black, 20, 20, 80, 100)
End Sub
Private Sub Form1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) _
Handles MyBase.Paint
Гр.DrawRectangle(Pens.Black, 10, 10, 200, 100)
End Sub
Пояснения. При нажатии на кнопку рисуется кружок. Чтобы создать заготовку процедуры-обработчика события Paint, выберите, если вы работаете в Visual Studio .NET 2003, в левом поле окна кода Form1 Events, а если в Visual Studio .NET – Base Class Events., а в правом – Paint. Введите в эту процедуру оператор рисования квадрата.
Запустите проект. Вы видите, что квадрат уже на форме, несмотря на то, что никаких кнопок вы не нажимали. Произошло это потому, что событие Paint уже успело сработать при появлении формы на экране. Щелкните по кнопке Button1. Появился еще и кружок. Теперь задвиньте форму частично за левый край экрана и снова выдвиньте. Вы увидите, что часть кружка стерта, а квадрат как будто и не стирался (см. Рис. 12.16).
Рис. 12.16
На самом деле он прекрасно стерся, но при выдвижении формы из-за края экрана много раз в секунду наступало событие Paint и он каждый раз рисовался снова. Причем рисовался настолько быстро, что человеческий глаз не успевал этого заметить.
Итак, мы видим, что власть над тем, каким фигурам стираться, а каким нет, находится полностью в руках программиста. Поэтому, если вы хотите, чтобы фигуры не стирались, помещайте операторы для их рисования в обработчик события Paint.
Событие Paint имеется не только у формы, но и у элементов управления. Можете проверить его, например, на метке.
Мы можем искусственно вызвать событие Paint и, значит, перерисовку поверхности формы или элемента управления, применяя их метод Refresh. Например, так:
Me.Refresh()
Кроме обработки события Paint VB предлагает и другие способы перерисовки, но мы на них останавливаться не будем.
Image не стирается. Очевиден такой способ борьбы со стиранием. Мы рисуем все, что нам нужно, в памяти на объекте Bitmap, а затем присваиваем получившийся Bitmap свойству Image элемента управления.
Текстурная кисть
Текстурной кистью называют такой способ заливки фигур, когда они вместо цвета или штрихового узора заполняются бесконечно повторяющейся картинкой, взятой вами из графического файла (см. Рис. 12.17).
Рис. 12.17
На этом рисунке эллипс заполнен квадратной картинкой, повторяющейся на нем полтора десятка раз. Создает такую заливку следующая программа:
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
Dim Текстура As New Bitmap("Дверь.jpg")
Dim Кисть_текстурная As New TextureBrush(Текстура)
Dim Гр As Graphics = Me.CreateGraphics
Гр.FillEllipse(Кисть_текстурная, 10, 50, 1200, 900)
End Sub
Пояснения. Конструктор текстурной кисти (вторая строка) может сконструировать ее только из объекта типа Image, содержащего подходящую картинку. Поэтому первой строкой я создал такой объект с именем Текстура. Последние две строки рисуют на форме эллипс, заполненный этой текстурой.
Большинство вариантов конструктора позволяют использовать для кисти не всю картинку, а вырезанный из нее прямоугольник. Если вы знаете размер картинки в пикселях, то можете этим воспользоваться:
Dim Текстура As New Bitmap("Дверь.jpg")
Dim П As New Rectangle(200, 100, 40, 70)
Dim Кисть_текстурная As New TextureBrush(Текстура, П)
Здесь я заранее знаю размер картинки в объекте Bitmap – 256 на 256. Из нее я вырезал прямоугольник с левым верхним углом в точке (200, 100) относительно левого верхнего угла картинки и с размерами 40 на 70. Этот прямоугольный фрагмент и стал новой картинкой для кисти. Если вы «залезете» вырезаемым прямоугольником за край исходной картинки, VB выдаст ошибку.
Несколько вариантов конструктора позволяют управлять ориентацией картинки в текстурной заливке и создавать простейшие комбинации с использованием этой картинки в разных ориентациях (см. например, Рис. 12.18).
Рис. 12.18
Для этого используется параметр конструктора WrapMode. Это перечисление из пространства имен System.Drawing.Drawing2D. Оно имеет несколько значений, определяющих форму комбинации. Одно из них – TileFlipXY – использовано в программе, создающей этот рисунок
Private Sub Button3_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button3.Click
Dim Картинка As New Bitmap("DIME.WMF")
Dim Текстура As New Bitmap(Картинка, 100, 100)
Dim Кисть_текстурн As New TextureBrush(Текстура, System.Drawing.Drawing2D.WrapMode.TileFlipXY)
Dim Гр As Graphics = Me.CreateGraphics
Гр.FillEllipse(Кисть_текстурн, 10, 10, 900, 600)
End Sub
Пояснения. Здесь я сначала, как положено, создал объект Bitmap для картинки, но поскольку картинка получилась слишком большая, я следующим оператором уменьшил ее до размеров 100 на 100. Следующий оператор из полученной картинки создает текстурную кисть с комбинацией TileFlipXY.
Системные цвета
Задаем системные цвета вручную. Попробуйте в режиме проектирования настроить цвет кнопки. Для этого в окне свойств зайдите в ее свойство BackColor, а в нем – в закладку System (см. Рис. 12.19) и выберите какой-нибудь цвет.
Рис. 12.19
В этой закладке представлены так называемые системные цвета. Это те цвета, на которые настроены элементы вашей Windows: окна, их заголовки, цвет текста в заголовках, меню и т.д. Например, цвет кнопок и многих других рельефных элементов в Windows называется Control (у меня на компьютере он серый), а цвет рабочего стола, когда на нем нет обоев, называется Desktop (у меня он темно-голубой). Эти цвета мы и видим на рисунке. Вы, наверное, уже обратили внимание, что и VS вслед за Windows всегда предлагает вам цвет Control в качестве цвета формы, кнопок и других элементов управления.
Итак, системные цвета VS берет от Windows. Проверим. Выйдем из VS в Windows. Перенастроим цветовую гамму Windows. Сейчас она у вас, скорее всего, стандартная, серенькая или синеватая. Выберем что-нибудь поярче. Для этого щелкнем правой клавишей мыши по рабочему столу Windows и в контекстном меню выберем Свойства, а там – закладку Оформление. В возникшем окне перенастроим цветовую схему Windows. Обратите внимание, что весь цветовой облик Windows после этого изменился, цвета всех ее элементов стали другими.
Попробуйте теперь в Windows открыть какое-нибудь солидное приложение, например, Word. Вы увидите, что привычные цвета элементов Word тоже изменились в согласии с выбранной в Windows цветовой схемой. Вернитесь в VS, в ваш проект. Вы видите, что изменился цвет кнопки и цвет формы. Снова зайдите в окно свойств, в свойство BackColor, а в нем – в закладку System. Обратите внимание, что все цвета здесь изменились в согласии с выбранной в Windows цветовой схемой. Попробуйте открыть какой-нибудь старый проект – цвета формы и элементов управления изменились и в нем.
Таким образом, изменение цветовой схемы Windows влияет на все ее приложения. Хорошо это или плохо? Как посмотреть. В любом случае программист должен иметь возможность сам решать, должны ли цвета элементов в его приложении зависеть от настройки цветовой схемы Windows.
Как решать? Просто. Если речь идет о выборе цвета в режиме проектирования, то выбрав цвет из закладки System, вы тем самым разрешаете ему меняться в зависимости от настройки цветовой схемы Windows на компьютере пользователя. Если же вы выбираете цвет из закладок Custom или Web, то запрещаете.
Если вы не используете в своем проекте системных цветов, цвета вашего приложения на любом компьютере будут одинаковы независимо от настроек Windows.
Опасайтесь делать цвета одних элементов проекта системными, а других – несистемными. Например, сделав цвет кнопки системным, а именно Desktop, а цвет текста на этой кнопке несистемным, а именно белым, вы сильно рискуете, потому что если пользователю вздумается сделать рабочий стол своего Windows белым, он ничего не сможет прочесть на вашей кнопке.
И вообще – цветовые схемы Windows составляли профессиональные дизайнеры. Если ваше приложение пользуется только системными цветами, то есть хороший шанс, что его цветовое решение будет достаточно гармоничным. Если ваше приложение пользуется только несистемными цветами, то такой шанс тоже есть, если у вас была пятерка по рисованию. Если же приложение пользуется и системными и несистемными цветами, то есть вероятность, что на чужом компьютере оно будет напоминать помесь попугая с вороной.
Задаем системные цвета в коде. До сих пор в коде мы с вами пользовались несистемными цветами, которые VB предоставлял нам в структуре Color и в классах Pens и Brushes пространства имен System.Drawing. Системные же цвета лежат соответственно в классах SystemColors, SystemPens и SystemBrushes пространства имен System.Drawing. Там вы найдете пару десятков примерно тех же цветов, что и в закладке System свойства BackColor. Из класса SystemPens вы берете готовые перья, из класса SystemBrushes вы берете готовые кисти, из класса SystemColors вы берете цвета для всех прочих нужд. Вот примеры использования системных цветов:
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
Button2.BackColor = SystemColors.ActiveCaption
Button2.ForeColor = SystemColors.ActiveCaptionText
Dim Гр As Graphics = Me.CreateGraphics
Гр.DrawEllipse(SystemPens.MenuText, 100, 0, 300, 200)
Гр.FillEllipse(SystemBrushes.ControlLightLight, 80, 80, 140, 180)
Dim Перо As New Pen(SystemColors.ControlDark)
Гр.DrawEllipse(Перо, 10, 50, 230, 150)
Dim Кисть As New SolidBrush(SystemColors.ControlDark)
Гр.FillEllipse(Кисть, 50, 50, 200, 100)
End Sub
Private Sub Button4_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button4.Click
Dim Гр As Graphics = Me.CreateGraphics
Гр.Clear(SystemColors.Control)
End Sub
Функция FromArgb
В VB существует 16 миллионов цветов с лишним (точнее - 16777216)! Их имен мы не знаем, да имен этих и не существует. Тем не менее, мы должны научиться управлять этими цветами. Чтобы навести порядок в этой массе цветов, VB предлагает функцию FromArgb. Она принадлежит структуре Color. Суть ее вот в чем.
Вспомним, что любую краску можно получить, смешав в определенной пропорции красную (Red), зеленую (Green) и синюю (Blue) краски. В VB каждой краски в смесь можно положить от 0 до 255 единиц. Функция FromArgb как раз и смешивает эти краски. Пусть мы хотим покрасить форму краской, в которую мы положили и смешали 200 единиц красной, 40 единиц зеленой и 250 единиц синей краски. Для этого пишем такой оператор:
Me.BackColor = Color.FromArgb(200, 40, 250)
Здесь мы использовали вариант функции FromArgb с тремя параметрами.
Примеры:
FromArgb (255, 0, 0) | Красный цвет | ||
FromArgb (0, 255, 0) | Зеленый цвет | ||
FromArgb (0, 0, 255) | Синий цвет | ||
FromArgb (255, 255, 0) | Желтый цвет |
Вы видите, что желтый цвет – это смесь красного с зеленым.
Чем меньше каждой краски мы положим, тем темнее будет цвет, чем больше – тем светлее:
FromArgb (70, 90, 88) | Темный цвет (потому что числа маленькие) | ||
FromArgb (210, 250, 202) | Светлый цвет (потому что числа большие) | ||
FromArgb (0, 0, 0) | Черный цвет | ||
FromArgb (255, 255, 255) | Белый цвет |
Если каждой краски положить поровну, получится серый цвет:
FromArgb (90, 90, 90) | Темно-серый цвет | ||
FromArgb (220, 220, 220) | Светло-серый цвет |
Вот программа, которая красит форму, рисует и заливает эллипс, как на Рис. 12.20:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim Цвет1, Цвет2 As Color
Цвет1 = Color.FromArgb(250, 230, 252)
Цвет2 = Color.FromArgb(220, 170, 240)
Me.BackColor = Цвет1 'Красим форму
Dim Перо As New Pen(Color.FromArgb(140, 120, 90), 40)
Dim Кисть As New SolidBrush(Цвет2)
Dim Гр As Graphics = Me.CreateGraphics
'Рисуем и заливаем эллипс:
Гр.DrawEllipse(Перо, 50, 30, 300, 200)
Гр.FillEllipse(Кисть, 50, 30, 300, 200)
End Sub
Здесь цвет формы, пера и кисти выбран при помощи функции FromArgb.
Рис. 12.20
Прозрачность
Функция FromArgb позволяет управлять прозрачностью цвета. Для этого используется ее вариант не с тремя, а с четырьмя параметрами. Второй, третий и четвертый параметры имеют привычный нам смысл количества красной, зеленой и синей краски. А вот первый параметр определяет прозрачность цвета. Если он равен 255, то цвет полностью непрозрачен, а если 0 – то полностью прозрачен (невидим).
Вот программа:
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
Dim Гр As Graphics = Me.CreateGraphics
Dim Цвет1, Цвет2, Цвет3, Цвет4 As Color
Цвет1 = Color.FromArgb(255, 50, 130, 152) 'Совершенно непрозрачный цвет
Цвет2 = Color.FromArgb(200, 50, 130, 152) 'Немного прозрачный цвет
Цвет3 = Color.FromArgb(150, 50, 130, 152) 'Более прозрачный цвет
Цвет4 = Color.FromArgb(100, 50, 130, 152) 'Еще более прозрачный цвет
Dim Перо As New Pen(Color.Black, 40) 'Черное перо для горизонтальной линии
Dim Перо1 As New Pen(Цвет1, 40)
Dim Перо2 As New Pen(Цвет2, 40)
Dim Перо3 As New Pen(Цвет3, 40)
Dim Перо4 As New Pen(Цвет4, 40)
Гр.DrawLine(Перо, 30, 100, 300, 100) 'Чертим горизонтальную линию
Гр.DrawLine(Перо1, 80, 60, 80, 200) 'Чертим левую вертикальную линию
Гр.DrawLine(Перо2, 130, 60, 130, 200) 'Чертим вторую слева вертикальную линию
Гр.DrawLine(Перо3, 180, 60, 180, 200) 'Чертим третью слева вертикальную линию
Гр.DrawLine(Перо4, 230, 60, 230, 200) 'Чертим правую вертикальную линию
End Sub
Результат ее работы вы видите на Рис. 12.21. Четыре вертикальные линии одного и того же цвета (50, 130, 152), но разной прозрачности нарисованы на фоне черной горизонтальной линии.
Рис. 12.21
Попробуйте несколько раз нажать на кнопку. Сможете объяснить результат?
Задание 71.
«Атака абстракциониста». На экране рисуются один за другим в быстром темпе залитые случайными цветами эллипсы или прямоугольники случайных размеров и местоположения. Получается очень ярко и живописно.
Задание 72.
Попробуйте из картинки в левой части Рис. 12.22 сделать картинку в правой.
Рис. 12.22
Указание: Для этого заполните пространство фотографии белыми концентрическими эллипсами с разной прозрачностью.
Задание 73.
Представьте себе куб, собранный из множества кубиков. Его высота – 256 кубиков, ширина и толщина – тоже по 256 кубиков. Получается ровно 16777216 кубиков – по числу цветов в VB. Каждый кубик покрашен в свой цвет. Цвета не повторяются. Система раскраски такая. Слева направо растет от 0 до 255 красная составляющая в цвете кубиков, сверху вниз – зеленая, от нас вдаль – синяя. Так что самый левый верхний ближний кубик получается абсолютно черным, а самый правый нижний дальний кубик – абсолютно белым. Сразу все кубики видеть мы, конечно, не можем, но мы можем делать срез куба в любом месте параллельно любой из его граней, в результате чего на срезе будем видеть квадрат, состоящий из 256*256 разноцветных квадратиков. Вот эту задачу среза я бы и хотел вам предложить. Программа просит пользователя выбрать один из трех основных цветов (это удобно сделать через меню) и его насыщенность (число от 0 до 255). Этим определяется место среза. Затем программа чертит на форме этот разноцветный срез. Конечно, квадратики получатся очень маленькими, но это ничего.
Указание: Используйте процедуру с двумя параметрами: выбранный пользователем цвет (один из трех) и его насыщенность.
Кстати, догадайтесь, из каких цветов составлена главная диагональ куба, проведенная между двумя упомянутыми мной кубиками.
Как узнать цвет точки на фотографии
GetPixel и свойства R, G, B. Задача: У вас есть фотография морского пляжа и вы хотите узнать, какого цвета зонтик вот у этой дамы слева.
Решение: Сначала вы заносите фото в компьютер и создаете объект Bitmap с этим фото. Затем, чтобы не было разницы между пикселями экрана и пикселями объекта, создаете еще один объект Bitmap с заданными размерами, как мы это делали в 12.3.4. Для наглядности нарисуйте это фото на форме. Затем вам нужно узнать координаты хоть какой-нибудь точки на зонтике. Я думаю, вы сами догадаетесь, как это сделать. Совершенно верно, методом «тыка». Здесь вам поможет маленькая окружность, которую вы рисуете на фото и координаты которой вы наугад подбираете так, чтобы попасть в зонтик. Как только вы увидите, что окружность оказалась прямо на зонтике, можно приступать к определению цвета в ее центре. Для этого вы используете функцию GetPixel объекта Bitmap и свойства R, G, B структуры Color:
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
Dim Гр As Graphics = Me.CreateGraphics
Dim Фото As New Bitmap("Spacescape.JPG")
Dim Фото1 As New Bitmap(Фото, 500, 400)
Dim Цвет_точки As Color
Dim x, y, Красный, Зеленый, Синий As Integer
Гр.DrawImage(Фото1, 0, 0)
x = InputBox("Введите горизонтальную координату точки")
y = InputBox("Введите вертикальную координату точки")
Гр.DrawEllipse(New Pen(Color.Yellow, 3), x - 5, y - 5, 10, 10) 'Рисуем маленькую окружность
Цвет_точки = Фото1.GetPixel(x, y) 'Определяем цвет заданной точки
Красный = Цвет_точки.R 'Количество красного
Зеленый = Цвет_точки.G 'Количество зеленого
Синий = Цвет_точки.B 'Количество синего
Debug.WriteLine(Цвет_точки)
Debug.WriteLine(Красный)
Debug.WriteLine(Зеленый)
Debug.WriteLine(Синий)
End Sub
Пояснения: Функция GetPixel
объекта Bitmap определяет цвет пикселя с заданными координатами в объекте Bitmap. Она возвращает значение типа Color. Свойства R, G, B структуры Color являются целыми числами в диапазоне 0-255 и имеют значение количества в ней красного, зеленого и синего цвета.
Программа выдаст результаты такого вида:
Color [A=255, R=209, G=183, B=98]
209
183
98
Вам придется удовлетвориться тем, что вы узнаете содержание основных цветов в нужной точке рисунка. Имейте в виду, что программа находит цвет одной точки на зонтике, а не всего зонтика. Если зонтик хоть чуть-чуть пестрый, разные точки на зонтике имеют разный цвет и результат будет дезориентирующим. Часто пестрый узор настолько мелок, что издалека он кажется вам однородным цветом, и лишь большое увеличение позволяет обнаружить пестроту.
У структуры Color имеется также свойство A, определяющее прозрачность в том смысле, о котором мы только что говорили.
SetPixel. Вы можете не только узнавать цвет пикселя, но и рисовать в любом месте объекта Bitmap пиксель нужного цвета. Для этого вы используете функцию SetPixel объекта Bitmap. Так, в условиях предыдущей задачи оператор
Фото1.SetPixel(x, y, Color.Blue)
поставил бы синюю точку в центр окружности. Только не забывайте после этого нарисовать содержимое объекта на экране, а то вы эту точку не увидите.
Вот фрагмент, рисующий на фотографии синий треугольник из пикселей:
x0 = InputBox("Введите горизонтальную координату точки")
y0 = InputBox("Введите вертикальную координату точки")
For y = y0 To y0 + 100
For x = x0 To y
Фото1.SetPixel(x, y, Color.Blue)
Next
Next
Гр.DrawImage(Фото1, 0, 0)
Задание 74.
Определить цвет заданной точки на картинке и выдать одно из трех сообщений:
В этом пикселе красной краски больше, чем двух остальных.
В этом пикселе зеленой краски больше, чем двух остальных.
В этом пикселе синей краски больше, чем двух остальных.
Перспективы. Распознав цвет точки на картинке, вы сделали первый шаг к решению великой и не решенной до сих пор наукой задачи распознавания зрительных образов. Пожалуй, вы уже сейчас в силах написать программу, которая в большинстве случаев правильно отличит фотографию песчаной пустыни от фотографии океана. Но знаете ли вы, что не родился еще гений, способный написать программу, надежно отличающую хотя бы фотографию собаки от фотографии кошки? Потому что здесь дело не столько в цвете, сколько в форме. А это уже гораздо сложнее.
Решение задачи распознавания образов – ключ к осуществлению величайшей и самой дерзкой мечты ученых – созданию искусственного интеллекта, электронного разума, равного человеческому или превосходящего его.
Работа с цветом
До сих пор мы с вами пользовались цветами, которые VB предоставлял нам в структуре Color и в классах Pens и Brushes. Это полторы сотни цветов со своими именами. Но есть и другие цвета. О них мы сейчас и поговорим.
Преобразования системы координат
Я уже вам говорил, что начало системы координат для элемента управления находится в его верхнем левом углу, причем ось Y направлена вниз. У объекта класса Graphics есть методы, позволяющие изменять положение начала координат, направление осей, их масштаб, вращать систему координат.
Сдвиг, растяжение, сжатие, смена направления на обратное. Поставим задачу поменять компьютерную систему координат на привычную нам по школе, когда ось Y направлена вверх и начало координат лежит посредине тетрадного листа. Создадим для этого программу:
Private Sub Button3_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button3.Click
Dim Гр As Graphics = Me.CreateGraphics
Гр.ScaleTransform(1, -1) 'Меняем направление оси Y
Гр.TranslateTransform(300, -100) 'Смещаем начало координат
Гр.DrawLine(Pens.Black, 0, 0, 150, 70)
Гр.DrawString("Новое начало координат", Me.Font, Brushes.Black, 0, 0)
Гр.DrawString("Точка 150, 70", Me.Font, Brushes.Black, 150, 70)
End Sub
Пояснения: Для изменения направления оси Y оператор
Гр.ScaleTransform(1, -1) 'Меняем направление оси Y
использует процедуру ScaleTransform, которая позволяет растягивать или сжимать обе оси координат и менять их направление. Первый параметр относится к оси X, второй – к оси Y. Знак минус означает, что ось меняет направление на обратное. Если бы первым параметром мы вместо 1 написали 3, то все размеры по оси X увеличились бы в 3 раза, то есть любая картинка или фигура троекратно растянулась бы по горизонтали.
Для смещения начала координат из верхнего левого угла приблизительно в середину формы использован оператор
Гр.TranslateTransform(300, -100) 'Смещаем начало координат
Первый параметр процедуры TranslateTransform смещает начало координат направо на 300 пикселей, второй – вниз на 100 пикселей. Не перепутайте: именно вниз, а не вверх, так как теперь, после того, как ось Y стала смотреть вверх, отрицательные величины означают направление вниз, а не вверх.
Следующий оператор для иллюстрации рисует (см. Рис. 12.23 с перевернутым текстом) отрезок от начала координат (0, 0) до точки (150, 70), причем обратите внимание, что начало координат теперь лежит где-то посредине формы, а точка (150, 70) находится правее и выше начала координат, что привычно для математика, но не для компьютерщика.
Рис. 12.23
Все бы хорошо, но когда мы после такого преобразования системы координат рисуем объекты, имеющие «форму», такие, как текст или фотографии, они, естественно, получаются перевернутыми, что и видно на рисунке, где изображен текст "Новое начало координат" и "Точка 150, 70".
Упражнение. Выполните Задание 84 о построении графика функции y = (Sin x)/x (см. Рис. 12.8) с использованием преобразования системы координат. Указание: После того, как вы начертите оси координат и сделаете все надписи, сместите начало координат в точку пересечения осей координат, поменяйте направление оси ординат и растяните ось x примерно в 10, а ось y – в 100 раз. Тогда для вычерчивания графика вам не придется преобразовывать числа x и y в координаты на экране.
Вращение. Мы можем и вращать систему координат. Рассмотрим программу:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim Гр As Graphics = Me.CreateGraphics
Dim Фото As New Bitmap("Spacescape.JPG")
Dim П1 As New Rectangle(400, 0, 150, 130)
Гр.DrawEllipse(Pens.Black, 100, 0, 250, 40)
Гр.DrawImage(Фото, П1)
Гр.RotateTransform(20) 'Вращаем систему координат вокруг ее начала на 20 градусов
Гр.DrawEllipse(Pens.Black, 100, 0, 250, 40)
Гр.DrawImage(Фото, П1)
End Sub
Результат ее работы вы видите на Рис. 12.24.
Рис. 12.24
Пояснения: Четвертая и пятая строки процедуры
Гр.DrawEllipse(Pens.Black, 100, 0, 250, 40)
Гр.DrawImage(Фото, П1)
рисуют верхний эллипс и верхнюю фотографию. Следующая строка
Гр.RotateTransform(20) ' Вращаем систему координат вокруг ее начала на 20 градусов
незримо поворачивает систему координат вокруг ее начала (а оно находится в левом верхнем углу формы) по часовой стрелке на 20 градусов. Делает это процедура RotateTransform.
Седьмая и восьмая строки процедуры
Гр.DrawEllipse(Pens.Black, 100, 0, 250, 40)
Гр.DrawImage(Фото, П1)
являются копиями четвертой и пятой строк, но несмотря на это, рисуют эллипс и фотографию в другом месте, так как система координат уже повернута.
Вы можете вращать систему координат вокруг любой точки формы, предварительно сместив туда начало координат процедурой TranslateTransform.
Задание 75.
Заставьте картинку (например, монетку с Рис. 12.15) непрерывно вращаться. Указание: Сместите начало координат примерно в середину формы и нарисуйте монетку так, чтобы ее центр был как можно ближе к новому началу координат. Затем немного поверните систему координат и снова нарисуйте монетку. И так далее.
Встроенный графический редактор VB
Если вы хотите с удобством нарисовать иконку или другую картинку, чтобы к тому же не приходилось специально заботиться о прозрачном цвете, используйте встроенный графический редактор VB (Image Editor).
Пусть вы хотите нарисовать иконку в виде флажка. Ваши действия: В режиме проектирования выберите Project ® Add New Item ® в возникшем окне Add New Item выбираете Icon File, даете ему имя, затем ® Open. Перед вами в окне проекта возникает окно графического редактора (Рис. 12.25)
Рис. 12.25
В окне Solution Explorer вы видите, что в папку проекта добавилась иконка – файл с расширением ico. В середине окна вы видите серое окошко размером 32х32 пикселя, в котором вы и будете рисовать флажок. Чуть левее вы видите тот же флажок в уменьшенном виде. Для рисования вам предоставляется панель инструментов со стандартным набором инструментов графического редактора и палитра цветов (и то и другое вы видите на рисунке). Если вам не хватает этих цветов, сделайте двойной щелчок по любому цвету и выберите нужный. Левый верхний цвет в палитре – прозрачный.
Зайдя в меню Image, вы сможете преобразовывать рисунок и выбирать размер в пикселях нового рисунка.
Вы можете нарисовать и картинку в формате BMP, выбрав в окне Add New Item пункт Bitmap File,. Вы можете нарисовать курсор, выбрав в окне Add New Item пункт Cursor File,
Чтобы редактировать в графическом редакторе нарисованную ранее картинку, дважды щелкните по ней в окне Solution Explorer.
Материализация привидений. Добудьте где-нибудь файл с фотографией мрачного замка. Нарисуйте в Image Editor привидение на прозрачном фоне. Напишите программу, по которой привидение постепенно возникает и растворяется на фоне замка. Это непросто и не обязательно.
Ненадолго расстаемся с графикой. Некоторые сложные фигуры требуют для своего рисования знания массивов, поэтому я перенес дальнейшее изучение графики в Глава 17. .
Переменные и литералы типа DateTime
Когда вы пишете в окне кода программу, в ней встречаются литералы: числа, строки, а теперь вы должны научиться писать в программе литерал даты и времени суток. Чтобы VB понял, что перед ним число, вы просто пишете цифры, и он понимает. Чтобы VB понял, что перед ним строка, вы пишете ряд букв и берете его в двойные кавычки, и VB понимает, что это строка. Чтобы VB понял, что перед ним дата или время суток, вы правильно записываете дату и время и заключаете их между значками #, и он понимает, что это литерал типа DateTime. Вот пример правильной записи: #2/16/2002#. Перед вами не что иное, как 16 февраля 2002 года. Как правильно записывать дату и время в других случаях, вы поймете из других примеров:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Debug.WriteLine(#6/25/2001#) '25 июня 2001 года
Debug.WriteLine(#2:22:57 PM#) '2 часа 22 минуты 57 секунд после полудня (PM)
Debug.WriteLine(#2/28/1998 10:45:00 PM#) '10 часов 45 минут вечера 28 февраля 1998 года
Dim D, T, DT As DateTime
D = #12/25/2044#
T = #2:00:32 AM# '2 часа 00 минут 32 секунды до полудня (AM)
DT = #1/15/2156 11:59:42 PM#
Debug.WriteLine(D)
Debug.WriteLine(T)
Debug.WriteLine(DT)
End Sub
Эта процедура напечатает такие результаты:
25.06.2001 0:00:00
01.01.0001 14:22:57
28.02.1998 22:45:00
25.12.2044 0:00:00
01.01.0001 2:00:32
15.01.2156 23:59:42
Пояснения: Как видите, в окне кода мы обязаны писать дату и время по-американски, то есть месяц писать раньше числа и разделять все это косыми чертами, а в обозначении времени суток VB заставляет вас указывать, до или после полудня было дело. А вот результаты по этим непривычным данным печатаются все равно по-нашему, вернее, так, как настроена Windows (а у большинства она настроена на Россию).
Вы можете задавать переменную типа DateTime и в виде строки.
D = "5/6/2003 10:45:12 PM"
В этом случае, если дата и время в строке записаны правильно, VB автоматически преобразует строку в тип DateTime. А вот если вы захотите задавать дату или время при помощи оператора
D = InputBox("Введите дату")
или из текстового поля, то вводить их по-американски нельзя и значки # тоже нельзя ставить.
Переменная типа DateTime включает в себя всегда и дату, и время суток. Но при задании значения такой переменной вы не обязаны задавать и то и другое одновременно. Если вы не задали время суток, то, как видите из результатов, считается, что временем суток данной переменной является 0:00:00, то есть полночь – начальный момент суток. Если вы не задали дату, то считается, что датой в данной переменной является 01.01.0001, то есть первый день нашей эры.
Что делать, если я хочу в результатах видеть только дату, а не время, или наоборот – только время, а не дату? Об этом чуть позже.
Кроме типа DateTime со временем в VB имеет дело тип TimeSpan. Его сфера компетенции – не столько конкретные даты и моменты времени, сколько протяженность промежутков времени. Мы не будем его рассматривать.
Свойства и методы структуры DateTime
Чтобы понять, что от типа DateTime есть какая-то польза, решим пару задач.
Задача 1. 29 мая 2004 года мне выдали задание и сказали, чтобы я уложился в 50 дней. Какого числа наступит крайний срок сдачи задания?
Программа:
Private Sub Button6_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button6.Click
Dim Сегодня, Крайний_срок As DateTime
Сегодня = #5/29/2004#
Крайний_срок = Сегодня.AddDays(50)
MsgBox(Крайний_срок)
End Sub
В окне MsgBox мы видим результат: 18.07.2004. То есть 18 июля 2004 года – крайний срок сдачи задания. Здесь я использовал метод AddDays («Добавить дни») структуры DateTime.
Ту же самую программу можно записать короче:
Dim Сегодня As DateTime = #5/29/2004#
MsgBox(Сегодня.AddDays(50))
И еще короче:
MsgBox(#5/29/2004#.AddDays(50))
Задача 2. В отпуск вы отправляетесь 1 июля 2005 года. Какой это день недели?
Программа:
Dim Начало_отпуска As DateTime = #7/1/2005#
MsgBox(Начало_отпуска.DayOfWeek)
Здесь я использовал метод DayOfWeek («День недели») структуры DateTime. В окне MsgBox мы видим результат: 5. Это пятый день недели – пятница. Кстати, воскресенье в VB – не 7-й день, а нулевой. Это потому, что у американцев неделя начинается с воскресенья, а не с понедельника, как у нас.
Перечень свойств и методов структуры DateTime. Не всех, но популярных. С пояснениями.
Пусть мы задали переменную:
Dim D As DateTime = #2/14/2005 11:41:39 PM#
Ее значение мы будем использовать в этом подразделе, как исходный материал.
Оператор
Debug.WriteLine(D.Date)
напечатает значение
14.02.2005 0:00:00
Мы видим, что свойство Date возвращает значение переменной с обнуленной составляющей времени суток, оставив только дату. Тип значения – Date. Заносим вышесказанное в таблицу:
Свойство или метод | Значение | Тип значения | Пояснения | ||||
Date | 14.02.2005 0:00:00 | Date | Функция обнуляет в значении переменной составляющую времени суток, оставив только дату |
ToUniversalTime |
14.02.2005 20:41:39 |
DateTime |
Если вы имеете в виду, что в переменной D задано время вашего пояса, то эта функция показывает время GMT. |
ToLocalTime |
15.02.2005 2:41:39 |
DateTime |
Наоборот: Если вы имеете в виду, что в переменной D задано время GMT, то эта функция показывает, сколько время в этот момент было в вашем поясе |
Следующие методы преобразуют значение из типа DateTime в тип String. После этого его удобно просматривать и к нему можно применять методы работы со строками.
ToString |
14.02.2005 23:41:39 |
String |
Просто преобразование без изменений |
ToLongDateString |
14 Февраль 2005 г. |
String |
Выделяется дата |
ToShortDateString |
14.02.2005 |
String |
Выделяется дата |
ToLongTimeString |
23:41:39 |
String |
Выделяется время |
ToShortTimeString |
23:41 |
String |
Выделяется время |
Следующие методы и свойства принадлежат структуре DateTime и для их использования не нужно создавать переменную типа DateTime. То есть, можно писать, например, просто
Debug.WriteLine(DateTime.Now)
DaysInMonth(1996, 2) |
29 |
Integer |
Сколько дней во 2 месяце 1996 года |
IsLeapYear(2004) |
True |
Boolean |
Правда ли, что 2004 год – високосный. |
Now |
23.08.2003 17:42:10 |
Date |
Дата и время на момент выполнения этого оператора, то есть текущие показания часов вашего компьютера |
Today |
23.08.2003 0:00:00 |
Date |
То же самое, только без времени суток |
Свойства и методы модуля DateAndTime
Полезные средства для работы с датами и временем унаследованы от Visual Basic 6.0. Вы можете воспользоваться ими, как свойствами и методами модуля DateAndTime пространства имен Microsoft.VisualBasic[‡]. Не путайте модуль DateAndTime со структурой DateTime. Любая переменная или литерал типа даты и времени является экземпляром структуры DateTime, и чтобы воспользоваться ее свойством или методом, вы просто пишете после имени переменной или литерала точку и за ней название свойства или метода, как мы делали это в предыдущем подразделе. Модуль же DateAndTime (как и все модули) не имеет права поставлять нам свои экземпляры для хранения переменных и литералов. Он поставляет свои свойства и методы в виде функций, параметрами которых и будут наши переменные и литералы. Вот пример:
Пусть D1 = #2/14/2009 4:45:07 PM#, D2 = #2/16/2009 11:32:43 AM#. Тогда
Свойство или метод | Значение | Тип значения | Пояснения | ||||
DateDiff("h", D1, D2) | 42 | Long | Количество часов, прошедших с момента D1 до момента D2 |
На то, что нас интересуют именно часы, указывает параметр "h". Если вас в этой или других функциях интересуют минуты или другие единицы времени, воспользуйтесь следующей таблицей:
Значения строкового параметра для функций работы с датами:
Строковый параметр | Смысл | ||
yyyy | Год | ||
q | Квартал в году | ||
m | Номер месяца в году | ||
y | Номер дня в году | ||
d | Номер дня в месяце | ||
w | Номер дня в неделе (с 1 по 7, а не с 0 по 6). Неделя начинается с воскресенья | ||
ww | Номер недели в году | ||
h | Час в сутках | ||
n | Минута в часе | ||
s | Секунда в минуте |
Функция DatePart работает аналогично ряду свойств структуры DateTime .
Пусть D = # 2/14/2009 4:45:07 PM # (это суббота). Тогда
DatePart("m", D) | 2 | Integer | Функция выделяет из даты номер месяца в году | ||||
DatePart("w", D) | 7 | Integer | Поскольку неделя начинается с воскресенья, то воскресенье имеет №1, а суббота – №7. | ||||
DatePart("w", D, vbMonday) | 6 | Integer | А здесь мы попросили функцию считать первым днем недели понедельник (vbMonday) и поэтому результат она выдала привычный для нас – 6-й день - суббота |
Форматирование даты и времени
В 5.4.7 вы уже сталкивались с форматированием чисел. Форматирование нужно для того, чтобы представить информацию в удобном для пользователя виде. Осуществляет форматирование функция Format. У нее два аргумента (параметра): первый – что форматировать, второй – как форматировать. Функция возвращает значение типа String.
Готовые форматы. Рассмотрим программу:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim t As DateTime = #8/10/2004 4:42:39 PM#
WriteLine(Format(t, "General Date")) '10.08.2004 16:42:39
WriteLine(Format(t, "Long Date")) '10 Август 2004 г.
WriteLine(Format(t, "Short Date")) '10.08.2004
WriteLine(Format(t, "Long Time")) '16:42:39
WriteLine(Format(t, "Short Time")) '16:42
WriteLine(Format(t, "f")) '10 Август 2004 г. 16:42
WriteLine(Format(t, "F")) '10 Август 2004 г. 16:42:39
WriteLine(Format(t, "g")) '10.08.2004 16:42
WriteLine(Format(t, "y")) 'Август 2004 г.
End Sub
Пояснения: Здесь первый аргумент – t. Второй – строка. Содержание строки определяет вид результата. То, что печатают операторы программы, написано в комментариях.
Форматы, определяемые программистом. Мы рассмотрели здесь наиболее популярные из готовых форматов, которые нам предоставляет VB. Но форматы можно конструировать и самому. Рассмотрим программу:
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
Dim t As DateTime = #8/5/2004 7:02:09 PM#
WriteLine(Format(t, "fff")) '000 - доли секунды (тысячные, потому что 3 буквы f)
WriteLine(Format(t, "%s")) '9 - секунды
WriteLine(Format(t, "ss")) '09 - секунды
WriteLine(Format(t, "%m")) '2 - минуты
WriteLine(Format(t, "mm")) '02 - минуты
WriteLine(Format(t, "%h")) '7 - часы до и после полудня
WriteLine(Format(t, "hh")) '07 - часы до и после полудня
WriteLine(Format(t, "%H")) '19 - часы
WriteLine(Format(#3:02:09 AM#, "HH")) '03 - часы
WriteLine(Format(t, "%d")) '5 - число месяца
WriteLine(Format(t, "dd")) '05 - число месяца
WriteLine(Format(t, "ddd")) 'Чт - день недели
WriteLine(Format(t, "dddd")) 'четверг - день недели
WriteLine(Format(t, "%M")) '8 - месяц
WriteLine(Format(t, "MM")) '08 - месяц
WriteLine(Format(t, "MMM")) 'авг - месяц
WriteLine(Format(t, "MMMM")) 'Август - месяц
WriteLine(Format(t, "%y")) '4 - год
WriteLine(Format(t, "yy")) '04 - год
WriteLine(Format(t, "yyyy")) '2004 - год
End Sub
Пояснения: В комментариях вы видите результаты работы операторов программы с пояснениями. Знак процента употребляется тогда, когда в кавычках стоит один единственный символ форматирования, чтобы не спутать его с символом готового формата.
Мы рассмотрели здесь, как выделять из переменной нужные нам элементарные части даты или времени. Теперь посмотрим, как их комбинировать, чтобы получить вразумительную строку:
Private Sub Button3_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button3.Click
Dim t As DateTime = #8/5/2004 7:02:09 PM#
WriteLine(Format(t, "dd-MM-yy")) '05-08-04
WriteLine(Format(t, "d MMM yyyy")) '5 авг 2004
WriteLine(Format(t, "Настал yyyy год.")) 'Настал 2004 год.
WriteLine(Format(t, "d/MM/yy")) '5.08.04
WriteLine(Format(t, "HH:m:ss")) '19:2:09
WriteLine(Format(t, "HH,m.ss")) '19,2.09
WriteLine(Format(t, "HH часов d MMM")) '19 часов 5 авг
WriteLine(Format(t, "Было %h часов вечера и m минуты")) 'Было 7 часов вечера и 2 минуты
End Sub
Пояснения: Если мы при разделении элементов даты пользуемся косой чертой, то в результате разделителем становится символ, на который настроена Windows (в России – точка, в США – косая черта). То же самое можно сказать о двоеточии для разделения элементов времени суток. Но мы можем для разделения сравнительно свободно пользоваться и другими символами: черточками, пробелами, точками, запятыми, буквами русского алфавита и др.
Задание 76.
Напишите программу, которая, ничего у вас не спрашивая, печатает, какое число будет через 52 недели.
Задание 77.
Напишите программу, которая, спросив у вас дату рождения и не спрашивая, какое сегодня число, печатает, сколько секунд вы живете на белом свете.
Задание 78.
Напишите программу, которая, спросив у вас дату рождения и не спрашивая, какое сегодня число и был ли у вас в этом году день рождения, печатает, сколько дней вам осталось до следующего дня рождения.
Задание 79.
Я знаю, что високосных годов ученым не хватает. Поэтому, не то где-то раз в много лет вклинивается лишний високосный год, не то иногда где-то в каком-то месяце бывает лишний день. Не знаю. Может быть VB подскажет?
Тип данных DateTime (Date)
Вы пока знакомы с числовыми, строковым и логическим типами данных. Тип даты и времени суток – DateTime тоже в каком-то смысле числовой, но специфический. Если, например, в нем к 0:40 прибавить 0:40, то получится, сами понимаете, не 0:80, а 1:20.
Тип DateTime представляет структуру
DateTime пространства имен System. Вы можете пользоваться или им, или эквивалентным ему типом Date, который достался нам в наследство от Visual Basic 6.0. Я не буду делать между ними различия. Во всяком случае VB распространяет на данные типа Date все свойства и методы структуры DateTime.
Таймер
Суть работы таймера. Создайте новый проект. Возьмите из Toolbox элемент управления Timer и поместите его на форму. Однако, подобно элементу управления Меню, таймер расположится не на форме, а под ней, на дизайнере компонентов. Установите его свойство Enabeled в True, а свойство Interval равным 10000. Сделайте двойной щелчок по таймеру. В появившуюся заготовку процедуры впишите одну строчку:
Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick
Debug.WriteLine("Процедура сработала")
End Sub
Запустите проект. Подождите немного. Через 10 секунд в окне Output появится строчка "Процедура сработала". Еще через 10 секунд появится такая же строчка, через 10 секунд еще одна, и так далее.
Если бы мы установили Interval равным 5000, то строчки появлялись бы каждые 5 с, а если равным 500, то – каждые 0,5 с. Получается, что свойство Interval задает промежуток времени в миллисекундах (мс – тысячных долях секунды).
Вывод: Таймер Timer1 – объект, вся работа которого заключается в том, чтобы через каждые Interval миллисекунд создавать событие (импульс), которое запускает процедуру Timer1_Tick.
Цикл без цикла. В процедуре Timer1_Tick мы можем написать все, что угодно. Например:
Dim i As Long = 1
Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick
Debug.WriteLine("Процедура сработала " & i & "-й раз")
i = i + 1
End Sub
Установите Interval в 1000. Запустите проект. Вы увидите через каждую секунду возникающие строчки:
Процедура сработала 1-й раз
Процедура сработала 2-й раз
Процедура сработала 3-й раз
Процедура сработала 4-й раз
Мы видим, что у нас заработал цикл, несмотря на то, что операторов цикла мы не писали! Чуть позже вы догадаетесь, как вовремя делать выход из цикла.
Свойство таймера Enabled. Установим свойство таймера Enabled (по-русски – «в рабочем состоянии») в False. Запустим проект. Никаких строк не появляется. Значит таймер выдает импульсы только тогда, когда свойство Enabled установлено в True.
Добавим в проект две кнопки и к ним две процедуры:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Timer1.Enabled = True
End Sub
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
Timer1.Enabled = False
End Sub
Запустите проект. Вы увидите, что новые строчки появляются только после щелчка по первой кнопке, а после щелчка по второй прекращают появляться.
«Холостые» импульсы. Что будет, если процедура Timer1_Tick еще не успела закончить работу, а от таймера уже пришел новый импульс? Проверим. Установим Interval равным 100, то есть импульсы от таймера должны посылаться примерно 10 раз в секунду. Напишем процедуру Timer1_Tick, которая очень долго работает:
Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick
Debug.WriteLine("Процедура таймера начала работу")
For i = 1 To 200000000 : Next
Debug.WriteLine("Процедура таймера закончила работу")
End Sub
Здесь долго работает пустой цикл
For i = 1 To 200000000 : Next
Несмотря на то, что делать ему нечего (тело у цикла отсутствует), у него много времени уходит на то, чтобы просто досчитать до 200 миллионов. На моем компьютере это занимает секунд пять, на вашем, наверное, по-другому. Это неважно, а важно то, что за эти пять секунд в процедуру врежется порядка 50 импульсов от таймера. И все как об стену горох – процедура на них не реагирует. Пока не завершит свою работу, как положено, оператором End Sub. После этого первый же следующий импульс, которому повезло, снова ее запускает. Вы увидите, что на экране не спеша, где-то раз в пять секунд появляется очередная пара строчек.
Точность таймера. Проведя эксперименты на компьютере Celeron 400, я обнаружил, что таймер выдает импульсы от интервала 10 мс и выше. То есть минимальное время между импульсами = 10 мс. Задавать интервал меньше 10 бесполезно. Однако, это все-таки гораздо лучше, чем в VB 6.0, где это время равнялось 1/18 доле секунды.
Задание 80.
Запрограммируйте с помощью таймера печать чисел от 100 до 110 через 1 секунду.
Несколько таймеров. Таймеров в проекте может быть несколько. Все они работают независимо друг от друга и каждый имеет свою собственную процедуру, которая реагирует только на его импульсы.
Поместите в проект два таймера. Интервал первого задайте равным 1000, второго – 3000. Вот программа:
Dim i As Long = 1
Dim j As Long = 1
Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick
Debug.WriteLine("Первый таймер сработал " & i & "-й раз")
i = i + 1
End Sub
Private Sub Timer2_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer2.Tick
Debug.WriteLine("Второй таймер сработал " & j & "-й раз")
j = j + 1
End Sub
А вот результаты:
Первый таймер сработал 1-й раз
Второй таймер сработал 1-й раз
Первый таймер сработал 2-й раз
Первый таймер сработал 3-й раз
Первый таймер сработал 4-й раз
Второй таймер сработал 2-й раз
Первый таймер сработал 5-й раз
Первый таймер сработал 6-й раз
Первый таймер сработал 7-й раз
Второй таймер сработал 3-й раз
Как видите, второй таймер срабатывает в 3 раза реже первого.
Перечисления
С перечислениями мы с вами сталкивались не раз. Каждый раз под перечислением понимался список значений какого-нибудь свойства или параметра, или чего-нибудь другого. Например, в 12.3.6мы использовали перечисление RotateFlipType в качестве параметра метода. У этого перечисления 16 значений, из которых можно выбирать любое.
Все перечисления, с которыми мы сталкивались, были созданы заранее профессиональными программистами, включены ими в библиотеку классов .NET Framework и достались нам готовыми к употреблению. Нам осталось только с удобством выбирать значения из всплывающего списка. Однако в реальном программировании часто встречаются ситуации, когда необходимо иметь перечисление, еще никем не созданное.
Пример 1. Пусть вам хотелось бы с удобством пользоваться списком своих любимых футбольных команд:
Real, MU, Вперед, Milan
VB позволяет вам самому создать такое (и вообще, какое угодно) перечисление и пользоваться им. Вот что нужно для этого сделать.
Сначала в верхней части окна кода, вне процедур, пишем такой текст:
Enum Мои_команды
Real
MU
Вперед
Milan
End Enum
Это не процедура, хоть и похожа. Слово Enum означает, что это перечисление. Мы придумали ему имя Мои_команды. Слова End Enum означают конец перечисления.
Это и не переменная. Вы не можете написать
Мои_команды = Вперед
То, что мы создали, это новый тип. Тип Мои_команды. Впервые мы не просто пользуемся типом, а создаем тип.
Чтобы извлечь выгоду из этого типа и воспользоваться перечислением, мы должны создать переменную данного типа. Делается это обычным образом:
Dim Команда As Мои_команды
Переменная Команда имеет право принимать только одно из четырех значений, перечисленных в объявлении типа.
Пользоваться этим перечислением так же удобно, как и остальными. Например, когда вы наберете текст
Команда =
VB любезно распахнет перед вами список из всех четырех значений перечисления. Правда, перед каждым значением он поставит точку и имя типа, что кажется несколько громоздким. Получится вот что:
Команда = Мои_команды.Milan
Теперь вы можете как угодно работать с переменной. Например, так:
If Команда = Мои_команды.Real Then MsgBox("Это не команда, а сборная мира!")
Пример 2. Все значения перечисления пронумерованы, начиная с нуля. Поэтому при работе с ними удобно применять операторы цикла. Проиллюстрирую работу с переменными перечислимого типа на примере перечисления дней недели:
Public Class Form1
Inherits System.Windows.Forms.Form
Windows Form Designer generated code
Enum Дни
понедельник
вторник
среда
четверг
пятница
суббота
воскресенье
End Enum
Dim День As Дни
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
День = 0
Debug.WriteLine(День) 'понедельник
День = 1
Debug.WriteLine(День) 'вторник
День = Дни.понедельник
Debug.WriteLine(День) 'понедельник
День = День + 2
Debug.WriteLine(День) 'среда
День = Дни.среда
Do Until День = Дни.воскресенье
День = День + 1
Debug.WriteLine(День) 'четверг пятница суббота воскресенье
Loop
For День = Дни.вторник To Дни.четверг
Debug.WriteLine(День) 'вторник среда четверг
Next
For День = Дни.воскресенье To Дни.понедельник Step -2
Debug.WriteLine(День) 'воскресенье пятница среда понедельник
Next
End Sub
End Class
Пояснения: То, что напечатают операторы этой программы, вы видите в комментариях. Поскольку все значения перечисления пронумерованы, то, например, все равно, как написать:
День = 1
или
День = Дни.вторник
Пример 3. В этом примере вы увидите, как можно анализировать с пользой для дела переменные перечислимого типа. Предполагается, что, как в предыдущем примере, определены тип Дни и переменная День.
Private Sub Button3_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button3.Click
День = InputBox("Введите номер дня недели")
Select Case День - 1 ' Чтобы понедельник был не нулевым, а первым
Case Дни.понедельник To Дни.среда
MsgBox("Начало недели")
Case Дни.четверг, Дни.пятница
MsgBox("Середина недели")
Case Дни.суббота, Дни.воскресенье
MsgBox("Отдых")
Case Is > Дни.воскресенье, Is < Дни.понедельник
MsgBox("Таких дней не бывает")
End Select
End Sub
Пояснения: Обратите внимание, что вводить день недели привычным образом буквами просто так не удастся, так как информация, вводимая в текстовое поле окна InputBox имеет тип String, который автоматически не преобразуется в перечислимый. Поэтому я выбрал ввод номера дня недели.
Настоящую пользу из собственных перечислений вы начнете извлекать в проекте «Будильник-секундомер» и когда изучите массивы.
Рамка (GroupBox)
Познакомимся с элементом управления GroupBox, который понадобится нам в проекте «Будильник-секундомер». Рамки являются удобным средством объединения элементов управления по смыслу. Пример вы видите на Рис. 13.1.
Рис. 13.1
Поместите GroupBox на форму и придайте ему довольно большие размеры. Получилась пустая рамка с заголовком GroupBox1. Затем возьмите из Toolbox и поместите внутрь этой рамки несколько разных элементов управления. А теперь передвиньте рамку по форме, ухватившись мышкой за ее границу. Вы видите, что все объекты внутри рамки передвинулись вместе с ней. Вот в этом и состоит удобство рамки: объекты, помещенные вами внутрь нее, она считает «своими» и таскает по форме вслед за собой. Посмотрите на значения свойств X и Y любого объекта внутри рамки. Теперь это координаты не относительно формы, а относительно рамки.
В режиме проектирования или в коде вы можете изменять значения свойств рамки X и Y. Объекты, объединенные в рамке, будут при этом перемещаться вместе с ней. Вы можете изменять значения свойств рамки Visible, Enabled,. При этом точно так же будут меняться значения этих свойств у всех объектов, объединенных в рамке. Это полезно, например, тогда, когда у вас на форме слишком много объектов и все они не умещаются на ней. Выход есть, если не все они нужны одновременно. Просто разделите их между наложенными друг на друга рамками и в каждый момент времени делайте видимой только одну из них. Замечу, что для этого можно пользоваться и вкладками.
Поменяв свойства рамки BackColor, ForeColor, Font, вы тем самым меняете эти свойства и у входящих в нее объектов (кроме тех, где эти свойства уже были изменены). Это же касается и формы, которая тоже является контейнером. Поэкспериментируйте.
Вы можете мышкой переместить в рамку уже размещенный на форме объект. Оказавшись внутри границ рамки, он становится «своим» и разъезжает вместе с рамкой. А вот когда вы попытаетесь обнять новой или старой рамкой уже существующий объект, у вас ничего не получится. Оказавшись внутри границ рамки, он останется «чужим». Убедитесь в этом, сдвинув рамку в сторону. Все уехали, объект остался.
Вставьте внутрь одной рамки другую, а в нее – несколько объектов. Подвигайте внешнюю рамку. Внутренняя рамка вместе со всеми своими объектами движется за ней. Все логично. Подвигайте теперь внутреннюю рамку внутри внешней. Вы видите, что внутренняя рамка ведет себя, как и следовало ожидать: для внешней рамки она «подчиненный», а для объектов внутри себя – «начальник».
Панель (Panel)
Панель во многом похожа на рамку. Интересующее нас отличие состоит в том, что если элементы управления не умещаются на панели, то ей можно придать полосы прокрутки, благодаря которым любой ее элемент управления может быть помещен в поле зрения (Рис. 13.2).
Рис. 13.2
Проделайте эксперимент. Поместите на форме большую панель. Разместите по всей ее поверхности несколько элементов управления. Затем уменьшите размеры панели. Многие элементы управления уйдут из поля зрения. Придайте панели полосы прокрутки, установив в True ее свойство AutoScroll. Покрутите полосы. Это очень экономит пространство на форме.
Вкладка (TabControl)
Вкладки вы видели не раз во многих приложениях Windows. Например, в VB вы можете видеть их в Tools ® Customize. Пример вкладки вы видите и на Рис. 13.3.
Рис. 13.3
Вкладка на этом рисунке состоит из трех страниц (TabPage). (В других подразделах книги я буду называть страницы закладками, так как этот термин более употребителен.) На каждой странице вы можете помещать любые элементы управления: кнопки, метки и пр. Я поместил на странице пейзажей несколько графических полей (PictureBox) с пейзажами. Если вы щелкнете по слову «Портреты», увидите страницу портретов. Страницы можно снабдить полосами прокрутки, благодаря чему на любой странице можно поместить огромное число элементов управления.
Сразу же бросается в глаза основное преимущество вкладки – она радикально экономит место на форме.
Создадим проект с вкладкой. Поместите на форму элемент управления TabControl. Обратите внимание, что перетаскивать вкладку мышкой можно только, ухватившись за ее бордюр. Зайдите в окне свойств в ее свойство TabPages и в возникшем окне Редактора вкладок щелкните несколько раз по кнопке Add (на Рис. 13.4 было щелкнуто 3 раза).
Рис. 13.4
В результате вкладка на форме приобретет такой вид, как на Рис. 13.5.
Рис. 13.5
В левой части Редактора вкладок вы видите список страниц вкладки. В правой части Редактора вкладок вы видите свойства той страницы вкладки, которая выделена. Свойства довольно обычные. Вы можете поменять имя, текст (название) страницы, ее цвет, бордюр и пр. При свойстве AutoScroll, установленном в True, на странице при необходимости будут возникать полосы прокрутки.
Пощелкав по названию страницы, выдвиньте ее на передний план. Щелкнув затем по пространству страницы, вы добьетесь, что выделена будет уже не вся вкладка, а только эта одна страница. Соответственно в окне свойств среды VB появятся свойства не вкладки, а этой страницы.
Помещайте теперь на эту страницу любые элементы управления. Затем выделяйте другую страницу и помещайте элементы управления на нее. Всеми этими элементами управления можно пользоваться обычным образом, как будто бы они расположены на форме.
Рамка (GroupBox), панель (Panel) и вкладка (TabControl)
Эти три элемента управления относятся к контейнерам – элементам управления, которые могут содержать на своей поверхности другие элементы управления. К контейнерам относится и форма.
Постановка задачи
Создать Будильник-секундомер следующего вида (см. Рис. 13.6).
Рис. 13.6
Если вы ставите задачу для серьезного проекта, то прежде всего вам нужно с максимальной подробностью описать, что должен делать ваш проект с точки зрения пользователя (а не программиста!). я начну с того же, но ввиду очевидности того, что изображено на рисунке, я поясню только то, чего не видно или может быть неправильно понято:
На верхнем циферблате должно отображаться текущее время суток (системное время Windows) в часах, минутах и секундах
Под ним – дата и день недели
Время будильника пользователь устанавливает, вручную редактируя (изменяя с клавиатуры) содержимое циферблата будильника
При нажатии на кнопку «Выключить будильник» надпись на кнопке меняется на «Включить будильник», а надпись над циферблатом меняется на «Будильник отключен»
При срабатывании будильника раздается какая-нибудь продолжительная мелодия, которая замолкает при нажатии на кнопку «Выключить звонок»
Секундомер измеряет время с точностью до 0.01 сек. На картинке вы видите секундомер в момент паузы. Цифры на секундомере замерли. Если нажать на ПУСК, то отсчет времени продолжится с 1 минуты 27,97 сек, которые вы видите на картинке, а надпись на кнопке сменится на ПАУЗА. Если снова нажать на кнопку, цифры на секундомере снова замрут.
При нажатии на кнопку «Обнулить» секундомер останавливается и сбрасывается в ноль. На циферблате – 00:00.00.
Делим проект на части
Нельзя создавать сразу весь проект одновременно. То есть было бы фатальной ошибкой сразу все нарисовать и пытаться все сразу программировать. Не пытайтесь ломать весь веник сразу, ломайте по прутикам. Когда Наполеон видел, что его армия слишком мала, чтобы сразу одолеть противника, он весь удар сосредотачивал на одной какой-то части вражеской армии и, разбив ее, нападал на следующую.
Надо разделить проект на части. Это не всегда легко. Некоторые проекты, особенно игры, представляют на первый взгляд единое неразрывное целое, так что нужен некоторый опыт, чтобы увидеть, «из каких кубиков построен домик». В нашем случае все более-менее просто. Три части просматриваются сразу, это:
Часы (с датой и днем недели)
Будильник
Секундомер
Отлично! За какую из этих частей браться вначале? Для ответа на этот вопрос нужно сначала решить, зависят ли друг от друга отдельные части. Здесь очень пригодился бы некоторый опыт программирования. А если его нет, подойдет житейский опыт. Действительно, если бы вы мастерили будильник-секундомер из шестеренок, что бы в нем от чего зависело? Ну, ясно, что будильник не сработал бы без часов, ведь он должен чувствовать, когда время на циферблатах часов и будильника совпадает. Значит, будильник от часов зависит. А вот часы ходят и без будильника, значит они от него не зависят. Секундомер же, видимо, представляет собой полностью независимую часть со своим механизмом.
Итак, проект распался на две независимые части:
Часы с будильником
Секундомер
Какой частью заняться вначале? Дело вкуса. Часы с будильником попроще, поэтому начнем с них. Ну а между ними что выбрать вначале – часы или будильник? Здесь сомнения неуместны –
раньше нужно делать ту часть, которая не зависит от других частей
– это часы.
Итак, мы разделили проект на части и определили порядок выполнения частей:
1.Часы
2.Будильник
3.Секундомер
Беремся за часы. И тут опять пошла дележка. Чем раньше заниматься – временем суток (будем называть это просто часами), датой или днем недели? Шестеренки смутно подсказывают нам, что дата переключается в полночь, а значит момент переключения зависит от часов. Значит дату будем делать позже часов. А день недели, ясно, определяется датой.
Итак, окончательная последовательность такая:
1.Часы (время суток)
2.Дата
3.День недели
4.Будильник
5.Секундомер
Лирическое отступление (утешение): Если вам не удалось разделить ваш проект на части, или вы чувствуете, что разделили неправильно, это не значит, что нужно от проекта отказываться. Программируйте напропалую! В процессе программирования отдельные части постепенно (не без мучений и многократных досадных переделок) встанут на свои места.
Ну что ж, «Задачи ясны, цели определены, за работу, товарищи!», как говорили при социализме.
Делаем часы
Три рамки. Создаем новый проект. Первым делом нужно придумать, из каких элементов управления мы будем его строить. Поглядев на Рис. 13.6, мы видим, что все элементы управления, какими бы они ни были, пространственно и по смыслу разделены на три четкие группы: часы, будильник, секундомер. Напрашиваются три рамки (GroupBox). Поместим их на форму и дадим имена:
Часы
Будильник
Секундомер
Для красоты покрасьте рамки, придав значения их свойству BackColor. На рисунке вы видите, что кроме этого вокруг рамок градиентной кистью нарисован бордюр. Но этим мы займемся потом. И вообще, если вы хотите в смысле графики сделать что-нибудь выдающееся, и украсить часы необыкновенными шедеврами, то на здоровье, пожалуйста!. Но не сейчас. Потом. Когда все заработает. Иначе потонете в подробностях и не откопаете в золотых лепесточках нужную вам стальную шестеренку.
Циферблат часов. Берем элемент управления метку и размещаем в рамке Часы. Это наш циферблат часов. В режиме проектирования я придал ему белый цвет, большой красивый шрифт и выровнял текст по центру.
Дадим метке имя Циферблат_часов. Еще раз предупреждаю: не надо сокращенных имен типа Циф_час. Сэкономив сейчас копейку, вы потом потеряете рубль.
Время на циферблате. Пришла пора программировать. Прежде всего нужна идея, как сделать так, чтобы часы показывали текущее время. Мы знаем, что есть функция Now, значение которой в любой момент равно текущей дате и времени суток. Стоит выполнить оператор
Циферблат_часов.Text = Format(Now, "HH:mm:ss")
и на часах появится текущее время суток. Попробуйте. Для этого временно создайте кнопку и поместите этот оператор в процедуру обработки щелчка по этой кнопке. Щелкайте по этой кнопке время от времени.
Получилось? Прекрасно! Но как сделать, чтобы время на циферблате менялось само? Нужно, чтобы этот оператор выполнялся не реже раза в секунду. Может быть, использовать оператор цикла? Но я должен вас предупредить, что на этом пути вас ждут разнообразные трудности, в суть которых не так-то легко вникнуть. Назову только одну. Вот, предположим, у вас заработал оператор цикла для часов. Тут вам захотелось включить секундомер. Значит, нужно, чтобы заработал другой оператор цикла – для секундомера. Значит, нужно выходить из цикла для часов. Так что же – часам останавливаться? Более глубоко я вникну в эту проблему в 27.3, а сейчас идем дальше.
Ввиду вышеизложенного программисты для таких дел используют таймеры. В нашем случае удобно использовать два таймера: для часов свой, для секундомера свой.
Выбросим кнопку и сотрем ее процедуру. Поместим на форму таймер и дадим ему имя Таймер_часов. Зададим ему пока интервал = 2500. Напишем такую программу:
Private Sub Таймер_часов_Tick(ByVal sender As Object, ByVal e As EventArgs) Handles Таймер_часов.Tick
Циферблат_часов.Text = Format(Now, "HH:mm:ss")
End Sub
Запустите проект. Вы увидите, что часы показывают правильное время, но обновляется оно раз в две с половиной секунды. Зададим интервал = 100. В этом случае процедура будет выполняться около 10 раз в секунду и не пропустит момента смены системного времени. Проверьте.
Ну что, все! Часы готовы.
Занимаемся датой
Занимаемся датой. Размещаем в рамке метку для даты. Даем метке имя Циферблат_даты. Чтобы там появилась дата в нужном нам виде, достаточно выполнить оператор
Циферблат_даты.Text = Format(Now, "Long Date")
Если его поместить в ту же процедуру таймера часов, то задача будет решена. Но мне жаль компьютер. 24 часа в сутки по 10 раз в секунду он будет спрашивать у Windows, какое нынче число, и выводить в текстовое поле одну и ту же дату, хотя делать это нужно только два раза – при запуске проекта и в полночь. Здесь еще и вопрос экономии: бегая, как белка в колесе, компьютер тратит ресурсы (силы), нужные в это же время для какого-нибудь другого дела, для того же секундомера хотя бы. Давайте облегчим жизнь компьютеру. Вспоминаем: при запуске проекта вырабатывается событие Form1_Load, а полночь – это когда показания часов равны нулю. Ага. Дописываем программу:
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
'Это чтобы дата появлялась на циферблате при запуске проекта:
Циферблат_даты.Text = Format(Now, "Long Date")
End Sub
Private Sub Таймер_часов_Tick(ByVal sender As Object, ByVal e As EventArgs) Handles Таймер_часов.Tick
Циферблат_часов.Text = Format(Now, "HH:mm:ss")
'Это чтобы дата менялась в полночь:
If Format(Now, "HH:mm:ss") = "00:00:00" Then Циферблат_даты.Text = Format(Now, "Long Date")
End Sub
Чтобы проверить, как работает проект, переставьте системные часы Windows на "Двенадцать без пяти" и подождите «полночи». Только потом не забудьте переставить обратно.
Наводим правильный стиль. Все работает, но мы начинаем допускать погрешности против правильного стиля программирования, которые в будущем могут выйти нам боком. Они и сейчас уже выходят. Придирчивый читатель даже видит, в каком месте. Действительно, никакой особой экономии ресурсов мы не добились, два раза подряд в одной процедуре Таймер_часов_Tick опрашивая компьютер функцией Format(Now, "HH:mm:ss"). А вот и два лекарства:
Первое. Показания часов напрашиваются быть переменной величиной. Ведь они нам еще понадобятся и для будильника. Мы их анализируем и еще будем анализировать, а это лучше делать с переменной величиной, а не со свойством Циферблат_часов.Text или функцией Format(Now, "HH:mm:ss"). Поэтому придумаем переменную Время_на_часах, объявим ее, как имеющую тип String и будем пользоваться только ей.
Второе. Как при запуске проекта, так и в полночь нам придется менять не только дату, но и день недели. Я предвижу повторяющийся фрагмент как минимум из двух операторов (пока это только один оператор Циферблат_даты.Text = Format(Now, "Long Date")). Поэтому оформим его, как процедуру с именем Смена_даты_и_дня_недели.
С учетом вышесказанного перепишем программу:
Dim Время_на_часах As String
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Смена_даты_и_дня_недели()
End Sub
Private Sub Таймер_часов_Tick(ByVal sender As Object, ByVal e As EventArgs)Handles Таймер_часов.Tick
Время_на_часах = Format(Now, "HH:mm:ss")
Циферблат_часов.Text = Время_на_часах
If Время_на_часах = "00:00:00" Then Смена_даты_и_дня_недели()
End Sub
Sub Смена_даты_и_дня_недели()
Циферблат_даты.Text = Format(Now, "Long Date")
End Sub
В этот момент вы совершенно искренне и с большим чувством можете сказать: «Ну зачем были все эти усложнения? Ведь все и так работало!» В ответ на это я могу только отослать вас к началу раздела.
Занимаемся днем недели
Нам не хочется трудиться, размещая в рамке метку, а потом еще и настраивая ее для дня недели. Замечаем, что она такая же, как для даты. Скопируем ее, как это принято в Windows, щелкнув по ней правой клавишей мыши и выбрав в контекстном меню Copy, а затем щелкнув правой клавишей мыши в рамке Часы и выбрав в контекстном меню Paste.
Даем метке имя Циферблат_дня_недели. Чтобы там появился правильный день недели, нужно выполнить оператор
Циферблат_дня_недели.Text = Format(Now, "dddd")
Раз мы организовали процедуру Смена_даты_и_дня_недели, то нам нет нужды вспоминать, в каких местах проекта оператор должен менять день недели. Вставляем его в эту процедуру:
Sub Смена_даты_и_дня_недели()
Циферблат_даты.Text = Format(Now, "Long Date")
Циферблат_дня_недели.Text = Format(Now, "dddd")
End Sub
Итак, часы с датой и днем недели готовы.
Делаем будильник
Поместим в рамку с именем Будильник все объекты, необходимые для будильника:
Метку с именем Метка_будильника
Текстовое поле с именем Циферблат_будильника
Кнопку с именем Кнопка_включения_выключения_будильника
Кнопку с именем Кнопка_выключения_звонка
В циферблат занесем в режиме проектирования текст 12:00:00. Просто для того, «чтобы было». В качестве циферблата мы выбрали текстовое поле, а не метку. А это для того, чтобы иметь возможность в режиме работы вручную устанавливать время будильника.
Заставляем будильник звучать. Давайте пока забудем обо всех этих кнопках, метках и прочих подробностях, а возьмем быка за рога и заставим будильник при совпадении времени на часах и на будильнике исполнять какую-нибудь мелодию.
Вспомните, как мы занимались музыкой в «Плеере». Поместите на форму Windows Media Player и дайте ему имя Звонок. Сделайте его невидимым. Для настройки звонка достаточно трех операторов в процедуре Form1_Load:
Звонок.AutoStart = False 'Чтобы не запускался слишком рано
Звонок.PlayCount = 0 'Чтобы закончив играть, начинал снова
Звонок.FileName = "Mozart's Symphony No. 40.RMI"
Эта троица, несмотря на то, что встретится в программе скорее всего только один раз, в глазах программиста просится стать процедурой. Здесь принцип такой:
Если группа операторов (или даже один сложный оператор) представляет собой некое единое целое в том смысле, что решает какую-то свою отдельную задачу, то оформляйте ее процедурой.
Это улучшит понятность и читабельность программы – один из важнейших факторов ее качества.
Запустит воспроизведение мелодии оператор Звонок.Play(), но он должен выполниться только тогда, когда время на циферблате часов совпадет со временем на циферблате будильника.
Вот как дополнится теперь наш проект (я показываю только те процедуры, которых коснулись дополнения):
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Настройка_звонка()
Смена_даты_и_дня_недели()
End Sub
Private Sub Таймер_часов_Tick( ByVal sender As Object, ByVal e As EventArgs) Handles Таймер_часов.Tick
Время_на_часах = Format(Now, "HH:mm:ss")
Циферблат_часов.Text = Время_на_часах
If Время_на_часах = "00:00:00" Then Смена_даты_и_дня_недели()
If Время_на_часах = Циферблат_будильника.Text Then Звонок.Play()
End Sub
Sub Настройка_звонка()
Звонок.AutoStart = False 'Чтобы не запускался слишком рано
Звонок.PlayCount = 0 'Чтобы закончив играть, начинал снова
Звонок.FileName = "Mozart's Symphony No. 40.RMI"
End Sub
Запустите проект и установите время на циферблате будильника. Дождитесь, когда будильник зазвонит. Завершите работу проекта, не дожидаясь конца мелодии.
Два состояния будильника. Пора заняться кнопками и меткой. Подумаем вот о чем. Во время работы будильник в каждый момент времени может находиться в одном из двух состояний: «Установлен» или «Не установлен». Состояние «Установлен» означает, что вы хотите, чтобы будильник, когда подойдет его срок, звонил. Состояние «Не установлен» означает, что вы не хотите, чтобы будильник звонил, даже когда подойдет его срок. Компьютер должен в любой момент времени знать, в каком именно состоянии находится будильник, чтобы принять решение – звонить или не звонить. В таких случаях, когда компьютер должен в любой момент что-то знать или помнить, программист всегда придумывает переменную величину, дает ей подходящее имя и тип и организует хранение в этой переменной той самой информации, которую нужно помнить.
Придумаем переменную и дадим ей имя Будильник_установлен. Каким типом ее объявить? Для этого надо понять, а какие значения будет принимать эта переменная? По смыслу задачи значений два – «да» и «нет». Можно придать ей тип String, но хорошая практика программирования диктует тип Boolean. Вы скоро привыкнете к нему и поймете, что он удобнее.
Кнопки и метка будильника. Теперь попробуем вообразить, что должно происходить при включении будильника:
Нужно сообщить компьютеру, что будильник установлен
Нужно, чтобы текст над циферблатом был такой: «Будильник установлен на»
Нужно, чтобы текст на кнопке был такой: «Выключить будильник»
В соответствии с этим пишем процедуру:
Sub Включить_будильник()
Будильник_установлен = True
Метка_будильника.Text = "Будильник установлен на"
Кнопка_включения_выключения_будильника.Text = "Выключить будильник"
End Sub
Теперь попробуем сообразить, что должно происходить при выключении будильника:
Нужно сообщить компьютеру, что будильник не установлен
Нужно, чтобы текст над циферблатом был такой: «Будильник отключен»
Нужно, чтобы текст на кнопке был такой: «Включить будильник»
В соответствии с этим пишем процедуру:
Sub Выключить_будильник()
Будильник_установлен = False
Метка_будильника.Text = "Будильник отключен"
Кнопка_включения_выключения_будильника.Text = "Включить будильник"
End Sub
Теперь нам удивительно легко написать процедуру щелчка по кнопке включения-выключения будильника:
Private Sub Кнопка_включения_выключения_будильника_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Кнопка_включения_выключения_будильника.Click
If Будильник_установлен Then Выключить_будильник() Else Включить_будильник()
End Sub
Обратите внимание, что благодаря удачному выбору имен и структуры программы язык программы по своей понятности приближается к естественному, человеческому.
Напомню, что поскольку переменная Будильник_установлен имеет логическое значение True или False, ее можно в одиночку использовать в качестве условия оператора If. Поэтому совсем не обязательно было писать
If Будильник_установлен = True Then ….
Процедуру щелчка по кнопке выключения звонка приведу без пояснений:
Private Sub Кнопка_выключения_звонка_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Кнопка_выключения_звонка.Click
Звонок.Stop()
End Sub
В последних четырех процедурах заключена вся механика работы кнопок и метки. Теперь надо позаботиться о том, чтобы будильник звенел только тогда, когда включен. Для этого вместо оператора
If Время_на_часах = Циферблат_будильника.Text Then Звонок.Play()
мы пишем оператор
If Будильник_установлен And Время_на_часах = Циферблат_будильника.Text Then Звонок.Play()
Теперь нужно решить, должен ли будильник после запуска проекта быть включен или выключен. Можно решить и так и эдак, на работоспособность проекта это никак не повлияет. Я предпочитаю, чтобы он был выключен, поэтому пишу в процедуру Form1_Load оператор Выключить_будильник().
Программа часов с будильником целиком. Часы с будильником готовы. Приведу полностью их программу:
Public Class Form1
Inherits System.Windows.Forms.Form
Windows Form Designer generated code
Dim Время_на_часах As String
Dim Будильник_установлен As Boolean
Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs)Handles MyBase.Load
Настройка_звонка()
Выключить_будильник()
Смена_даты_и_дня_недели()
End Sub
Private Sub Таймер_часов_Tick(ByVal sender As Object, ByVal e As EventArgs) Handles Таймер_часов.Tick
Время_на_часах = Format(Now, "HH:mm:ss")
Циферблат_часов.Text = Время_на_часах
If Время_на_часах = "00:00:00" Then Смена_даты_и_дня_недели()
If Будильник_установлен And Время_на_часах = Циферблат_будильника.Text Then Звонок.Play()
End Sub
Sub Настройка_звонка()
Звонок.AutoStart = False
Звонок.PlayCount = 0
Звонок.FileName = "Mozart's Symphony No. 40.RMI"
End Sub
Sub Смена_даты_и_дня_недели()
Циферблат_даты.Text = Format(Now, "Long Date")
Циферблат_дня_недели.Text = Format(Now, "dddd")
End Sub
Sub Включить_будильник()
Будильник_установлен = True
Метка_будильника.Text = "Будильник установлен на:"
Кнопка_включения_выключения_будильника.Text = "Выключить будильник"
End Sub
Sub Выключить_будильник()
Будильник_установлен = False
Метка_будильника.Text = "Будильник отключен"
Кнопка_включения_выключения_будильника.Text = "Включить будильник"
End Sub
Private Sub Кнопка_включения_выключения_будильника_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Кнопка_включения_выключения_будильника.Click
If Будильник_установлен Then Выключить_будильник() Else Включить_будильник()
End Sub
Private Sub Кнопка_выключения_звонка_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Кнопка_выключения_звонка.Click
Звонок.Stop()
End Sub
End Class
Еще раз о стиле. Вы, наверное, заметили, что если после запуска проекта вы заставляли будильник звенеть несколько раз, то каждый раз он начинал мелодию не с начала, а с того места, где вы звонок выключили. Мне это все равно, но если вам это не нравится, то вы должны включать звонок не одним оператором
Звонок.Play()
а парой операторов:
Звонок. CurrentPosition = 0
Звонок.Play()
Но в этом случае вместо
If Будильник_установлен And Время_на_часах = Циферблат_будильника.Text Then Звонок.Play()
вам придется писать
If Будильник_установлен And Время_на_часах = Циферблат_будильника.Text Then
Звонок.CurrentPosition = 0
Звонок.Play()
End If
Вы видите, что оператор включения звонка теряет прозрачность и краткость. Выход один: оформлять эту пару операторов, как процедуру Включить_звонок. Тогда можно будет написать
If Будильник_установлен And Время_на_часах = Циферблат_будильника.Text Then Включить_звонок()
Вот теперь оператор включения звонка приобрел элегантность и максимальную понятность. Теперь никто не обвинит нас в корявости стиля. К тому же, если нам в будущем захочется как-то улучшить включение звонка, то все изменения мы сможем делать внутри процедуры Включить_звонок, не трогая сам оператор.
Делаем секундомер
Размести в проекте второй таймер, дав ему имя Таймер_секундомера. Поместим в рамку с именем Секундомер элементы управления для секундомера и дадим им имена:
Циферблат_секундомера (это метка)
Кнопка_пуска_паузы_секундомера
Кнопка_обнуления_секундомера
а также не забудем метку с текстом «Секундомер».
Таймер нужен секундомеру для того же, для чего и часам, а именно – чтобы вовремя менялись цифры на циферблате, а собственный
таймер предпочтительно (но в нашем случае не обязательно) иметь для того, чтобы не зависеть от часов. Поскольку нам нужно отслеживать сотые доли секунды, установим ему интервал меньше 10. Когда секундомер считает, таймер секундомера должен работать:
Таймер_секундомера.Enabled = True
а когда он в паузе или в нуле, таймер может и отдохнуть:
Таймер_секундомера.Enabled = False
Режимы работы секундомера. Если будильник в каждый момент времени может находиться в одном из двух состояний (режимов) – установлен или не установлен, то секундомер – в трех: считает, стоит в паузе, стоит в нуле. Придумаем переменную и дадим ей имя Режим _секундомера. Объявить ее типом Boolean явно недостаточно, ведь в типе Boolean всего два возможных значения, а нам нужно три. Здесь напрашивается перечисление:
Enum Режим
считает
пауза
в_нуле
End Enum
Dim Режим_секундомера As Режим
Для каждого режима нам придется организовать свою переменную для отсчета секунд на секундомере:
Dim Секунды_на_секундомере As Double
Dim Секунды_при_запуске_секундомера As Double
Dim Секунды_на_паузе_секундомера As Double
Обратите внимание, что эти переменные, хоть и имеют смысл времени, объявлены, как дробные числовые, а не как Date. Почему так получилось и почему переменных три, а не одна, сейчас попытаюсь пояснить.
Для отсчета времени на секундомере мы будем использовать известную вам функцию
Timer класса DateAndTime (не путайте с элементом управления Timer), потому что она выдает секунды с дробной частью (нас устроят сотые доли секунды – какой же секундомер без сотых долей). Поскольку она выдает число секунд, прошедших с полуночи, а нам нужно число секунд, прошедших с момента запуска секундомера, да еще с учетом того, что во время паузы на секундомере уже стояли какие-то показания, нам придется поразмыслить, как это сделать.
Засечем в момент пуска секундомера значение функции Timer оператором
Секунды_при_запуске_секундомера = DateAndTime.Timer
Тогда в каждый момент времени после запуска секундомера выражение
DateAndTime.Timer - Секунды_при_запуске_секундомера
как раз и будет равняться числу секунд, прошедших с момента запуска секундомера. А если нам удастся правильно засечь Секунды_на_паузе_секундомера (имеющие смысл секунд, прошедших с момента пуска до паузы), то дело решит оператор
Секунды_на_секундомере = DateAndTime.Timer - Секунды_при_запуске_секундомера _
+ Секунды_на_паузе_секундомера
Если поместить его в процедуру таймера, то он будет оперативно выдавать нужные для циферблата секунды.
Внешний вид показаний секундомера. Переменная Секунды_на_секундомере – это дробное число типа Double. Например, такое – 65.28194. Но на секундомере эти самые цифры, сами понимаете, должны выглядеть по-другому – 00:01:05.28. Для этого нам нужно как-то преобразовать число секунд в стандартный формат времени. Организуем переменную и константу:
Dim Время_на_секундомере As Date
Const Полночь As Date = #12:00:00 AM#
Здесь время #12:00:00 AM# обозначает, как ни странно, полночь по-американски.
Дело решает пара операторов:
Время_на_секундомере = Полночь.AddSeconds(Секунды_на_секундомере)
Циферблат_секундомера.Text = Format(Время_на_секундомере, "mm:ss.ff")
Кнопки секундомера. Теперь нам нужно подумать о том, что должно происходить во время нажатия на кнопки секундомера. Наши размышления должны течь примерно в том же русле, что и размышления о кнопках и метке будильника. Никакой новой идеологии по сравнению с будильником вы не обнаружите. По аналогии с будильником вы, наверное, должны придти к выводу о необходимости создания трех процедур:
Sub Секундомер_обнулить()
Sub Секундомер_остановить()
Sub Секундомер_запустить()
Я думаю, эти размышления вам полезно будет довести до конца самостоятельно. А проверить себя вы сможете, заглянув в окончательный текст программы ниже. Все процедуры, касающиеся будильника собраны вместе и вы их легко найдете.
Еще о режиме. Самые дотошные из вас, разобравшись в программе, скажут мне, что нечего было огород городить – создавать перечисление, когда можно было обойтись логической переменной Секундомер_считает. Верно. Но неправильно. Потому что нужно оставлять простор для дальнейшего развития проекта. Например, вы можете в будущем захотеть, чтобы во время паузы цифры на секундомере мигали, а в нуле – нет.
Рисуем бордюры вокруг рамок
Взгляните еще раз на рисунок. Три рамки – три бордюра. Но это не бордюры. Просто я нарисовал на форме три залитых градиентной кистью прямоугольника. Каждый – за своей рамкой, чуть побольше ее по размерам. Рамки непрозрачные и поэтому от прямоугольников мы видим только их края.
Поскольку бордюров три, я создал процедуру Бордюр_вокруг, которая рисует бордюр вокруг любого элемента управления. Вот ее заголовок:
Sub Бордюр_вокруг(ByVal Объект As Control, ByVal Цвет As Color, ByVal Толщина As Single)
а тело ее вы можете видеть ниже, в полном тексте программы. В процедуре три параметра. Параметр Объект имеет тип Control. Это как раз тот самый любой элемент управления. Параметр Толщина – толщина бордюра. Параметр Цвет определяет первый из двух цветов градиентной кисти. Когда вы разберетесь в тексте процедуры, вы увидите, что я мог бы включить в число параметров и второй цвет, и координаты точек, определяющих градиент. Но я не стал этого делать, чтобы не затемнять изложение. А вы вполне можете попробовать.
Чтобы бордюры не стирались при случайном загораживании наших часов другими окнами, я включил обращения к процедуре Бордюр_вокруг в процедуру Form1_Paint. Получилось очень изящно и прозрачно:
Private Sub Form1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) _
Handles MyBase.Paint
Бордюр_вокруг(Часы, Color.Blue, 20)
Бордюр_вокруг(Будильник, Color.Red, 20)
Бордюр_вокруг(Секундомер, Color.Green, 20)
End Sub
Косая граница градиента для всех трех бордюров получилась единой потому, что координаты точек, определяющих градиент, едины для всех трех бордюров.
Полный текст программы «Будильник-секундомер»
Привожу полный текст программы «Будильник-секундомер». В том, что касается будильника, я добавил упоминавшуюся мной процедуру Включить_звонок, причем включил в нее для привлечения внимания пользователя разворачивание будильника на весь экран. Везде, где можно, я объявления переменных перенес из верхней части окна кода внутрь процедур.
Public Class Form1
Inherits System.Windows.Forms.Form
Windows Form Designer generated code
Enum Режим
считает
пауза
в_нуле
End Enum
Dim Режим_секундомера As Режим
Dim Будильник_установлен As Boolean
Dim Секунды_на_секундомере As Double
Dim Секунды_при_запуске_секундомера As Double
Dim Секунды_на_паузе_секундомера As Double
'НАЧАЛЬНАЯ УСТАНОВКА МЕХАНИЗМА
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Настройка_звонка()
Выключить_будильник()
Смена_даты_и_дня_недели()
Секундомер_обнулить()
End Sub
'ПРОЦЕДУРЫ РАБОТЫ ЧАСОВ И БУДИЛЬНИКА
Private Sub Таймер_часов_Tick(ByVal sender As Object, ByVal e As EventArgs) Handles Таймер_часов.Tick
Dim Время_на_часах As String
Время_на_часах = Format(Now, "HH:mm:ss")
Циферблат_часов.Text = Время_на_часах
If Время_на_часах = "00:00:00" Then Смена_даты_и_дня_недели()
If Будильник_установлен And Время_на_часах = Циферблат_будильника.Text Then Включить_звонок()
End Sub
Sub Смена_даты_и_дня_недели()
Циферблат_даты.Text = Format(Now, "Long Date")
Циферблат_дня_недели.Text = Format(Now, "dddd")
End Sub
Private Sub Кнопка_включения_выключения_будильника_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Кнопка_включения_выключения_будильника.Click
If Будильник_установлен Then Выключить_будильник() Else Включить_будильник()
End Sub
Sub Включить_будильник()
Будильник_установлен = True
Метка_будильника.Text = "Будильник установлен на"
Кнопка_включения_выключения_будильника.Text = "Выключить будильник"
End Sub
Sub Выключить_будильник()
Будильник_установлен = False
Метка_будильника.Text = "Будильник отключен"
Кнопка_включения_выключения_будильника.Text = "Включить будильник"
End Sub
Sub Настройка_звонка()
Звонок.AutoStart = False
Звонок.PlayCount = 0
Звонок.FileName = "Mozart's Symphony No. 40.RMI"
End Sub
Sub Включить_звонок()
Me.WindowState = FormWindowState.Maximized
Звонок.CurrentPosition = 0
Звонок.Play()
End Sub
Private Sub Кнопка_выключения_звонка_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Кнопка_выключения_звонка.Click
Звонок.Stop()
End Sub
'ПРОЦЕДУРЫ РАБОТЫ СЕКУНДОМЕРА
Private Sub Таймер_секундомера_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) _
Handles Таймер_секундомера.Tick
Dim Время_на_секундомере As Date
Const Полночь As Date = #12:00:00 AM#
Секунды_на_секундомере = DateAndTime.Timer - Секунды_при_запуске_секундомера + _
Секунды_на_паузе_секундомера
Время_на_секундомере = Полночь.AddSeconds(Секунды_на_секундомере)
Циферблат_секундомера.Text = Format(Время_на_секундомере, "mm:ss.ff")
End Sub
Private Sub Кнопка_пуска_паузы_секундомера_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Кнопка_пуска_паузы_секундомера.Click
If Режим_секундомера <> Режим_секундомера.считает Then Секундомер_запустить() _
Else Секундомер_остановить()
End Sub
Sub Секундомер_запустить()
Секунды_при_запуске_секундомера = DateAndTime.Timer
Режим_секундомера = Режим.считает
Таймер_секундомера.Enabled = True
Кнопка_пуска_паузы_секундомера.Text = "ПАУЗА"
End Sub
Sub Секундомер_остановить()
Секунды_на_паузе_секундомера = Секунды_на_секундомере
Режим_секундомера = Режим.пауза
Таймер_секундомера.Enabled = False
Кнопка_пуска_паузы_секундомера.Text = "ПУСК"
End Sub
Private Sub Кнопка_обнуления_секундомера_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Кнопка_обнуления_секундомера.Click
Секундомер_обнулить()
End Sub
Sub Секундомер_обнулить()
Секунды_на_паузе_секундомера = 0
Циферблат_секундомера.Text = "00:00.00"
Режим_секундомера = Режим.в_нуле
Таймер_секундомера.Enabled = False
Кнопка_пуска_паузы_секундомера.Text = "ПУСК"
End Sub
'ПРОЦЕДУРЫ ПОСТРОЕНИЯ БОРДЮРОВ
Private Sub Form1_Paint( ByVal sender As Object, ByVal e As PaintEventArgs) Handles MyBase.Paint
Бордюр_вокруг(Часы, Color.Blue, 20)
Бордюр_вокруг(Будильник, Color.Red, 20)
Бордюр_вокруг(Секундомер, Color.Green, 20)
End Sub
Sub Бордюр_вокруг(ByVal Объект As Control, ByVal Цвет As Color, ByVal Толщина As Single)
Dim Гр As Graphics = Me.CreateGraphics
Dim Т1 As New Point(0, 0)
Dim Т2 As New Point(430, 430)
Dim Кисть_град As New System.Drawing.Drawing2D.LinearGradientBrush(Т1, Т2, Цвет, Color.Yellow)
'Бордюр - залитый градиентом прямоугольник:
Гр.FillRectangle(Кисть_град, Объект.Left - Толщина, Объект.Top - Толщина, _
Объект.Width + 2 * Толщина, Объект.Height + 2 * Толщина)
'Черный ободок вокруг бордюра:
Гр.DrawRectangle(Pens.Black, Объект.Left - Толщина, Объект.Top - Толщина, _
Объект.Width + 2 * Толщина, Объект.Height + 2 * Толщина)
End Sub
End Class
Недоработки проекта
Будильник готов. Вы можете установить на нем время звонка и свернуть окно, чтобы он не мешал работать. Продолжайте делать на компьютере другие дела. В свернутом виде будильник не спит. В нужный момент вы услышите звонок, и будильник, чтобы привлечь ваше внимание, развернется на весь экран.
Проект работает на первый взгляд нормально, я многократно проверял и часы, и будильник, и секундомер. Но несмотря на это, в нем вполне возможны ошибки и недоработки. Вот те ошибки и недоработки, которые я сумел заметить, но не стал исправлять, предоставив это вам:
На протяжение той секунды, когда время на часах и будильнике одинаково, процедура таймера выполняется с десяток раз, а значит и сигнал будильника запускается с десяток раз. Вы слышите, что мелодия в первую секунду получается смазанной. Это нехорошо.
На циферблате вы видите «11 Сентябрь 2003 г.». А хотелось бы «11 сентября 2003 года». Это касается всех 12 месяцев.
Хорошо бы иметь меню со сменой мелодий, цветов, шрифтов и т.п.
Когда вы вручную устанавливаете время на циферблате будильника, то есть вводите текст в текстовое поле, вы должны быть очень внимательны, чтобы не нарушить формат строки: цифра-цифра-двоеточие-цифра-цифра-двоеточие-цифра-цифра. Например, нельзя вводить 8:30:00 вместо 08:30:00. И никаких пробелов! Иначе будильник не сработает. Происходит это потому, что программа сравнивает строку на циферблате часов со строкой на циферблате будильника. На циферблате же часов время имеет формат HH:mm:ss. В чем состоит проблема для программиста? Проблема в том, что пользователь не любит напрягаться и быть внимательным. Программист должен холить и лелеять пользователя, он должен сделать так, чтобы будильник «съел» все, что пользователь введет в циферблат, и тем не менее сработал правильно. В разумных пределах, конечно. Или же будильник просто не должен воспринимать неправильные вводимые символы, тогда пользователь волей-неволей введет все правильно. И то, и другое вы сможете делать позже.
Я не запускал секундомер перед полуночью и не думал над тем, как это может повлиять на правильность его работы.
Таймер и моделирование
Теперь о роли таймера в компьютерном конструировании реальных механизмов. Заглянем-ка еще раз в процедуру таймера часов:
Private Sub Таймер_часов_Tick(ByVal sender As Object, ByVal e As EventArgs) Handles Таймер_часов.Tick
Dim Время_на_часах As String
Время_на_часах = Format(Now, "HH:mm:ss")
Циферблат_часов.Text = Время_на_часах
If Время_на_часах = "00:00:00" Then Смена_даты_и_дня_недели()
If Будильник_установлен And Время_на_часах = Циферблат_будильника.Text Then Включить_звонок()
End Sub
Вы видите, что эта процедура является главным мотором, приводящим в движение весь механизм часов с будильником, главным штабом, планирующим всю их работу. Таймер, как сердце, несколько раз в секунду посылает импульсы, каждый из которых заставляет выполнится эту процедуру. А что за операторы составляют тело процедуры? Это как раз главные операторы проекта, которые, выполняясь, в свою очередь управляют выполнением вспомогательных процедур Смена_даты_и_дня_недели и Включить_звонок. За один импульс таймера механизм совершает весь цикл своего функционирования с начала до конца и выполняет всю работу, которую положено выполнить: время на часах обновлено; если подошел момент, то именно на этом импульсе заменены дата и день недели; и если подошел момент, то именно на этом же импульсе отдан приказ на включение звонка будильника. На следующем импульсе все повторяется. И так беспрерывно, бесконечно.
Этот принцип применения таймера подойдет для компьютерного конструирования бесчисленного множества других механизмов и аппаратов: холодильника, синтезатора, автоматической метеостанции, беспилотного космического аппарата и пр. и пр. Нужно только знать принцип их работы – и вы сможете создать проект, «вживую» показывающий их деятельность. Это называется моделированием, а сама программа называется моделью.
Вот мы с вами создали модель будильника, которая настолько хороша, что работает не хуже оригинала. Однако, мы не копировали работу реальных
шестеренок из бабушкиных ходиков, нам это было не нужно. Таким образом, надо понимать, что мы создали модель только внешнего, а не внутреннего поведения механического будильника. Внутреннее поведение у нашего будильника совсем другое, мы попросту брали готовое время у Windows, и все. Чтобы смоделировать внутреннюю работу реальных механических часов с учетом движения и зацепления каждой шестеренки, нужен гораздо более сложный проект. Спрашивается, кому он нужен? Наверное, тем, кто конструирует часы для любителей старины.
Моделировать можно и работу механизмов, управляемых человеком: автомобиля, корабля, самолета и др. В этом случае роль человека будете исполнять вы, щелкая мышкой по созданным вами кнопкам и рычагам управления. Кстати, вы только что создали такой проект. Ведь будильник – это тоже механизм, работающий под управлением человека, щелкающего по кнопкам будильника.
Вы можете пойти дальше и смоделировать на компьютере поведение человека. Тогда вам не придется самому нажимать на кнопки и рычаги, это будет делать человек на экране.
Задание 81.
«Время в разных странах».Это задание – на любителя. Его делать не нужно, если вы собираетесь выполнить следующее Задание 97 «Шахматные часы».
Усовершенствуйте часы. Пусть они по вашему желанию показывают время в любом из нескольких десятков городов мира. Пользователю достаточно ввести название города в текстовое поле. Для программирования вам нужно самим знать поясное время в других городах. Для этого можете сделать двойной щелчок мышкой по индикатору времени на панели задач Windows и в открывшемся окне загляните в список Time Zone. Основой программы можете сделать большой оператор Select Case. Будильник же не должен обращать внимание на чужое время на циферблате, он все равно должен звонить по нашему местному времени.
Задание 82.
«Шахматные часы». Это задание большое, но его нужно сделать обязательно. Это будет ваш первый опыт создания настоящего проекта.
По шахматным правилам шахматист не может думать над ходом бесконечно. На обдумывание первых 40 ходов ему дается, скажем, суммарное время 2,5 часа. При этом все равно, сколько времени он обдумывает каждый ход. Таким образом, партия, дошедшая до 41 хода, не может продолжаться дольше 5 часов. На следующие ходы тоже отводится какой-то лимит времени. Чтобы шахматист мог следить за тем, сколько времени осталось на обдумывание ему и противнику, существуют специальные шахматные часы. Они представляют собой единый корпус с двумя циферблатами – счетчиками времени, двумя кнопками и двумя флажками. Перед началом партии шахматистов А и В каждый счетчик показывает 2,5 часа. Пусть шахматисту А выпало начинать. Как только партия начинается, судья запускает счетчик А, который начинает обратный отсчет времени, уменьшая свои показания. Таким образом, в каждый момент партии счетчик показывает, сколько времени осталось шахматисту на обдумывание. Пока работает счетчик А, счетчик В, естественно, стоит. Как только шахматист А сделал ход, он тут же нажимает кнопку А, которая останавливает его счетчик и запускает счетчик В. За обдумывание принимается шахматист В. Сделав ход, он нажимает кнопку В, которая останавливает его счетчик и запускает счетчик А. И так далее. Если шахматист просрочит время, его флажок падает – он проиграл.
Для удобства шахматистов добавьте к часам счетчик ходов.
Проект «Будильник-секундомер»
Вы узнали о VB вполне достаточно, чтобы рваться в бой. Наверняка у вас в голове зреет идея создать некий элегантный и одновременно вполне «убойный» проект строчек на 200, способный поразить в самое сердце ваших друзей и, что не менее важно, подруг. Однако, спешу вам сказать, что вы торопитесь. Ибо! Друзья и подруги может быть и понесут вас на руках до ближайшего кафе-мороженого, но человек, мало-мальски разбирающийся в программировании, бросив скучающий взгляд на ваше жалкое детище, спросит: «Юноша, в каком году вы заканчивали церковно-приходскую школу?» И не потому, что проект работает плохо, он вполне может проделывать на экране что-нибудь любопытное. Но программа! Программа! Она не выдерживает никакой критики. В ней невозможно разобраться! 200 строк вы осилили, а сколько вы возились? Неделю? А веди могли все сделать за день! Сколько у вас процедур? 8? А нужно было 40! И так далее. Когда придет пора делать проект из 1000 строк, вы не сможете его одолеть никогда! Так что не воображайте, что любая программа, которая выдает правильный результат – это хорошая программа.
Что же делать? Все очень просто. Вы ни в чем не виноваты. Ведь я пока только провозгласил вам, что программу надо писать правильно. Например, писать много маленьких процедур. Но ведь не научил
еще, как это делать. А учиться надо на примерах.
Пришла пора создать проект, «правильно» составленный из процедур. Проекты «Калькулятор» и «Плеер», написанный нами ранее, не подходит в качестве учебного пособия, потому что логика их работы слишком проста: нажал кнопку – выполнилась соответствующая процедура и все. Проект «Парк под луной» хорош, но он все-таки учебный и поэтому тоже упрощенный. Нужна более реальная задача. И «Будильник-секундомер» в этом смысле подходит идеально.
Для ее решения мы создадим небольшую программу примерно из 100 строк, включающую 17 процедур. Поскольку создаваемая программа представляет для многих из вас неведомую страну, я буду применять метод изложения «за ручку», который уже применял в 1.3. Повторяйте за мной все, что я буду делать.
Начнем с постановки задачи.
Суть анимации
Суть анимации лучше всего объяснять, используя методы рисования.
В начале Глава 8. я объяснил идею создания иллюзии движения картинок по экрану. Там же мы двигали по форме объекты – элементы управления. Попробуем заставить двигаться по экрану не объекты, а нарисованные нами геометрические фигуры. Пусть слева направо должна двигаться окружность. Для этого мы должны сначала нарисовать ее слева и быстро стереть, для чего нарисовать ее на том же месте, но цветом фона. Несмотря на то, что мы окружность быстро стерли, она успеет мелькнуть на экране, и глаз это заметит. Затем нужно нарисовать и стереть такую же окружность чуть правее, затем еще правее и т.д.
Ввиду причин, упомянутых в 13.5.3, откажемся от операторов цикла. Будем использовать таймеры. Создадим проект. Поместим в него таймер. Установим его интервал в 30. Вот программа:
Dim x As Integer = 100 'Координаты окружности
Dim y As Integer = 150
Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick
Dim Граф As Graphics = Me.CreateGraphics
Dim Черное_перо As Pen = New Pen(Color.Black, 5)
Dim Перо_цвета_фона As Pen = New Pen(Me.BackColor, 5)
Dim i As Integer
Граф.DrawEllipse(Черное_перо, x, y, 20, 20) 'Рисуем окружность
For i = 1 To 5000000 : Next 'Пустой цикл для задания паузы
Граф.DrawEllipse(Перо_цвета_фона, x, y, 20, 20) 'Стираем окружность
x = x + 1 'Перемещаемся немного направо
End Sub
Пояснения: Когда вы попробуете выполнить эту программу на компьютере, изображение движущейся окружности может получиться некачественным – окружность в процессе движения будет мерцать и пульсировать. Это связано с разверткой электронно-лучевой трубки вашего монитора. Если создать маленькую паузу между рисованием и стиранием окружности, нежелательные эффекты уменьшатся. Пауза нужна для того, чтобы окружность не слишком быстро исчезала с экрана. Эту паузу я создаю пустым циклом. Поэкспериментируйте с диаметром, толщиной окружности, продолжительностью паузы или шагом движения по горизонтали. Последние две величины и интервал таймера определяют скорость движения.
Задание 83.
Пусть по экрану движется «вагон» – прямоугольник и два кружочка.
Движем объекты
Мы больше не будем заниматься анимацией при помощи графических методов, потому что элементы управления двигать гораздо приятнее. Чем самим стараться создавать иллюзию движения окружности, рисуя и стирая ее, лучше нарисовать ее один раз на каком-нибудь элементе управления и затем двигать сам элемент.
Поместим на форму кнопку, объект PictureBox и таймер. Вот программа:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim Граф As Graphics = PictureBox1.CreateGraphics
Dim Черное_перо As Pen = New Pen(Color.Black, 5)
Граф.DrawEllipse(Черное_перо, 3, 3, 20, 20) 'Рисуем один раз окружность
Timer1.Enabled = True 'Включаем движение
End Sub
Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick
PictureBox1.Left = PictureBox1.Left + 1 'Перемещаем PictureBox немного направо
End Sub
Пояснения: На объекте PictureBox1 по нажатию на кнопку рисуется наша окружность и включается таймер, движущий PictureBox1 направо. Обратите внимание, что в отличие от предыдущей программы окружность движется спокойно, не мерцая. Дело в том, что VB создает иллюзию движения гораздо искуснее, чем это умеем делать мы с вами. А ведь тоже рисует и стирает.
Вместо нарисованной примитивной окружности мы могли бы придать элементу PictureBox1 какую-нибудь картинку или фото (присвоить свойству Image или же нарисовать на поверхности PictureBox1 методом DrawImage).. И тогда бы двигалась вся картинка.
Задание 84.
Нарисуйте в графическом редакторе Paint два самолетика на белом фоне и сохраните их изображения. Придайте форме и двум объектам PictureBox на ней белый цвет. Поместите самолетики на объекты PictureBox. Пусть они летят один за другим справа налево.
Задание 85.
Пусть пять кнопок движутся одновременно в разных направлениях: вправо, влево, вверх, вниз, наискосок.
Отскок от края формы. Заставим какой-нибудь объект двигаться направо, а затем самостоятельно отскочить от правого края формы:
Dim Шаг As Integer = 2
Dim x As Integer = 0
Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick
Button1.Left = x
x = x + Шаг
If x > Width Then Шаг = -2 'Если объект улетел за правый край формы, то лететь обратно
End Sub
Пояснения: Я написал Width, а не Me.Width, но VB не пожаловался, так как считает, что если «хозяин» свойства не указан, то в данном случае «хозяином» является форма.
Задание 86.
Заставьте объект бесконечно двигаться, отскакивая от правого и левого краев формы.
Задание 87.
«Биллиардный шар». Нарисуйте «биллиардный стол» – большой прямоугольник. Шар под углом летает по столу, отскакивая от его краев по закону отражения (Рис. 13.7). Попав в «лузу» (любой из четырех углов стола), он останавливается. Объектом здесь удобно взять маленький PictureBox с загруженной иконкой в виде шарика (подходящие иконки есть в папке VS).
Рис. 13.7
Указание: В переводе на компьютерный язык слова «по закону отражения» означают вот что. Обозначим dx шаг по горизонтали, dy – по вертикали. Если они оба отличаются от нуля, шарик летит наискосок Удаpившись о левый или пpавый боpт, шаpик меняет гоpизонтальную составляющую скоpости на пpотивоположную: dx = -dx. Аналогично, удаpившись о верхний или нижний боpт, шаpик меняет вертикальную составляющую скоpости на пpотивоположную: dy = -dy.
Задание 88.
«Часы со стрелками». Если вы в ладах с градусной мерой угла, сделайте часы со стрелками: часовой, минутной, секундной. Задача упростится, если вы выберете в качестве стрелок тоненькие сектора окружностей.
Задание 89.
Изобразите полет камня, брошенного с башни, для Задание 47. Камнем может служить PictureBox с подходящей загруженной иконкой. Необходимо, чтобы время полета равнялось реальному времени полета камня.
Задание 90.
Сделайте игру: Пушка на экране стреляет в цель ядрами. С какого выстрела она поразит противника? Между пушкой и целью расположена небольшая гора. Перед началом игры случайно задается горизонтальная координата цели. Затем рисуется картинка (Рис. 13.8).
Рис. 13.8
Перед каждым выстрелом компьютер отображает в текстовом поле номер выстрела и запрашивает у человека стартовую скорость ядра v и угол a наклона ствола пушки к земле. Затем летит ядро. Полет ядра подчиняется двум уравнениям: s=v*t*cosa и h=v*t*sina – 9.81*t2/2 (см. предыдущее задание). Считается, что цель поражена, если ядро ее коснулось, не коснувшись горы. Вы можете запрограммировать автоматическое определение попадания в цель. Указание: Для этого нужно в момент, когда ядро при падении пересекло уровень земли, сравнить горизонтальные координаты ядра и цели. Если они достаточно близки, то фиксируйте попадание. Определение прикосновения к горе – чуть более хлопотное занятие, но идея та же.
Движем» свойства объектов
Реклама убивает мысль! Сделаем антирекламный ролик: Из черной глубины экрана на нас наплывают, увеличиваясь, красные слова «Реклама убивает мысль!»
Растянем на всю форму черную метку. Придадим ее свойству Text значение нужного текста, а свойству ForeColor – красный цвет. Программа:
Dim Размер_шрифта As Integer = 10
Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick
Dim Шрифт As New Font("Times", Размер_шрифта)
Label1.Font = Шрифт
Размер_шрифта = Размер_шрифта + 1
End Sub
Задание 91.
Пусть текст также плавно меняет свой цвет.
Zoom. Пусть теперь экран монитора – это передний иллюминатор космического корабля. Мы приближаемся к планете, она вырастает в иллюминаторе. Чтобы создать такой эффект, распахните черную форму на весь экран. Придайте фото планеты объекту PictureBox. Установив в True его свойство StrechImage, увеличивайте размеры PictureBox:
Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick
PictureBox1.Width = PictureBox1.Width + 1
PictureBox1.Height = PictureBox1.Height + 1
End Sub
Подобным образом можно оживлять многие объекты, меняя любые их свойства, имеющие численное значение.
А теперь создадим два мультфильма: «Летающая тарелка» и «Человечек».