Программный код большинства примеров данной лекции можно найти в проектах, доступных для просмотра: BookOne, BookTwo, DocOne, DocTwo.
Класс является обобщением понятия типа данных и задает свойства и поведение объектов класса - экземпляров класса. Каждый объект принадлежит некоторому классу. Отношение между объектом и его классом такое же, как между переменной и ее типом. Класс - это объединение данных и обрабатывающих их процедур и функций. Данные называются также переменными класса, а процедуры и функции - методами класса. Переменные определяют свойства объекта, а совокупность их значений - состояние объекта. Наряду со свойствами и методами с классом связывается еще одно понятие - события. Каждый класс имеет определенный набор событий, которые могут возникать при работе с объектами класса, - чаще всего при определенных действиях пользователя, иногда, как результат действия системы. При возникновении события, связанного с тем или иным объектом, система посылает сообщение объекту, которое может быть обработано методом - обработчиком события, специально созданным при конструировании объекта. События обеспечивают большую гибкость при работе с объектами. Методы класса выполняются одинаково для всех объектов класса, а на события каждый объект реагирует индивидуально, поскольку имеет собственный обработчик события.
"Программирование в классах" является основным способом работы современного программирования.
За примерами далеко ходить не нужно. Так построена операционная система Windows, - окно являются ее основным объектом. Сам Office 2000 полностью построен на классах и работе с объектами этих классов, - здесь все, начиная от приложения и кончая отдельным символом, рассматривается как объект некоторого класса. Профессиональный прикладной программист, работающий в некоторой проблемной области и решающий разнообразные задачи из этой области, как правило, начинает с создания классов, описывающих специфику данной проблемной области. Затем уже решение тех или иных специальных задач он описывает в терминах работы с объектами данной проблемной области.
VBA позволяет программисту создавать собственные классы. Синтаксически класс представляет отдельный модуль специального вида - модуль класса. Мы постараемся сейчас детально и на примерах разобраться во всех особенностях этой важной конструкции.
Тема классов находится на магистральном пути развития Офисной среды. По сравнению с предыдущей версией сделаны три важных нововведения:
Instancing, регулирующее способы взаимодействия с классами в системе документов других проектов.Все эти возможности будут подробно рассмотрены в этой и следующей лекции. А начнем мы с примера создания собственного класса.
В предыдущей лекции о типах данных мы ввели пользовательский тип Person. Там же мы показали, что прямой доступ к полям записи позволяет программисту свободно работать с данными этого типа. И все же Person в чем-то "ущербен"... А не хватает ему собственных операций: если бы наши переменные Петров и Козлов имели тип, например, Integer, можно было бы их сравнивать, складывать и умножать, присваивать значения и многое другое. Понятно, что умножение данных типа Person вряд ли разумно, но зато для него можно определить свои операции. Вот, например, какие операции можно определить для этого типа:
InitPerson - инициализация полей записи;PrintPerson - печать полей записи;CopyPerson(Source As Person) - копирование источника (записи Source ) - аналог оператора присвоения;WhoIs - более специфическая операция, с определенной достоверностью определяющая пол, анализируя имя и фамилию.Всякий раз при определении пользовательского типа, так или иначе, но следует определить операции над данными этого типа. Естественно собрать определение типа и операции над ним в одном месте. Такое объединение представляет уже почти полное, с точностью до событий определение типа. Для типов, определенных подобным образом, введен новый термин - класс .
Синтаксически классы в VBA оформляются в виде модуля класса . Поэтому начинать создание класса в Редакторе Visual Basic нужно с выбора в меню Insert пункта Class Module. Этот модуль имеет такую же структуру, как и стандартный модуль, о котором мы подробно уже рассказали. Модуль состоит из двух разделов - объявлений и методов. В первом из них естественным образом описываются свойства класса, а во втором - его методы. И здесь действуют спецификаторы области действия Public и Private. Public - свойства и Public -методы составляют интерфейс класса. Только к этим свойствам и методам можно обращаться при работе с объектами класса, объявленными в других модулях, где класс является видимым.
Мы расширили ранее созданный тип Person до класса, названного нами Личность, путем добавления операций над данными класса. Часть из этих операций уже упомянута, другие ясны из контекста. Нам кажется разумным привести с самого начала полное описание класса, а потом уже, когда полная картина ясна, переходить к деталям. Создав модуль класса " Личность, мы поместили туда следующий текст:
Option Explicit
'Класс Личность
'Свойства класса: имя, отчество, фамилию, дату рождения
'закроем от прямого доступа,
'получить и изменить их можно только через методы класса
Private Имя As String
Private Отчество As String
Private Фамилия As String
Private ДатаРождения As Date
Public Sub InitPerson(ByVal FN As String, ByVal LN As String, _
ByVal DoB As Date)
'Инициализация личности
Имя = FN
Фамилия = LN
ДатаРождения = DoB
End Sub
Public Sub PrintPerson()
'Печать в отладочном окне Immediate
Dim S As String
If WhoIs Then S = "родилась" Else S = "родился"
Debug.Print Имя, Отчество, Фамилия, S, ДатаРождения
End Sub
Public Sub CopyPerson(You As Личность)
Имя = You.ВашеИмя
Фамилия = You.ВашаФамилия
ДатаРождения = You.ВашаДатаРождения
End Sub
Public Function WhoIs() As Boolean
'Пытается определить пол личности, анализируя имя и фамилию
'Возвращает True, если думает, что имеет дело с женщиной.
Dim F1 As Boolean, F2 As Boolean
F1 = ПоследняяБуква(Имя) = "А" Or ПоследняяБуква(Имя) = "Я"
F2 = ПоследняяБуква(Фамилия) = "А" Or ПоследняяБуква(Фамилия) = "Я"
If F1 And F2 Then
'можно полагать, что наша Личность - женщина
WhoIs = True
ElseIf Not F1 And Not F2 Then
WhoIs = False
Else 'Есть сомнения
If Отчество = "" Then
Отчество = InputBox(Имя " " Фамилия _
"! " "Назовите отчество, пожалуйста.")
End If
WhoIs = ПоследняяБуква(Отчество) = "А"
End If
End Function
Public Sub SayWhoIs()
' Вывод сообщения о поле и возрасте личности
If WhoIs Then
MsgBox ("Думаю," Имя _
", Вы из прекрасной половины человечества!")
ElseIf Year(ДатаРождения) > 1967 Then
MsgBox ("Думаю, " Имя ", Вы - молодой человек!")
Else
MsgBox ("Думаю, " Фамилия ", - мужчина!")
End If
End Sub
Private Function ПоследняяБуква(ByVal W As String) As String
'Внутренняя функция: возвращает в верхнем регистре
'последнюю букву слова W
ПоследняяБуква = UCase(Right(W, 1))
End Function
Public Property Get ВашеИмя() As String
ВашеИмя = Имя
End Property
Public Property Let ВашеИмя(ByVal vNewValue As String)
Имя = vNewValue
End Property
Public Property Get ВашеОтчество() As String
ВашеОтчество = Отчество
End Property
Public Property Let ВашеОтчество(ByVal vNewValue As String)
Отчество = vNewValue
End Property
Public Property Get ВашаФамилия() As String
ВашаФамилия = Фамилия
End Property
Public Property Let ВашаФамилия(ByVal NewValue As String)
Фамилия = NewValue
End Property
Public Property Get ВашаДатаРождения() As Date
ВашаДатаРождения = ДатаРождения
End Property
Public Property Let ВашаДатаРождения(ByVal NewValue As Date)
ДатаРождения = NewValue
End Property
Private Sub Class_Initialize()
Имя = "Адам"
Фамилия = "Человек"
ДатаРождения = #1/1/100#
End Sub
Прокомментируем этот довольно длинный текст.
Личность совпадают с полями типа Person. Кстати, мы сделали все свойства закрытыми ( Private ), и теперь вне класса нет прямого доступа к его полям (свойствам).Личность добавлено 5 общих методов: InitPerson, PrintPerson, CopyPerson, WhoIs , SayWhoIs и по паре методов Get и Let на каждое закрытое свойство. В классе есть и закрытая для внешнего использования функция " ПоследняяБуква ", и обработчик события Initialize.Init в том или ином виде должен быть определен в каждом классе. Это первый вызываемый метод объекта. Прежде чем начать работу с объектом, его нужно инициализировать. В нашем классе эту работу и делает метод InitPerson.Print также присутствует почти в каждом классе - нужно же распечатать информацию об объекте! - в PrintPerson вызывается метод WhoIs .CopyPerson - еще один общий, часто необходимый метод, позволяющий реализовать настоящее присвоение, когда копируется не ссылка, а значения полей класса, что позволяет иметь не две ссылки на один объект, а два идентичных объекта.WhoIs - метод, специфический для нашей задачи. Это попытка определить пол по имени и фамилии. Применяется примитивный, но обеспечивающий высокую достоверность для русских имен и фамилий алгоритм. По ходу дела потребовалось ввести вспомогательную функцию " ПоследняяБуква ".SayWhoIs вызывает WhoIs , дополнительно определяет возраст (только для мужчин) и выводит соответствующее сообщение в окно Message.ПоследняяБуква возвращает в верхнем регистре последнюю букву слова. Эта задача решается двумя вызовами функций работы со строками Right и UСase. Первая возвращает "хвост" слова, вторая - преобразует результат в верхний регистр.Get и Let. Вообще говоря, свойства можно не закрывать, а специальные методы не вводить. Но VBA позволяет следовать традициям объектно-ориентированного программирования, согласно которым считается правильным не давать возможности непосредственного изменения свойств, поскольку иногда это может привести к некорректному состоянию объекта. Поэтому доступ к свойствам закрывается, для чего достаточно объявить их с атрибутом Private, но зато вводятся специальные методы Get (для получения значения свойства) и Let (для изменения значения на новое). Заготовки для этой пары свойств строятся автоматически, если Вы при вставке метода указали (пометив флажок Property ), что он должен быть свойством.Initialize и Terminate. Первое возникает при первоначальном обращении к объекту, второе - по окончании работы с ними. В нашем примере в методе Initialize даем объекту инициализацию, восходящую к "Адаму".Завершим изложение примером работы с объектами класса, его методами и свойствами:
Public Sub Знакомство()
Dim UserOne As New Личность
Dim UserTwo As New Личность
Dim UserThree As New Личность
Debug.Print UserOne.ВашеИмя
UserOne.InitPerson FN:="Петр", LN:="Петров", DoB:=#1/23/1968#
UserTwo.InitPerson FN:="Анна", LN:="Козлова", DoB:=#7/21/1968#
UserOne.PrintPerson
UserTwo.PrintPerson
UserOne.SayWhoIs
UserTwo.SayWhoIs
UserTwo.ВашаФамилия = UserOne.ВашаФамилия "а"
Debug.Print UserOne.ВашаФамилия
Debug.Print UserTwo.ВашаФамилия
UserThree.InitPerson FN:="Анна", LN:="Керн", DoB:=#5/17/1803#
UserThree.PrintPerson
UserThree.SayWhoIs
End Sub
Вот какие результаты отладочной печати будут выданы в окно Immediate (отладки):
Адам Петр Петров родился 23.01.68 Анна Козлова родилась 21.07.68 Петров Петрова Анна Петровна Керн родилась 17.05.1803
А такие окна сообщений появятся на экране, при уточнении данных о личности Анны Керн:

(рис 4.2) Отчество Анны Керн(рис 4.1) Кто Анна?
Попробуем обобщить сведения об объектах и переменных. Начнем с того, что у них общего и что их различает. Если есть описания:
Dim X As T, Y As T1
то без контекста понять нельзя, что есть X и Y - "обычные" переменные или объекты. Например, если T - тип, заданный пользователем, а T1 - определенный им класс, то X - это переменная, а Y - объект. С нашей точки зрения, класс и тип - понятия, если не эквивалентные, то близкие по смыслу. Также близки понятия объекта и переменной. Класс - это специальная форма определения типа. Если есть тип T и класс T1, то можно объявить произвольное число экземпляров типа T и класса T1. Экземпляры типа T называются переменными, класса T1 - объектами. Класс задает свойства, методы и события своих объектов. Тип всегда задает свойства, неявно - методы, но никогда - события.
В объявлении: Dim Y As T1, где T1 - класс, объект Y мы часто называем переменной, говоря, что она имеет тип Object. Переменные типа Object рассматриваются как ссылки, задающие адрес памяти, где хранится объект. У них фиксированная длина 4 байта. При объявлении такой переменной память для самого объекта может и не отводиться в отличие от обычных переменных, поэтому не определено и значение ссылки. Задать ссылку - связать переменную с объектом - можно двумя путями:
Есть два способа связывания: раннее и позднее . При позднем связывании переменная объявляется так:
Dim V As Object
Это объявление говорит о том, что переменная является объектом (ссылкой), но ничего о классе этого объекта - он может быть произвольным, и выяснится это только динамически при выполнении программы, когда переменная V будет связываться с только что созданным или существующим объектом того или иного класса. Поэтому такое связывание называется поздним, или динамическим. Естественно, что необъявленные переменные или те, которым тип при объявлении не задан (значит, у них по умолчанию тип Variant ), допускают только позднее связывание. При раннем связывании в момент объявления указывается класс объекта, например:
Dim Петров As Личность, Козлов As Личность
Это позволяет еще на этапе трансляции проверять, допустимы ли те или иные операции над объектами Козлов и Петров. За исключением весьма сомнительного преимущества позднего связывания - возможностью связаться с объектом любого типа, ранее связывание предпочтительнее во всех отношениях. Для программиста особенно важно, что при раннем связывании, когда задан класс объекта, в процессе работы с этим объектом идет подсказка о его свойствах и методах. Подсказка облегчает жизнь программиста, и лишаться ее не стоит. Поэтому наш совет - выбирайте раннее связывание.
Теперь о том, как создаются новые объекты, и как происходит связывание с объектами, уже существующими. Для удобства разделим все объекты на три группы:
Личность, созданного в этой лекции;ActiveX и Com AddIns -объекты, в частности, объекты других приложений Office 97 при их подключении к исходному приложению. Например, в Word можно подключить объекты Excel, а документы Word включить в рабочие листы Excel.Понятно, что при работе с такими объектами хотя бы часть из них необходимо первоначально создать, и делается это при их объявлении:
{Dim | Private | Public | Static }<имя переменной> As New <имя типа>
Спецификатор New указывает, что в момент объявления нужно создать новый объект, то есть выделить ему память. В этот же момент ссылка на объект получает конкретное значение. Выделение памяти еще не означает инициализации значений свойств объекта. Инициализацию можно задать при определении события Initialize или в специально построенном методе Init, который следует запускать в начале работы с объектом. Вот пример создания объекта с последующей его инициализацией:
Public Sub MyCreateObject()
'Нельзя создавать объекты собственного класса, используя CreateObject
Dim Nemo As Личность
'Set Nemo = CreateObject("Личность")
'Можно их создавать, используя New
Set Nemo = New Личность
Nemo.InitPerson FN:="Prince", LN:="Dakar", DoB:=#1/23/1838#
Nemo.PrintPerson
End Sub
Только метод New позволяет создать новый объект для классов, определенных программистом. Для этой цели нельзя, например, использовать метод CreateObject. Но, заметьте, спецификатор New не является обязательным, его можно опускать при объявлении таких переменных класса, которые в дальнейшем получат значение, благодаря ссылке на другие, ранее созданные объекты. Задать ссылку на существующий объект нельзя обычным оператором присвоения - для этого в VBA есть специальный оператор Set:
Set <имя переменной - объекта> = {<объектное выражение>| Nothing }
Set - специально выделенный, частный случай оператора присвоения, в его левой части стоит имя переменной, определенной как объект при объявлении, а в правой - некоторое выражение, значение которого - ссылка на объект. В итоге переменная левой части получает значение выражения правой части. Вот пример некоторого объявления объекта Q. Следующие строки мы добавили в конец предыдущей процедуры:
Dim Q As Личность Set Q = Nemo Q.PrintPerson
В результате будет отпечатан следующий текст:
Prince Dakar родился 23.01.1838
Каждый VBA-проект погружен в "родное" приложение Office 97, объекты которого доступны в проекте. Для создания таких объектов не используется ни спецификатор New, ни метод CreateObject. Новые объекты, если и создаются, то специальными методами своего класса. Например, метод Add позволяет добавлять элементы в коллекции. При работе с объектами этой группы объявляются переменные соответствующего класса, затем они связываются с уже существующими объектами. Связывание выполняет оператор Set. Все примеры из первой лекции книги посвящены работе с объектами этой группы. Напомним тот, где участвует один из наиболее употребительных объектов - Range:
Public Sub FirstComment()
Dim myRange As Range
'Добавление комментария
With ActiveDocument
Set myRange =.Paragraphs(1).Range
.Comments.Add myRange, "Эта лекция рассказывает о классах объектов"
'Передвигается объект Range
myRange.Move Unit:=wdParagraph, Count:=2
End With
End Sub
Здесь вначале объявляется объект myRange класса Range, что обеспечивает раннее связывание. Объектное выражение в правой части оператора Set возвращает ссылку на существующий объект (первый параграф активного документа), и эта ссылка становится значением myRange. А затем с этим объектом можно работать, используя свойства и методы соответствующего класса, в данном случае - Range. Его можно использовать, например, в методе Add при работе с коллекцией комментариев или вызвать метод Move для изменения области, отведенной объекту.
Технология ActiveX обеспечивает взаимодействие приложений - Автоматизацию (Automation), при которой одно приложение управляет работой другого. На этой технологии построено взаимодействие приложений Office 97. И хотя у нас уже были примеры на эту тему, позволим привести еще один.
Чтобы начать работу с ActiveX -объектом, нужно объявить соответствующую переменную, создать сам объект и связать переменную с объектом. Один из способов выполнения этой работы - использовать спецификатор New в операторе объявления переменных, который в этой ситуации имеет вид:
Dim <имя переменной> As New <имя приложения.имя класса>
По сравнению с обычной конструкцией оператора объявления здесь более сложно задается тип (класс) создаваемого объекта. Первая часть - имя_приложения - указывает приложение, создающее объект, и в то же время это имя библиотеки типов данного приложения - TypeLib. Приложение является сервером, который вызывается для обеспечения работы с объектом. Вторая часть - имя_класса - задает тот конкретный класс, определенный в сервере, экземпляр ActiveX -объекта которого будет создан. Библиотека типов подключаемого приложения должна быть видимой, для чего надо включить ссылку на нее в меню References. Повторим с небольшими вариациями наш старый пример, в котором Excel подключается в приложении Word:
Public Sub WorkWithActiveX()
'Создание объекта Excel.Application при работе с документом Word
Dim xlApp As Excel.Application
'Set xlApp = CreateObject("Excel.Application.9")
Set xlApp = New Excel.Application
xlApp.Application.Visible = True
'Добавить новую книгу
xlApp.Workbooks.Add
'Теперь можно работать и с ячейками данной книги
xlApp.Workbooks(1).Activate
xlApp.Range("A1:A2") = 2
xlApp.Range("B1") = "=A1+A2"
xlApp.Range("B2") = "=A1*A2"
'Закрываем открытую книгу без сохранения изменений
xlApp.Workbooks(1).Close SaveChanges:=False
'Закрываем приложение
xlApp.Quit
End Sub
Создание Excel приложения в данном случае возможно двумя способами - применением конструкции New или функции CreateObject. Однако, использование New для создания ActiveX -объектов не всегда возможно. Не все приложения допускают такой способ подключения, и не все объекты доступны, - например, недоступны внутренние ActiveX -объекты, которые совместно могут храниться в одном и том же файле. Более универсальный способ создания ActiveX -объектов - вызов специальных функций CreateObject и GetObject. Как правило, вызов этих функций помещается в правую часть оператора Set. В результате его выполнения объектное выражение возвращает ссылку на созданный объект, которая и становится значением переменной.
Синтаксис функции CreateObject таков:
CreateObject(<класс>)
Параметр класс - строка, задающая класс объекта, у которой две обязательные части: "имя_приложения.имя_класса"; ее вид совпадает с видом задания класса ActiveX -объектов в операторе объявления.
Функция GetObject имеет дополнительный параметр:
GetObject(<путь>, <класс>)
Параметр путь задает полный путь к файлу, содержащему ActiveX -объект. Этот объект и активизируется. Второй параметр при этом необязателен. Но если файл содержит группу ActiveX -объектов разных классов, задать его необходимо. Можно опустить и первый параметр, если ActiveX -объект этого класса уже создан, и тогда возвращается ссылка на GetObject. Обратите внимание, книга создается без предварительного создания объекта Application:
Public Sub WorkWithGetObject()
Const MY_PATH As String = "e:\O2000\CD2000\Ch4\"
Dim myBook As Excel.Workbook
Set myBook = GetObject(MY_PATH "BookOne.xls")
With myBook
.Application.Visible = True
'Чтобы увидеть книгу, включите ее в окне UnHide из меню Window
.Charts("Динамика Продаж").Activate
'Посмотрев на диаграмму, переключимся на рабочий лист
MsgBox ("Вы можете перейти в Excel")
.Worksheets(1).Activate
.Worksheets(1).Range("A40") = 777
.Save
.Application.Quit
End With
End Sub
Мы начали эту лекцию с примера создания класса " Личность " и привели полное его описание, что позволило целиком охватить всю картину. Теперь разберем детали построения класса. Шаг за шагом построим еще один простой класс Rational, определяющий рациональные числа. Начнем построение класса, как обычно, с комментария, содержательно описывающего назначение класса, его свойства и поведение. Вот комментарий, к нашему классу:
' Класс Rational 'Определяет новый тип данных - рациональные числа и основные 'операции над ними - сложение, вычитание, умножение и деление. 'Рациональное число задается парой целых чисел (m,n) и изображается, 'обычно, в виде дроби m/n. Число m называется числителем, а 'n - знаменателем. Для каждого рационального числа существует 'множество его представлений, например, - 1/2, 2/4, 3/6, 4/8, … 'задают одно и тоже рациональное число. Среди всех представлений 'можно выделить то, в котором числитель и знаменатель несократимы. 'Именно такие представители будут храниться в нашем классе. 'Операции над рациональными числами определяются естественным 'образом. Лучшим их описанием будут соответствующие им методы.
После комментария следует описание переменных, задающих свойства класса. Это могут быть терминальные свойства, заданные обычными переменными VBA, как, например, Имя, Фамилия в классе " Личность ", так и свойства - участники. Напомним, что свойства - участники это объекты других классов. Без них не обойтись, если мы строим семейство классов. Помните, что в Office 2000 нет наследования классов в классическом понимании, - его заменяет встраивание. Так что при построении семейства классов, приходится иметь дело с "толстыми" объектами, свойства которых являются объектами, имеющими свойства, являющиеся объектами и так далее. Взгляните еще раз на рисунок 1.1 первой лекции и просмотрите ее раздел "встраивание против наследования", имеющиеся там примеры поясняют ситуацию.
В разбираемом нами случае речь не идет о семействе классов, поэтому класс Rational имеет только терминальные свойства. Пояснять, что это за свойства нет необходимости. Вот их определение:
'Свойства класса Rational Private m As Integer 'числитель Private n As Integer 'знаменатель
Возникает вопрос, почему свойства объявлены с описателем Private, а не Public Заметьте, таким же образом мы описывали свойства и в классе " Личность ". Такова общепринятая практика объектно-ориентированного программирования. Если бы свойства имели атрибут Public, - были бы открытыми, тогда при работе с объектом, можно было бы иметь к ним прямой доступ, как при чтении, так и записи. Но такая свобода, как правило, недопустима. Вот возможные стратегии при работе со свойствами:
Read - Write ).Read, Write-once ).Read-only ).Write-only ).Not Read - Not Write ), - свойство закрыто.Открытость свойств не позволяет реализовать только первую из стратегий. В нашем примере с классом Rational открытость свойств недопустима, поскольку числитель и знаменатель преобразуются скрытым от внешнего пользователя способом, позволяющим всегда задавать единственного представителя рационального числа.
При задании свойств делайте их закрытыми.
Для доступа к закрытым свойствам и реализации перечисленных выше стратегий работы с ними предложены специальные методы - свойства Property Let, Set и Get, примеры их мы видели в классе " Личность ", а подробный разговор еще предстоит.
Следующим шагом в создании класса является разработка его конструкторов и деструктора. Напомним, что новый объект - экземпляр класса создается конструкцией New, например:
Dim MyRationalNumber As New Rational
В этот момент:
MyRationalNumber,Rational,В языках объектного программирования, как, например, в языке C++ конструкторов может быть несколько, среди которых выделяется конструктор без параметров, являющийся конструктором по умолчанию. Остальные конструкторы имеют параметры, позволяющие задать свойства объекта в момент инициализации. Важно понимать роль конструкторов особенно в ситуации, когда создается "толстый" объект. В момент создания такого объекта будет вызвана цепочка конструкторов, начиная с самого внутреннего, создающего самый внутренний объект, затем будет создан охватывающий объект, пока не будет вызван внешний конструктор, создающий объект - оболочку. В таком конструкторе приходится задавать параметры для инициализации всех внутренних объектов.
В VBA все проще. Во многом это объясняется тем, что здесь, в отличие от многих других языков программирования, есть разумная стратегия начальной инициализации переменных, - мы о ней говорили ранее. Поэтому здесь есть только конструктор по умолчанию - конструктор без параметров, да и тот часто не определяется, полагаясь на стандартную инициализацию. Конечно же, инициализировать объект "настоящими" значениями все равно придется в какой-то момент. Поэтому всегда для класса создаются свои конструкторы, синтаксически являющиеся методами, - их может быть несколько. В классе " Личность " таким конструктором с параметрами является метод InitPerson. Заметьте, мы могли бы определить еще один конструктор, который в отличие от первого, проводил бы полную инициализацию всех свойств, включая Отчество нашей личности.
Деструктор вызывается автоматически при уничтожении объекта. В VBA нет динамического уничтожения объекта в момент, определенный программистом, объекты уничтожаются также как и переменные, при выходе из области их действия. Поэтому деструктор, как правило, не пишется.
Говоря о конструкторах и деструкторах, мы не сказали главного. Роль конструктора по умолчанию в классах VBA играет обработчик события Initialize. Это общее для многих объектов событие, встречается, когда объект загружается, для объектов - экземпляров классов оно возникает при создании объекта. У обработчика этого события нет параметров, поэтому он играет роль конструктора по умолчанию, не имеющего параметров. Роль деструктора играет обработчик события Terminate. Он вызывается, когда все ранее установленные ссылки на экземпляр объекта получают значение Nothing или все указатели перестают существовать, выйдя из области своего определения. Заметьте, что при ненормальном завершении программы это событие не возникает. Обработчик этого события (деструктор) пишется значительно реже, поскольку в момент его вызова объект и так корректно будет уничтожен.
Вернемся теперь к нашему классу и определим для него конструктор по умолчанию и "настоящий" свой конструктор:
'Конструкторы класса Rational
Private Sub Class_Initialize()
'Конструктор по умолчанию
'инициализирует рациональное число дробью 1/1
m = 1
n = 1
End Sub
Public Sub CreateRational(ByVal a As Integer, ByVal b As Integer)
'Собственный конструктор
'Выполняет довольно сложные действия,
'прежде чем свойства получат значения
Dim d As Integer 'Наибольший общий делитель a и b
If b = 0 Then
MsgBox " Ошибка при создании рационального числа!" _
Chr(13) "Знаменатель не должен равняться 0."
Else
' приведение знака
If b < 0 Then
b = -b: a = -a
End If
' приведение к несократимой дроби
d = nod(a, b) ' d - НОД(a,b)
m = a \ d
n = b \ d
End If
End Sub
' Скрытая функция вычисления НОД(m,n)
Private Function nod(ByVal m As Integer, ByVal n As Integer) As Integer
Dim p As Integer
m = Abs(m): n = Abs(n)
If n > m Then
p = m: m = n: n = p
End If
Do
p = m Mod n: m = n: n = p
Loop Until n = 0
nod = m
End Function
Обратите внимание, конструктор по умолчанию, являясь обработчиком события Initialize, строился, как обычный обработчик события, используя стандартную заготовку. Он, естественно, является закрытым, поскольку вызывается только системой и не доступен для обычного вызова. Наш собственный обработчик открыт. Он выполняет довольно сложную работу и не сводится к простому присвоению свойств объекта. Это довольно типичная ситуация. По ходу дела пришлось написать вспомогательную функцию вычисления наибольшего общего делителя. Поскольку она носит служебный характер, то она закрыта и не доступна для вызова при работе с объектами класса Rational.
Для того чтобы можно было получить доступ к закрытым свойствам, предусмотрены специальные процедуры - свойства:
Property Let позволяет установить новое значение терминального свойства, выполняя операцию Write (присваивание).Property Set выполняет те же действия, что и предыдущая процедура, но применима к свойствам - участникам. Вы понимаете, что в VBA присваивание значений обычным переменным и объектам выполняется двумя различными операторами - Let и Set.Property Get является дополнительной к предыдущим процедурам, выполняя операцию чтения Read. Она применима к терминальным свойствам и свойствам - участникам.Рассмотрим пять ранее описанных стратегий применения свойств. В первой стратегии Read - Write каждому закрытому свойству будет соответствовать пара взаимно дополняющих процедур - свойств Let ( Set ) - Get. Эту стратегию мы реализовали для свойств класса " Личность ". Стратегия Write-once предполагает, что значение свойства должно быть записано только при первом обращении, а далее не должно изменяться. Для реализации этой стратегии процедуры - свойства Let и Set должны включать в себя код, осуществляющий специальную проверку, проводилось ли ранее присвоение значения свойству. Для объектов это проверка типа " value Is Nothing ", для переменных типа Variant - " value = Empty " и так далее. Мы не станем приводить подробный пример, подобную проверку мы осуществляли при работе со свойством Отчество в процедуре класса " Личность. В следующих двух стратегиях используется только одна из процедур пары, либо Get, либо Let ( Set ). Пятая стратегия предполагает, что доступ к свойствам закрыт, и процедуры - свойства Get, Let ( Set ) в классе вообще не объявляются.
Обратим внимание на одну важную особенность, когда свойство имеет тип Variant, его значениями могут быть как обычные данные (числа, строки), так и объекты. В этой ситуации необходимо реализовать все три процедуры, применяя Let или Set в зависимости от того, с чем мы имеем дело - объектом или переменной.
В классе Rational есть два закрытых свойства, заданных переменными m и n и определяющими соответственно числитель и знаменатель дроби, представляющей рациональное число. Зададим для каждого из этих свойств пару Property Let - Property Get с именами Числитель и Знаменатель соответственно. Для создания процедур - свойств обычной практикой является использование заготовок, создаваемых автоматически. Выбрав в Редакторе Visual Basic пункт меню Insert | Procedure, мы задали в появившемся диалоговом окне Add Procedure значение Property для типа процедуры. Задав еще имя процедуры - свойства " Числитель ", и, щелкнув OK, мы получили две стандартные заготовки Property Let и Property Get.
(рис 4.3) Создание процедур - свойствПовторив эти действия, мы получили вторую пару заготовок с именем " Знаменатель ". Заготовки затем наполняются, как обычно, содержанием и слегка модифицируются. В заготовках предусмотрен общий тип Variant для параметра в Property Let и возвращаемого значения в Property Get. Разумно изменить этот общий тип на конкретный тип, используемый в данной ситуации. Естественно, что иногда приходится заменить Let на Set или руками добавить еще одну заготовку для Set. Приведем тексты этих процедур для класса Rational после их модификации и заполнения:
Public Property Get Числитель() As Integer
Числитель = m
End Property
Public Property Let Числитель(ByVal NewValue As Integer)
CreateRational NewValue, n
End Property
Public Property Get Знаменатель() As Integer
Знаменатель = n
End Property
Public Property Let Знаменатель(ByVal NewValue As Integer)
CreateRational m, NewValue
End Property
Обратите внимание, для задания новых значений свойств в процедурах Let Числитель и Let Знаменатель мы вызываем конструктор, который может изменить нужным образом и числитель и знаменатель.
Замечание:
Для класса Rational доступ к свойствам Числитель и Знаменатель следовало бы закрыть полностью. Рациональные числа представляются для пользователей неделимыми и их внутренняя структура должна быть недоступной. Для них следовало бы использовать пятую стратегию работы со свойствами, когда свойства Let и Get не объявляются. Причину, по которой были введены эти процедуры - свойства, мы объясним позже, когда речь пойдет о реализации методов класса.
Процедуры - свойства Property Let, Property Set, Property Get играют важную роль в определении большинства классов. Поэтому имеет смысл разобрать их точный синтаксис.
Оператор Property Let используется для установки значения терминального свойства и имеет следующий синтаксис:
[Public | Private | Friend] [Static] Property Let имя-свойства ([список-параметров,] значение) [Операторы] [Exit Property] [Операторы] End Property
Оператор Property Set используется для установки значения свойства - участника (объекта) и имеет следующий синтаксис:
[Public | Private | Friend] [Static] Property Set имя-свойства ([список-параметров,] ссылка) [Операторы] [Exit Property] [Операторы] End Property
Оператор Property Get используется для получения значения свойства и имеет следующий синтаксис:
[Public | Private | Friend] [Static] Property Get имя-свойства [(список-параметров)] [As Type] [Операторы] [Exit Property] [Операторы] [имя-свойства = выражение] End Property
Рассмотрим детали синтаксиса:
Public означает доступность Property - свойств во всех процедурах во всех модулях во всех проектах, если только нет дополнительных ограничений. Ключевое слово Private хотя и возможно синтаксически, но лишено смысла, - Property пишут для того, чтобы они были открытыми.Static, как обычно, означает, что значения локальных переменных процедуры, если они есть, не будут изменяться в промежутке между ее вызовами.имя-свойства задает имя определяемого и изменяемого свойства. Заметьте, что когда задается пара процедур Property Let (Set) - Get или все три процедуры, все они имеют одно и тоже имя.список-параметров используется чаще всего при задании свойства, значения которого образуют массив. В нем через запятую перечисляются параметры, передаваемые процедуре, например, в качестве индексов. Парные процедуры должны иметь один и тот же список параметров. Синтаксис списка-параметров такой же, как и у параметров обычных процедур, определяемых оператором Sub. Как обычно, если нужно передать переменное число параметров, то можно использовать ParamArray.значение в Property Let и ссылка в Property Set - это имя переменной (объекта), значение которого передается свойству. Тип значения, возвращаемого процедурой Property Get, должен совпадать с соответствующим типом параметра значение (ссылки).операторы задает программу вычисления значения свойства. В теле процедуры можно использовать оператор Exit Property для немедленного выхода из процедуры.К классу Rational, создание которого еще не закончено, мы еще вернемся. Пока же продолжим разговор о процедурах - свойствах, поскольку они являются важными компонентами класса. В этом разделе пойдет речь об одном из их довольно неожиданных применений, когда они позволяют выступать классу в роли привлекательной упаковки внутренних и служебных функций. Но обо всем по порядку.
Построим класс Plan, в котором по существу есть только одно свойство, хранящее текущий месяц. Хранится это свойство, как целое, но для внешнего мира оно выглядит как обычное имя месяца. Вот как это реализовано.
'Класс Plan
'Свойство класса
Private CurMonth As Integer
'Закрытый массив, играющий служебную роль
Private Месяцы(1 To 12) As String
Private Sub Class_Initialize()
CurMonth = Month(Now)
Месяцы(1) = "Январь": Месяцы(2) = "Февраль": Месяцы(3) = "Март"
Месяцы(4) = "Апрель": Месяцы(5) = "Май": Месяцы(6) = "Июнь"
Месяцы(7) = "Июль": Месяцы(8) = "Август": Месяцы(9) = "Сентябрь"
Месяцы(10) = "Октябрь": Месяцы(11) = "Ноябрь": Месяцы(12) = "Декабрь"
End Sub
Public Property Get ТекущийМесяц() As String
ТекущийМесяц = Месяцы(CurMonth)
End Property
Public Property Let ТекущийМесяц(ByVal NewValue As String)
Dim i As Byte
i = 1
Do While i <= 12
If Месяцы(i) = NewValue Then
CurMonth = i
Exit Do
End If
i = i + 1
Loop
If i = 13 Then 'Неверно задан месяц
CurMonth = Month(Now)
End If
End Property
Обратите внимание, работу с закрытым свойством CurMonth обеспечивают процедуры - свойства Property Let ТекущийМесяц и Property Get ТекущийМесяц. С их помощью можно читать и изменять значение свойства CurMonth. Одновременно Let и Get занимаются преобразованием данных, что является распространенной практикой. Пример этот интересен и с позиций построения конструктора по умолчанию, который выполняет все необходимые внутренние инициализации, в данном случае, задавая значение массиву Месяцы.
Приведем еще процедуру, которая работает с данными этого класса:
Public Sub WorkWithPlan() Dim MyPlan As New Plan Debug.Print MyPlan.ТекущийМесяц MyPlan.ТекущийМесяц = "Апрель" Debug.Print MyPlan.ТекущийМесяц MyPlan.ТекущийМесяц = "Янв." Debug.Print MyPlan.ТекущийМесяц End Sub
В результате ее работы, (процедура запускалась в марте месяце) напечатаны значения:
Март Апрель Март
Задумаемся над сутью решаемой задачи, - нам хочется при обращении к свойству класса получать значение текущего месяца. Но ведь текущий месяц, он и есть текущий, и пользователь не должен изменять его значение. Так что первое, что следовало бы сделать, это воспользоваться стратегией Read- only и не разрешать изменять значение свойства. Поэтому исключим Property Let из нашего класса. Но в этом примере есть куда более интересная особенность. Исключив Property Let, мы понимаем, что теперь теряет смысл хранение самого свойства CurMonth. Его значение мы можем (и должны по смыслу свойства) получать динамически, обращаясь к системной процедуре Month(Now). Удалим из класса и переменную CurMonth. Наш новый класс PlanNew стал проще:
'Класс PlanNew
'Закрытый массив, играющий служебную роль
Private Месяцы(1 To 12) As String
Private Sub Class_Initialize()
Месяцы(1) = "Январь": Месяцы(2) = "Февраль": Месяцы(3) = "Март"
Месяцы(4) = "Апрель": Месяцы(5) = "Май": Месяцы(6) = "Июнь"
Месяцы(7) = "Июль": Месяцы(8) = "Август": Месяцы(9) = "Сентябрь"
Месяцы(10) = "Октябрь": Месяцы(11) = "Ноябрь": Месяцы(12) = "Декабрь"
End Sub
Public Property Get ТекущийМесяц() As String
ТекущийМесяц = Месяцы(Month(Now))
End Property
Свойство ТекущийМесяц теперь имеет статус Read- only. Но главная суть не в этом. Теперь отчетливо видна настоящая роль класса PlanNew, - он является красивой упаковкой вызова служебных функций Month(Now). Стоило ли создавать класс для этой цели? Не проще ли просто написать стандартную процедуру или вообще все делать самому, обращаясь к служебной функции и затем преобразуя номер месяца в его название? Для одной функции, как в нашем примере, вероятно, не стоит создавать класс, но для десятка функций это вполне целесообразно. Эта техника с особым успехом используется, когда нужно упаковать обращение к Win API функциям. Так что, подводя итог, отметим, что иногда классы используются как упаковка, расширяющая стандартные возможности и особую роль в этом процессе играют процедуры - свойства.
Когда создается семейство классов, без свойств - участников не обойтись. В классах семейства, основанных на механизме встраивания, обязательно есть свойства, представляющие собой объекты. Эти свойства, как и терминальные, обычно, закрываются и для работы с ними используются процедуры - свойства Property Set и Property Get. Иногда приходится передавать и дополнительные параметры при работе с ними. Рассмотрим типичный пример. Ранее мы построили класс " Личность ", теперь построим класс " Группа ", содержащий группу личностей. При построении этого класса ограничимся минимальными средствами, необходимыми для демонстрации работы со свойствами:
'Класс Группа Личностей
Const РазмерГруппы As Byte = 25
'Свойства
Private Group(1 To РазмерГруппы) As Личность
'Процедуры-свойства
Public Property Get ЧленГруппы(num As Byte) As Личность
'Если номер корректен
If (num >= 1) And (num <= РазмерГруппы) Then
'Если существует в группе личность с таким номером
If Not (Group(num) Is Nothing) Then
Set ЧленГруппы = Group(num)
Else: MsgBox ("В группе нет человека с номером " num)
End If
Else: MsgBox ("Некорректно задан номер в группе - " num)
End If
End Property
Public Property Set ЧленГруппы(num As Byte, NewValue As Личность)
'Если номер корректен
If (num >= 1) And (num <= РазмерГруппы) Then
'Если в группе нет личности с таким номером, то она создается
If Group(num) Is Nothing Then
Set Group(num) = NewValue
Else: MsgBox ("В группе уже есть человек с номером " num)
End If
Else: MsgBox ("Некорректно задан номер в группе - " num)
End If
End Property
Public Sub Сведения()
Dim i As Byte
For i = 1 To РазмерГруппы
If Not (Group(i) Is Nothing) Then
Group(i).PrintPerson
End If
Next i
End Sub
Массив объектов класса " Личность " является закрытым свойством класса " Группа ". Процедуры - свойства Property Get ЧленГруппы и Property Set ЧленГруппы обеспечивают доступ к элементам этого массива. Индекс элемента является дополнительным параметром. Заметьте, процедура Set имеет статус Write-once, - если элемент с заданным номером уже определен в группе, то он не переопределяется. При создании этих процедур, нам, конечно же, пришлось модифицировать стандартные заготовки Let и Get. Для полноты картины приведем процедуру, в которой показано, как работать с группой:
Public Sub WorkWithGroup()
Dim UserOne As New Личность
Dim UserTwo As New Личность
Dim UserThree As New Личность
Dim Знакомые As New Группа
Dim NewUser As New Личность
'Личности
UserOne.InitPerson FN:="Петр", LN:="Петров", DoB:=#1/23/1968#
UserTwo.InitPerson FN:="Анна", LN:="Козлова", DoB:=#7/21/1968#
UserThree.InitPerson FN:="Анна", LN:="Керн", DoB:=#5/17/1803#
'Группа
Set Знакомые.ЧленГруппы(1) = UserOne
Set Знакомые.ЧленГруппы(2) = UserTwo
Set Знакомые.ЧленГруппы(1) = UserThree
Set Знакомые.ЧленГруппы(3) = UserThree
Set NewUser = Знакомые.ЧленГруппы(7)
Set NewUser = Знакомые.ЧленГруппы(3)
Знакомые.Сведения
End Sub
Не останавливаясь на тех диалогах, которые будут появляться по ходу выполнения, приведем результаты печати сведений о наших старых знакомых:
Петр Петров родился 23.01.68 Анна Козлова родилась 21.07.68 Анна Петровна Керн родилась 17.05.1803
Мы уже говорили о том, что классы могут выступать в роли привлекательной упаковки при работе со служебными функциями. Такую же "упаковочную" роль они могут играть и при работе со встроенными объектами. Свойствами такого класса могут быть, например, элементы управления, - списки, кнопки. Класс - упаковка позволит обращаться к методам и свойствам объекта, может быть в более привлекательной для конечного пользователя форме. Кроме того, в классе могут быть определены и дополнительные свойства и методы, расширяющие стандартные возможности.
Без процедур - свойств можно обойтись, заменив их обычными методами класса. Правда, такая замена приведет, обычно, к некоторой потере эффективности. Но методы являются основным способом работы с данными (свойствами), определяя поведение объектов класса.
Не будем повторяться. О методах было сказано уже достаточно. Любая процедура ( Sub ) или функция ( Function ), описанная в разделе методов класса, является его методом. Напомним синтаксис методов класса:
[Private | Public | Friend] [Static] Sub name [(arglist)] [statements] [Exit Sub] [statements] End Sub [Public | Private | Friend] [Static] Function name [(arglist)] [As type] [statements] [name = expression] [Exit Function] [statements] [name = expression] End Function
Роль всех ключевых слов в этих определениях уже пояснялась, чуть позже мы подробнее скажем о спецификаторе Friend, который могут иметь методы класса. Сейчас же отметим, что почти каждый класс, независимо от его специфики имеет некоторый "джентльменский" набор методов. В него входит:
Initialize.Вернемся к проектированию класса Rational. Большую часть джентльменского его набора мы уже определили. Специфика этого класса определяется операциями над рациональными числами. Ограничимся четырьмя основными операциями - сложением, вычитанием, умножением и делением. Добавим к ним еще и метод печати дробей. Вот тексты этих методов, заканчивающих определение нашего класса:
Public Function Plus(a As Rational) As Rational
Dim d As Integer, u As Integer, v As Integer
Dim R As New Rational
u = m * a.Знаменатель + n * a.Числитель
v = n * a.Знаменатель
d = nod(u, v)
R.Числитель = u \ d
R.Знаменатель = v \ d
Set Plus = R
End Function
Public Function Minus(a As Rational) As Rational
Dim d As Integer, u As Integer, v As Integer
Dim R As New Rational
u = m * a.Знаменатель - n * a.Числитель
v = n * a.Знаменатель
d = nod(u, v)
R.Числитель = u \ d
R.Знаменатель = v \ d
Set Minus = R
End Function
Public Function Mult(a As Rational) As Rational
Dim d As Integer, u As Integer, v As Integer
Dim R As New Rational
u = m * a.Числитель
v = n * a.Знаменатель
d = nod(u, v)
R.Числитель = u \ d
R.Знаменатель = v \ d
Set Mult = R
End Function
Public Function Divide(a As Rational) As Rational
Dim d As Integer, u As Integer, v As Integer
Dim R As New Rational
u = m * a.Знаменатель
v = n * a.Числитель
If v = 0 Then
MsgBox ("деление на нуль невозможно")
Else
d = nod(u, v)
R.Числитель = u \ d
R.Знаменатель = v \ d
Set Divide = R
End If
End Function
Public Sub PrintRational()
Debug.Print (m "/" n)
End Sub
Замечание:
Внутри класса при реализации операций над рациональными числами доступ к закрытым свойствам объекта, естественно, разрешен, и можно обращаться непосредственно к переменным m и n. Однако нельзя обратиться к закрытым свойствам параметров класса Rational, передаваемых методам Plus, Minus, Mult, Divide. Точно также, нельзя обратиться к свойствам локальных объектов (объекту R ), объявленных внутри методов. Единственный выход состоит в том, чтобы использовать для них Public Property Let и Public Property Get. Но тогда и вне класса можно использовать эти процедуры - свойства, изменяя, например, значение знаменателя дроби. Разрешать этого не хотелось бы.
На наш взгляд имеет место концептуальная некорректность поведения закрытых свойств. Внутри класса доступ к ним должен быть открыт не только для самого объекта, но и для объектов этого класса, заданных в качестве параметров методов класса. Точно также доступ должен быть открыт и для локальных объектов. Так поступают в большинстве языков программирования.
Приведем процедуру, которая работает с рациональными числами:
Public Sub WorkWithRational()
Dim a As New Rational, b As New Rational, c As New Rational
a.CreateRational 5, 7
b.CreateRational -12, -18
Set c = a.Plus(b)
c.PrintRational
Set a = c.Minus(b)
a.PrintRational
Set c = a.Mult(b)
c.PrintRational
Set b = c.Divide(a)
b.PrintRational
End Sub
При ее запуске получены следующие результаты:
29/21 5/7 10/21 2/3
Методы класса, в том числе процедуры - свойства Get, Let и Set могут иметь описатель Friend наряду с описателями Public и Private. Каждый из них задает свою область видимости метода, область программы, где метод доступен для вызова. Напомним, Private делает метод видимым только внутри класса, Public - делает метод доступным и область действия может распространяться не только на тот проект, в котором определен данный класс, но и на другие проекты. Чтобы управлять этим процессом, расширяя или сужая область видимости Public объектов, применяют специальные спецификаторы, например, опцию Option Private, которая закрывает модуль от других проектов. Методы с описателем Friend называют дружественными . Описатель Friend, действуя только на один метод, распространяет его область видимости на проект, в котором описан класс с Friend
методом. Но для других проектов дружественные методы не доступны, даже в том случае, когда там доступны Public методы. Так что метод дружит только со своим проектом и не более. Кто программировал на С++ и других языках, где есть наследование классов, знаком с дружественными методами, - там они дружат с классами - потомками родительского класса, в котором задан Friend метод. Вне семейства классов дружественные методы не доступны. На VBA роль семейства играет проект.
В мире объектов время от времени происходят события. Причиной их могут быть действия пользователя или реакция системы на ранее происшедшие события. При возникновении события, объекту с ним связанному, система посылает сообщение, при получении которого вызывается специальный метод - обработчик события. Эти методы, вызываемые специальным образом, и называются событиями. Итак, каждый класс имеет свойства, методы и события. Все объекты - экземпляры класса имеют одинаковый набор свойств, методов и событий, заданных определением класса. Методы у всех экземпляров класса одинаковы. Но, заметьте, обработчики событий у каждого экземпляра собственные! Впрочем, это же касается и значений свойств, - каждый экземпляр уникален. Все командные кнопки имеют метод Delete, который всегда выполняется одинаково, удаляя кнопку. Но щелчок по каждой кнопке, вызывающий событие Click, приводит к совершенно разным результатам. В этом сила событий, - поведение объектов становится уникальным.
VBA позволяет работать с объектами Office 2000, которым несть числа, и собственными объектами, классы которых мы умеем определять. Большинство из объектов Office 2000 не имеет событий, например, прославленный объект Range имеет множество свойств и методов, но ни одно из событий с ним не связывается. С другой стороны все объекты, предназначенные для организации интерфейса с пользователем, - формы, кнопки, списки и прочие объекты обладают набором событий. Но не только интерфейсные объекты имеют события. Объекты, задающие документы Word и Excel, страницы Application также имеют встроенный набор событий. Для большинства из них мы подробно рассмотрели, как создаются обработчики событий для конкретных экземпляров. Напомним, есть специальный тип модулей, связанных с объектами, имеющими события, - сюда относятся модули форм, документов и другие. В окне кода этого модуля можно выбрать конкретный объект, выбрать для него возможное событие, построить заготовку обработчика события и наполнить затем ее содержанием. Подробнее об этом сказано в предыдущей лекции, посвященной модулям. Нам осталось рассмотреть две серьезные задачи:
With Events. В эту группу входят, например, объекты Application. По умолчанию эти объекты появляются как объекты, не имеющие событий, поэтому у них нет специального модуля, в окне кода которого можно было бы найти объект, найти список его возможных событий, создать обработчик события. Первая задача состоит в том, чтобы для таких стандартных объектов, классы которых обладают событиями, уметь создавать объекты с событиями ( With Events ), создавать для них модуль, в котором будут появляться заготовки обработчиков событий.К решению этих двух важных задач мы сейчас и приступим.
Объекты Application являются наиболее известными объектами, имеющими события, но появляющимися по умолчанию как объекты без событий. С этих объектов и начнем и, поскольку технология включения событий во всех случаях одинакова, достаточно рассмотреть задачу создания обработчиков событий для объектов Application. Для того чтобы изложение сделать конкретным, будем полагать, что целью является создание обработчиков событий объекта Excel.Application. По ходу дела нам надо решить две основные задачи:
Excel.Application With Events и связать его с текущим Excel приложением.Заметьте, тут есть нюанс, - можно эту работу выполнять в самом Excel, после того, как открыта одна из его
Модуль, в котором будут появляться обработчики событий объекта Excel.Application, синтаксически является модулем класса, который нужно создать. Этот класс очень прост по своей структуре и при создании он содержит единственное закрытое свойство - соответствующий объект Application, объявленный со спецификатором WithEvents. Так что в нашем примере все описание создаваемого класса, который назовем, например, ExcelWithEvents сводится к одной содержательной строке:
Option Explicit 'Класс ExcelWithEvents Public WithEvents xlApp As Excel.Application
Как только такое свойство определено в классе, в окне кода этого модуля появляется объект xlApp, обладающий событиями, тут же можно выбрать из списка соответствующее событие, создать заготовку и затем наполнить ее содержанием. Взгляните на соответствующий рисунок:
(рис 4.4) Класс с объектом Excel.Application WithEventsМы ограничились созданием двух обработчиков события NewWorkbook и OpenWorkbook. Вот их текст:
Private Sub xlApp_NewWorkbook(ByVal Wb As Excel.Workbook)
MsgBox("Рад, что вы открыли книгу " Wb.Name)
End Sub
Private Sub xlApp_WorkbookOpen(ByVal Wb As Excel.Workbook)
MsgBox ("Рад, что вы открыли книгу " Wb.Name)
End Sub
Как видите, первая проблема решается достаточно просто и не требует введения новых средств. Но, обратите внимание, остается непонятным, что характеризует наш единственный объект xlApp, - конкретный экземпляр приложения, для которого создаются обработчики событий, или эти обработчики распространяются на множество экземпляров объектов класса ExcelWithEvents Но, поскольку объекты Application присутствуют в одном экземпляре, то не будем сейчас вдаваться в эти тонкости, позже мы еще вернемся к этому вопросу.
Вторая проблема решается также довольно просто. Но здесь есть одна тонкость, которую следует понимать. Реально создаются два объекта, мы называем их двойниками. Один из них это обычный объект Excel.Application, другой - объект Excel.ApplicationWithEvents из класса ExcelWithEvents. Эти два объекта должны быть связаны одной ссылкой. Вот процедура, которая в нашем примере вызывается в документе Word в тот момент, когда пользователь решит начать работу с книгами Excel:
Public Sub CreateExApp()
Const PathToMyDir = "e:\O2000\CD2000\Ch4\"
'Чистый Excel
Dim MyEx As New Excel.Application
'Excel With Events
Dim MyExWithEv As New ExcelWithEvents
'Связывание
Set MyExWithEv.xlApp = MyEx
'Добавляем новую книгу и открываем существующую
'Обработчики событий New и Open работают!
With MyEx
.Visible = True
.Workbooks.Add 1
.Workbooks.Add PathToMyDir "BookOne"
End With
End Sub
Поверьте, обработчики событий действительно вызывались, как при создании новой книги, так и открытии книги, основанной на существующем шаблоне. Осталось сказать несколько слов о том, как это все могло выглядеть, если всю эту процедуру осуществлять в самом Excel. Обратите внимание, в нашем примере Excel Application с событиями появляется до того, как открыта какая либо из его книг. Если же работать в Excel, то описание объекта с событиями и связывание необходимо поместить в первую из открываемых книг. Мы проделали эту работу и в проекте книги BookOne создали класс ExcelWithEvents, а в одном из стандартных модулей создали процедуру:
Public Sub ConnectingWithEvents()
Const PathToMyDir = "e:\O2000\CD2000\Ch4\"
'Excel With Events
Dim MyExWithEv As New ExcelWithEvents
'Связывание
Set MyExWithEv.xlApp = Excel.Application
'Добавляем новую книгу и открываем существующую
'Обработчики событий New и Open работают!
With Application
.Visible = True
.Workbooks.Add 1
.Workbooks.Add PathToMyDir "BookTwo"
End With
End Sub
Как видите, эта процедура отличается от процедуры CreateExApp лишь незначительными деталями. Возникает вопрос, как и когда запускать эту процедуру. Можно это делать по-разному, например, вызывать ее в обработчике события Click командной кнопки, можно вызывать ее в обработчике события OpenDocument. В последнем случае, обработчики событий объекта Application будут выполняться для всех книг, сразу после открытия книги BookOne.
Возможность создавать события собственных классов - это новинка Office 2000. Ранее было возможным для классов иметь только два стандартных события - Initialize и Terminate, играющих роль конструктора и деструктора по умолчанию. Теперь можно в классе создать набор событий и для каждого из экземпляров класса написать обработчики любого из этих событий. Тем самым расширяются возможности созданных Вами объектов. Это существенное продвижение в сторону классики объектного программирования. Попробуем разобраться:
Первый вопрос, который необходимо решить при создании класса с событиями, лежит вне программирования, прежде всего, нужно решить, каким должен быть набор событий у объектов класса. Если с объектом связан некоторый графический образ, то ситуация более или менее стандартна, всегда разумно иметь события Click, Move, Resize и другие, типичные для графических объектов. Эти события будут возникать, когда пользователь будет работать с графическим образом, выполняя над ним те или иные типичные действия. Во многих случаях, наши объекты могут и не иметь графического образа, но и для них не менее полезно определить события. Общие рекомендации давать трудно, поэтому обратимся к примеру и рассмотрим ранее созданный класс Личность. Какие события разумно было бы определить для объектов этого класса? Учитывая, что свойств у объектов класса не так много, число разумных событий также не велико. Давайте определим два события - ИзменениеФамилии и ДеньРождения. Первое из этих событий возникает в ответ на изменение значения свойства Фамилия, второе, - когда текущая дата отличается от даты дня рождения Личности не более чем на сутки.
Поняв, какие события должен иметь наш класс, рассмотрим, как создать набор событий в классе. Все делается очевидным и естественным образом. Достаточно в разделе объявлений модуля класса объявить каждое из событий, используя ключевое слово Event. Вот как выглядит объявление класса Личность, имеющего два предусмотренных нами события:
Option Explicit 'Класс Личность 'События класса Public Event ИзменениеФамилии(Fam As String, NewFam As String) Public Event ДеньРождения(Dat As Date) 'Далее следует уже знакомое определение свойств и методов класса 'Смотри раздел "Создание класса Личность" этой лекции
Итак, чтобы создать класс с событиями просто добавьте список этих событий в объявление класса!
Чтобы событие, связанное с объектом возникло, его нужно зажечь или возбудить ( Raise ). Зажигается событие специальной процедурой RaiseEvent, имеющей следующий синтаксис:
Sub RaiseEvent имя_события (параметры)
Эта процедура не только возбуждает событие, но и передает некоторые параметры, которые могут быть использованы в обработчике события. Что стоит за терминами "зажечь" ("возбудить") событие? При выполнении этой процедуры операционной системе посылается уведомление о том, что та должна послать объекту сообщение о возникновении события с указанным именем, в ответ на получение которого будет вызван обработчик этого события. Так что при выполнении этой процедуры плавный ход выполнения программы прерывается, происходит обмен сообщениями, вызов обработчика события и только по его завершении будет продолжено выполнение операторов, стоящих после вызова RaiseEvent.
Процедура RaiseEvent должна выполняться в одном из методов класса с событиями. Правильное определение ее места, это также одна из задач, которую нужно решить при проектировании класса, возможно, придется создавать специальный метод для этой цели. Обратимся к нашему примеру, когда и где нужно зажечь событие ИзменениеФамилии. Здесь ответ очевиден, - всякий раз, когда у объекта изменяется значение свойства Фамилия, должно возникать это событие. Но изменять значение для Private свойства следует единственным образом, - вызовом Property Let ВашаФамилия. Поэтому эта процедура именно то место, где должно возбуждаться событие. А в процедуре разумно поставить вызов RaiseEvent непосредственно перед оператором, изменяющим фамилию. Вот как выглядит эта процедура:
Public Property Let ВашаФамилия(ByVal NewValue As String)
'Зажигает событие ИзменениеФамилии
RaiseEvent ИзменениеФамилии(Фамилия, NewValue)
Фамилия = NewValue
End Property
Заметьте, мы передаем обработчику два параметра, старую и новую фамилию. Важно понимать, что можно не только передавать обработчику данные, но и получать информацию от него. Добавим еще один параметр Permission нашему событию, так чтобы обработчик события, проверив допустимость изменения фамилии, мог вернуть разрешение на изменение. Новый вариант события теперь выглядит так:
Public Event ИзменениеФамилии(Fam As String, NewFam As String, _
Permission As Boolean)
Соответственно изменится и процедура - свойство Property Let. Вот другой, более разумный вариант этой процедуры:
Public Property Let ВашаФамилия(ByVal NewValue As String)
'Зажигает событие ИзменениеФамилии
Dim Permission As Boolean 'Разрешение на изменение фамилии
Permission = True
RaiseEvent ИзменениеФамилии(Фамилия, NewValue, Permission)
'Обработчик события может запретить изменение фамилии
If Permission Then Фамилия = NewValue
End Property
Менее ясно, где и когда следует зажигать событие ДеньРождения. Мы решили возбуждать это событие в тот момент, когда пользователь интересуется датой рождения объекта, и вставили соответствующую проверку в Property Get ВашаДатаРождения процедуру:
Public Property Get ВашаДатаРождения() As Date
'Зажигает событие ДеньРождения
'в зависимости от значения текущей даты
If (Month(Now) = Month(ДатаРождения)) And _
(Abs(Day(Now) - Day(ДатаРождения)) <= 1) Then
RaiseEvent ДеньРождения(ДатаРождения)
End If
ВашаДатаРождения = ДатаРождения
End Property
Здесь событие возбуждается при выполнении некоторого условия, - текущая дата отличается от даты дня рождения не более чем на сутки. Дата рождения передается обработчику события, а тот, в свою очередь, может организовать уведомление и поздравление объекта с днем рождения, как это делает, например, Outlook для контактов из списка контактов.
Заметьте, процедура - свойство Property Get, в которой мы разместили оператор RaiseEvent, возбуждающий событие, - это не единственно возможное место для такого оператора. Вообще, одно и то же событие может возбуждаться в разных местах. В данном случае, может быть, имело смысл написать еще и специальный метод для возбуждения события, который вызывался бы пользователем в нужный момент.
Задача состоит в том, что нужно определить модуль, в котором можно объявлять объекты With Events. В окне кода этого модуля должен быть доступен
Заметьте, создание этого модуля, создание обработчиков событий не имеет непосредственного отношения к созданию класса объектов с событиями. Это другая сторона проблемы, - это проблема того, кто использует класс объектов, кто создает экземпляры этих объектов и создает обработчики событий этих конкретных объектов. Вполне возможно, что вся эта работа будет выполняться в нескольких документах, других проектах, отличных от того проекта, где создан класс объектов с событиями.
Модуль, в котором можно объявлять объекты With Events может быть либо модулем класса, либо модулем формы, но, заметьте, это не может быть стандартный модуль. В большинстве случаев предпочтение отдается модулю класса. Позже мы остановимся подробнее на ситуации, когда объекты с событиями объявляются в модуле формы. Предположим, что для решения некоторой задачи, нам потребовалось работать с двумя конкретными объектами класса Личность, - нашим старым приятелем и знакомой молодой девушкой. Для решения задачи создадим класс, который назовем " Личности " и поместим туда объявление двух объектов MyFriendOne и MyFriendTwo. Объекты объявлены со спецификатором WithEvents, указывающим на то, что они должны реагировать на события. Как только эти объявления появятся в разделе объявлений класса, сами объекты появятся в окне кода в списке объектов. Теперь, выбрав объект из списка, можно открыть список возможных событий объекта и создать
вначале заготовку обработчика событий, а потом наполнить ее содержанием.
(рис 4.5) Создание обработчиков событийЗаметьте, эта технология работы нам уже хорошо знакома. Она использовалась при создании объектов Application With Events. Теперь становится понятным, что тогда шла речь о частном случае. Помните, при создании класса ExcelWithEvents, говоря о его структуре, мы обращали внимание на то, что в классе объявлен всего лишь один объект с событиями. Теперь становится понятным, это отражало специфику частного случая, - приложение, обычно, существует в единственном экземпляре. Личностей же, реагирующих на события, может быть много, - для каждой из них нужно задать свой экземпляр объекта в классе, чтобы была возможность написать индивидуальный обработчик события. Взгляните на реализацию класса Личности:
Option Explicit
Private WithEvents myFriendOne As Личность
Private WithEvents myFriendTwo As Личность
Private Sub myFriendOne_ДеньРождения(Dat As Date)
BirthDay (Dat)
End Sub
Private Sub myFriendOne_ИзменениеФамилии(Fam As String, _
NewFam As String, Permission As Boolean)
MsgBox ("Изменение фамилии " Fam " на " NewFam Chr(13) _
"не разрешается.")
Permission = False
End Sub
Private Sub myFriendTwo_ДеньРождения(Dat As Date)
BirthDay (Dat)
End Sub
Private Sub myFriendTwo_ИзменениеФамилии(Fam As String, _
NewFam As String, Permission As Boolean)
MsgBox ("Поздравляю с замужеством, дорогая " _
Fam "-" NewFam "!")
Permission = True
End Sub
Public Sub BirthDay(Dat As Date)
Debug.Print Dat, "-", Now
Select Case Day(Dat)
Case Day(Now)
MsgBox ("Сегодня День Рождения!")
Case Is < Day(Now)
MsgBox ("Вчера был День Рождения!")
Case Else
MsgBox ("Завтра День Рождения!")
End Select
End Sub
Public Sub Connect()
Set myFriendOne = FriendOne
Set myFriendTwo = FriendTwo
End Sub
Как видите, в этом классе два Private объекта, реагирующих на события, и для каждого из них написаны по два обработчика событий класса. Сейчас не суть важно, что делают эти обработчики. В одном случае обработчик события ИзменениеФамилии разрешает это изменение, в другом - нет, и выдает по этому поводу, естественно, разные сообщения. Обработчики события ДеньРождения выполняют одну и ту же задачу, поэтому они вызывают общую процедуру работы. Это, хотя и интересные, но все-таки детали. Более важную для понимания играет роль процедура, названная Connect.
Особенностью рассматриваемой технологии работы с объектами, реагирующими на события, является то, что существует пара объектов - двойников. Один из них, реальный объект, объявляется как объект без событий, другой, его двойник, объявляется в специальном классе с описателем With Events. Для двойника создаются обработчики событий, но, чтобы все это заработало нужным образом, эту пару объектов нужно связать законными узами брака. Так мы поступали при работе с объектами Excel.Application и Excel.Application WithEvents. Также мы поступили в процедуре Connect с двумя парами объектов - двойников: MyFriendOne - FriendOne и MyFriendTwo - FriendTwo.
Остался заключительный штрих в завершение разговора о том, как создаются классы и как ведется работа с объектами, реагирующими на события. Нужно еще рассказать о создании реальных объектов, двойники которых описаны в специальном модуле класса, в нашем примере - в классе Личности. Эти реальные объекты, обычно, находятся в стандартном модуле. В какой -то момент своей жизни, как правило, в самом начале появления, они связываются с двойниками и после этого могут реагировать на события. События инициируются в процессе работы с этими объектами, при вызове некоторых из их методов. Какие из методов класса зажигают события, - этот вопрос решается при проектировании класса, а в какой момент метод будет вызываться, - это прерогатива пользователя, работающего с объектами.
Для класса Личность мы уже описали методы, зажигающие события. Поэтому нам осталось только привести описание стандартного модуля Примеры, в котором ведется работа с двумя объектами, названными FriendOne и FriendTwo:
Option Explicit
'Модуль Примеры
Public FriendOne As New Личность
Public FriendTwo As New Личность
Public FOne As New Личности
Public Sub Знакомство()
'Вызывается конструктор с параметрами
'и происходит знакомство с объектами
FriendOne.InitPerson FN:="Станислав", LN:="Федотов", _
DOB:="21.05.39"
FriendTwo.InitPerson FN:="Катя", LN:="Павлова", _
DOB:="22.03.79"
FriendOne.PrintPerson
FriendTwo.PrintPerson
FriendOne.SayWhoIs
FriendTwo.SayWhoIs
'Связывание с двойниками.
'Теперь объекты могут реагировать на события!
FOne.Connect
End Sub
Public Sub CallEvents()
Dim DOB As Date
'Вызов методов приведет к возникновению событий!
'При замене фамилии возникнет событие ИзменениеФамилии
'Заметьте, не всегда фамилия будет изменена!
FriendOne.ВашаФамилия = "Фидотов"
FriendTwo.ВашаФамилия = "Волконская"
'При попытке узнать дату рождения
'может быть вызван обработчик события ДеньРождения.
DOB = FriendOne.ВашаДатаРождения
DOB = FriendTwo.ВашаДатаРождения
FriendOne.PrintPerson
FriendTwo.PrintPerson
End Sub
В этом модуле объявлены два объекта класса Личность и один объект класса Личности. Две процедуры - Знакомство и CallEvents вызываются в ответ на щелчки командных кнопок в соответствующих обработчиках событий. Первой должна вызываться процедура Знакомство, где объекты FriendOne и FriendTwo получают "нормальные" значения и происходит Знакомство с ними. Важно, что здесь же происходит связывание этих объектов с их двойниками. Обратите внимание, нам понадобился объект FOne класса Личности, чтобы вызвать его метод Connect и связать эти пары объектов. В процессе работы этих модулей будет вестись диалог с пользователем. На следующих рисунках показаны сообщения, выдаваемые обработчиками событий ИзменениеФамилии и ДеньРождения.

(рис 4.7) Обработчик события ИзменениеФамилии объекта One(рис 4.6) Обработчик события ИзменениеФамилии объекта Two
(рис 4.8) Обработчик события ДеньРождения объекта TwoПриведем еще результаты отладочной печати:
Станислав Федотов родился 21.05.39 Катя Павлова родилась 22.03.79 22.03.79 - 22.03.99 13:14:01 Станислав Федотов родился 21.05.39 Катя Волконская родилась 22.03.79
В заключение, коротко опишем всю картину создания класса и работы с объектами, реагирующими на события. Прежде всего, нужно создать сам класс объектов, в котором предусмотрен набор событий, возможных для объектов класса. В методах класса следует предусмотреть возможность возбуждения событий. На следующем этапе следует создать класс, в котором объявлены объекты WithEvents. В этом классе создаются обработчики событий для каждого экземпляра класса. Кроме этих объектов - двойников создаются и "реальные" объекты, как правило, в стандартном модуле. После того, как происходит связывание двойников, объекты могут реагировать на события. Всякий раз, когда объект вызывает метод класса, в котором возбуждается событие, это приводит к тому, что вызывается обработчик события. Можно передавать и получать информацию от обработчика событий.
Одно из возможных применений этой технологии работы состоит в том, что классы с событиями могут выступать как упаковка для элементов управления. Цель такого класса состоит в расширении свойств элемента управления и, в частности, в добавлении новых событий, на которые мог бы реагировать этот элемент управления. Надеемся, что данное описание позволит Вам самостоятельно построить такое приложение. Заметим только, что в таких случаях используется модуль формы, для объявления объектов WithEvents и создания обработчиков событий этих объектов.
Программный код большинства примеров данной лекции можно найти в проектах, доступных для просмотра: BookOne, BookTwo, DocOne, DocTwo.
Класс является обобщением понятия типа данных и задает свойства и поведение объектов класса - экземпляров класса. Каждый объект принадлежит некоторому классу. Отношение между объектом и его классом такое же, как между переменной и ее типом. Класс - это объединение данных и обрабатывающих их процедур и функций. Данные называются также переменными класса, а процедуры и функции - методами класса. Переменные определяют свойства объекта, а совокупность их значений - состояние объекта. Наряду со свойствами и методами с классом связывается еще одно понятие - события. Каждый класс имеет определенный набор событий, которые могут возникать при работе с объектами класса, - чаще всего при определенных действиях пользователя, иногда, как результат действия системы. При возникновении события, связанного с тем или иным объектом, система посылает сообщение объекту, которое может быть обработано методом - обработчиком события, специально созданным при конструировании объекта. События обеспечивают большую гибкость при работе с объектами. Методы класса выполняются одинаково для всех объектов класса, а на события каждый объект реагирует индивидуально, поскольку имеет собственный обработчик события.
"Программирование в классах" является основным способом работы современного программирования.
За примерами далеко ходить не нужно. Так построена операционная система Windows, - окно являются ее основным объектом. Сам Office 2000 полностью построен на классах и работе с объектами этих классов, - здесь все, начиная от приложения и кончая отдельным символом, рассматривается как объект некоторого класса. Профессиональный прикладной программист, работающий в некоторой проблемной области и решающий разнообразные задачи из этой области, как правило, начинает с создания классов, описывающих специфику данной проблемной области. Затем уже решение тех или иных специальных задач он описывает в терминах работы с объектами данной проблемной области.
VBA позволяет программисту создавать собственные классы. Синтаксически класс представляет отдельный модуль специального вида - модуль класса. Мы постараемся сейчас детально и на примерах разобраться во всех особенностях этой важной конструкции.
Тема классов находится на магистральном пути развития Офисной среды. По сравнению с предыдущей версией сделаны три важных нововведения:
Instancing, регулирующее способы взаимодействия с классами в системе документов других проектов.Все эти возможности будут подробно рассмотрены в этой и следующей лекции. А начнем мы с примера создания собственного класса.
В предыдущей лекции о типах данных мы ввели пользовательский тип Person. Там же мы показали, что прямой доступ к полям записи позволяет программисту свободно работать с данными этого типа. И все же Person в чем-то "ущербен"... А не хватает ему собственных операций: если бы наши переменные Петров и Козлов имели тип, например, Integer, можно было бы их сравнивать, складывать и умножать, присваивать значения и многое другое. Понятно, что умножение данных типа Person вряд ли разумно, но зато для него можно определить свои операции. Вот, например, какие операции можно определить для этого типа:
InitPerson - инициализация полей записи;PrintPerson - печать полей записи;CopyPerson(Source As Person) - копирование источника (записи Source ) - аналог оператора присвоения;WhoIs - более специфическая операция, с определенной достоверностью определяющая пол, анализируя имя и фамилию.Всякий раз при определении пользовательского типа, так или иначе, но следует определить операции над данными этого типа. Естественно собрать определение типа и операции над ним в одном месте. Такое объединение представляет уже почти полное, с точностью до событий определение типа. Для типов, определенных подобным образом, введен новый термин - класс .
Синтаксически классы в VBA оформляются в виде модуля класса . Поэтому начинать создание класса в Редакторе Visual Basic нужно с выбора в меню Insert пункта Class Module. Этот модуль имеет такую же структуру, как и стандартный модуль, о котором мы подробно уже рассказали. Модуль состоит из двух разделов - объявлений и методов. В первом из них естественным образом описываются свойства класса, а во втором - его методы. И здесь действуют спецификаторы области действия Public и Private. Public - свойства и Public -методы составляют интерфейс класса. Только к этим свойствам и методам можно обращаться при работе с объектами класса, объявленными в других модулях, где класс является видимым.
Мы расширили ранее созданный тип Person до класса, названного нами Личность, путем добавления операций над данными класса. Часть из этих операций уже упомянута, другие ясны из контекста. Нам кажется разумным привести с самого начала полное описание класса, а потом уже, когда полная картина ясна, переходить к деталям. Создав модуль класса " Личность, мы поместили туда следующий текст:
Option Explicit
'Класс Личность
'Свойства класса: имя, отчество, фамилию, дату рождения
'закроем от прямого доступа,
'получить и изменить их можно только через методы класса
Private Имя As String
Private Отчество As String
Private Фамилия As String
Private ДатаРождения As Date
Public Sub InitPerson(ByVal FN As String, ByVal LN As String, _
ByVal DoB As Date)
'Инициализация личности
Имя = FN
Фамилия = LN
ДатаРождения = DoB
End Sub
Public Sub PrintPerson()
'Печать в отладочном окне Immediate
Dim S As String
If WhoIs Then S = "родилась" Else S = "родился"
Debug.Print Имя, Отчество, Фамилия, S, ДатаРождения
End Sub
Public Sub CopyPerson(You As Личность)
Имя = You.ВашеИмя
Фамилия = You.ВашаФамилия
ДатаРождения = You.ВашаДатаРождения
End Sub
Public Function WhoIs() As Boolean
'Пытается определить пол личности, анализируя имя и фамилию
'Возвращает True, если думает, что имеет дело с женщиной.
Dim F1 As Boolean, F2 As Boolean
F1 = ПоследняяБуква(Имя) = "А" Or ПоследняяБуква(Имя) = "Я"
F2 = ПоследняяБуква(Фамилия) = "А" Or ПоследняяБуква(Фамилия) = "Я"
If F1 And F2 Then
'можно полагать, что наша Личность - женщина
WhoIs = True
ElseIf Not F1 And Not F2 Then
WhoIs = False
Else 'Есть сомнения
If Отчество = "" Then
Отчество = InputBox(Имя " " Фамилия _
"! " "Назовите отчество, пожалуйста.")
End If
WhoIs = ПоследняяБуква(Отчество) = "А"
End If
End Function
Public Sub SayWhoIs()
' Вывод сообщения о поле и возрасте личности
If WhoIs Then
MsgBox ("Думаю," Имя _
", Вы из прекрасной половины человечества!")
ElseIf Year(ДатаРождения) > 1967 Then
MsgBox ("Думаю, " Имя ", Вы - молодой человек!")
Else
MsgBox ("Думаю, " Фамилия ", - мужчина!")
End If
End Sub
Private Function ПоследняяБуква(ByVal W As String) As String
'Внутренняя функция: возвращает в верхнем регистре
'последнюю букву слова W
ПоследняяБуква = UCase(Right(W, 1))
End Function
Public Property Get ВашеИмя() As String
ВашеИмя = Имя
End Property
Public Property Let ВашеИмя(ByVal vNewValue As String)
Имя = vNewValue
End Property
Public Property Get ВашеОтчество() As String
ВашеОтчество = Отчество
End Property
Public Property Let ВашеОтчество(ByVal vNewValue As String)
Отчество = vNewValue
End Property
Public Property Get ВашаФамилия() As String
ВашаФамилия = Фамилия
End Property
Public Property Let ВашаФамилия(ByVal NewValue As String)
Фамилия = NewValue
End Property
Public Property Get ВашаДатаРождения() As Date
ВашаДатаРождения = ДатаРождения
End Property
Public Property Let ВашаДатаРождения(ByVal NewValue As Date)
ДатаРождения = NewValue
End Property
Private Sub Class_Initialize()
Имя = "Адам"
Фамилия = "Человек"
ДатаРождения = #1/1/100#
End Sub
Прокомментируем этот довольно длинный текст.
Личность совпадают с полями типа Person. Кстати, мы сделали все свойства закрытыми ( Private ), и теперь вне класса нет прямого доступа к его полям (свойствам).Личность добавлено 5 общих методов: InitPerson, PrintPerson, CopyPerson, WhoIs , SayWhoIs и по паре методов Get и Let на каждое закрытое свойство. В классе есть и закрытая для внешнего использования функция " ПоследняяБуква ", и обработчик события Initialize.Init в том или ином виде должен быть определен в каждом классе. Это первый вызываемый метод объекта. Прежде чем начать работу с объектом, его нужно инициализировать. В нашем классе эту работу и делает метод InitPerson.Print также присутствует почти в каждом классе - нужно же распечатать информацию об объекте! - в PrintPerson вызывается метод WhoIs .CopyPerson - еще один общий, часто необходимый метод, позволяющий реализовать настоящее присвоение, когда копируется не ссылка, а значения полей класса, что позволяет иметь не две ссылки на один объект, а два идентичных объекта.WhoIs - метод, специфический для нашей задачи. Это попытка определить пол по имени и фамилии. Применяется примитивный, но обеспечивающий высокую достоверность для русских имен и фамилий алгоритм. По ходу дела потребовалось ввести вспомогательную функцию " ПоследняяБуква ".SayWhoIs вызывает WhoIs , дополнительно определяет возраст (только для мужчин) и выводит соответствующее сообщение в окно Message.ПоследняяБуква возвращает в верхнем регистре последнюю букву слова. Эта задача решается двумя вызовами функций работы со строками Right и UСase. Первая возвращает "хвост" слова, вторая - преобразует результат в верхний регистр.Get и Let. Вообще говоря, свойства можно не закрывать, а специальные методы не вводить. Но VBA позволяет следовать традициям объектно-ориентированного программирования, согласно которым считается правильным не давать возможности непосредственного изменения свойств, поскольку иногда это может привести к некорректному состоянию объекта. Поэтому доступ к свойствам закрывается, для чего достаточно объявить их с атрибутом Private, но зато вводятся специальные методы Get (для получения значения свойства) и Let (для изменения значения на новое). Заготовки для этой пары свойств строятся автоматически, если Вы при вставке метода указали (пометив флажок Property ), что он должен быть свойством.Initialize и Terminate. Первое возникает при первоначальном обращении к объекту, второе - по окончании работы с ними. В нашем примере в методе Initialize даем объекту инициализацию, восходящую к "Адаму".Завершим изложение примером работы с объектами класса, его методами и свойствами:
Public Sub Знакомство()
Dim UserOne As New Личность
Dim UserTwo As New Личность
Dim UserThree As New Личность
Debug.Print UserOne.ВашеИмя
UserOne.InitPerson FN:="Петр", LN:="Петров", DoB:=#1/23/1968#
UserTwo.InitPerson FN:="Анна", LN:="Козлова", DoB:=#7/21/1968#
UserOne.PrintPerson
UserTwo.PrintPerson
UserOne.SayWhoIs
UserTwo.SayWhoIs
UserTwo.ВашаФамилия = UserOne.ВашаФамилия "а"
Debug.Print UserOne.ВашаФамилия
Debug.Print UserTwo.ВашаФамилия
UserThree.InitPerson FN:="Анна", LN:="Керн", DoB:=#5/17/1803#
UserThree.PrintPerson
UserThree.SayWhoIs
End Sub
Вот какие результаты отладочной печати будут выданы в окно Immediate (отладки):
Адам Петр Петров родился 23.01.68 Анна Козлова родилась 21.07.68 Петров Петрова Анна Петровна Керн родилась 17.05.1803
А такие окна сообщений появятся на экране, при уточнении данных о личности Анны Керн:

(рис 4.2) Отчество Анны Керн(рис 4.1) Кто Анна?
Попробуем обобщить сведения об объектах и переменных. Начнем с того, что у них общего и что их различает. Если есть описания:
Dim X As T, Y As T1
то без контекста понять нельзя, что есть X и Y - "обычные" переменные или объекты. Например, если T - тип, заданный пользователем, а T1 - определенный им класс, то X - это переменная, а Y - объект. С нашей точки зрения, класс и тип - понятия, если не эквивалентные, то близкие по смыслу. Также близки понятия объекта и переменной. Класс - это специальная форма определения типа. Если есть тип T и класс T1, то можно объявить произвольное число экземпляров типа T и класса T1. Экземпляры типа T называются переменными, класса T1 - объектами. Класс задает свойства, методы и события своих объектов. Тип всегда задает свойства, неявно - методы, но никогда - события.
В объявлении: Dim Y As T1, где T1 - класс, объект Y мы часто называем переменной, говоря, что она имеет тип Object. Переменные типа Object рассматриваются как ссылки, задающие адрес памяти, где хранится объект. У них фиксированная длина 4 байта. При объявлении такой переменной память для самого объекта может и не отводиться в отличие от обычных переменных, поэтому не определено и значение ссылки. Задать ссылку - связать переменную с объектом - можно двумя путями:
Есть два способа связывания: раннее и позднее . При позднем связывании переменная объявляется так:
Dim V As Object
Это объявление говорит о том, что переменная является объектом (ссылкой), но ничего о классе этого объекта - он может быть произвольным, и выяснится это только динамически при выполнении программы, когда переменная V будет связываться с только что созданным или существующим объектом того или иного класса. Поэтому такое связывание называется поздним, или динамическим. Естественно, что необъявленные переменные или те, которым тип при объявлении не задан (значит, у них по умолчанию тип Variant ), допускают только позднее связывание. При раннем связывании в момент объявления указывается класс объекта, например:
Dim Петров As Личность, Козлов As Личность
Это позволяет еще на этапе трансляции проверять, допустимы ли те или иные операции над объектами Козлов и Петров. За исключением весьма сомнительного преимущества позднего связывания - возможностью связаться с объектом любого типа, ранее связывание предпочтительнее во всех отношениях. Для программиста особенно важно, что при раннем связывании, когда задан класс объекта, в процессе работы с этим объектом идет подсказка о его свойствах и методах. Подсказка облегчает жизнь программиста, и лишаться ее не стоит. Поэтому наш совет - выбирайте раннее связывание.
Теперь о том, как создаются новые объекты, и как происходит связывание с объектами, уже существующими. Для удобства разделим все объекты на три группы:
Личность, созданного в этой лекции;ActiveX и Com AddIns -объекты, в частности, объекты других приложений Office 97 при их подключении к исходному приложению. Например, в Word можно подключить объекты Excel, а документы Word включить в рабочие листы Excel.Понятно, что при работе с такими объектами хотя бы часть из них необходимо первоначально создать, и делается это при их объявлении:
{Dim | Private | Public | Static }<имя переменной> As New <имя типа>
Спецификатор New указывает, что в момент объявления нужно создать новый объект, то есть выделить ему память. В этот же момент ссылка на объект получает конкретное значение. Выделение памяти еще не означает инициализации значений свойств объекта. Инициализацию можно задать при определении события Initialize или в специально построенном методе Init, который следует запускать в начале работы с объектом. Вот пример создания объекта с последующей его инициализацией:
Public Sub MyCreateObject()
'Нельзя создавать объекты собственного класса, используя CreateObject
Dim Nemo As Личность
'Set Nemo = CreateObject("Личность")
'Можно их создавать, используя New
Set Nemo = New Личность
Nemo.InitPerson FN:="Prince", LN:="Dakar", DoB:=#1/23/1838#
Nemo.PrintPerson
End Sub
Только метод New позволяет создать новый объект для классов, определенных программистом. Для этой цели нельзя, например, использовать метод CreateObject. Но, заметьте, спецификатор New не является обязательным, его можно опускать при объявлении таких переменных класса, которые в дальнейшем получат значение, благодаря ссылке на другие, ранее созданные объекты. Задать ссылку на существующий объект нельзя обычным оператором присвоения - для этого в VBA есть специальный оператор Set:
Set <имя переменной - объекта> = {<объектное выражение>| Nothing }
Set - специально выделенный, частный случай оператора присвоения, в его левой части стоит имя переменной, определенной как объект при объявлении, а в правой - некоторое выражение, значение которого - ссылка на объект. В итоге переменная левой части получает значение выражения правой части. Вот пример некоторого объявления объекта Q. Следующие строки мы добавили в конец предыдущей процедуры:
Dim Q As Личность Set Q = Nemo Q.PrintPerson
В результате будет отпечатан следующий текст:
Prince Dakar родился 23.01.1838
Каждый VBA-проект погружен в "родное" приложение Office 97, объекты которого доступны в проекте. Для создания таких объектов не используется ни спецификатор New, ни метод CreateObject. Новые объекты, если и создаются, то специальными методами своего класса. Например, метод Add позволяет добавлять элементы в коллекции. При работе с объектами этой группы объявляются переменные соответствующего класса, затем они связываются с уже существующими объектами. Связывание выполняет оператор Set. Все примеры из первой лекции книги посвящены работе с объектами этой группы. Напомним тот, где участвует один из наиболее употребительных объектов - Range:
Public Sub FirstComment()
Dim myRange As Range
'Добавление комментария
With ActiveDocument
Set myRange =.Paragraphs(1).Range
.Comments.Add myRange, "Эта лекция рассказывает о классах объектов"
'Передвигается объект Range
myRange.Move Unit:=wdParagraph, Count:=2
End With
End Sub
Здесь вначале объявляется объект myRange класса Range, что обеспечивает раннее связывание. Объектное выражение в правой части оператора Set возвращает ссылку на существующий объект (первый параграф активного документа), и эта ссылка становится значением myRange. А затем с этим объектом можно работать, используя свойства и методы соответствующего класса, в данном случае - Range. Его можно использовать, например, в методе Add при работе с коллекцией комментариев или вызвать метод Move для изменения области, отведенной объекту.
Технология ActiveX обеспечивает взаимодействие приложений - Автоматизацию (Automation), при которой одно приложение управляет работой другого. На этой технологии построено взаимодействие приложений Office 97. И хотя у нас уже были примеры на эту тему, позволим привести еще один.
Чтобы начать работу с ActiveX -объектом, нужно объявить соответствующую переменную, создать сам объект и связать переменную с объектом. Один из способов выполнения этой работы - использовать спецификатор New в операторе объявления переменных, который в этой ситуации имеет вид:
Dim <имя переменной> As New <имя приложения.имя класса>
По сравнению с обычной конструкцией оператора объявления здесь более сложно задается тип (класс) создаваемого объекта. Первая часть - имя_приложения - указывает приложение, создающее объект, и в то же время это имя библиотеки типов данного приложения - TypeLib. Приложение является сервером, который вызывается для обеспечения работы с объектом. Вторая часть - имя_класса - задает тот конкретный класс, определенный в сервере, экземпляр ActiveX -объекта которого будет создан. Библиотека типов подключаемого приложения должна быть видимой, для чего надо включить ссылку на нее в меню References. Повторим с небольшими вариациями наш старый пример, в котором Excel подключается в приложении Word:
Public Sub WorkWithActiveX()
'Создание объекта Excel.Application при работе с документом Word
Dim xlApp As Excel.Application
'Set xlApp = CreateObject("Excel.Application.9")
Set xlApp = New Excel.Application
xlApp.Application.Visible = True
'Добавить новую книгу
xlApp.Workbooks.Add
'Теперь можно работать и с ячейками данной книги
xlApp.Workbooks(1).Activate
xlApp.Range("A1:A2") = 2
xlApp.Range("B1") = "=A1+A2"
xlApp.Range("B2") = "=A1*A2"
'Закрываем открытую книгу без сохранения изменений
xlApp.Workbooks(1).Close SaveChanges:=False
'Закрываем приложение
xlApp.Quit
End Sub
Создание Excel приложения в данном случае возможно двумя способами - применением конструкции New или функции CreateObject. Однако, использование New для создания ActiveX -объектов не всегда возможно. Не все приложения допускают такой способ подключения, и не все объекты доступны, - например, недоступны внутренние ActiveX -объекты, которые совместно могут храниться в одном и том же файле. Более универсальный способ создания ActiveX -объектов - вызов специальных функций CreateObject и GetObject. Как правило, вызов этих функций помещается в правую часть оператора Set. В результате его выполнения объектное выражение возвращает ссылку на созданный объект, которая и становится значением переменной.
Синтаксис функции CreateObject таков:
CreateObject(<класс>)
Параметр класс - строка, задающая класс объекта, у которой две обязательные части: "имя_приложения.имя_класса"; ее вид совпадает с видом задания класса ActiveX -объектов в операторе объявления.
Функция GetObject имеет дополнительный параметр:
GetObject(<путь>, <класс>)
Параметр путь задает полный путь к файлу, содержащему ActiveX -объект. Этот объект и активизируется. Второй параметр при этом необязателен. Но если файл содержит группу ActiveX -объектов разных классов, задать его необходимо. Можно опустить и первый параметр, если ActiveX -объект этого класса уже создан, и тогда возвращается ссылка на GetObject. Обратите внимание, книга создается без предварительного создания объекта Application:
Public Sub WorkWithGetObject()
Const MY_PATH As String = "e:\O2000\CD2000\Ch4\"
Dim myBook As Excel.Workbook
Set myBook = GetObject(MY_PATH "BookOne.xls")
With myBook
.Application.Visible = True
'Чтобы увидеть книгу, включите ее в окне UnHide из меню Window
.Charts("Динамика Продаж").Activate
'Посмотрев на диаграмму, переключимся на рабочий лист
MsgBox ("Вы можете перейти в Excel")
.Worksheets(1).Activate
.Worksheets(1).Range("A40") = 777
.Save
.Application.Quit
End With
End Sub
Мы начали эту лекцию с примера создания класса " Личность " и привели полное его описание, что позволило целиком охватить всю картину. Теперь разберем детали построения класса. Шаг за шагом построим еще один простой класс Rational, определяющий рациональные числа. Начнем построение класса, как обычно, с комментария, содержательно описывающего назначение класса, его свойства и поведение. Вот комментарий, к нашему классу:
' Класс Rational 'Определяет новый тип данных - рациональные числа и основные 'операции над ними - сложение, вычитание, умножение и деление. 'Рациональное число задается парой целых чисел (m,n) и изображается, 'обычно, в виде дроби m/n. Число m называется числителем, а 'n - знаменателем. Для каждого рационального числа существует 'множество его представлений, например, - 1/2, 2/4, 3/6, 4/8, … 'задают одно и тоже рациональное число. Среди всех представлений 'можно выделить то, в котором числитель и знаменатель несократимы. 'Именно такие представители будут храниться в нашем классе. 'Операции над рациональными числами определяются естественным 'образом. Лучшим их описанием будут соответствующие им методы.
После комментария следует описание переменных, задающих свойства класса. Это могут быть терминальные свойства, заданные обычными переменными VBA, как, например, Имя, Фамилия в классе " Личность ", так и свойства - участники. Напомним, что свойства - участники это объекты других классов. Без них не обойтись, если мы строим семейство классов. Помните, что в Office 2000 нет наследования классов в классическом понимании, - его заменяет встраивание. Так что при построении семейства классов, приходится иметь дело с "толстыми" объектами, свойства которых являются объектами, имеющими свойства, являющиеся объектами и так далее. Взгляните еще раз на рисунок 1.1 первой лекции и просмотрите ее раздел "встраивание против наследования", имеющиеся там примеры поясняют ситуацию.
В разбираемом нами случае речь не идет о семействе классов, поэтому класс Rational имеет только терминальные свойства. Пояснять, что это за свойства нет необходимости. Вот их определение:
'Свойства класса Rational Private m As Integer 'числитель Private n As Integer 'знаменатель
Возникает вопрос, почему свойства объявлены с описателем Private, а не Public Заметьте, таким же образом мы описывали свойства и в классе " Личность ". Такова общепринятая практика объектно-ориентированного программирования. Если бы свойства имели атрибут Public, - были бы открытыми, тогда при работе с объектом, можно было бы иметь к ним прямой доступ, как при чтении, так и записи. Но такая свобода, как правило, недопустима. Вот возможные стратегии при работе со свойствами:
Read - Write ).Read, Write-once ).Read-only ).Write-only ).Not Read - Not Write ), - свойство закрыто.Открытость свойств не позволяет реализовать только первую из стратегий. В нашем примере с классом Rational открытость свойств недопустима, поскольку числитель и знаменатель преобразуются скрытым от внешнего пользователя способом, позволяющим всегда задавать единственного представителя рационального числа.
При задании свойств делайте их закрытыми.
Для доступа к закрытым свойствам и реализации перечисленных выше стратегий работы с ними предложены специальные методы - свойства Property Let, Set и Get, примеры их мы видели в классе " Личность ", а подробный разговор еще предстоит.
Следующим шагом в создании класса является разработка его конструкторов и деструктора. Напомним, что новый объект - экземпляр класса создается конструкцией New, например:
Dim MyRationalNumber As New Rational
В этот момент:
MyRationalNumber,Rational,В языках объектного программирования, как, например, в языке C++ конструкторов может быть несколько, среди которых выделяется конструктор без параметров, являющийся конструктором по умолчанию. Остальные конструкторы имеют параметры, позволяющие задать свойства объекта в момент инициализации. Важно понимать роль конструкторов особенно в ситуации, когда создается "толстый" объект. В момент создания такого объекта будет вызвана цепочка конструкторов, начиная с самого внутреннего, создающего самый внутренний объект, затем будет создан охватывающий объект, пока не будет вызван внешний конструктор, создающий объект - оболочку. В таком конструкторе приходится задавать параметры для инициализации всех внутренних объектов.
В VBA все проще. Во многом это объясняется тем, что здесь, в отличие от многих других языков программирования, есть разумная стратегия начальной инициализации переменных, - мы о ней говорили ранее. Поэтому здесь есть только конструктор по умолчанию - конструктор без параметров, да и тот часто не определяется, полагаясь на стандартную инициализацию. Конечно же, инициализировать объект "настоящими" значениями все равно придется в какой-то момент. Поэтому всегда для класса создаются свои конструкторы, синтаксически являющиеся методами, - их может быть несколько. В классе " Личность " таким конструктором с параметрами является метод InitPerson. Заметьте, мы могли бы определить еще один конструктор, который в отличие от первого, проводил бы полную инициализацию всех свойств, включая Отчество нашей личности.
Деструктор вызывается автоматически при уничтожении объекта. В VBA нет динамического уничтожения объекта в момент, определенный программистом, объекты уничтожаются также как и переменные, при выходе из области их действия. Поэтому деструктор, как правило, не пишется.
Говоря о конструкторах и деструкторах, мы не сказали главного. Роль конструктора по умолчанию в классах VBA играет обработчик события Initialize. Это общее для многих объектов событие, встречается, когда объект загружается, для объектов - экземпляров классов оно возникает при создании объекта. У обработчика этого события нет параметров, поэтому он играет роль конструктора по умолчанию, не имеющего параметров. Роль деструктора играет обработчик события Terminate. Он вызывается, когда все ранее установленные ссылки на экземпляр объекта получают значение Nothing или все указатели перестают существовать, выйдя из области своего определения. Заметьте, что при ненормальном завершении программы это событие не возникает. Обработчик этого события (деструктор) пишется значительно реже, поскольку в момент его вызова объект и так корректно будет уничтожен.
Вернемся теперь к нашему классу и определим для него конструктор по умолчанию и "настоящий" свой конструктор:
'Конструкторы класса Rational
Private Sub Class_Initialize()
'Конструктор по умолчанию
'инициализирует рациональное число дробью 1/1
m = 1
n = 1
End Sub
Public Sub CreateRational(ByVal a As Integer, ByVal b As Integer)
'Собственный конструктор
'Выполняет довольно сложные действия,
'прежде чем свойства получат значения
Dim d As Integer 'Наибольший общий делитель a и b
If b = 0 Then
MsgBox " Ошибка при создании рационального числа!" _
Chr(13) "Знаменатель не должен равняться 0."
Else
' приведение знака
If b < 0 Then
b = -b: a = -a
End If
' приведение к несократимой дроби
d = nod(a, b) ' d - НОД(a,b)
m = a \ d
n = b \ d
End If
End Sub
' Скрытая функция вычисления НОД(m,n)
Private Function nod(ByVal m As Integer, ByVal n As Integer) As Integer
Dim p As Integer
m = Abs(m): n = Abs(n)
If n > m Then
p = m: m = n: n = p
End If
Do
p = m Mod n: m = n: n = p
Loop Until n = 0
nod = m
End Function
Обратите внимание, конструктор по умолчанию, являясь обработчиком события Initialize, строился, как обычный обработчик события, используя стандартную заготовку. Он, естественно, является закрытым, поскольку вызывается только системой и не доступен для обычного вызова. Наш собственный обработчик открыт. Он выполняет довольно сложную работу и не сводится к простому присвоению свойств объекта. Это довольно типичная ситуация. По ходу дела пришлось написать вспомогательную функцию вычисления наибольшего общего делителя. Поскольку она носит служебный характер, то она закрыта и не доступна для вызова при работе с объектами класса Rational.
Для того чтобы можно было получить доступ к закрытым свойствам, предусмотрены специальные процедуры - свойства:
Property Let позволяет установить новое значение терминального свойства, выполняя операцию Write (присваивание).Property Set выполняет те же действия, что и предыдущая процедура, но применима к свойствам - участникам. Вы понимаете, что в VBA присваивание значений обычным переменным и объектам выполняется двумя различными операторами - Let и Set.Property Get является дополнительной к предыдущим процедурам, выполняя операцию чтения Read. Она применима к терминальным свойствам и свойствам - участникам.Рассмотрим пять ранее описанных стратегий применения свойств. В первой стратегии Read - Write каждому закрытому свойству будет соответствовать пара взаимно дополняющих процедур - свойств Let ( Set ) - Get. Эту стратегию мы реализовали для свойств класса " Личность ". Стратегия Write-once предполагает, что значение свойства должно быть записано только при первом обращении, а далее не должно изменяться. Для реализации этой стратегии процедуры - свойства Let и Set должны включать в себя код, осуществляющий специальную проверку, проводилось ли ранее присвоение значения свойству. Для объектов это проверка типа " value Is Nothing ", для переменных типа Variant - " value = Empty " и так далее. Мы не станем приводить подробный пример, подобную проверку мы осуществляли при работе со свойством Отчество в процедуре класса " Личность. В следующих двух стратегиях используется только одна из процедур пары, либо Get, либо Let ( Set ). Пятая стратегия предполагает, что доступ к свойствам закрыт, и процедуры - свойства Get, Let ( Set ) в классе вообще не объявляются.
Обратим внимание на одну важную особенность, когда свойство имеет тип Variant, его значениями могут быть как обычные данные (числа, строки), так и объекты. В этой ситуации необходимо реализовать все три процедуры, применяя Let или Set в зависимости от того, с чем мы имеем дело - объектом или переменной.
В классе Rational есть два закрытых свойства, заданных переменными m и n и определяющими соответственно числитель и знаменатель дроби, представляющей рациональное число. Зададим для каждого из этих свойств пару Property Let - Property Get с именами Числитель и Знаменатель соответственно. Для создания процедур - свойств обычной практикой является использование заготовок, создаваемых автоматически. Выбрав в Редакторе Visual Basic пункт меню Insert | Procedure, мы задали в появившемся диалоговом окне Add Procedure значение Property для типа процедуры. Задав еще имя процедуры - свойства " Числитель ", и, щелкнув OK, мы получили две стандартные заготовки Property Let и Property Get.
(рис 4.3) Создание процедур - свойствПовторив эти действия, мы получили вторую пару заготовок с именем " Знаменатель ". Заготовки затем наполняются, как обычно, содержанием и слегка модифицируются. В заготовках предусмотрен общий тип Variant для параметра в Property Let и возвращаемого значения в Property Get. Разумно изменить этот общий тип на конкретный тип, используемый в данной ситуации. Естественно, что иногда приходится заменить Let на Set или руками добавить еще одну заготовку для Set. Приведем тексты этих процедур для класса Rational после их модификации и заполнения:
Public Property Get Числитель() As Integer
Числитель = m
End Property
Public Property Let Числитель(ByVal NewValue As Integer)
CreateRational NewValue, n
End Property
Public Property Get Знаменатель() As Integer
Знаменатель = n
End Property
Public Property Let Знаменатель(ByVal NewValue As Integer)
CreateRational m, NewValue
End Property
Обратите внимание, для задания новых значений свойств в процедурах Let Числитель и Let Знаменатель мы вызываем конструктор, который может изменить нужным образом и числитель и знаменатель.
Замечание:
Для класса Rational доступ к свойствам Числитель и Знаменатель следовало бы закрыть полностью. Рациональные числа представляются для пользователей неделимыми и их внутренняя структура должна быть недоступной. Для них следовало бы использовать пятую стратегию работы со свойствами, когда свойства Let и Get не объявляются. Причину, по которой были введены эти процедуры - свойства, мы объясним позже, когда речь пойдет о реализации методов класса.
Процедуры - свойства Property Let, Property Set, Property Get играют важную роль в определении большинства классов. Поэтому имеет смысл разобрать их точный синтаксис.
Оператор Property Let используется для установки значения терминального свойства и имеет следующий синтаксис:
[Public | Private | Friend] [Static] Property Let имя-свойства ([список-параметров,] значение) [Операторы] [Exit Property] [Операторы] End Property
Оператор Property Set используется для установки значения свойства - участника (объекта) и имеет следующий синтаксис:
[Public | Private | Friend] [Static] Property Set имя-свойства ([список-параметров,] ссылка) [Операторы] [Exit Property] [Операторы] End Property
Оператор Property Get используется для получения значения свойства и имеет следующий синтаксис:
[Public | Private | Friend] [Static] Property Get имя-свойства [(список-параметров)] [As Type] [Операторы] [Exit Property] [Операторы] [имя-свойства = выражение] End Property
Рассмотрим детали синтаксиса:
Public означает доступность Property - свойств во всех процедурах во всех модулях во всех проектах, если только нет дополнительных ограничений. Ключевое слово Private хотя и возможно синтаксически, но лишено смысла, - Property пишут для того, чтобы они были открытыми.Static, как обычно, означает, что значения локальных переменных процедуры, если они есть, не будут изменяться в промежутке между ее вызовами.имя-свойства задает имя определяемого и изменяемого свойства. Заметьте, что когда задается пара процедур Property Let (Set) - Get или все три процедуры, все они имеют одно и тоже имя.список-параметров используется чаще всего при задании свойства, значения которого образуют массив. В нем через запятую перечисляются параметры, передаваемые процедуре, например, в качестве индексов. Парные процедуры должны иметь один и тот же список параметров. Синтаксис списка-параметров такой же, как и у параметров обычных процедур, определяемых оператором Sub. Как обычно, если нужно передать переменное число параметров, то можно использовать ParamArray.значение в Property Let и ссылка в Property Set - это имя переменной (объекта), значение которого передается свойству. Тип значения, возвращаемого процедурой Property Get, должен совпадать с соответствующим типом параметра значение (ссылки).операторы задает программу вычисления значения свойства. В теле процедуры можно использовать оператор Exit Property для немедленного выхода из процедуры.К классу Rational, создание которого еще не закончено, мы еще вернемся. Пока же продолжим разговор о процедурах - свойствах, поскольку они являются важными компонентами класса. В этом разделе пойдет речь об одном из их довольно неожиданных применений, когда они позволяют выступать классу в роли привлекательной упаковки внутренних и служебных функций. Но обо всем по порядку.
Построим класс Plan, в котором по существу есть только одно свойство, хранящее текущий месяц. Хранится это свойство, как целое, но для внешнего мира оно выглядит как обычное имя месяца. Вот как это реализовано.
'Класс Plan
'Свойство класса
Private CurMonth As Integer
'Закрытый массив, играющий служебную роль
Private Месяцы(1 To 12) As String
Private Sub Class_Initialize()
CurMonth = Month(Now)
Месяцы(1) = "Январь": Месяцы(2) = "Февраль": Месяцы(3) = "Март"
Месяцы(4) = "Апрель": Месяцы(5) = "Май": Месяцы(6) = "Июнь"
Месяцы(7) = "Июль": Месяцы(8) = "Август": Месяцы(9) = "Сентябрь"
Месяцы(10) = "Октябрь": Месяцы(11) = "Ноябрь": Месяцы(12) = "Декабрь"
End Sub
Public Property Get ТекущийМесяц() As String
ТекущийМесяц = Месяцы(CurMonth)
End Property
Public Property Let ТекущийМесяц(ByVal NewValue As String)
Dim i As Byte
i = 1
Do While i <= 12
If Месяцы(i) = NewValue Then
CurMonth = i
Exit Do
End If
i = i + 1
Loop
If i = 13 Then 'Неверно задан месяц
CurMonth = Month(Now)
End If
End Property
Обратите внимание, работу с закрытым свойством CurMonth обеспечивают процедуры - свойства Property Let ТекущийМесяц и Property Get ТекущийМесяц. С их помощью можно читать и изменять значение свойства CurMonth. Одновременно Let и Get занимаются преобразованием данных, что является распространенной практикой. Пример этот интересен и с позиций построения конструктора по умолчанию, который выполняет все необходимые внутренние инициализации, в данном случае, задавая значение массиву Месяцы.
Приведем еще процедуру, которая работает с данными этого класса:
Public Sub WorkWithPlan() Dim MyPlan As New Plan Debug.Print MyPlan.ТекущийМесяц MyPlan.ТекущийМесяц = "Апрель" Debug.Print MyPlan.ТекущийМесяц MyPlan.ТекущийМесяц = "Янв." Debug.Print MyPlan.ТекущийМесяц End Sub
В результате ее работы, (процедура запускалась в марте месяце) напечатаны значения:
Март Апрель Март
Задумаемся над сутью решаемой задачи, - нам хочется при обращении к свойству класса получать значение текущего месяца. Но ведь текущий месяц, он и есть текущий, и пользователь не должен изменять его значение. Так что первое, что следовало бы сделать, это воспользоваться стратегией Read- only и не разрешать изменять значение свойства. Поэтому исключим Property Let из нашего класса. Но в этом примере есть куда более интересная особенность. Исключив Property Let, мы понимаем, что теперь теряет смысл хранение самого свойства CurMonth. Его значение мы можем (и должны по смыслу свойства) получать динамически, обращаясь к системной процедуре Month(Now). Удалим из класса и переменную CurMonth. Наш новый класс PlanNew стал проще:
'Класс PlanNew
'Закрытый массив, играющий служебную роль
Private Месяцы(1 To 12) As String
Private Sub Class_Initialize()
Месяцы(1) = "Январь": Месяцы(2) = "Февраль": Месяцы(3) = "Март"
Месяцы(4) = "Апрель": Месяцы(5) = "Май": Месяцы(6) = "Июнь"
Месяцы(7) = "Июль": Месяцы(8) = "Август": Месяцы(9) = "Сентябрь"
Месяцы(10) = "Октябрь": Месяцы(11) = "Ноябрь": Месяцы(12) = "Декабрь"
End Sub
Public Property Get ТекущийМесяц() As String
ТекущийМесяц = Месяцы(Month(Now))
End Property
Свойство ТекущийМесяц теперь имеет статус Read- only. Но главная суть не в этом. Теперь отчетливо видна настоящая роль класса PlanNew, - он является красивой упаковкой вызова служебных функций Month(Now). Стоило ли создавать класс для этой цели? Не проще ли просто написать стандартную процедуру или вообще все делать самому, обращаясь к служебной функции и затем преобразуя номер месяца в его название? Для одной функции, как в нашем примере, вероятно, не стоит создавать класс, но для десятка функций это вполне целесообразно. Эта техника с особым успехом используется, когда нужно упаковать обращение к Win API функциям. Так что, подводя итог, отметим, что иногда классы используются как упаковка, расширяющая стандартные возможности и особую роль в этом процессе играют процедуры - свойства.
Когда создается семейство классов, без свойств - участников не обойтись. В классах семейства, основанных на механизме встраивания, обязательно есть свойства, представляющие собой объекты. Эти свойства, как и терминальные, обычно, закрываются и для работы с ними используются процедуры - свойства Property Set и Property Get. Иногда приходится передавать и дополнительные параметры при работе с ними. Рассмотрим типичный пример. Ранее мы построили класс " Личность ", теперь построим класс " Группа ", содержащий группу личностей. При построении этого класса ограничимся минимальными средствами, необходимыми для демонстрации работы со свойствами:
'Класс Группа Личностей
Const РазмерГруппы As Byte = 25
'Свойства
Private Group(1 To РазмерГруппы) As Личность
'Процедуры-свойства
Public Property Get ЧленГруппы(num As Byte) As Личность
'Если номер корректен
If (num >= 1) And (num <= РазмерГруппы) Then
'Если существует в группе личность с таким номером
If Not (Group(num) Is Nothing) Then
Set ЧленГруппы = Group(num)
Else: MsgBox ("В группе нет человека с номером " num)
End If
Else: MsgBox ("Некорректно задан номер в группе - " num)
End If
End Property
Public Property Set ЧленГруппы(num As Byte, NewValue As Личность)
'Если номер корректен
If (num >= 1) And (num <= РазмерГруппы) Then
'Если в группе нет личности с таким номером, то она создается
If Group(num) Is Nothing Then
Set Group(num) = NewValue
Else: MsgBox ("В группе уже есть человек с номером " num)
End If
Else: MsgBox ("Некорректно задан номер в группе - " num)
End If
End Property
Public Sub Сведения()
Dim i As Byte
For i = 1 To РазмерГруппы
If Not (Group(i) Is Nothing) Then
Group(i).PrintPerson
End If
Next i
End Sub
Массив объектов класса " Личность " является закрытым свойством класса " Группа ". Процедуры - свойства Property Get ЧленГруппы и Property Set ЧленГруппы обеспечивают доступ к элементам этого массива. Индекс элемента является дополнительным параметром. Заметьте, процедура Set имеет статус Write-once, - если элемент с заданным номером уже определен в группе, то он не переопределяется. При создании этих процедур, нам, конечно же, пришлось модифицировать стандартные заготовки Let и Get. Для полноты картины приведем процедуру, в которой показано, как работать с группой:
Public Sub WorkWithGroup()
Dim UserOne As New Личность
Dim UserTwo As New Личность
Dim UserThree As New Личность
Dim Знакомые As New Группа
Dim NewUser As New Личность
'Личности
UserOne.InitPerson FN:="Петр", LN:="Петров", DoB:=#1/23/1968#
UserTwo.InitPerson FN:="Анна", LN:="Козлова", DoB:=#7/21/1968#
UserThree.InitPerson FN:="Анна", LN:="Керн", DoB:=#5/17/1803#
'Группа
Set Знакомые.ЧленГруппы(1) = UserOne
Set Знакомые.ЧленГруппы(2) = UserTwo
Set Знакомые.ЧленГруппы(1) = UserThree
Set Знакомые.ЧленГруппы(3) = UserThree
Set NewUser = Знакомые.ЧленГруппы(7)
Set NewUser = Знакомые.ЧленГруппы(3)
Знакомые.Сведения
End Sub
Не останавливаясь на тех диалогах, которые будут появляться по ходу выполнения, приведем результаты печати сведений о наших старых знакомых:
Петр Петров родился 23.01.68 Анна Козлова родилась 21.07.68 Анна Петровна Керн родилась 17.05.1803
Мы уже говорили о том, что классы могут выступать в роли привлекательной упаковки при работе со служебными функциями. Такую же "упаковочную" роль они могут играть и при работе со встроенными объектами. Свойствами такого класса могут быть, например, элементы управления, - списки, кнопки. Класс - упаковка позволит обращаться к методам и свойствам объекта, может быть в более привлекательной для конечного пользователя форме. Кроме того, в классе могут быть определены и дополнительные свойства и методы, расширяющие стандартные возможности.
Без процедур - свойств можно обойтись, заменив их обычными методами класса. Правда, такая замена приведет, обычно, к некоторой потере эффективности. Но методы являются основным способом работы с данными (свойствами), определяя поведение объектов класса.
Не будем повторяться. О методах было сказано уже достаточно. Любая процедура ( Sub ) или функция ( Function ), описанная в разделе методов класса, является его методом. Напомним синтаксис методов класса:
[Private | Public | Friend] [Static] Sub name [(arglist)] [statements] [Exit Sub] [statements] End Sub [Public | Private | Friend] [Static] Function name [(arglist)] [As type] [statements] [name = expression] [Exit Function] [statements] [name = expression] End Function
Роль всех ключевых слов в этих определениях уже пояснялась, чуть позже мы подробнее скажем о спецификаторе Friend, который могут иметь методы класса. Сейчас же отметим, что почти каждый класс, независимо от его специфики имеет некоторый "джентльменский" набор методов. В него входит:
Initialize.Вернемся к проектированию класса Rational. Большую часть джентльменского его набора мы уже определили. Специфика этого класса определяется операциями над рациональными числами. Ограничимся четырьмя основными операциями - сложением, вычитанием, умножением и делением. Добавим к ним еще и метод печати дробей. Вот тексты этих методов, заканчивающих определение нашего класса:
Public Function Plus(a As Rational) As Rational
Dim d As Integer, u As Integer, v As Integer
Dim R As New Rational
u = m * a.Знаменатель + n * a.Числитель
v = n * a.Знаменатель
d = nod(u, v)
R.Числитель = u \ d
R.Знаменатель = v \ d
Set Plus = R
End Function
Public Function Minus(a As Rational) As Rational
Dim d As Integer, u As Integer, v As Integer
Dim R As New Rational
u = m * a.Знаменатель - n * a.Числитель
v = n * a.Знаменатель
d = nod(u, v)
R.Числитель = u \ d
R.Знаменатель = v \ d
Set Minus = R
End Function
Public Function Mult(a As Rational) As Rational
Dim d As Integer, u As Integer, v As Integer
Dim R As New Rational
u = m * a.Числитель
v = n * a.Знаменатель
d = nod(u, v)
R.Числитель = u \ d
R.Знаменатель = v \ d
Set Mult = R
End Function
Public Function Divide(a As Rational) As Rational
Dim d As Integer, u As Integer, v As Integer
Dim R As New Rational
u = m * a.Знаменатель
v = n * a.Числитель
If v = 0 Then
MsgBox ("деление на нуль невозможно")
Else
d = nod(u, v)
R.Числитель = u \ d
R.Знаменатель = v \ d
Set Divide = R
End If
End Function
Public Sub PrintRational()
Debug.Print (m "/" n)
End Sub
Замечание:
Внутри класса при реализации операций над рациональными числами доступ к закрытым свойствам объекта, естественно, разрешен, и можно обращаться непосредственно к переменным m и n. Однако нельзя обратиться к закрытым свойствам параметров класса Rational, передаваемых методам Plus, Minus, Mult, Divide. Точно также, нельзя обратиться к свойствам локальных объектов (объекту R ), объявленных внутри методов. Единственный выход состоит в том, чтобы использовать для них Public Property Let и Public Property Get. Но тогда и вне класса можно использовать эти процедуры - свойства, изменяя, например, значение знаменателя дроби. Разрешать этого не хотелось бы.
На наш взгляд имеет место концептуальная некорректность поведения закрытых свойств. Внутри класса доступ к ним должен быть открыт не только для самого объекта, но и для объектов этого класса, заданных в качестве параметров методов класса. Точно также доступ должен быть открыт и для локальных объектов. Так поступают в большинстве языков программирования.
Приведем процедуру, которая работает с рациональными числами:
Public Sub WorkWithRational()
Dim a As New Rational, b As New Rational, c As New Rational
a.CreateRational 5, 7
b.CreateRational -12, -18
Set c = a.Plus(b)
c.PrintRational
Set a = c.Minus(b)
a.PrintRational
Set c = a.Mult(b)
c.PrintRational
Set b = c.Divide(a)
b.PrintRational
End Sub
При ее запуске получены следующие результаты:
29/21 5/7 10/21 2/3
Методы класса, в том числе процедуры - свойства Get, Let и Set могут иметь описатель Friend наряду с описателями Public и Private. Каждый из них задает свою область видимости метода, область программы, где метод доступен для вызова. Напомним, Private делает метод видимым только внутри класса, Public - делает метод доступным и область действия может распространяться не только на тот проект, в котором определен данный класс, но и на другие проекты. Чтобы управлять этим процессом, расширяя или сужая область видимости Public объектов, применяют специальные спецификаторы, например, опцию Option Private, которая закрывает модуль от других проектов. Методы с описателем Friend называют дружественными . Описатель Friend, действуя только на один метод, распространяет его область видимости на проект, в котором описан класс с Friend
методом. Но для других проектов дружественные методы не доступны, даже в том случае, когда там доступны Public методы. Так что метод дружит только со своим проектом и не более. Кто программировал на С++ и других языках, где есть наследование классов, знаком с дружественными методами, - там они дружат с классами - потомками родительского класса, в котором задан Friend метод. Вне семейства классов дружественные методы не доступны. На VBA роль семейства играет проект.
В мире объектов время от времени происходят события. Причиной их могут быть действия пользователя или реакция системы на ранее происшедшие события. При возникновении события, объекту с ним связанному, система посылает сообщение, при получении которого вызывается специальный метод - обработчик события. Эти методы, вызываемые специальным образом, и называются событиями. Итак, каждый класс имеет свойства, методы и события. Все объекты - экземпляры класса имеют одинаковый набор свойств, методов и событий, заданных определением класса. Методы у всех экземпляров класса одинаковы. Но, заметьте, обработчики событий у каждого экземпляра собственные! Впрочем, это же касается и значений свойств, - каждый экземпляр уникален. Все командные кнопки имеют метод Delete, который всегда выполняется одинаково, удаляя кнопку. Но щелчок по каждой кнопке, вызывающий событие Click, приводит к совершенно разным результатам. В этом сила событий, - поведение объектов становится уникальным.
VBA позволяет работать с объектами Office 2000, которым несть числа, и собственными объектами, классы которых мы умеем определять. Большинство из объектов Office 2000 не имеет событий, например, прославленный объект Range имеет множество свойств и методов, но ни одно из событий с ним не связывается. С другой стороны все объекты, предназначенные для организации интерфейса с пользователем, - формы, кнопки, списки и прочие объекты обладают набором событий. Но не только интерфейсные объекты имеют события. Объекты, задающие документы Word и Excel, страницы Application также имеют встроенный набор событий. Для большинства из них мы подробно рассмотрели, как создаются обработчики событий для конкретных экземпляров. Напомним, есть специальный тип модулей, связанных с объектами, имеющими события, - сюда относятся модули форм, документов и другие. В окне кода этого модуля можно выбрать конкретный объект, выбрать для него возможное событие, построить заготовку обработчика события и наполнить затем ее содержанием. Подробнее об этом сказано в предыдущей лекции, посвященной модулям. Нам осталось рассмотреть две серьезные задачи:
With Events. В эту группу входят, например, объекты Application. По умолчанию эти объекты появляются как объекты, не имеющие событий, поэтому у них нет специального модуля, в окне кода которого можно было бы найти объект, найти список его возможных событий, создать обработчик события. Первая задача состоит в том, чтобы для таких стандартных объектов, классы которых обладают событиями, уметь создавать объекты с событиями ( With Events ), создавать для них модуль, в котором будут появляться заготовки обработчиков событий.К решению этих двух важных задач мы сейчас и приступим.
Объекты Application являются наиболее известными объектами, имеющими события, но появляющимися по умолчанию как объекты без событий. С этих объектов и начнем и, поскольку технология включения событий во всех случаях одинакова, достаточно рассмотреть задачу создания обработчиков событий для объектов Application. Для того чтобы изложение сделать конкретным, будем полагать, что целью является создание обработчиков событий объекта Excel.Application. По ходу дела нам надо решить две основные задачи:
Excel.Application With Events и связать его с текущим Excel приложением.Заметьте, тут есть нюанс, - можно эту работу выполнять в самом Excel, после того, как открыта одна из его
Модуль, в котором будут появляться обработчики событий объекта Excel.Application, синтаксически является модулем класса, который нужно создать. Этот класс очень прост по своей структуре и при создании он содержит единственное закрытое свойство - соответствующий объект Application, объявленный со спецификатором WithEvents. Так что в нашем примере все описание создаваемого класса, который назовем, например, ExcelWithEvents сводится к одной содержательной строке:
Option Explicit 'Класс ExcelWithEvents Public WithEvents xlApp As Excel.Application
Как только такое свойство определено в классе, в окне кода этого модуля появляется объект xlApp, обладающий событиями, тут же можно выбрать из списка соответствующее событие, создать заготовку и затем наполнить ее содержанием. Взгляните на соответствующий рисунок:
(рис 4.4) Класс с объектом Excel.Application WithEventsМы ограничились созданием двух обработчиков события NewWorkbook и OpenWorkbook. Вот их текст:
Private Sub xlApp_NewWorkbook(ByVal Wb As Excel.Workbook)
MsgBox("Рад, что вы открыли книгу " Wb.Name)
End Sub
Private Sub xlApp_WorkbookOpen(ByVal Wb As Excel.Workbook)
MsgBox ("Рад, что вы открыли книгу " Wb.Name)
End Sub
Как видите, первая проблема решается достаточно просто и не требует введения новых средств. Но, обратите внимание, остается непонятным, что характеризует наш единственный объект xlApp, - конкретный экземпляр приложения, для которого создаются обработчики событий, или эти обработчики распространяются на множество экземпляров объектов класса ExcelWithEvents Но, поскольку объекты Application присутствуют в одном экземпляре, то не будем сейчас вдаваться в эти тонкости, позже мы еще вернемся к этому вопросу.
Вторая проблема решается также довольно просто. Но здесь есть одна тонкость, которую следует понимать. Реально создаются два объекта, мы называем их двойниками. Один из них это обычный объект Excel.Application, другой - объект Excel.ApplicationWithEvents из класса ExcelWithEvents. Эти два объекта должны быть связаны одной ссылкой. Вот процедура, которая в нашем примере вызывается в документе Word в тот момент, когда пользователь решит начать работу с книгами Excel:
Public Sub CreateExApp()
Const PathToMyDir = "e:\O2000\CD2000\Ch4\"
'Чистый Excel
Dim MyEx As New Excel.Application
'Excel With Events
Dim MyExWithEv As New ExcelWithEvents
'Связывание
Set MyExWithEv.xlApp = MyEx
'Добавляем новую книгу и открываем существующую
'Обработчики событий New и Open работают!
With MyEx
.Visible = True
.Workbooks.Add 1
.Workbooks.Add PathToMyDir "BookOne"
End With
End Sub
Поверьте, обработчики событий действительно вызывались, как при создании новой книги, так и открытии книги, основанной на существующем шаблоне. Осталось сказать несколько слов о том, как это все могло выглядеть, если всю эту процедуру осуществлять в самом Excel. Обратите внимание, в нашем примере Excel Application с событиями появляется до того, как открыта какая либо из его книг. Если же работать в Excel, то описание объекта с событиями и связывание необходимо поместить в первую из открываемых книг. Мы проделали эту работу и в проекте книги BookOne создали класс ExcelWithEvents, а в одном из стандартных модулей создали процедуру:
Public Sub ConnectingWithEvents()
Const PathToMyDir = "e:\O2000\CD2000\Ch4\"
'Excel With Events
Dim MyExWithEv As New ExcelWithEvents
'Связывание
Set MyExWithEv.xlApp = Excel.Application
'Добавляем новую книгу и открываем существующую
'Обработчики событий New и Open работают!
With Application
.Visible = True
.Workbooks.Add 1
.Workbooks.Add PathToMyDir "BookTwo"
End With
End Sub
Как видите, эта процедура отличается от процедуры CreateExApp лишь незначительными деталями. Возникает вопрос, как и когда запускать эту процедуру. Можно это делать по-разному, например, вызывать ее в обработчике события Click командной кнопки, можно вызывать ее в обработчике события OpenDocument. В последнем случае, обработчики событий объекта Application будут выполняться для всех книг, сразу после открытия книги BookOne.
Возможность создавать события собственных классов - это новинка Office 2000. Ранее было возможным для классов иметь только два стандартных события - Initialize и Terminate, играющих роль конструктора и деструктора по умолчанию. Теперь можно в классе создать набор событий и для каждого из экземпляров класса написать обработчики любого из этих событий. Тем самым расширяются возможности созданных Вами объектов. Это существенное продвижение в сторону классики объектного программирования. Попробуем разобраться:
Первый вопрос, который необходимо решить при создании класса с событиями, лежит вне программирования, прежде всего, нужно решить, каким должен быть набор событий у объектов класса. Если с объектом связан некоторый графический образ, то ситуация более или менее стандартна, всегда разумно иметь события Click, Move, Resize и другие, типичные для графических объектов. Эти события будут возникать, когда пользователь будет работать с графическим образом, выполняя над ним те или иные типичные действия. Во многих случаях, наши объекты могут и не иметь графического образа, но и для них не менее полезно определить события. Общие рекомендации давать трудно, поэтому обратимся к примеру и рассмотрим ранее созданный класс Личность. Какие события разумно было бы определить для объектов этого класса? Учитывая, что свойств у объектов класса не так много, число разумных событий также не велико. Давайте определим два события - ИзменениеФамилии и ДеньРождения. Первое из этих событий возникает в ответ на изменение значения свойства Фамилия, второе, - когда текущая дата отличается от даты дня рождения Личности не более чем на сутки.
Поняв, какие события должен иметь наш класс, рассмотрим, как создать набор событий в классе. Все делается очевидным и естественным образом. Достаточно в разделе объявлений модуля класса объявить каждое из событий, используя ключевое слово Event. Вот как выглядит объявление класса Личность, имеющего два предусмотренных нами события:
Option Explicit 'Класс Личность 'События класса Public Event ИзменениеФамилии(Fam As String, NewFam As String) Public Event ДеньРождения(Dat As Date) 'Далее следует уже знакомое определение свойств и методов класса 'Смотри раздел "Создание класса Личность" этой лекции
Итак, чтобы создать класс с событиями просто добавьте список этих событий в объявление класса!
Чтобы событие, связанное с объектом возникло, его нужно зажечь или возбудить ( Raise ). Зажигается событие специальной процедурой RaiseEvent, имеющей следующий синтаксис:
Sub RaiseEvent имя_события (параметры)
Эта процедура не только возбуждает событие, но и передает некоторые параметры, которые могут быть использованы в обработчике события. Что стоит за терминами "зажечь" ("возбудить") событие? При выполнении этой процедуры операционной системе посылается уведомление о том, что та должна послать объекту сообщение о возникновении события с указанным именем, в ответ на получение которого будет вызван обработчик этого события. Так что при выполнении этой процедуры плавный ход выполнения программы прерывается, происходит обмен сообщениями, вызов обработчика события и только по его завершении будет продолжено выполнение операторов, стоящих после вызова RaiseEvent.
Процедура RaiseEvent должна выполняться в одном из методов класса с событиями. Правильное определение ее места, это также одна из задач, которую нужно решить при проектировании класса, возможно, придется создавать специальный метод для этой цели. Обратимся к нашему примеру, когда и где нужно зажечь событие ИзменениеФамилии. Здесь ответ очевиден, - всякий раз, когда у объекта изменяется значение свойства Фамилия, должно возникать это событие. Но изменять значение для Private свойства следует единственным образом, - вызовом Property Let ВашаФамилия. Поэтому эта процедура именно то место, где должно возбуждаться событие. А в процедуре разумно поставить вызов RaiseEvent непосредственно перед оператором, изменяющим фамилию. Вот как выглядит эта процедура:
Public Property Let ВашаФамилия(ByVal NewValue As String)
'Зажигает событие ИзменениеФамилии
RaiseEvent ИзменениеФамилии(Фамилия, NewValue)
Фамилия = NewValue
End Property
Заметьте, мы передаем обработчику два параметра, старую и новую фамилию. Важно понимать, что можно не только передавать обработчику данные, но и получать информацию от него. Добавим еще один параметр Permission нашему событию, так чтобы обработчик события, проверив допустимость изменения фамилии, мог вернуть разрешение на изменение. Новый вариант события теперь выглядит так:
Public Event ИзменениеФамилии(Fam As String, NewFam As String, _
Permission As Boolean)
Соответственно изменится и процедура - свойство Property Let. Вот другой, более разумный вариант этой процедуры:
Public Property Let ВашаФамилия(ByVal NewValue As String)
'Зажигает событие ИзменениеФамилии
Dim Permission As Boolean 'Разрешение на изменение фамилии
Permission = True
RaiseEvent ИзменениеФамилии(Фамилия, NewValue, Permission)
'Обработчик события может запретить изменение фамилии
If Permission Then Фамилия = NewValue
End Property
Менее ясно, где и когда следует зажигать событие ДеньРождения. Мы решили возбуждать это событие в тот момент, когда пользователь интересуется датой рождения объекта, и вставили соответствующую проверку в Property Get ВашаДатаРождения процедуру:
Public Property Get ВашаДатаРождения() As Date
'Зажигает событие ДеньРождения
'в зависимости от значения текущей даты
If (Month(Now) = Month(ДатаРождения)) And _
(Abs(Day(Now) - Day(ДатаРождения)) <= 1) Then
RaiseEvent ДеньРождения(ДатаРождения)
End If
ВашаДатаРождения = ДатаРождения
End Property
Здесь событие возбуждается при выполнении некоторого условия, - текущая дата отличается от даты дня рождения не более чем на сутки. Дата рождения передается обработчику события, а тот, в свою очередь, может организовать уведомление и поздравление объекта с днем рождения, как это делает, например, Outlook для контактов из списка контактов.
Заметьте, процедура - свойство Property Get, в которой мы разместили оператор RaiseEvent, возбуждающий событие, - это не единственно возможное место для такого оператора. Вообще, одно и то же событие может возбуждаться в разных местах. В данном случае, может быть, имело смысл написать еще и специальный метод для возбуждения события, который вызывался бы пользователем в нужный момент.
Задача состоит в том, что нужно определить модуль, в котором можно объявлять объекты With Events. В окне кода этого модуля должен быть доступен
Заметьте, создание этого модуля, создание обработчиков событий не имеет непосредственного отношения к созданию класса объектов с событиями. Это другая сторона проблемы, - это проблема того, кто использует класс объектов, кто создает экземпляры этих объектов и создает обработчики событий этих конкретных объектов. Вполне возможно, что вся эта работа будет выполняться в нескольких документах, других проектах, отличных от того проекта, где создан класс объектов с событиями.
Модуль, в котором можно объявлять объекты With Events может быть либо модулем класса, либо модулем формы, но, заметьте, это не может быть стандартный модуль. В большинстве случаев предпочтение отдается модулю класса. Позже мы остановимся подробнее на ситуации, когда объекты с событиями объявляются в модуле формы. Предположим, что для решения некоторой задачи, нам потребовалось работать с двумя конкретными объектами класса Личность, - нашим старым приятелем и знакомой молодой девушкой. Для решения задачи создадим класс, который назовем " Личности " и поместим туда объявление двух объектов MyFriendOne и MyFriendTwo. Объекты объявлены со спецификатором WithEvents, указывающим на то, что они должны реагировать на события. Как только эти объявления появятся в разделе объявлений класса, сами объекты появятся в окне кода в списке объектов. Теперь, выбрав объект из списка, можно открыть список возможных событий объекта и создать
вначале заготовку обработчика событий, а потом наполнить ее содержанием.
(рис 4.5) Создание обработчиков событийЗаметьте, эта технология работы нам уже хорошо знакома. Она использовалась при создании объектов Application With Events. Теперь становится понятным, что тогда шла речь о частном случае. Помните, при создании класса ExcelWithEvents, говоря о его структуре, мы обращали внимание на то, что в классе объявлен всего лишь один объект с событиями. Теперь становится понятным, это отражало специфику частного случая, - приложение, обычно, существует в единственном экземпляре. Личностей же, реагирующих на события, может быть много, - для каждой из них нужно задать свой экземпляр объекта в классе, чтобы была возможность написать индивидуальный обработчик события. Взгляните на реализацию класса Личности:
Option Explicit
Private WithEvents myFriendOne As Личность
Private WithEvents myFriendTwo As Личность
Private Sub myFriendOne_ДеньРождения(Dat As Date)
BirthDay (Dat)
End Sub
Private Sub myFriendOne_ИзменениеФамилии(Fam As String, _
NewFam As String, Permission As Boolean)
MsgBox ("Изменение фамилии " Fam " на " NewFam Chr(13) _
"не разрешается.")
Permission = False
End Sub
Private Sub myFriendTwo_ДеньРождения(Dat As Date)
BirthDay (Dat)
End Sub
Private Sub myFriendTwo_ИзменениеФамилии(Fam As String, _
NewFam As String, Permission As Boolean)
MsgBox ("Поздравляю с замужеством, дорогая " _
Fam "-" NewFam "!")
Permission = True
End Sub
Public Sub BirthDay(Dat As Date)
Debug.Print Dat, "-", Now
Select Case Day(Dat)
Case Day(Now)
MsgBox ("Сегодня День Рождения!")
Case Is < Day(Now)
MsgBox ("Вчера был День Рождения!")
Case Else
MsgBox ("Завтра День Рождения!")
End Select
End Sub
Public Sub Connect()
Set myFriendOne = FriendOne
Set myFriendTwo = FriendTwo
End Sub
Как видите, в этом классе два Private объекта, реагирующих на события, и для каждого из них написаны по два обработчика событий класса. Сейчас не суть важно, что делают эти обработчики. В одном случае обработчик события ИзменениеФамилии разрешает это изменение, в другом - нет, и выдает по этому поводу, естественно, разные сообщения. Обработчики события ДеньРождения выполняют одну и ту же задачу, поэтому они вызывают общую процедуру работы. Это, хотя и интересные, но все-таки детали. Более важную для понимания играет роль процедура, названная Connect.
Особенностью рассматриваемой технологии работы с объектами, реагирующими на события, является то, что существует пара объектов - двойников. Один из них, реальный объект, объявляется как объект без событий, другой, его двойник, объявляется в специальном классе с описателем With Events. Для двойника создаются обработчики событий, но, чтобы все это заработало нужным образом, эту пару объектов нужно связать законными узами брака. Так мы поступали при работе с объектами Excel.Application и Excel.Application WithEvents. Также мы поступили в процедуре Connect с двумя парами объектов - двойников: MyFriendOne - FriendOne и MyFriendTwo - FriendTwo.
Остался заключительный штрих в завершение разговора о том, как создаются классы и как ведется работа с объектами, реагирующими на события. Нужно еще рассказать о создании реальных объектов, двойники которых описаны в специальном модуле класса, в нашем примере - в классе Личности. Эти реальные объекты, обычно, находятся в стандартном модуле. В какой -то момент своей жизни, как правило, в самом начале появления, они связываются с двойниками и после этого могут реагировать на события. События инициируются в процессе работы с этими объектами, при вызове некоторых из их методов. Какие из методов класса зажигают события, - этот вопрос решается при проектировании класса, а в какой момент метод будет вызываться, - это прерогатива пользователя, работающего с объектами.
Для класса Личность мы уже описали методы, зажигающие события. Поэтому нам осталось только привести описание стандартного модуля Примеры, в котором ведется работа с двумя объектами, названными FriendOne и FriendTwo:
Option Explicit
'Модуль Примеры
Public FriendOne As New Личность
Public FriendTwo As New Личность
Public FOne As New Личности
Public Sub Знакомство()
'Вызывается конструктор с параметрами
'и происходит знакомство с объектами
FriendOne.InitPerson FN:="Станислав", LN:="Федотов", _
DOB:="21.05.39"
FriendTwo.InitPerson FN:="Катя", LN:="Павлова", _
DOB:="22.03.79"
FriendOne.PrintPerson
FriendTwo.PrintPerson
FriendOne.SayWhoIs
FriendTwo.SayWhoIs
'Связывание с двойниками.
'Теперь объекты могут реагировать на события!
FOne.Connect
End Sub
Public Sub CallEvents()
Dim DOB As Date
'Вызов методов приведет к возникновению событий!
'При замене фамилии возникнет событие ИзменениеФамилии
'Заметьте, не всегда фамилия будет изменена!
FriendOne.ВашаФамилия = "Фидотов"
FriendTwo.ВашаФамилия = "Волконская"
'При попытке узнать дату рождения
'может быть вызван обработчик события ДеньРождения.
DOB = FriendOne.ВашаДатаРождения
DOB = FriendTwo.ВашаДатаРождения
FriendOne.PrintPerson
FriendTwo.PrintPerson
End Sub
В этом модуле объявлены два объекта класса Личность и один объект класса Личности. Две процедуры - Знакомство и CallEvents вызываются в ответ на щелчки командных кнопок в соответствующих обработчиках событий. Первой должна вызываться процедура Знакомство, где объекты FriendOne и FriendTwo получают "нормальные" значения и происходит Знакомство с ними. Важно, что здесь же происходит связывание этих объектов с их двойниками. Обратите внимание, нам понадобился объект FOne класса Личности, чтобы вызвать его метод Connect и связать эти пары объектов. В процессе работы этих модулей будет вестись диалог с пользователем. На следующих рисунках показаны сообщения, выдаваемые обработчиками событий ИзменениеФамилии и ДеньРождения.

(рис 4.7) Обработчик события ИзменениеФамилии объекта One(рис 4.6) Обработчик события ИзменениеФамилии объекта Two
(рис 4.8) Обработчик события ДеньРождения объекта TwoПриведем еще результаты отладочной печати:
Станислав Федотов родился 21.05.39 Катя Павлова родилась 22.03.79 22.03.79 - 22.03.99 13:14:01 Станислав Федотов родился 21.05.39 Катя Волконская родилась 22.03.79
В заключение, коротко опишем всю картину создания класса и работы с объектами, реагирующими на события. Прежде всего, нужно создать сам класс объектов, в котором предусмотрен набор событий, возможных для объектов класса. В методах класса следует предусмотреть возможность возбуждения событий. На следующем этапе следует создать класс, в котором объявлены объекты WithEvents. В этом классе создаются обработчики событий для каждого экземпляра класса. Кроме этих объектов - двойников создаются и "реальные" объекты, как правило, в стандартном модуле. После того, как происходит связывание двойников, объекты могут реагировать на события. Всякий раз, когда объект вызывает метод класса, в котором возбуждается событие, это приводит к тому, что вызывается обработчик события. Можно передавать и получать информацию от обработчика событий.
Одно из возможных применений этой технологии работы состоит в том, что классы с событиями могут выступать как упаковка для элементов управления. Цель такого класса состоит в расширении свойств элемента управления и, в частности, в добавлении новых событий, на которые мог бы реагировать этот элемент управления. Надеемся, что данное описание позволит Вам самостоятельно построить такое приложение. Заметим только, что в таких случаях используется модуль формы, для объявления объектов WithEvents и создания обработчиков событий этих объектов.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.