Основы офисного программирования и язык VBA

Активные объекты и метод Activate

Объект Selection является примером косвенной адресации. Он задает некоторую выделенную область, и его свойства и методы позволяют изменять состояние этой области. Другой пример косвенной адресации - активные элементы. Как в подокне может быть только один выделенный элемент, так и активный элемент может быть единственным в своем классе - одно активное окно, один активный документ или рабочий лист. Обычно активный элемент находится в фокусе клавиатуры, именно ему направлен ввод пользователя. Сделав активным новый элемент, мы отменяем, деактивируем элемент, ранее находившийся в фокусе клавиатуры.
Метод Activate делает активным элемент, его вызвавший. Этим методом обладают элементы верхних уровней иерархии: Application, Window, Pane, Document, WorkBook, Worksheet, Chart и др. В приложениях Word и PowerPoint эти объекты обладают также булевым свойством Active, имеющим статус "только для чтения". При активизации объекта свойство Active получает значение True, при деактивации - False. Значение свойства позволяет понять, в каком состоянии находится тот или иной объект этих приложений.
Чтобы обратиться к выделенному объекту, достаточно иметь одно глобальное свойство Selection, поскольку в каждый текущий момент существует только один выделенный элемент. Активных элементов одновременно существует несколько, по одному в каждом классе. Поэтому для обращения к активному элементу надо указывать его класс. Поэтому для каждого класса имеется свое глобальное свойство. Чтобы обратиться к активному элементу некоторого класса Z, нужно вызвать соответствующее глобальное свойство ActiveZ, например ActiveDocument или ActiveWindow. В наших примерах мы неоднократно уже использовали обращение к активному документу - ActiveDocument. Рассмотрим еще один пример, в котором пользователь в приложении Excel выбирает рабочий лист. В зависимости от его выбора активизируется указанный лист, и в двух первых ячейках появляется приветствие, текст которого также зависит от выбора пользователя:
Public Sub Actis() Dim Answer As Integer Answer = InputBox(Prompt:="Выберите номер страницы для Вашей работы(1/2/3)", _ Default:=1) 'Активизирует рабочий лист, выбранный пользователем ActiveWorkbook.Sheets(Answer).Activate 'Работа с активным листом ActiveSheet.Cells(1, 1) = "Привет," Select Case Answer Case 1: ActiveSheet.Cells(1, 2) = "Михаил!" Case 2: ActiveSheet.Cells(1, 2) = "Владимир!" Case Else: ActiveSheet.Cells(1, 2) = "Люди!" End Select End Sub
На этом мы заканчиваем первое знакомство с общеупотребительными объектами Office 2000. В заключение мы хотим рассказать уже не о каких-либо конкретных объектах, а об удивительном программном средстве, позволяющем работать с ними. Речь пойдет о Macrorecorder.

Объект Selection является примером косвенной адресации. Он задает некоторую выделенную область, и его свойства и методы позволяют изменять состояние этой области. Другой пример косвенной адресации - активные элементы. Как в подокне может быть только один выделенный элемент, так и активный элемент может быть единственным в своем классе - одно активное окно, один активный документ или рабочий лист. Обычно активный элемент находится в фокусе клавиатуры, именно ему направлен ввод пользователя. Сделав активным новый элемент, мы отменяем, деактивируем элемент, ранее находившийся в фокусе клавиатуры.
Метод Activate делает активным элемент, его вызвавший. Этим методом обладают элементы верхних уровней иерархии: Application, Window, Pane, Document, WorkBook, Worksheet, Chart и др. В приложениях Word и PowerPoint эти объекты обладают также булевым свойством Active, имеющим статус "только для чтения". При активизации объекта свойство Active получает значение True, при деактивации - False. Значение свойства позволяет понять, в каком состоянии находится тот или иной объект этих приложений.
Чтобы обратиться к выделенному объекту, достаточно иметь одно глобальное свойство Selection, поскольку в каждый текущий момент существует только один выделенный элемент. Активных элементов одновременно существует несколько, по одному в каждом классе. Поэтому для обращения к активному элементу надо указывать его класс. Поэтому для каждого класса имеется свое глобальное свойство. Чтобы обратиться к активному элементу некоторого класса Z, нужно вызвать соответствующее глобальное свойство ActiveZ, например ActiveDocument или ActiveWindow. В наших примерах мы неоднократно уже использовали обращение к активному документу - ActiveDocument. Рассмотрим еще один пример, в котором пользователь в приложении Excel выбирает рабочий лист. В зависимости от его выбора активизируется указанный лист, и в двух первых ячейках появляется приветствие, текст которого также зависит от выбора пользователя:
Public Sub Actis() Dim Answer As Integer Answer = InputBox(Prompt:="Выберите номер страницы для Вашей работы(1/2/3)", _ Default:=1) 'Активизирует рабочий лист, выбранный пользователем ActiveWorkbook.Sheets(Answer).Activate 'Работа с активным листом ActiveSheet.Cells(1, 1) = "Привет," Select Case Answer Case 1: ActiveSheet.Cells(1, 2) = "Михаил!" Case 2: ActiveSheet.Cells(1, 2) = "Владимир!" Case Else: ActiveSheet.Cells(1, 2) = "Люди!" End Select End Sub
На этом мы заканчиваем первое знакомство с общеупотребительными объектами Office 2000. В заключение мы хотим рассказать уже не о каких-либо конкретных объектах, а об удивительном программном средстве, позволяющем работать с ними. Речь пойдет о Macrorecorder.

Библиотека объектов Office 2000 и Object Browser

Мы уже говорили, что Office 2000, так же как и Office 97, задается совокупностью библиотек классов. Каждому приложению соответствует своя библиотека классов объектов. Кроме того, есть библиотеки с общими для всех приложений классами объектов. Библиотеки классов, задающих приложения, устроены одинаково. Есть корневой класс (корневой объект) Application (Excel, Word и т.д.). Это "толстый объект", как матрешка. В него вложено большое число объектов, эти объекты задают свойства корневого объекта и являются так называемыми свойствами - участниками. Помимо этого корневой объект имеет терминальные свойства, методы и события. Каждый вложенный объект устроен аналогично - он тоже "толстый", может быть более толстый, чем корневой.
Сложность самой системы и большое количество составляющих ее объектов не позволяют описать все эти объекты в книгах наподобие этой. Так или иначе, но необходимо обращаться за справками к электронной документации. Справочная система по объектам в Office 97 была сделана, на наш взгляд, совсем неплохо, в Office 2000 она только улучшена. У Редактора VBE (Visual Basic Editor) есть инструментальное средство Object Browser (Окно просмотра объектов или, как теперь принято говорить, браузер объектов). Он позволяет просмотреть объекты, которые могут быть использованы в документе, увидеть их структуру - свойства, методы, события, получить справку и (или) пример использования.
Запомните, браузер объектов - это Ваш постоянный спутник при работе над проектом. Освойте работу с ним прежде всего. Невозможно запомнить, да и не нужно, множество объектов. Важнее, уметь получить нужную справку в нужный момент.
В браузере можно выбрать одну из библиотек каркаса документа, просмотреть ее структуру, для каждого класса объектов просмотреть его свойства, методы и события и тут же получить справку по каждому элементу. Вызывается Object Browser из редактора VBE выбором из меню пунктов View | Object Browser. А можно и быстрее: напомним, редактор вызывается нажатием клавиш Alt +F11, а Object Browser - F2.

Взгляните еще раз на рис. 1.2 и рис. 1.4 , где показан Object Browser. В левом верхнем углу окна просмотра объектов расположен раскрывающийся комбинированный список Project/Library, содержащий имена доступных проектов и библиотек. В окне Classes отображается структура выбранного из списка класса или проекта - совокупность встроенных объектов. Выбрав один из объектов, в соседнем окне Members Вы увидите элементы этого объекта (свойства, методы, события). Каждый элемент - библиотека, класс, проект, свойство, метод, событие, свойство типа перечисление - сопровождается соответствующим значком. Щелкнув кнопку "справка" (со значком "?"), Вы получите быструю справку по выбранному элементу.

Крайне полезное нововведение, сделанное в Office 2000, состоит в том, что в открывшемся окне справки можно щелкнуть кнопочку печати, позволяющее тут же напечатать справку. Более важно, что по другой кнопочке можно перейти в окно поиска информации по индексу, по оглавлению и в диалоге с Мастером Ответов (Answer Wizard). Единственный недостаток, справка и книги документации не согласованы, как это сделано в среде Visual C++, так что, найдя справку, вы не можете перейти в книгу документации, из которой эта справка взята, и просмотреть связный контекст. Так что есть еще место для совершенства справочной системы.

Следует заметить, что в Office 2000 справку выдает Мастер ответов, обладающий новыми возможностями. Его можно настроить так, что он будет выдавать справки и по объектам, спроектированным программистом.

Просматривать все объекты в поисках нужного - занятие довольно утомительное, поэтому в Object Browser имеются инструменты поиска. Ниже окна Project/Library расположен еще один комбинированный список, где нужно задать имя элемента поиска, а затем щелкнуть рядом расположенную кнопку поиска. При необходимости поиск можно осуществлять в одной из библиотек или во всех библиотеках (All libraries). Результаты поиска отображаются в специальном окне Search Results. На рис. 1.5 показаны результаты поиска слова Data в одной из библиотек.


Библиотека объектов Office 2000 и Object Browser
увеличить изображение
Рис. 1.5.  Окно с результатами поиска

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

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

Библиотека объектов Office 2000 - каркас приложений

В тех книгах, которые мы написали последнее время по Visual C++ и по Офисному программированию, большая часть достаточно солидного введения посвящалась основам объектно-ориентированного программирования. Мы полагаем, что каждый программист должен владеть этим аппаратом, поскольку без четкого осознания таких понятий, как класс, интерфейс класса, полиморфизм, наследование, встраивание, невозможно проектирование и разработка сколь либо серьезной системы документов. В этой книге мы не будем останавливаться на этих понятиях, полагая, что читатели с ними знакомы, если же нет, то можно порекомендовать обратиться к нашим книгам по Visual C++ и Офисному программированию, или к любым другим, где эти вопросы хорошо изложены.
Но на одном термине стоит все-таки остановиться подробнее. Речь пойдет о понимании совокупности библиотек классов, как каркаса приложений (каркаса документов). Когда программист вводит собственный класс объектов, то чаще всего он рассматривает его, как специфический тип данных, расширяющий возможности типов данных, встроенных в язык. Первые библиотеки классов, предоставляемые системой программисту, по существу, представляли расширенный набор типов данных. Но затем стало понятным, что объектный подход может быть полезным и при построении самого приложения. Само приложение стало рассматриваться, как совокупность объектов со своими свойствами, методами и событиями. Более того, стало ясно, что можно строить приложения различной архитектуры и стандартных скелетов приложений не так уже и много. При работе в среде Visual C++ , например, классической является архитектура Документ - Облик (Document -View).
В современных программных средах большую часть классов, входящих в библиотеку (совокупность библиотек), составляют классы, предназначенные для построения самого приложения, его основы или каркаса. В соответствии с этим такие библиотеки называются каркасом приложений (FrameWork Applications). Такую роль играют библиотеки Visual C++, Delphi и других программных сред.

Как же обстоит дело в Office 2000? Для программиста Office 2000 - это ничто иное, как обычная совокупность библиотек классов. И так же, как и для других программных сред, их классы представляют собой каркас приложений, или, что может быть точнее с содержательной точки зрения, - каркас документов.

Дело в том, что изменились цели программиста, работающего в среде Office 2000. Ранее целью программиста было создание приложения, понимаемого как программа, программный проект. Теперь программист является одним из участников (возможно единственным) создания системы документов. Документ, а не программа, становится целью разработки. Программный проект - это лишь часть документа. В Office 2000 программный проект неразрывно связан с документом, хранится, как часть документа, и не может существовать независимо от него. Отсюда становится понятным, что и библиотека классов Office 2000 содержит каркасы основных документов - текстовых документов, документов, основу которых составляют электронные таблицы, презентации, базы данных. Всякий раз, когда создается новый документ, его каркас составляют объекты библиотек, отобранных для построения этого конкретного каркаса документа.

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

Что определяет объект Range?

Пусть задана коллекция элементов - последовательность индексированных элементов. Индексы (ключи) элементов задают порядок их расположения в коллекции. Индексы могут быть как числами, так и именами. Объект Range позволяет задать произвольный диапазон этой последовательности, для чего достаточно задать индексы первого и последнего элемента. Диапазон может быть стянут в точку, так что объект Range может определять единственный элемент, а в приложении Word и пустой элемент - точку вставки. Используя свойства и методы объекта Range, можно выполнять групповые операции над всеми элементами диапазона. В одном из предыдущих примеров встречаются строки, использующие объект Range в Excel:
xlApp.Range("A1:A2") = 2 xlApp.Range("B1") = "=A1+A2" xlApp.Range("B2") = "=A1*A2"
В первой из этих строк задается диапазон из двух ячеек. Индексами являются имена этих ячеек. Одним присваиванием все ячейки указанного диапазона получают значение. В следующих строках опять-таки используется Range, но теперь работа идет с отдельным элементом. Этот пример отражает двойственную природу объекта Range.

DocFive

Пример: Печать глобальной переменной


                                   

DocThree

Пример: Печать глобальной переменной                     




Коллекции Characters, Words, Sentences

Свойства Characters, Words, Sentences возвращающие коллекции символов, слов или предложений, имеют объекты класса Documents, Selection, Range. Все эти коллекции устроены довольно просто. Они имеют известные нам свойства:
Application, Count, Creator, First, Last, Parent
и единственный метод Item(Index). Этот метод для всех коллекций возвращает объект Range, позволяющий работать как с единственным символом, так и с совокупностью символов, соответствующей слову или предложению. Добавление в коллекцию и удаление из нее элементов осуществляется методами объекта Range.
Поскольку никаких новых свойств и методов эти коллекции не имеют, нет смысла приводить примеры работы с ними. По существу все сведется к вызову методов объекта Range, а таких примеров было достаточно (см. последнюю процедуру).

Коллекции Excel: Workbooks, Sheets

"Excel позволяет одновременно работать с одной или несколькими рабочими книгами. Каждая книга состоит из одного или более листов. Листы бывают разных типов: рабочие листы, диаграммы, модули, листы макросов. Один из наиболее употребительных - рабочий лист - состоит из совокупности ячеек. Мощь Excel во многом определяется операциями, которые можно выполнить над ячейками рабочего листа…"
Примерно так можно было бы начать описание Excel. Мы начали его с введения основных понятий. При объектном подходе каждому понятию ставится в соответствие класс объектов. В терминах классов и объектов это же описание Excel может выглядеть так:
"Объект Application. Excel, представляющий приложение Excel, имеет свойство-участник (встроенный объект) WorkBooks, возвращающее объект WorkBooks - коллекцию всех открытых рабочих книг. Каждый элемент коллекции - рабочая книга - является объектом класса WorkBook.
Объект WorkBook, представляющий одну рабочую книгу, имеет свойство Sheets. При обращении к этому свойству возвращается объект Sheets - коллекция листов данной книги. Каждый элемент коллекции - лист - имеет свой тип и потому является объектом, например, класса Worksheet (рабочий лист) или Chart (диаграмма).
Объект Worksheet, представляющий один рабочий лист, имеет свойство Cells, возвращающее объект Range. Используя этот объект, можно задавать как все ячейки рабочего листа - всю таблицу, так и любую ее часть…"
Как видите, при объектном подходе к изучению Excel мы начинаем с введения классов объектов и заметьте, коллекции появляются уже на первых шагах. Коллекции WorkBooks, Worksheets представляют центральные понятия Excel. С них мы и начнем подробное рассмотрение коллекций объектов Office 2000.

Коллекции и конструкция For Each … Next

При работе с коллекциями довольно часто приходится организовывать цикл по всем элементам коллекции. До сих пор во всех наших примерах мы использовали обычные средства - цикл "For", где количество шагов цикла определялось свойством Count (его имеет каждая коллекция). Наша конструкция выглядела так:
With <объект> For i = 1 To .Count .Item(i) ' Доступ к i-му объекту и работа с ним Next i End With
VBA предлагает специальную конструкцию " For Each" для организации подобных циклов. Синтаксис конструкции таков:
For Each <Элемент> In <Группа> [<Операторы>] [Exit For] [<Операторы>] Next [<Элемент>]
где:
<Группа> - имя объекта-коллекции или массива.
<Элемент> - объект (переменная), совпадающий по классу с элементами коллекции, или имеющий тип Variant. Для массивов допустим только тип Variant. Нормально цикл заканчивается, когда переменная цикла получила все возможные значения и коллекция исчерпана. Но можно организовать досрочный выход. Для этого используется внутренняя конструкция Exit For. Обычно она помещается в Then-ветвь оператора If. Если условие выхода выполняется, следом выполняются Exit For и следующие за ним операторы, требуемые для корректного завершения цикла. После чего управление покидает цикл.
Цикл For Each не может использоваться для массивов с элементами, определенными пользователем. Последнее объясняется тем, что переменная типа Variant не может принимать пользовательский тип.
Приведем пример использования "старой" и "новой" конструкций:
With Documents For i = 1 To .Count For Each doc In Documents Debug.Print .Item(i) Debug.Print doc Next i Next doc End With
Преимущество "новой" очевидно: удобнее иметь сам элемент doc вместо индекса элемента i. Особенно это может быть важно, когда в ходе цикла приходится многократно обращаться к элементу doc.
При работе с коллекциями используйте, как правило, конструкцию For Each. Кроме всего прочего, она выполняется намного эффективнее по времени исполнения.

Мы уже говорили, что коллекцию

Мы уже говорили, что коллекцию можно рассматривать, как структуру данных, способ их организации. При таком подходе свойства и методы коллекции определяют эту структуру и не учитывают специфику объектов, составляющих коллекцию. Этот подход характерен для класса Collection языка VBA. В других приложениях Office 2000 преобладает несколько иная точка зрения: здесь для большинства коллекций учитываются особенности их объектов, что находит отражение в появлении специфических свойств и методов у различных коллекций. Сейчас мы подробно рассмотрим некоторые коллекции объектов Office 2000. Выбранные для ознакомления коллекции интересны сами по себе, ввиду их широкого использования. С другой стороны, нам важно дать общее представление о возможных вариациях при организации специальных коллекций. Надеемся, приведенных примеров будет достаточно для ориентации во всех имеющихся классах коллекций.

в Office 2000 являются коллекциями.

Почти половина классов объектов в Office 2000 являются коллекциями. Это объясняется тем, что элементы большинства классов можно объединить в упорядоченную совокупность, получив тем самым новый класс - коллекцию элементов. Коллекция это способ организации динамической структуры данных, и потому методы коллекций во многом схожи между собой. Но поскольку специфика элементов различна, то, как следствие, коллекции имеют и специальные свойства и методы. В современных языках программирования коллекции становятся столь же употребительной частью языка, как массивы. Рассмотрим некоторые коллекции объектов Office 2000 и те вариации, которыми они отличаются друг от друга.

Коллекции Word: Documents, Paragraphs и другие

Word позволяет одновременно работать с одним или несколькими документами. Документы и коллекция документов - это центральные понятия Word. Классы Document и Documents играют для Word ту же роль, что и классы WorkBook и WorkBooks для Excel. Документ Word организован довольно сложно. Текст - основа большинства документов - может быть дополнен рисунками и таблицами, с ним могут быть связаны комментарии и исправления, сноски и колонтитулы и т. д. Но об этом речь впереди, а пока чуть подробнее поговорим о тексте. Конечно, текст можно рассматривать как последовательность символов, его составляющих. Но ведь куда как часто мы оперируем с более крупными единицами! Word использует привычные понятия: символы, слова, предложения, абзацы, разделы.
Существуют понятия - существуют и классы, соответствующие этим понятиям. Объект Application. Word, представляющий приложение Word, имеет свойство (встроенный объект) Documents. При обращении к свойству возвращается объект класса Documents - коллекция всех открытых документов. Элемент коллекции - документ - это объект класса Document.
Объект Document, представляющий один документ, имеет свойства:
СвойствоВозвращает
Characters объект класса Characters, представляющий коллекцию символов текста документа
Words объект, представляющий коллекцию слов
Sentences объект, представляющий коллекцию предложений
Paragraphs объект, представляющий коллекцию абзацев
Sections объект, представляющий коллекцию разделов или секций документа
ListParagraphs объект, представляющий коллекцию списков, входящих в текст документа

Конечно, это лишь часть многочисленных свойств, связанных с текстом документа. Заметьте: данными свойствами обладает не только сам документ. Ими обладают и объекты Selection и Range, представляющие части документа. При этом возвращаемые объекты представляют соответствующие коллекции данной части документа.
Казалось бы, раз есть коллекции Characters или Words, то должны существовать и классы Character и Word, представляющие отдельный символ или отдельное слово. Но это не так. Элементами коллекций Characters, Sentences и Words являются объекты класса Range. Нет операций, отдельно определенных над символами, словами или предложениями, - они определены над интервалами, последовательностью символов, представляемых объектом Range. Ситуация здесь подобна Excel, где элементами коллекции Cells являются не ячейки, а все тот же объект Range.
Абзацу и разделу соответствуют свои классы объектов - Paragraph и Section соответственно. Но у нас сейчас разговор пойдет не о классах отдельных объектов, а о коллекциях. Мы начнем с коллекции документов.

Коллекция Documents

Коллекция Documents представляет все открытые документы Word. По своим свойствам и поведению она напоминает коллекцию WorkBooks, представляющую все открытые рабочие книги Excel. Поэтому мы будем часто ссылаться на уже описанные свойства и методы коллекции WorkBooks.
Документы, как и рабочие книги, могут существовать постоянно и храниться во внешней памяти - как файлы с обычным для документов Word расширением .doc. При открытии приложения Word автоматически открывается уже существующий или новый пустой документ. Так что в момент открытия приложения коллекция документов не пуста и содержит минимум один документ. Для добавления в коллекцию новых элементов используются методы:
  • Add - добавляет новые пустые документы;
  • Open - открывает файл, содержащий существующий документ и автоматически добавляет его в коллекцию.

  • Метод Add имеет синтаксис:
    Add(Template,NewTemplate)
    Параметр Template, задающий шаблон открытия документа, является строкой с именем файла, хранящего шаблон. Параметр не является обязательным и, если он опущен, используется стандартный шаблон Normal (файл Normal. dot). Второй параметр (тоже необязательный) - NewTemplate - булевого типа; он равен True, если документ открывается, как шаблон. Так что коллекция Documents содержит не только документы, но и шаблоны.
    У метода Open десять параметров. Обязательным является только первый - FileName; он задает имя файла, содержащего документ (шаблон). Остальные параметры определяют режимы открытия: пароль, статус файла, преобразование формата при открытии и т. п.
    Документ удаляется из коллекции при его закрытии. Вне коллекции не может существовать открытый документ. Метод Close закрывает документ и одновременно удаляет его из коллекции. Его синтаксис:
    Close(SaveChanges, OriginalFormat, RouteDocument)
    Все параметры являются необязательными. Первый указывает действия, предпринимаемые при закрытии документа, и может быть одной из констант: wd SaveChanges, wdDoNotSaveChanges, wdPromptToSaveChanges. В зависимости от константы сделанные в документе изменения будут сохранены, проигнорированы или решение будет предоставлено пользователю в режиме диалога.
    По умолчанию изменения сохраняются при закрытии документа. Параметр OriginalFormat указывает формат сохранения документа и может быть одной из констант: wdWordDocument, wdOriginalFormat, wdPromptUser. В первом случае при сохранении используется формат документов Word, во втором - оригинальный формат документа - тот, что он имел при открытии; в последнем случае решение принимает пользователь в режиме диалога. По умолчанию предполагается формат документа Word. Третий параметр - булевого типа - равен True, если документ после закрытия направляется следующему пользователю, который принимает участие в работе над этим документом. Данный параметр имеет смысл лишь при коллективной работе над документом в сети. По умолчанию он равен False.

    Метод Save позволяет сохранять документы (шаблоны) без закрытия и удаления из коллекции. Этот метод имеют как объекты класса Document, так и коллекции - объекты Documents. Если метод Save вызывает коллекция, то сохраняются все документы этой коллекции. При этом его синтаксис:

    Save(NoPrompt, OriginalFormat)

    Оба параметра необязательны. Если значение первого - True, Word автоматически сохраняет все документы, в противном случае возникнет диалог с пользователем о сохранении каждого из документов. Параметр OriginalFormat задает формат сохранения так же, как и в методе Close. У метода Save, используемого для сохранения отдельного документа, параметров нет. Если сохраняется новый документ, с которым еще не связано имя файла, его хранящего, будет вызвано диалоговое окно SaveAs, в котором пользователь задаст это имя и, если надо, путь.

    Для получения доступа к элементу коллекции, как обычно, можно вызвать метод Item. Его единственный параметр Index задает порядковый номер или имя документа в коллекции. Item является методом по умолчанию, так что индекс можно непосредственно указывать у объекта, задающего коллекцию.

    Класс Documents имеет типичные для коллекций свойства:

  • Application;
  • Count;
  • Creator;
  • Parent.


  • Эти свойства мы описали, когда речь шла о коллекции WorkBooks.Закончим рассмотрение этой коллекции примером:

    Пример 1.5.

    (html, txt)

    Приведем результаты отладочной печати (В данном примере испульзуюся документы Dex1, DocFive, DocThree:

    Число документов в коллекции Documents при открытии приложения Word = 1 Число документов после 4-х вызовов методов Add и Open = 5 Имена документов в коллекции: DocFive.doc DocThree.doc Template2 Document3 Dex1.dot Число документов после двух вызовов метода Close = 3 Имена документов, оставшихся в коллекции: DocFive.doc Template2 Dex1.dot

    Обратите внимание на то, что элементы добавляются в начало коллекции, а не в ее конец, как это было при добавлении элементов в коллекцию WorkBooks Excel. При удалении элемента происходит перенумерация индексов, поэтому удаляется второй и четвертый элемент коллекции при последовательном выполнении методов Item(2). Close, Item(3).Close.


    Закончим рассмотрение этой коллекции примером:

    Public Sub WorkWithDocuments() 'Работа с коллекцией документов Dim N As Long, I As Byte Dim PathDir As String PathDir = "e:\O2000\CD2000\Ch1\" With Documents N = .Count Debug.Print "Число документов в коллекции Documents " & _ "при открытии приложения Word = ", N ' Добавление 2-х новых документов (второй представляет шаблон) .Add .Add NewTemplate:=True 'Добавление двух существующих документов .Open (PathDir & "DocThree") .Open (PathDir & "DocFive") N = .Count Debug.Print "Число документов после 4-х вызовов методов " _ & "Add и Open =", N Debug.Print "Имена документов в коллекции:" For I = 1 To .Count Debug.Print .Item(I).Name Next 'Закрытие двух документов и, следовательно, удаление их из коллекции .Item(2).Close .Item(3).Close N = .Count Debug.Print "Число документов после двух вызовов " _ & "метода Close =", N Debug.Print "Имена документов, оставшихся в коллекции:" For I = 1 To .Count Debug.Print .Item(I).Name Next End With End Sub

    Пример 1.5.

    Приведем результаты отладочной печати (В данном примере испульзуюся документы Dex1, DocFive, DocThree:

    Число документов в коллекции Documents при открытии приложения Word = 1 Число документов после 4-х вызовов методов Add и Open = 5 Имена документов в коллекции: DocFive.doc DocThree.doc Template2 Document3 Dex1.dot Число документов после двух вызовов метода Close = 3 Имена документов, оставшихся в коллекции: DocFive.doc Template2 Dex1.dot

    Обратите внимание на то, что элементы добавляются в начало коллекции, а не в ее конец, как это было при добавлении элементов в коллекцию WorkBooks Excel. При удалении элемента происходит перенумерация индексов, поэтому удаляется второй и четвертый элемент коллекции при последовательном выполнении методов Item(2). Close, Item(3).Close.

    Коллекция Paragraphs

    Свойство Paragraphs, возвращающее коллекцию абзацев, имеют объекты класса Documents, Selection, Range. Абзац - это не только последовательность символов, но и структурно выделенная единица текста, характеризуемая параметрами: отступом или выступом по отношению к границам листа документа, межстрочными интервалами, расположением первой строки, стилем и пр. Конечно, все эти характеристики связаны в первую очередь с самим объектом, точнее с классом Paragraph. Но, как это часто бывает в Office 2000, свойства и методы объекта включаются и в коллекцию, содержащую эти объекты. В какой-то мере происходит наследование снизу вверх.
    У коллекции Paragraphs два метода, типичные для всех коллекций: Item и Add. Первый, как всегда, определяет элемент коллекции, заданный параметром Index. Абзацы не имеют имен, и индекс всегда задает порядковый номер элемента. Нумерация абзацев в коллекции начинается с 1. Метод Add используется для добавления нового пустого абзаца. Его синтаксис:
    Add(Range)
    Необязательный параметр Range указывает точку вставки, - абзац вставляется перед той частью текста, которую задает объект Range. Если параметр опущен, абзац вставляется в конец документа или после объекта Selection и Range, если они вызвали метод Add.
    Для вставки пустого абзаца чаще всего применяются методы объектов Range и Selection: InsertParagraph, InsertParagraphAfter, InsertParagraphBefore. Они не имеют параметров. Новый абзац заменяет текст или вставляется после или перед частью текста, заданного объектом Range или Selection, вызвавшего соответствующий метод.
    Предупреждение:
    Осторожнее с методом InsertParagraph: он заменяет весь имеющийся текст новым, пустым абзацем!
    Для вставки текста в созданный пустой (или непустой) абзац могут быть использованы методы объектов Range и Selection: InsertAfter и InsertBefore. Их единственный параметр - Text - задает текст, добавляемый в начало или в конец части текста, заданной объектами Range и Selection, после чего объекты расширяются, дабы включить добавленный текст.
    Для удаления абзацев может использоваться метод Delete объектов Range и Selection.

    В коллекции Paragraphs много методов, "наследуемых" у объекта Paragraph. Чаще всего они применяются к отдельному абзацу, но иногда вызываются коллекцией, чтобы применить метод ко всем абзацам коллекции. Вот эти методы:

  • OpenUp, CloseUp, OpenOrCloseUp - вставляют или удаляют фиксированное количество пробелов (12) перед абзацем. OpenOrCloseUp - переключатель: если есть пробелы, он их удаляет, нет - вставляет.
  • Indent и Outdent - увеличивают и уменьшают отступ абзаца (или всех абзацев коллекции) от края листа.
  • OutlineDemote, OutlinePromote и OutlineDemoteToBody - понижают и повышают уровень заголовка в интервале Heading1 : Heading8. При понижении последнего уровня -Heading 8 стиль заголовка меняется на стиль Normal.
  • Reset - удаляет форматирование, сделанное вручную, применяя формат, заданный стилем абзаца.
  • Space1, Space2, Space15 устанавливают в абзаце межстрочный интервал: одинарный, двойной или полуторный.
  • TabHangingIndent(Count) и TabIndent(Count) изменяют выступ или отступ абзаца от края листа на заданное количество позиций, определенное параметром Count. Если значение этого параметра положительно, происходит увеличение, иначе - уменьшение отступа (выступа).


  • Коллекция Paragraphs имеет общие свойства Count, Parents, Creator и Application. Кроме того, есть много свойств и методов, специфических для абзацев. Иногда, одной и той же цели можно добиться, вызывая метод или изменяя свойство. За подробностями отсылаем к документации.

    Коллекция Sections

    Свойство Sections, возвращающее коллекцию разделов, имеют объекты класса Documents, Selection, Range. Разделение документа на разделы позволяет придать документу подходящую структуру. Обычно раздел - довольно крупная единица текста в отличие от абзаца. Например, если документ представляет книгу, то каждый ее параграф или глава может быть выделена в отдельный раздел. При установке разделов вручную из меню Insert выбирается пункт Break, позволяющий закончить предыдущий раздел и начать новый. Раздел кончается специальным символом прерывания определенного типа. В зависимости от типа символа прерывания новый раздел может начинаться:
  • на той же странице;
  • на следующей странице;
  • на следующей четной странице;
  • на следующей нечетной странице.

  • Конечно, с разделом связывается не только последовательность символов, но и стиль форматирования, установки, принятые для его страниц, установки верхних и нижних колонтитулов и другие характеристики форматирования.
    Вернемся к коллекции Sections. Она имеет свойства, уже известные нам по другим коллекциям. Поэтому мы их только перечислим:
    Application, Count, Creator, Parent, First, Last.
    Новым, ранее не упоминавшимся является свойство PageSetup. При обращении к нему возвращается объект класса PageSetup, свойства и методы которого позволяют установить характеристики страницы одновременно для всех разделов коллекции. Метод Item позволяет получить доступ к элементу коллекции. Метод Add позволяет добавить новый раздел в документ:
    Add(Range, Start)
    Возможный параметр Range указывает объект, перед которым будет вставлен символ прерывания, заканчивающий раздел. После символа прерывания будет начинаться новый раздел объектом Range. Если параметр Range опущен, символ прерывания вставляется в конец документа. Возможный параметр Start определяет тип символа прерывания раздела. Он может быть задан одной из констант:
    wdSectionNewPage, wdSectionContinuous, wdSectionEvenPage, wdSectionOddPage, wdSectionNewColumn
    Смысл, задаваемый этими константами, ранее уже был определен.
    В зависимости от их значения новый раздел будет начинаться со следующей страницы, на той же странице, на четной и нечетной странице. Последняя константа используется при работе с таблицами, позволяя по-разному форматировать каждый из ее столбцов. Если параметр Start опущен, новый раздел будет начинаться со следующей страницы.

    Для удаления раздела из коллекции может быть использован метод Delete объектов Selection и Range.

    Взгляните на пример работы с текстом документа, в котором создаются новые разделы и добавляются абзацы:

    Пример 1.6.

    (html, txt)

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

    Sections.Last.Range.Paragraphs(1).Range.InsertBefore (";Лекция 2";)
  • Свойство Sections активного документа возвращает объект Sections, задающий коллекцию разделов этого документа.
  • Свойство Last объекта возвращает объект Section, задающий последний раздел в этой коллекции.
  • Свойство Range возвращает объект Range, связанный с данным разделом. Теперь становятся доступны методы и свойства объекта, в частности свойство Paragraphs, которым объект Section не обладает.
  • Вызов Paragraphs(1) возвращает объект класса Paragraph, задающий первый абзац из коллекции абзацев данного раздела. Заметьте: опущен метод по умолчанию Item, так что запись Paragraphs(1) является краткой формой записи Paragraphs. Item (1)
  • И снова мы получаем объект Range, теперь уже связанный с абзацем.
  • На последнем шаге вызывается метод InsertBefore объекта Range, позволяющий добавить в начало абзаца текст, заданный аргументом метода. В нашем случае - лекция 2.



  • End Sub

    Пример 1.6.

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

    Sections.Last.Range.Paragraphs(1).Range.InsertBefore (";Лекция 2";)
  • Свойство Sections активного документа возвращает объект Sections, задающий коллекцию разделов этого документа.
  • Свойство Last объекта возвращает объект Section, задающий последний раздел в этой коллекции.
  • Свойство Range возвращает объект Range, связанный с данным разделом. Теперь становятся доступны методы и свойства объекта, в частности свойство Paragraphs, которым объект Section не обладает.
  • Вызов Paragraphs(1) возвращает объект класса Paragraph, задающий первый абзац из коллекции абзацев данного раздела. Заметьте: опущен метод по умолчанию Item, так что запись Paragraphs(1) является краткой формой записи Paragraphs. Item (1)
  • И снова мы получаем объект Range, теперь уже связанный с абзацем.
  • На последнем шаге вызывается метод InsertBefore объекта Range, позволяющий добавить в начало абзаца текст, заданный аргументом метода. В нашем случае - лекция 2.


  • Коллекция Sheets

    Коллекция Sheets представляет коллекцию листов двух типов: рабочих листов и диаграмм, - так что каждый элемент коллекции может быть объектом одного из двух классов: Worksheet или Chart. Наряду с коллекцией Sheets существуют коллекции Worksheets и Charts. Они представляют коллекции рабочих листов и диаграмм, выделенных из коллекции Sheets. Но объекты, задающие эти коллекции, принадлежат классу Sheets и используют его свойства и методы. К их рассмотрению мы и переходим.
    Когда создается или открывается рабочая книга, она содержит уже некоторое число листов. Так что коллекция Sheets не пуста в момент открытия книги. Для добавления новых листов в книгу используется знакомый нам метод Add:
    Add(Before,After,Count,Type)
    Все параметры являются необязательными. Первые два - Before и After задают позицию вставки листа - индекс листа, перед или после которого вставляется новый лист. Из этих параметров одновременно может быть задан только один. Если оба параметра опущены, вставка производится перед активным листом данной книги. Параметр Count определяет количество вставляемых листов (по умолчанию - 1). Последний параметр определяет тип вставляемых листов, задаваемый соответствующей константой: xlWorksheet, xlChart, xlExcel4MacroSheet, xlExcel4IntlMacroSheet (по умолчанию - xlWorksheet).
    Замечание:
    Рабочая книга имеет листы четырех типов. Но при вызове свойства Sheets объекта WorkBook возвращается объект класса Sheets с листами только двух типов. Чтобы получить листы других типов, нужно обратиться к другому свойству объекта WorkBook, например Excel4IntlMacroSheet, возвращающему также объект класса Sheets, но содержащий листы с так называемыми международными макросами, созданными под Excel4.
    Следует обратить внимание на то, что:
  • в методе Add класса Sheets нет параметра, задающего ключ элемента. Однако фактически ключ задается. Его роль играет имя листа, заданное в виде свойства Name, имеющееся у листа каждого типа. Имя листа наряду с числовым индексом можно использовать для поиска листа в методе Item.
  • Из коллекции Sheets листы удаляются методом Delete, не имеющим параметров.
    В такой форме этот метод имеют многие классы объектов, в том числе Sheets, Worksheet и Chart. Если Delete вызывает объект Sheets, удаляется вся коллекция; если же его вызывают объекты Worksheet или Chart, удаляется соответствующий лист.
  • Получить доступ к элементу позволяет метод (свойство) Item:

    Item(Index)

    Параметр Index задает имя или порядковый номер листа. Метод Item является методом по умолчанию и может быть опущен.
  • Напомним, что иногда бывает трудно провести четкую грань, отличающую метод от свойства. Item является классическим примером подобной ситуации и потому в одной части документации он называется методом, а в другой - свойством.

    Класс Sheets имеет уже известные нам свойства: Application, Creator, Parent и Count. Рассмотрим новые возможности, появившиеся у коллекции Sheets:

  • Свойство Visible - имеет значение True, если объект видимый, и False - в противном случае. Для рабочих листов и диаграмм это свойство может быть также задано константой xlVeryHidden (упрятано, да не просто, а очень уж). Если лист имеет это свойство, он становится невидимым и пользователь никак не сумеет его увидеть, пока программно это свойство не будет установлено в True. Тип свойства двоякий: Boolean или Long.
  • Свойства HPageBreak и VPageBreak - возвращают соответственно коллекции горизонтальной и вертикальной разбивки листа на страницы.
  • Метод Copy - позволяет создать новый лист книги, копируя содержимое уже имеющегося листа. Его синтаксис:

    Copy(Before, After)

    Параметры имеют привычный смысл, указывая точку вставки копируемого листа. Только один из них может быть задан при вызове метода. Метод Copy, вызванный без параметров, копирует содержимое объекта в буфер.
  • Метод Move - модификация метода Copy, позволяет передвинуть лист книги в новое положение. Его синтаксис:

    Move(Before,After)

    Параметры имеют тот же смысл, что и в методе Copy.
  • Метод Paste - копирует содержимое буфера. Параметров не имеет.
  • Метод Select - выделяет объект, его вызвавший. Методом Select обладают объекты многих классов.


    Если его вызывает коллекция Sheets, метод используется без параметров и выделяет все листы коллекции. Если же он вызван отдельным листом, то может иметь параметр:

    Select(Replace)

    Параметр Replace - булевого типа: если он равен True, происходит замена объекта Selection, и выделенным становится лист, вызвавший метод. Если же значение параметра - False, происходит расширение области выделения, так чтобы она включала ранее выделенный объект и новый лист.
  • Методы PrintPreview и PrintOut - позволяют осуществлять просмотр печати и печать объекта, вызвавшего метод. Как и большинство других рассматриваемых методов, они принадлежат многим классам, в том числе классам Sheets, Charts, Chart, Worksheets, Worksheet, WorkBook и др. Его синтаксис:

    PrintOut(From, To, Copies, Preview, ActivePrinter, PrintToFile, Collate)

    Все параметры необязательны. Первые два задают начальную и конечную страницу печати. Если их нет, печать идет с первой по последнюю страницу. Параметр Copies, если указан, задает количество копий при печати. Если значение булевого параметра Preview - True, предварительно происходит просмотр печати, если же параметр опущен или равен False, просмотра перед печатью нет. Параметр ActivePrinter устанавливает имя активного принтера. Значение булевого параметра PrintToFile - True, если предполагается, что результаты печати посылаются не на принтер, а записываются в файл. При этом появляется диалоговое окно, в котором пользователь задает имя файла, хранящего результаты печати. Если значение параметра Collate задано True, будут сличаться результаты копий, выводимых на печать.
  • Метод FillAcrossSheets копирует содержимое области, заданной объектом Range на рабочие листы коллекции. Его синтаксис:

    FillAcrossSheets(Range, Type)

    Параметр Range указывает область копирования. Она должна принадлежать одному из листов коллекции. Этот параметр обязателен. Необязательный параметр - Type - указывает, как происходит копирование. Он может принимать значение, заданное одной из следующих констант: xlFillWithAll, xlFillWithContents или xlFillWithFormulas.


    Значение по умолчанию - xlFillWithAll.


  • В заключение приведем пример, демонстрирующий большинство методов этой коллекции.

    Пример 1.4.

    (html, txt)

    Работая с тестовым документом - книгой BookOne. Мы последовательно щелкали командные кнопки, запускающие процедуры WorkWithBooks и WorkWithSheets. Вот результаты отладочной печати при работе последней процедуры:

    Число листов при первоначальном открытии книги = 3 Имена листов: Sheet1 Sheet2 Sheet3 Число листов книги после вставки 3-х листов = 6 Имена и типы листов после переименования: One -4167 Two -4167 Three 3 Four -4167 Five 3 Six -4167 Имена, типы листов и содержимое двадцатой ячейки: One -4167 6765 Two -4167 Four -4167 6765 Six -4167 6765 Seven -4167 6765

    Кратко прокомментируем работу программы. Вначале активизируется книга с именем Book2 из коллекции книг, созданных предыдущим макросом. Эта книга содержит три листа со стандартными именами: Sheet1 -Sheet3. Мы добавляем в эту книгу еще три листа разных типов и переименовываем все листы, давая им имена от One до Six. Четыре листа книги являются рабочими листами и имеют тип xlWorksheet (значение константы -4167). Третий лист - это лист диаграммы типа xlChart, а пятый является листом макросов и имеет тип xlExcel4MacroSheet.

    Замечание:

    В Office 97 и Бета версии Office 2000 тип листа диаграммы (свойство Type) устанавливался неверно. Вместо правильного значения, равного -4109, устанавливалось значение 3, что соответствует листу макроса. В процессе тестирования Office 2000 мы сообщили об этой ошибке и надеемся, что в заключительной версии она уже исправлена.

    Затем третий и пятый лист удаляются, и в книге остаются только рабочие листы. Следующие шаги программы демонстрируют работу методов Copy и Move. Вначале активизируется первый лист, вызывается макрос Fibonachi, заполняющий последовательностью чисел Фибоначчи область листа - ячейки A1:A20. Затем создается копия этого листа, и вновь созданный лист - One(2) - помещается после второго листа книги. Он переименовывается, получает имя Seven и перемещается, заняв позицию после листа Six.Метод FillAcrossSheet позволяет скопировать область одного листа в соответствующие области нескольких листов той же книги. Для этого вначале создается массив X с именами листов, в которые будут копироваться данные. Обратите внимание на этот важный, часто используемый метод. Заметьте: в список массива X должен быть включен и лист One, с которого будет производиться копирование.


    Пример 1.4.

    Работая с тестовым документом - книгой BookOne. Мы последовательно щелкали командные кнопки, запускающие процедуры WorkWithBooks и WorkWithSheets. Вот результаты отладочной печати при работе последней процедуры:

    Число листов при первоначальном открытии книги = 3 Имена листов: Sheet1 Sheet2 Sheet3 Число листов книги после вставки 3-х листов = 6 Имена и типы листов после переименования: One -4167 Two -4167 Three 3 Four -4167 Five 3 Six -4167 Имена, типы листов и содержимое двадцатой ячейки: One -4167 6765 Two -4167 Four -4167 6765 Six -4167 6765 Seven -4167 6765

    Кратко прокомментируем работу программы. Вначале активизируется книга с именем Book2 из коллекции книг, созданных предыдущим макросом. Эта книга содержит три листа со стандартными именами: Sheet1 -Sheet3. Мы добавляем в эту книгу еще три листа разных типов и переименовываем все листы, давая им имена от One до Six. Четыре листа книги являются рабочими листами и имеют тип xlWorksheet (значение константы -4167). Третий лист - это лист диаграммы типа xlChart, а пятый является листом макросов и имеет тип xlExcel4MacroSheet.

    Замечание:

    В Office 97 и Бета версии Office 2000 тип листа диаграммы (свойство Type) устанавливался неверно. Вместо правильного значения, равного -4109, устанавливалось значение 3, что соответствует листу макроса. В процессе тестирования Office 2000 мы сообщили об этой ошибке и надеемся, что в заключительной версии она уже исправлена.

    Затем третий и пятый лист удаляются, и в книге остаются только рабочие листы. Следующие шаги программы демонстрируют работу методов Copy и Move. Вначале активизируется первый лист, вызывается макрос Fibonachi, заполняющий последовательностью чисел Фибоначчи область листа - ячейки A1:A20. Затем создается копия этого листа, и вновь созданный лист - One(2) - помещается после второго листа книги. Он переименовывается, получает имя Seven и перемещается, заняв позицию после листа Six. Метод FillAcrossSheet позволяет скопировать область одного листа в соответствующие области нескольких листов той же книги.Для этого вначале создается массив X с именами листов, в которые будут копироваться данные. Обратите внимание на этот важный, часто используемый метод. Заметьте: в список массива X должен быть включен и лист One, с которого будет производиться копирование.

    Коллекция Workbooks

    Коллекция WorkBooks представляет все открытые книги приложения Excel. Рассмотрим свойства и методы этой коллекции. Как обычно, начнем с методов, позволяющих добавить элемент в коллекцию, удалить его и получить к нему доступ.
    Одна из специфических черт элементов этой коллекции состоит в том, что большинство рабочих книг существуют до появления коллекции. Они хранятся во внешней памяти, как файлы, обычно с расширением .xls. Поэтому для добавления элементов в коллекцию применяются два различных метода: Add и Open. Первый добавляет новую, пустую книгу в коллекцию, второй - уже существующую книгу, хранящуюся во внешней памяти.
    Синтаксис метода Add таков:
    Add(Template)
    Необязательный параметр Template задает шаблон создания книги. Этот параметр может быть строкой, задающей имя файла, который хранит некую рабочую книгу. В этом случае книга играет роль шаблона для создания новой книги. Параметр Template может также принимать значение одной из предопределенных констант: xlWBATChart, xlWBATWorksheet, xlWBATExcel4IntlMacroSheet, xlWBATExcel4MacroSheet. В этом случае создается рабочая книга с одним листом, тип которого и задает константа. Если же параметр Template опущен, то создается книга с несколькими пустыми листами, число которых устанавливает свойство SheetsInNewWorkbook.
    У метода Open более десяти параметров. Обязательным является только первый - FileName. Он должен быть строкой, которая задает имя файла, хранящего книгу.
    Чтобы удалить рабочую книгу из коллекции, нужно закрыть соответствующий файл (в коллекцию входят лишь открытые рабочие книги). Закрытие книги и, следовательно, ее удаление выполняет метод Close. Он определен как для коллекции, так и для рабочей книги - объекта класса WorkBook. В первом случае метод Close не имеет параметров и закрывает всю коллекцию - все рабочие книги. Чаще всего этот метод вызывается объектом - рабочей книгой для своего закрытия. В этом случае его синтаксис:
    Close(SaveChanges, FileName, RouteWorkbook)
    Все параметры являются необязательными.
    Первый - булевого типа: если его значение True, сделанные изменения будут сохранены, в противном случае они не сохраняются. Когда же этот параметр опускается, то при выполнении появляется соответствующее диалоговое окно, в котором свой выбор может сделать пользователь. Второй параметр - FileName - необходим, если речь идет о закрытии новой книги, с которой еще не связано имя файла. Если параметр опущен, а указание имени файла необходимо, то, естественно, появляется диалоговое окно. Третий параметр, как и первый, булевого типа. Он задается, когда рабочая книга может быть направлена нескольким участникам для поочередной работы с ней. Если его значение True, книга направляется следующему по списку ее разработчику.

    Чтобы получить доступ к рабочей книге в коллекции, можно воспользоваться методом Item:

    Item(Index)

    Индекс может указывать порядковый номер книги в коллекции, либо быть именем книги. Заметьте, имя книги это имя файла, в котором она хранится. Когда создается новая пустая книга, то ей по умолчанию система присваивает имя типа " BookN ". При сохранении книги в файле это имя может быть изменено. Метод Item является методом по умолчанию и чаще всего опускается. В этом случае индекс связывается с самим объектом - коллекцией рабочих книг.

    Свойство Count возвращает количество элементов коллекции. Доступно только для чтения, имеет тип возвращаемого значения Long.

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

  • Свойство Application возвращает корневой объект Application.
  • Свойство Creator аналогично свойству Application. Оно возвращает код приложения (32-битное целое), который используется при работе с Macintosh.
  • Свойство Parent возвращает родительский объект (для коллекции WorkBooks это все тот же объект Application).
  • Метод OpenText является модификацией метода Open. Он создает новую рабочую книгу с одной страницей, используя для ее создания текстовый файл.


    В процессе открытия происходит анализ и разбор данных, записанных в текстовом файле. Метод имеет 13 параметров, из которых обязателен лишь первый - FileName, задающий имя текстового файла.


  • Приведем теперь пример программы, позволяющий продемонстрировать детали работы с коллекцией WorkBooks. Наша коллекция будет включать как новые, создаваемые в программе, так и ранее существовавшие книги. Книги будут добавляться в коллекцию и удаляться. Отладочная печать позволит проследить за этим процессом.

    Пример 1.3.

    (html, txt)

    Вот результаты отладочной печати:

    Число книг в коллекции WorkBooks при открытии приложения Excel =1 Число книг после 2-х вызовов методов Add и Open = 5 Имена книг в коллекции: BookOne.xls Book4 Book5 BookThree.xls BookFive.xls Число книг после двух вызовов метода Close = 3 Имена книг, оставшихся в коллекции: BookOne.xls Book5 BookFive.xls

    Поясним пример. При открытии Excel коллекция WorkBooks состоит из одной открытой книги, в данном случае - книги с именем BookOne. Затем метод Add добавил в коллекцию две новые книги, которые получили имена " Book1 " и " Book2 " соответственно. Еще две книги появляются в коллекции при открытии существующих книг. Затем две книги закрываются, следовательно, удаляются из коллекции. Обратите внимание, удаляются 2-я и 4-я книги коллекции, хотя индексы удаляемых книг имеют значение 2 и 3. Это связано с тем, что после каждого удаления элемента коллекции происходит перенумерация, и книги получают новые индексы.

    Если при работе с элементами коллекции выполняется операция удаления, то работать с индексами элементов крайне опасно, предпочтительнее для задания элементов указывать их имена!

    Macrorecorder

    Ничего удивительного в Macrorecorder нет. Его появление - естественное следствие объектно-ориентированного, событийно-управляемого и визуального программирования. Интерфейс большинства приложений теперь строится так, чтобы дать конечному пользователю как можно больше свободы выбора по управлению работой этого приложения. Пользователь "видит" объекты приложения (их образы). Он может выбирать в меню команды, может щелкнуть ту или иную кнопку, перемещать по экрану объекты, менять их размеры и другие характеристики, открывать и закрывать окна - да мало ли что может теперь делать пользователь! Его действия являются причиной событий в мире объектов. Возникает событие, - объектам посылается сообщение, - в ответ вызывается метод, обрабатывающий это сообщение.
    Так возникает двойственность. Одни и те же вещи можно сделать и визуально, и программно - эффект будет один. Большинство приложений Office 2000, и в первую очередь Word и Excel, рассчитаны на визуальный способ работы с ними. Но программистов не обманешь - они-то знают, что всякий раз, когда пользователь визуально инициирует действие, вызывается программа, выполняющая это действие. Дело в том, что система следит за действиями пользователя, анализирует их и запускает программы, соответствующие этим действиям. Иначе говоря, происходит трансляция с языка действий в язык программирования. Поскольку этот язык объектный, то можно говорить, что действия пользователя (события) интерпретируются и приводят к вызову методов и изменению свойств в мире объектов. Поскольку есть интепретатор действий, то можно построить и транслятор.
    Macrorecorder - это и есть транслятор. Всякий раз, когда пользователь запускает Macrorecorder, создается программа (макрос) на языке VBA, которая является результатом трансляции действий пользователя с момента запуска Macrorecorder до момента окончания записи макроса. Пользователь оперирует с образами объектов на экране, программа - с объектами Office 2000. Заметьте: одного и того же эффекта, пользователь иногда может достичь разными путями - он может вызвать команду меню или щелкнуть соответствующую кнопку или нажать комбинацию "горячих" клавиш.
    С этими разными событиями может связываться одна и та же процедура обработки. Поэтому созданная Macrorecorder программа в таких случаях будет одинаковой, не зависящей от того, как пользователь добился нужного ему эффекта.

    На сегодня Macrorecorder не очень "интеллектуален" - он слепо копирует действия пользователя и не занимается оптимизацией созданной им программы. Так, если пользователь записал в ячейку А1 число 15, в А2 - 17, а потом передумал и решил вернуться к ячейке А1 и заменить 15 на 21, то Macrorecorder в программе повторит все его действия. "Любимый" объект Macrorecorder - Selection, и это понятно: всякий раз, когда пользователь выбирает новый объект, Macrorecorder создает новый объект Selection. Сегодня типична ситуация, когда программист использует макрос, созданный Macrorecorder, как заготовку, оптимизируя код вручную. Думается, интеллект Macrorecorder повысится в следующих версиях. Но и сейчас это крайне полезное средство. Office 2000 позволяет чрезвычайно просто решить вручную массу задач (в этом вся его суть). Вместе с тем необходимость в решении таких задач может возникать неоднократно, и потому полезно иметь соответствующую программу. Так что технология такова: один раз задача решается вручную, Macrorecorder создает макрос, транслируя наши действия, затем макрос, если надо, оптимизируется и запускается всякий раз, когда возникает необходимость решения данной задачи.

    Завершим описание примером. Наиболее удобен для демонстрации Excel, - именно здесь вручную можно решать очень необычные задачи. Поэтому и Macrorecorder интенсивнее всего используется в Excel. Хотя, привыкнув, Вы будете применять его и в других приложениях Office 2000. Рассмотрим написание в Excel программы, создающей последовательность первых N чисел Фибоначчи, - типичная задача для студентов первого курса, изучающих программирование. В ней есть рекуррентные соотношения, цикл, она требует понимания понятия "переменная". Рекуррентные соотношения для получения чисел Фибоначчи таковы:


    A1 = 1; A2 = 2; A k= A k-1 + A k-2; k = 3…N;

    Macrorecorder
    увеличить изображение
    Рис. 1.11. 

    Вот как мы решали эту задачу вручную ( рис. 1.11). Вначале вызвали Macrorecorder, выбрав в меню Tools пункт Macro, затем - Record New Macro (проще щелкнуть кнопку начала записи макроса, если она есть на панели). В появившемся диалоговом окне задали имя макроса - Fibonachi. Заметьте: появляется кнопка окончания записи макроса, если ее не было на соответствующей панели. Теперь наши дальнейшие действия будут транслироваться в соответствующую программу. В ячейку А1 мы записали текст "Числа Фибоначчи", в ячейки А2 и А3 - числа 1 и 2, в ячейку А4 - формулу "= А2+А3", а затем скопировали эту формулу до ячейки А20 включительно. По ходу копирования формула пересчитывалась, и в ячейках появлялись значения, соответствующих чисел Фибоначчи. Затем мы, щелкнув кнопку, выключили запись макроса. Результаты действий - на рис. 1.7. Созданный Macrorecorder макрос Fibonachi теперь доступен, и его можно вызывать в любой момент. Вот его текст:

    Sub Fibonachi() ' ' Fibonachi Macro ' Macro recorded 28.02.1999 by Vladimir Billig

    Selection.Font.Bold = True ActiveCell.FormulaR1C1 = "Числа Фибоначчи" Range("A2").Select ActiveCell.FormulaR1C1 = "1" Range("A3").Select ActiveCell.FormulaR1C1 = "2" Range("A4").Select ActiveCell.FormulaR1C1 = "=R[-2]C+R[-1]C" Selection.AutoFill Destination:=Range("A4:A20"), Type:=xlFillDefault Range("A4:A20").Select End Sub

    В созданном макросе используются уже известные нам объекты: Range, Selection, ActiveCell и метод Select объекта Range. В комментариях нуждается свойство ячеек Excel - FormulaR1C1, - позволяющее связать с ячейкой значение или формулу (для ячейки А4), заданную в относительных координатах R (Row - строка) и C (Column - столбец). Отсчет координат ведется от активной ячейки, поэтому для ячейки А4 формула "=А2 +А3" в этих координатах принимает вид: "=R[-2]C+R[-1]C ".


    Основная операция копирования не потребовала организации цикла в программе. Этот цикл реализован внутри метода AutoFill, вызванного для объекта Selection. Два параметра этого метода Destination и Type задают область и тип копирования. В оптимизации этот макрос не нуждается

    Ранее мы говорили, что браузер объектов - это Ваш неизменный спутник и помощник в процессе программистской работы. Другим таким же полезным помощником является Macrorecoder!

    Дело даже не в том, чтобы получить макрос для многократного использования, - Вы и сами его можете написать. Но часто возникает ситуация, когда Macrorecoder запускается для того, чтобы понять, какие объекты и какие методы следует использовать для решения той или иной задачи. Например, Вы могли бы не знать, как скопировать формулу в соседние ячейки, чтобы она корректно изменилась при копировании. Макрос Fibonachi подсказал бы Вам, что для этого следует использовать метод AutoFill. Заодно Вы бы поняли, что в область копирования (destination) должен быть включен и источник копирования. Так что не забывайте Macrorecoder - это Ваше подручное средство.

    На этом мы заканчиваем предварительное знакомство с объектами Office 2000 и переходим к систематическому изучению самого языка программирования VBA.

    Метод Range

    Ранее мы говорили, что объект класса Paragraph имеет свойство Range, иначе говоря, в него вложен объект Range., задающий последовательность символов данного абзаца. Объектов, имеющих свойство Range, немало. Это понятно, ведь не только с абзацами полезно связать диапазон, соответствующий объекту. При обращении к свойству Range таких объектов в качестве результата получаем соответствующий диапазон - ссылку на объект Range.
    В ряде случаев можно определить новый объект класса Range, используя метод Range. Заметьте: метод, а не свойство. Так объекты более высокой иерархии, например объект класса Document, имеют метод Range и не имеют соответствующего свойства. Синтаксис метода таков:
    Function Range(Start, End) As Range
    Он имеет два параметра: Start и End - и возвращает как результат объект класса Range.
    Если объект Range уже построен, то значения его параметров Start и End можно переопределить, вызвав его метод SetRange, в котором можно задать новые значения этих параметров.
    В следующем примере работы с документом Word демонстрируется как применение свойства Range, так и метода Range. В нем же показано и применение метода SetRange.
    Пример 1.7.
    (html, txt)

    Метод Select и свойство Selection

    Рассматривая в приложении Word объект Range, мы видели, что он может быть получен как результат вызова метода Range объектов одних классов и свойства Range объектов других классов. Свойством Range обладают объекты более низких уровней иерархии - коллекции слов и ячеек, абзацы и строки. Считается, что в каждый такой объект вложен объект Range, задающий диапазон, связанный с объектом. При вызове свойства этот существующий объект возвращается, и с ним можно работать. У объектов верхнего уровня иерархии: Application, Windows - встроенного диапазона нет, но зато есть метод Range, позволяющий создать новый объект этого класса.
    С объектом Selection ситуация аналогична. И здесь свойство Selection возвращает уже существующий объект, а метод Select позволяет создать новый объект. Но здесь ситуация двойственна - объекты, имеющие свойство Range, обладают методом Select и, наоборот, объекты верхнего уровня иерархии имеют свойство Selection. Объяснение очевидно: объект Selection существует в единственном экземпляре в каждом подокне, и поэтому объекты более низких уровней иерархии просто не могут иметь встроенное свойство Selection. Им могут обладать только объекты класса Pane (подокно), Windows и Application.
    Рассмотрим синтаксис метода Select:
  • Sub Select()
  • Sub Select([Replace])

  • В первом случае создается новая область выделения - объект Selection, во втором - область выделения можно расширить или заменить в зависимости от значения параметра Replace. Заметьте: метод Select реализован не как функция, явно возвращающая результат, а как процедура (Sub), которая объекта Selection не возвращает. Создание этого объекта - результат побочного эффекта этой процедуры. Созданный объект связывается с объектами верхнего уровня иерархии: окном и приложением. Его можно получить, обратившись к глобальному свойству Selection. Но Вы не можете получить ссылку на этот объект, а, следовательно, не можете создать переменную этого класса, так, как это могли делать при работе с объектами класса Range (вспомните переменную MyRange из наших примеров).
    Это предохраняет нас от соблазна "обмануть систему" и иметь две переменные - два объекта Selection одновременно.

    Типичный и правильный способ работы с объектом Selection таков:

  • Создается или выбирается объект, область которого должна быть выделена.
  • Для этого объекта вызывается метод Select, возвращающий в качестве результата объект Selection.
  • Проводится работа с объектом Selection в области выделения.
  • Если нужно перейти к новой области выделения, то либо область выделения перемещается (как в нашем примере), либо повторяются действия, начиная с первого пункта.


  • Рассмотрим пример, в котором пользователь переключается между двумя областями выделения

    Пример 1.9.

    (html, txt)

    В этом макросе почти все применяемые средства хорошо знакомы. До сих пор мы не пользовались только стандартным для Visual Basic методом InputBox, который открывает диалоговое окно, позволяющее принять ввод пользователя. Заметьте, что работу в выделенной области осуществляет процедура ItInSel. Ее можно было сделать довольно сложной, но мы ограничились одним оператором:

    Public Sub ItInSel() Selection.Font.Italic = True End Sub

    Анализируя этот макрос, обратите внимание на два обстоятельства:

  • Если мы хотим постоянно работать с некоторыми областями, имеет смысл создать соответствующие им объекты. "Хранителями" областей выступают объекты класса Range В нашем примере это переменные myRange1 и myRange2. Объекты Selection появляются в последнюю минуту, когда область нужно выделить и начать с ней работать.
  • Работа с объектом Selection может быть предпочтительнее, так как явные ссылки заменяются косвенными. Это позволяет создать процедуру, не зависимую от области. Подобную ситуацию демонстрирует вызов процедуры ItInSel.


  • Методы объектов Application

    Казалось бы, объекты Application не должны иметь много методов, а среди имеющихся методов большая часть должна быть общей для приложений разных типов. Ведь специфика приложения должна проявляться на более низком уровне иерархии, когда начинается работа, например, с документами Word или рабочими книгами Excel. Но это не так, - реально методов много, а общих методов мало. Более того, даже методы Activate, Run, Quit, имеющиеся у всех приложений, совпадающие по именам и предназначенные для решения стандартных задач (активизация приложения, запуск макроса, выход из приложения) организованы по-разному. Так, метод Quit() в Excel не имеет параметров - он, естественно, завершает приложение. А вот в приложении Word у этого метода 3 параметра: Quit([SaveChanges], [OriginalFormat], [RouteDocument]), - позволяющие в момент выхода указать, сохранять ли сделанные изменения и их формат, пересылать ли документ всем, кто работает с ним.
    Лишь приложение Power Point "правильно" устроено с нашей точки зрения, - его объект Application имеет всего четыре метода; помимо трех методов, только что перечисленных, у него еще есть метод Help. Это тоже один из общих методов, которого нет только у приложения Access.
    Из общих в Office 2000 методов для Word, Access и Excel отметим группу DDE-методов, обеспечивающих динамический обмен данных. Word и Excel имеют общий метод OnTime, о котором стоит рассказать подробно. Он позволяет запустить некоторый макрос в заданное время. Каждое утро, например, можно запускать макрос, позволяющий провести антивирусную профилактику, сохранить резервные копии и т. п. Синтаксис метода в приложении Word таков:
    OnTime (When, Name, Tolerance)
    Первый обязательный параметр When задает время запуска - некоторое выражение, значением которого является строка, задающая время. При задании этого параметра часто используются функция Now, которая возвращает текущее время, и функции TimeValue и TimeSerial, преобразующие время, заданное в относительно произвольной форме к фиксированному стандарту.

    Параметр Name - также обязательный - задает имя запускаемого макроса. Макрос должен быть доступен как в момент выполнения процедуры OnTime, так и когда наступает время его выполнения, указанное в параметре When. Поэтому разумно такие макросы помещать, например, в проект Normal, доступный для всех приложений Word.

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

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

    Sub InTime() 'Запуск одного из двух макросов "Приветствие" 'Один запускается по утрам в 10.00, другой - через 1 минуту, 'начиная отсчет от момента выполнения метода OnTime If Now < "10:00:00" Then Application.OnTime TimeValue("10:00 am"), "Morning" Else Application.OnTime Now + TimeValue("00:01:00"), "Hello" End If End Sub

    Заметьте: запускается только один из макросов. "Завести два будильника", дважды вызвав метод OnTime, нельзя - ведь только один таймер следит за запуском макроса. Пока "заведенный" макрос не будет исполнен, любая попытка установить время для запуска следующего макроса приведет к отмене предыдущего указания.

    Для полноты картины приведем тексты макросов-приветствий:

    Sub Morning() ' Утреннее приветствие MsgBox Prompt:="Привет! Я рад, работать с Вами!", Buttons:=vbOKOnly End Sub

    Sub Hello() 'Приветствие MsgBox Prompt:="Привет! Будем работать!", Buttons:=vbOKOnly End Sub

    Итак, подводя итоги, отметим, что только объект Power Point.Application имеет всего четыре метода.


    Остальные объекты Application имеют десятки методов, большей частью отражающих специфику приложения. Например, в Office 2000 объект Access.Application имеет:

  • Группу из 9 методов Create, позволяющих программно создать проект, элемент управления, форму, отчет и другие элементы приложения.
  • Группу D - функций, общих с Excel, позволяющих работать с запросами над списками Excel.
  • Группу Get и Set - методов, позволяющих, например, получить или установить те или иные опции.
  • Методы Open, Close, New, позволяющие открывать, закрывать или делать текущей новую базу данных.
  • Другие методы.


  • Следует понимать причину "изобилия" методов у объектов Application. Дело в том, что большинство методов, это, фактически, методы объектов нижних уровней, вынесенные на верхний уровень иерархии.

    Один и тот же метод X может быть определен как для объекта Application, так и для объектов разных классов, вложенных на разных уровнях иерархии. Иногда это позволяет выполнять массовые операции над всеми объектами, входящими в иерархию. Так, метод Calculate в Excel применим к объектам Application, WorkBook и Worksheet. Вызов Application.Calculate приведет к запуску вычисления формул всех рабочих листов всех рабочих книг.

    Но будьте осторожны: методы - "тезки" могут отличаться параметрами и реализацией. Например, методы проверки правописания и грамматики CheckSpelling и CheckGrammar определены для трех объектов Word: Application, Document и Range. Вызванные объектами Document и Range, они проверяют текст в диапазоне, связанном с объектом, вызвавшим метод. А у одноименных методов объекта Application появляется дополнительный параметр, задающий проверяемую строку.

    Еще один пример - метод Move, которым обладают объекты:

  • Application, Task, Range, Selection - в приложении Word;
  • Chart, Charts, Worksheet, Worksheets, Sheets в приложении Excel;
  • общие объекты Assistant, CommandBarControl из библиотеки объектов Office;
  • Control, Controls из библиотеки объектов MSForms.


  • За общим именем скрываются похожие, но разные по количеству параметров и по действию методы.


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

    Осталось еще рассказать, какие новые методы появились у Application -объектов приложений Word, Access и Excel в Office 2000 в сравнении с предыдущей версией Office 97. Общих новых методов нет.

    В приложении Word у объекта Application появилось 8 новых методов. Два из них расширяют набор метрических преобразований, четыре определены над клавиатурой и связаны с расширенной поддержкой национальных языков в Office 2000. Еще один метод следует упомянуть подробнее, - речь идет о функции DefaultWebOptions, возвращающей объект подобного класса. Если взглянуть на ранее приведенную таблицу новых свойств, то можно заметить, что приложения Power Point и Excel имеют новое свойство, возвращающее объект этого класса. Приложение Word вместо этого имеет метод, возвращающий тот же объект. Один Access пока что не имеет ни подобного свойства, ни метода.

    В приложении Excel, как ни странно, нет новых методов, Более того, исчезли два старых метода: CentimetersToPoints, InchesToPoints, занимающихся метрическими преобразованиями. Вместо того, чтобы добавлять новые методы этой группы, как это сделано в Word, решили убрать эту группу методов из ведения объекта Application.

    Больше всего новых методов появилось у объекта Access.Application, - их 18. Прежде всего, пополнилась группа Create - методов. Теперь можно создавать не только формы и отчеты, но и проекты (CreateAccessProject), страницы доступа (CreateDataAccessPage), элементы управления с расширенным набором свойств, размещаемые, возможно, в отчете(CreateControlEx, CreateReportControlEx).С проектами работают еще два метода, - OpenAccessProject, NewAccessProject. Несколько методов добавлено для работы с текстами. Детали оставим на будущее.

    Объекты Application

    Всех объектов в Office 2000 не перечесть, но знать главные - обязательно. Давайте начнем знакомство с корней - объектов Application. Заодно посмотрим, что нового появилось в структуре этих объектов в сравнении с предыдущей версией. Чтобы облегчить и систематизировать нашу работу, мы по отдельности рассмотрим свойства - участники, терминальные свойства, методы и события этих объектов.

    Объекты Range

    Роль коллекций во всех приложениях Office 2000 велика. Если одновременно могут существовать несколько объектов (экземпляров) одного и того же класса, то существует и объединяющий их класс - коллекция, в которой собраны все экземпляры. Класс коллекция позволяет работать с последовательностью элементов - экземпляров класса.
    Но двух классов объектов недостаточно: мало иметь возможность работать с элементом или с последовательностью всех элементов - нужно уметь работать и с частью этих объектов, - произвольной подпоследовательностью элементов. Для этой цели и придуман класс объектов Range. При работе в Word и Excel программисту чаще всего приходится оперировать именно с этим классом объектов - словами, абзацами, символами документов Word, ячейками и их всевозможными диапазонами в Excel.

    Объекты Selection

    Еще один общеупотребительный объект - Selection. Как и Range, этот объект присутствует в большинстве приложений Office 2000. Он задает выделенную область документа Word, рабочего листа Excel, область в окне презентации. Его особенность в том, что в каждом окне (точнее, в каждом подокне) может существовать только один объект Selection. Каждое новое выделение некоторой области в подокне отменяет или модифицирует старое. Этим он отличается от объектов Range, несколько экземпляров которых могут существовать одновременно. Selection, как и объект Range, задает непрерывную область элементов. Но есть и принципиальное отличие: объект Selection не является диапазоном, заданным своим началом и концом, - это некоторая область, например таблица или ее столбец.
    Но объекты Selection и Range имеют и много общего. Прежде всего, оба предназначены для того, чтобы задавать некоторую область элементов, и позволяют выполнять действия над элементами этой области. Возможности этих объектов для работы с элементами области примерно одинаковы. Так, у приложения Word свойства и методы данных классов объектов совпадают на 90%. Вот почему почти каждую задачу, которую можно решить в терминах объекта Selection, можно решить и в терминах объекта Range. Macrorecorder (о нем еще будет подробный разговор) любит использовать объект Selection в своей работе, и очень часто программист "подправляет" программу, переписывая ее для повышения эффективности в терминах свойств и методов объекта Range.
    Отметим еще одну связь (ее можно назвать двойственностью) между этой парой объектов. У объекта Range есть метод Select, который выделяет заданный диапазон, создавая объект Selection, после чего можно использовать свойства и методы этого объекта. Также и объект Selection имеет метод Range, который определяет диапазон, связанный с областью выделения, после чего можно работать с этим диапазоном, используя свойства и методы объекта Range. Перепишем наш пример, где использовался объект Range, задействовав объект Selection:

    Пример 1.8.

    (html, txt)

    Первоначально объект Selection создается как результат вызова метода Select объекта Range. После этого можно работать с этим объектом. При изменении диапазона объекта Selection приходится работать не с параметрами Start и End, как это делается для объектов Range, а использовать специальные методы Move, часть из которых мы применили в нашем примере. Вот их краткое описание:

  • метод MoveLeft сжимает область выделения до точки и передвигает ее в начало диапазона. В примере точка вставки перемещается к началу первого абзаца;
  • метод Move сжимает область выделения до точки и передвигает ее в нужное место. В примере точка вставки перемещается на два абзаца;
  • метод MoveDown расширяет область выделения. Параметр Unit указывает единицы, в которых ведется отсчет при расширении области (у нас - в абзацах); параметр Count задает число этих единиц. Параметр Extend указывает, нужно ли расширение или перемещение области, сжатой в точку вставки.


  • Построение каркаса документа

    Мы уже сказали, что совокупность библиотек классов Office 2000 представляет каркас документов, которые могут порождаться в этой программной среде. Давайте же рассмотрим, как создается каркас одного документа. Обычно, это тривиальная операция, выполняемая почти автоматически. В свое время, рассматривая программирование в среде Office 97, мы не считали необходимым введения самого термина "каркас документа". В Office 2000 возросли возможности создания разнообразных каркасов, потому стоит ввести и сам термин и обратить на это особое внимание.
    Итак, всякий раз, когда открывается новый документ в одном из приложений Office 2000, например Word, Excel или Power Point , автоматически создается каркас этого документа. Это означает, что из всей совокупности библиотек выбираются те библиотеки, которые и будут составлять каркас этого документа. И это означает, что все объекты, классы которых определены в этих библиотеках, будут доступны в данном документе.
    Какие же библиотеки входят в каркас документа по умолчанию? Определяющую роль играет то, в каком приложении Вы создаете документ. В каркас всегда входит библиотека, задающая это приложение, - Word 9, Excel 9 или тот же Power Point 9. В состав этой библиотеки входит корневой объект Application (Word9.Application, Excel9.Application и т.д.). Этот корневой объект и определяет основную структуру документа. Но помимо этой библиотеки в каркас всегда входят и другие библиотеки. Взгляните на каркас по умолчанию документа Word:
    Построение каркаса документа
    увеличить изображение
    Рис. 1.2.  Каркас по умолчанию документа Word
    Как можно видеть, в каркас входят кроме библиотеки Word еще три библиотеки и два проекта:
  • Word - библиотека, задающая основу документов Word. Здесь хранится корневой объект Application и все классы объектов, вложенных в корневой объект.
  • Office - библиотека объектов, общих для всех приложений Office 2000. Здесь находятся классы, задающие Помощника (объект Assistant и все классы, связанные с ним). Здесь же находятся классы, определяющие инструментальные панели - CommandBar и классы других общих объектов.
    В частности, появился новый объект, которого не было в предыдущей версии - Мастер Ответов (Answer Wizard) и его файлы.
  • Stdole - библиотека классов, позволяющая работать с OLE - объектами и реализовать Автоматизацию.
  • VBA - библиотека классов, связанных с языком VBA. Здесь хранятся все стандартные функции, встроенные в язык, и многое другое.
  • Project - проект по умолчанию, связанный с документом. Классы, которые могут программистом создаваться в этом проекте, методы, свойства, - все это доступно для просмотра, так же, как и объекты классов, встроенных в стандартные библиотеки.
  • Normal - проект, доступный для всех документов Word. Здесь могут храниться функции и классы, используемые всеми документами.


  • Состав каркаса для документов, открываемых в Excel или Power Point, почти такой же. Естественно, библиотека Word заменяется библиотекой соответствующего приложения. Кроме того, отсутствует проект Normal, характерный только для документов Word.

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

    А теперь о том, как это делается. Прежде всего, заметим, что расширение каркаса делается руками, а не программно. Для того чтобы добавить в документ новые возможности, необходимо вначале войти в режим редактора программного проекта, для чего достаточно из меню Tools | Macro выбрать пункт Visual Basic Editor или нажать комбинацию клавиш Alt + F11. Затем уже в Редакторе из меню Tools следует выбрать пункт References и в появившемся списке всех возможных библиотек, включить те, которые отвечают Вашим потребностям. На рис. 1.3 показан процесс подключения к документу Word новых библиотек.

    Построение каркаса документа


    Рис. 1.3.  Расширение возможностей каркаса документа

    В частности, мы подключили библиотеки объектов трех приложений - Outlook, Access и Excel, библиотеку с WEB - компонентами, Мастер этих компонент и элемент управления для работы в Internet. В результате каркас документа существенно обновился, и наш документ теперь обладает потенциально большими возможностями, чем документ, создаваемый по умолчанию. В этом документе можно организовать совместную работу четырех приложений Office 2000, работать с WEB - компонентами и использовать возможности встроенного элемента управления при работе над документом в Internet. Конечно, каркас обеспечивает только потенциальную возможность, чтобы все заработало, нужно многое сделать, наполняя каркас плотью. Например, необходимо описать и создать соответствующие объекты Application для каждого из совместно работающих приложений, прежде чем начать с ними работать.

    На рис. 1.3 можно увидеть лишь небольшую часть всей совокупности библиотек (их несколько десятков), доступных в Office 2000. Профессионалу знать их, конечно, совершенно необходимо. Но знакомство с ними, хотя бы шапочное, выходит за пределы этой книги, посвященной VBA . О них и об объектах, в них хранящихся, пойдет разговор в других книгах, связанных общей темой офисного программирования в среде Office 2000.

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

    Построение каркаса документа
    увеличить изображение
    Рис. 1.4.  Окно просмотра объектов каркаса документа

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

    Объявление переменных на уровне модуля

    ' Объявление переменных на уровне модуля Dim LeftW As Long, TopW As Long, HeightW As Long, WidthW As Long Dim StateW As WdWindowState
    Sub ChangeSizeWindow() With Application 'Запоминаем характеристики окна StateW = .WindowState LeftW = .Left TopW = .Top HeightW = .Height WidthW = .Width ' Изменяем характеристики окна .WindowState = wdWindowStateNormal .Left = 100 .Top = 100 .Height = 400 .Width = 400 End With End Sub
    Public Sub ResetSizeWindow() With Application ' Восстанавливаем характеристики окна .Left = LeftW .Top = TopW .Height = HeightW .Width = WidthW .WindowState = StateW End With End Sub
    Пример 1.1.
    Закрыть окно




    Public Sub WorkWithExcel() ' НЕ забудьте включить ссылку на Excel в меню References 'Объявление и создание объектов Excel.Application Dim MyXlApp As New Excel.Application 'Работа с приложением Excel With MyXlApp .Visible = True 'Excel появился на линейке и его можно раскрыть 'Добавить новую книгу .Workbooks.Add 'Теперь можно работать и с ячейками данной книги .Range("A1") = "Hello!" 'Идет работа с книгой .Range("B1") = "By-By" .Workbooks(1).Activate 'Закрываем открытую книгу MsgBox ("Закрываем рабочую книгу Excel?") .Workbooks(1).Close 'Будет задан вопрос о необходимости сохранения книги 'Закрываем приложение .Quit End With End Sub
    Public Sub WorkWithAccess() 'НЕ забудьте включить ссылку на Access и Dao 3.6 в меню References 'Приложение Access и его компоненты: 'База данных, форма и путь к базе Dim MyAc As New Access.Application Dim MFDb As Database Dim FormPoetAges As Form Dim PathDb As String 'Открываем базу данных Access PathDb = "E:\O2000\CD2000\Ch1\AgeOfPoet.mdb" MyAc.OpenCurrentDatabase (PathDb) Set MFDb = MyAc.CurrentDb 'Открываем форму MyAc.DoCmd.OpenForm ("AgeOfPoets") Set FormPoetAges = MyAc.Forms("AgeOfPoets") MyAc.Visible = True FormPoetAges.SetFocus
    'Закрываем базу данных MsgBox ("Закрываем базу данных Access?") MyAc.Quit End Sub
    Пример 1.2.
    Закрыть окно




    Public Sub WorkWithbooks() ' Работа с коллекцией книг Dim N As Long, i As Byte Dim PathDir As String PathDir = "e:\O2000\CD2000\Ch1\" With Workbooks N = .Count Debug.Print "Число рабочих книг в коллекции Workbooks " & _ "при открытии приложения Excel = ", N ' Добавление 2-х новых книг .Add .Add 'Добавление двух существующих книг .Open (PathDir & "BookThree.xls") .Open (PathDir & "BookFive.xls") N = .Count Debug.Print "Число книг после 2-х вызовов методов Add и Open =", N Debug.Print "Имена книг в коллекции:" For i = 1 To .Count Debug.Print .Item(i).Name Next 'Закрытие двух книг и, следовательно, удаление их из коллекции .Item(2).Close .Item(3).Close N = .Count Debug.Print "Число книг после двух вызовов метода Close =", N Debug.Print "Имена книг, оставшихся в коллекции:" For i = 1 To .Count Debug.Print .Item(i).Name Next End With
    End Sub
    Пример 1.3.
    Закрыть окно




    Public Sub WorkWithSheets() 'Работа с коллекцией Sheets - листами рабочей книги Dim N As Long, i As Byte Dim X As Variant 'Активизация книги Book2 Workbooks("Book2").Activate With ActiveWorkbook.Sheets N = .Count Debug.Print " Число листов при первоначальном открытии" _ & "книги = ", N Debug.Print "Имена листов:" For i = 1 To .Count Debug.Print .Item(i).Name Next ' Переименование листов .Item(1).Name = "Two" .Item(2).Name = "Four" .Item(3).Name = "Six" 'Добавление листов One, Three, Five .Add Before:=.Item("Two") .Add After:=.Item("Two"), Type:=xlChart .Add After:=.Item("Four"), Type:=xlExcel4MacroSheet .Item(1).Name = "One" .Item(3).Name = "Three" .Item(5).Name = "Five" N = .Count Debug.Print "Число листов книги после вставки" _ & "3-х листов =", N Debug.Print "Имена и типы листов после переименования:" For i = 1 To .Count Debug.Print .Item(i).Name, .Item(i).Type Next 'Удаление 3-го и 5-го листов .Item("Three").Delete .Item("Five").Delete 'Работа с первым листом .Item("One").Activate 'Запуск макроса Fibonachi, заполняющего область листа A1:A20 'последовательностью чисел Фибоначчи Fibonachi 'Копирование первого листа на вновь создаваемый третий лист .Item("One").Copy After:=.Item("Two") 'Переименование и перемещение листа ActiveSheet.Name = "Seven" .Item("Seven").Move After:=.Item("Six") 'Копирование области одного листа на последовательность листов X = Array("One", "Four", "Six") Sheets(X).FillAcrossSheets Range:=Worksheets("One").Range("A1:A20") Debug.Print "Имена,типы листов и содержимое двадцатой ячейки:" For i = 1 To .Count Debug.Print .Item(i).Name, .Item(i).Type, _ .Item(i).Range("A20").Value Next End With End Sub
    Пример 1.4.
    Закрыть окно




    Public Sub WorkWithDocuments() 'Работа с коллекцией документов Dim N As Long, I As Byte Dim PathDir As String PathDir = "e:\O2000\CD2000\Ch1\" With Documents N = .Count Debug.Print "Число документов в коллекции Documents " & _ "при открытии приложения Word = ", N ' Добавление 2-х новых документов (второй представляет шаблон) .Add .Add NewTemplate:=True 'Добавление двух существующих документов .Open (PathDir & "DocThree") .Open (PathDir & "DocFive") N = .Count Debug.Print "Число документов после 4-х вызовов методов " _ & "Add и Open =", N Debug.Print "Имена документов в коллекции:" For I = 1 To .Count Debug.Print .Item(I).Name Next 'Закрытие двух документов и, следовательно, удаление их из коллекции .Item(2).Close .Item(3).Close N = .Count Debug.Print "Число документов после двух вызовов " _ & "метода Close =", N Debug.Print "Имена документов, оставшихся в коллекции:" For I = 1 To .Count Debug.Print .Item(I).Name Next End With End Sub
    Пример 1.5.
    Закрыть окно




    Public Sub WorkWithSecAndPar() 'Работа с разделами и абзацами 'Добавление новой книги Documents.Add Documents(1).Activate With ActiveDocument 'Вставка двух разделов в документ 'Раздел начнется с нечетной страницы .Sections.Add Start:=wdSectionOddPage .Sections.Add ' Вставка абзаца во второй раздел .Sections(2).Range.Paragraphs.Add .Sections(2).PageSetup.LeftMargin = 3 .Sections(2).Range.Style = "Heading 1" .Sections(2).Range.Font.Name = "Arial" .Sections(2).Range.Paragraphs(1).Range.InsertBefore ("Лекция 1") 'Вставка нового раздела и абзаца .Sections.Add Start:=wdSectionNewPage .Sections.Last.Range.Paragraphs.Last.Range.InsertBefore ("Параграф 1") .Sections.Last.Range.Paragraphs.Add 'Вставка нового раздела и абзаца .Sections.Add Start:=wdSectionNewPage .Sections.Last.Range.Paragraphs.Last.Range.InsertBefore ("Параграф 2") .Sections.Last.Range.Paragraphs.Add 'Вставка нового раздела в конец документа 'Раздел начнется с нечетной страницы .Sections.Add Start:=wdSectionOddPage .Sections.Last.PageSetup.LeftMargin = 23 .Sections.Last.Range.Font.Name = "TimesNewRoman" 'Вставка абзаца .Sections.Last.Range.Paragraphs(1).Range.InsertBefore ("Лекция 2") .Sections.Last.Range.Paragraphs.Add Debug.Print "Число разделов документа =", .Sections.Count Debug.Print "Число абзацев документа =", .Paragraphs.Count 'Удаление раздела выполняет метод объекта Range .Sections(1).Range.Delete Debug.Print "Число разделов документа =", .Sections.Count End With
    End Sub
    Пример 1.6.
    Закрыть окно




    Public Sub WorkWithRange() Dim myr As Range, myr1 As Range Dim i As Byte 'Добавляем новый документ Documents.Add With ActiveDocument 'Добавляем 7 абзацев в текст созданного документа For i = 1 To 7 .Paragraphs.Last.Range.Text = "Абзац " & i .Paragraphs.Add Next i 'Используется свойство Range Set myr = .Paragraphs(1).Range 'Выделен первый абзац myr.Select 'Новый объект myr1 задает пустой объект - позицию курсора 'В правой части вызывается метод Range Set myr1 = ActiveDocument.Range(Start:=myr.Start, End:=myr.Start) myr1.Select 'Теперь объект myr1 задает единственный символ -первый символ текста 'Здеесь использован метод SetRange myr1.SetRange Start:=myr1.Start, End:=myr1.End + 1 myr1.Select 'Три абзаца с третьего по пятый выделяются курсивом myr1.SetRange Start:=.Paragraphs(3).Range.Start, End:=.Paragraphs(5).Range.End myr1.Font.Italic = True myr1.Select End With End Sub
    Пример 1.7.
    Закрыть окно




    Public Sub WorkWithSelection() Dim myr As Range Dim i As Byte 'Добавляем новый документ Documents.Add With ActiveDocument 'Добавляем 7 абзацев в текст созданного документа For i = 1 To 7 .Paragraphs.Last.Range.Text = "Абзац " & i .Paragraphs.Add Next i 'Используется свойство Range Set myr = .Paragraphs(1).Range 'Выделен первый абзац. Создаем объект Selection myr.Select 'Действия с объектом Selection 'Стягивание в начало абзаца - точку вставки Selection.MoveLeft 'Расширение на один абзац вправо, снова выделяя первый абзац Selection.MoveDown Unit:=wdParagraph, Count:=1, Extend:=wdExtend 'Передвинемся к началу третьего абзаца Selection.Move Unit:=wdParagraph, Count:=2 'Три абзаца с третьего по пятый выделяются курсивом Selection.MoveDown Unit:=wdParagraph, Count:=3, Extend:=wdExtend Selection.Font.Italic = True End With End Sub
    Пример 1.8.
    Закрыть окно




    Sub WorkWithTwoReg() ' Переключение между двумя областями выделения документа 'Создание двух областей Dim myRange1 As Range Dim myRange2 As Range Dim i As Byte Dim Answer As Variant ' Выбор пользователя 'Добавляем новый документ Documents.Add With ActiveDocument 'Добавляем 7 абзацев в текст созданного документа For i = 1 To 7 .Paragraphs.Last.Range.Text = "Абзац " & i .Paragraphs.Add Next i Set myRange1 = .Range(Start:=.Paragraphs(2).Range.Start, _ End:=.Paragraphs(3).Range.End)
    Set myRange2 = .Range(Start:=.Paragraphs(6).Range.Start, _ End:=.Paragraphs(7).Range.End) Answer = InputBox(prompt:=" Выберите область выделения (1/2)", _ Default:=1) If Answer = 1 Then myRange1.Select 'Макрос ItInSel работает с первой выделенной областью ItInSel Else myRange2.Select 'Макрос ItInSel работает со второй выделенной областью ItInSel End If End With
    End Sub
    Пример 1.9.
    Закрыть окно



    События объектов Application

    У объекта Access.Application событий нет. Все события вынесены на более низкий уровень иерархии и возникают при работе с документами Access - отчетами, формами и т.д.
    Объекты Application приложений Word, Excel и Power Point имеют примерно одинаковый набор событий. Фактически эти события возникают при работе с объектами нижних уровней иерархии - окнами, документами Word, рабочими книгами и страницами (Sheets) Excel, презентациями и слайдами Power Point. Всякий раз, когда окно или тот или иной документ создается, открывается, изменяется, закрывается, - возникает соответствующее событие, которое можно обработать. Обработчик события можно связывать непосредственно с самим объектом, при работе с которым событие возникло. Но иногда удобнее вынести обработчик на верхний уровень приложения и связать его с объектом Application. Это особенно полезно, если один и тот же обработчик применяется ко всем документам приложения.

    Совместная работа приложений. Создание объектов Application. Раннее и позднее связывание

    Мы уже не раз говорили, что эффективность работы в среде Office 2000 во многом определяется возможностью совместной работы его приложений. С программистской точки зрения это означает, что в документе одновременно определены несколько объектов Application, задающих совместно работающие приложения. Давайте рассмотрим, как обеспечивается такая возможность. При открытии какого - либо из приложений, например, документа Word по умолчанию считается определенным соответствующий объект Application, а следовательно и все объекты, в него вложенные. Более того, определены и все объекты, входящие в каркас документа, построенный по умолчанию.
    Если мы хотим обеспечить совместную работу приложений, например, Word, Excel и Access, то можно начинать работу с открытия документа любого из этих приложений, и тогда корневой объект Application этого приложения будет доступен автоматически. Остальные объекты Application необходимо подключить к уже открытому документу. Пусть для определенности мы открыли документ Word и хотим подключить Excel и Access. Вот, что для этого нужно сделать:
  • Расширить каркас документа Word, установив ссылки на библиотеки, содержащие приложения Excel и Access . Напомним, что это делается в редакторе VBE в окне References меню Tools.
  • Объявить объекты (указатели на объекты) соответствующего класса.
  • Создать объекты и связать указатели с вновь созданными объектами.

  • Когда корневой объект Application создан и доступен, то автоматически становятся доступными все вложенные в него объекты, так что теперь с приложением можно работать в обычном режиме, вызывая свойства и методы его объектов. При объявлении объектов возможны варианты. При этом следует помнить, что объекты в VBA задаются с помощью указателей. Рассмотрим три возможных варианта объявления:
  • Dim MyXlApp As Object, MyAcApp As Object Этот вариант задает так называемое позднее связывание. На этапе объявления известно, что MyApp переменные являются объектами, а точнее, указателями на объекты, но еще не известно с объектами какого класса они будут связаны.
    Это станет известно динамически, в ходе выполнения программы, когда реально будет создан объект - экземпляр соответствующего класса и наш указатель получит на него ссылку. Для программиста эти переменные являются нетипизированными указателями.
  • Dim MyXlApp As Excel.Application, MyAcApp As Access.Application Это объявление задает раннее связывание. Наши переменные теперь представляют собой типизированные указатели. И уже на этапе объявления известно, что в дальнейшем они будут связаны с объектами соответствующего класса. Чтобы при этом объявлении не возникло конфликтов, конечно же, необходимо предварительно подключить соответствующую библиотеку. Важно также понимать, что на этапе объявления память выделена только указателям. Сами объекты еще не созданы, и потому значения указателей не определены. Объекты еще предстоит создать по ходу выполнения программы и указатели должны быть связаны с ними.
  • Dim MyXlApp As New Excel.Application, MyAcApp As New Access.Application В этом случае раннее связывание реализуется в полном объеме. Создается указатель, благодаря спецификатору New создается сам объект, и указатель получает ссылку на вновь созданный объект.


  • Эти три варианта характерны не только при работе с объектами Application, а во всех случаях работы с объектами. Конечно же, в большинстве случаев предпочтительнее третья конструкция, когда создание объекта и указателя совмещаются в одном операторе. В Office 2000 такая конструкция является допустимой для объектов Application и ее разумно использовать.

    Заметьте, что не все приложения допускают конструкцию New для создания Active X объектов, каковыми являются объекты Application. В ряде случаев для создания Active X объектов, хранящихся в отдельных файлах, следует использовать метод CreateObject, первый параметр которого задает имя класса объекта. Этот метод следует выполнить в нужный момент, связав заодно указатель с вновь созданным объектом. Конечно, и в Office 2000 допустимо создать объекты Application следующим образом:

    Set MyXlApp = CreateObject("Excel.Application ") Set MyAcApp = CreateObject("Access.Application ")


    Теперь еще несколько общих слов о раннем и позднем связывании. Раннее связывание предпочтительнее по двум важным причинам: оно обеспечивает более быструю работу приложения и интеллектуальную поддержку при работе с объектом. Последнее означает, что появляется список доступных свойств и методов, как только ставится точка после имени объекта. При позднем связывании такая поддержка отсутствует, так как класс объекта не известен на этапе трансляции до начала выполнения программы. Следует заметить, что VBA не является чисто интерпретируемым языком. На этапе трансляции до выполнения программы проверяется наличие ошибок в программе, проводится распределение памяти и обеспечивается интеллектуальная поддержка объявленным переменным.

    В заключение рассмотрим пример, иллюстрирующий работу трех приложений: Word, Excel и Access:

    Пример 1.2.

    (html, txt)

    Макросы WorkWithExcel и WorkWithAccess могут быть вызваны из приложения Word. В наших тестовых примерах мы вызывали их в ответ на щелчок соответствующих командных кнопок. Заметьте, второй пример довольно содержателен. Здесь показано, как, не выходя из документа Word, просматривать и редактировать данные формы, созданной в приложении Access для работы с базой данных.

    На этом мы прервем знакомство с корневыми объектами Application. Помните, явно или неявно они всегда будут присутствовать в наших примерах.

    Свойства-участники

    Свойства, представляющие вложенные объекты, называют свойствами-участниками. Если X является свойством - участником объекта Application, то обращение к свойству X возвращает ссылку на объект X. Обращение Application.X.Y.Z, где X, Y и Z - свойства-участники, позволяет добраться до объекта Z, находящегося на третьем уровне вложенности. Обычно цепочка именования начинается спецификатором (объектом) Application, но иногда его можно опустить. Некоторые свойства и методы объекта Application относятся к глобальным. Для них спецификатор Application разрешается опускать, непосредственно именуя глобальный элемент. Вот пример нескольких обращений к элементам объекта Application:
    Application.ActiveDocument ' Можно короче: ActiveDocument Application.ActiveWorkbook.ActiveSheet.Range("A1") = "Hi"' Можно так: ActiveSheet.Range ("A2") = "By-By" ' Можно и так: Range("A3") = "I'm glad to see you" 'Можно даже так! Application.Quit 'Только так! Quit не является глобальным методом.
    Познакомимся на верхнем уровне со структурой приложений и посмотрим, как она изменилась при переходе от Office 97 к Office 2000. На рис. 1.6 , рис. 1.7 , рис. 1.8 и рис. 1.9 показано, как выглядели объекты Application четырех основных приложений в Office 97. Что можно отметить, анализируя эту структуру:
  • Среди объектов, вложенных в Application, есть основные, задающие специфику приложения, - это документы Word (коллекция объектов Document), рабочие книги Excel (коллекция объектов WorkBook), презентации PowerPoint (коллекция объектов Presentation), отчеты Access (коллекция объектов Report). Позже мы познакомимся с ними подробнее, сейчас же отметим только, что каждый из них организован не менее сложно, чем их родитель - объект Application. Кроме основных объектов есть и вспомогательные объекты, специфичные для данного приложения. Например, у приложения Word это коллекции CustomDictionaries, Languages - коллекции пользовательских словарей и языков, используемых в документах Word.
  • Есть объекты, общие для всех приложений.
    К ним относятся коллекции, CommandBars, Dialogs, Windows объекты Assistant, FileSearch, VBE. Эти объекты хранятся в библиотеке Office, присоединяемой к каркасу каждого документа. Они определяют такие общие для всех документов свойства, как возможность работы с инструментальными панелями, содержащих меню, элементы управления и кнопки; использование стандартных диалогов в документах и другие свойства, задающие, в целом, единый интерфейс документов в среде Office 2000.
  • На общем фоне в Office 97 выделяется объект Access.Application. Структура его бедна. У него нет общих объектов, весьма ограничено число вспомогательных объектов, определяющих специфику данного приложения.

  • Свойства-участники
    увеличить изображение
    Рис. 1.6. 
    Свойства-участники
    Рис. 1.7. 
    Свойства-участники
    увеличить изображение
    Рис. 1.8. 
    Свойства-участники
    Рис. 1.9. 
    Что же нового появилось в Office 2000? Прежде всего, следует отметить, что все свойства, которыми объекты обладали, у них остались. Но, конечно, появились и новые свойства. Ниже в таблице показаны основные, новые свойства, встроенные в объекты Application в Office 2000.

    Таблица 1.1. Новые свойства объектов Application в Office 2000СвойстваWordExcelPower PointAccess
    AnswerWizard++++
    COMAddIns++++
    LanguageSettings++++
    EmailOptions+---
    DefaultWebOptions-++-
    MsoDebugOptions--+-

    Появилась возможность подключения COM - объектов, расширяющих функциональные возможности документов; коллекция COMAddIns стала частью библиотеки общих объектов Office 2000. Напомним, что ранее каждое из приложений имело свою специфическую для данного приложения коллекцию AddIns. Каждый из элементов AddIn дополнял приложение новыми функциями, но мог работать только в рамках одного приложения. Этим коллекциям и компонентному программированию будет посвящена отдельная лекция этой книги.
    Новые свойства отражают основные направления развития Office 2000 - создание и работа над документами в Интернете, работа в многоязычной среде, повышение интеллектуальности, использование компонент.
    Особо следует отметить и другую линию существенных изменений, направленных на унификацию средств, используемых в разных приложениях.


    Так объект Access.Application приблизился к своим "братьям" и по структуре свойств в меньшей степени выбивается из общего ряда. Объекты из библиотеки Office, задающие общие для приложений Office 2000 свойства, теперь доступны и в Access Заметим, правда, что сама библиотека Office все еще не подключается по умолчанию при создании каркаса документа Access. Однако, теперь, как и в общем случае, можно щелчком Alt+F11 вызвать Редактор VBE и в меню References подключить эту и другие библиотеки.
    Важно и то, что у объекта Access.Application, как и положено, помимо главных свойств (Forms, Reports, Modules) появились на верхнем уровне и дополнительные свойства - участники. Отметим некоторые из них:
  • CodeData. Этот объект позволяет получить доступ к разнообразным коллекциям данных - AllQueries, AllTables, AllViews, AllDataBaseDiagrams, AllStoredProcedures - запросам, таблицам, диаграммам, хранимым процедурам.
  • CodeProject. Этот объект позволяет получать доступ к коллекциям основных объектов приложения - AllForms, AllMacros, AllModules, AllReports, AllDataAccessPages. У объекта есть и методы, позволяющие, например, устанавливать связь с базой данных - OpenConnection, CloseConnection.
  • CurrentData. Представляет косвенную ссылку на текущий объект данных и обладает всеми свойствами объекта CodeData.
  • CurrentProject. Представляет косвенную ссылку на текущий проект и обладает всеми свойствами объекта CodeProject.
  • DBEngine. Объект, представляющий машину вывода со всеми обычными для этого объекта свойствами и методами, как, например, - BeginTrans, CreateDataBase, CreateWorkSpace, OpenConnection - начать транзакцию, создать базу данных, рабочее пространство, открыть связь.
  • DataAccessPages. Эта коллекция объектов, задающих страницы доступа к данным. Каждый из этих объектов содержит такие свойства, как WebOptions, ConnectionString и другие.


  • VBA и объекты. Обзор

    В этой книге основное внимание мы хотим уделить самому языку VBA - его ядру, средствам, определяющим этот язык, как обычный язык программирования. Но заранее можно предсказать, что "столкновения" или лучше сказать встречи с объектами нам не избежать. Все-таки, сутью VBA является его возможность непосредственной работы с объектами. Поэтому эту лекцию мы решили посвятить первому знакомству с объектами. Подробное их рассмотрение будет предметом следующей книги.
    Office - это среда, в которой многие задачи можно решать без всякого программирования. Office 2000 - это среда, в которой класс таких задач существенно расширился. Но для программиста ценность офисной среды состоит в том, что все задачи, решение которых может быть получено "руками", можно получить и программно. И все, что нельзя сделать руками, можно сделать программно. Для программирования используется язык VBA (Visual Basic for Applications), представляющий расширение ядра языка VB (Visual Basic) объектами, определяющими приложения Office 2000.
    Процесс работы в Office 2000 может быть, конечно, разным, - все зависит от того, работает ли профессионал - одиночка, или коллектив исполнителей разрабатывает большой проект. Но давайте будем представлять его следующим образом:
  • В работе участвуют специалисты в проблемной области и программисты. Целью работы является создание системы "живых" документов офиса или предприятия. Работа строится на мощной основе, которую составляют документы, предоставляемые средой Office 2000. Документами могут быть бланки, отчеты, справки, базы данных, запросы к ним, графики, диаграммы. Документы мы называем живыми, поскольку в процессе работы конечных пользователей (специалистов) они могут изменяться. Эти изменения являются результатом выполнения различных функций над данными, размещенными в документах.
  • Библиотека объектов Office 2000 представляет собой каркас документов (чуть позже мы определим более точно этот термин). Открывая впервые одно из приложений Office 2000, например Word, специалист получает готовый каркас некоторого документа, - для Word это документ, в котором основным типом данных будут тексты.
    Затем специалист берется за дело и наполняет этот каркас плотью и кровью, создавая уже реальный документ, входящий в общую систему. Однако, как бы ни увеличивались стандартные возможности среды, всегда для оживления документа чего-нибудь да не хватает, (вспомните, мечту о дефиците одного из героев Райкина). И тогда приходит время программиста.
  • В процессе создания "живых" документов программист будет работать на VBA, едином языке программирования, объединяющем мир объектов среды Office 2000. VBA представляет мощный, универсальный язык программирования и на нем можно делать все, или почти все, что и на любом другом языке программирования. Но главное его достоинство состоит в том, что в нем определены и доступны для программиста все объекты Office 2000. Это значит, что доступны все документы, все их части, и части этих частей, вплоть до символа, рисунка, ячейки. Это и позволяет программисту легко проводить настройку системы документов с учетом их специфики. (В английском языке для этой деятельности есть хороший термин - customization, но, может быть, и термин "настройка" не хуже).


  • Ранее уже говорилось, о том, что такой стиль работы становится популярным. Язык VBA является теперь отчуждаемым от среды Office 2000, он может быть встроен в любую другую среду. Реально этот процесс уже идет, мы приводили пример с программными продуктами фирмы Corel, но есть и большое число других подобных примеров.

    Встраивание вместо наследования

    Обычно, классы, входящие в библиотеку, связаны между собой. Типично, на классах вводится отношение наследования, так что в иерархически связанном семействе классов есть родительские классы и классы - потомки. Наследование - мощный и естественный механизм порождения новых классов. Прародитель семейства может задавать некоторые общие, наиболее фундаментальные свойства и поведение объектов этого семейства, а его многочисленные потомки задают то или иное специализированное поведение. Кроме того, отметим, что наследование позволяет модифицировать поведение объектов - потомков, не меняя поведения объектов исходного родительского класса. Это важный принцип - "не разрушай то, что создано ранее". Ценность такого подхода, в частности, состоит и в том, что каркас конкретного приложения составляют классы - потомки, чьи родители находятся в библиотеке. Поэтому можно добавлять новые свойства, изменять и добавлять новое поведение у приложения, - все это никак не сказывается на родительских классах, хранящихся в библиотеке.
    Наследование, конечно же, не единственно возможное отношение между классами. Программистам хорошо известно встраивание - еще один способ построения нового класса, использующий ранее созданные классы. Пусть построен класс А, тогда при определении нового класса В его элементами - переменными (свойствами класса В) могут быть объекты класса А. В этом случае говорят, что объекты класса А встроены (вложены) в класс В. Встраивание, как и наследование, - транзитивное отношение. Реально это означает, что возможна сколь угодно высокая степень вложенности объектов. Следует отличать непосредственную вложенность от вложенности уровня n. Так, в новый класс С можно встроить объект класса В, и он будет непосредственно вложен в класс С, а объект класса А, встроенный в объект В, будет иметь уровень вложенности 2 по отношению к классу С.
    Хотя встраивание полезно само по себе, оно иногда применяется как альтернатива наследованию и в ряде случаев приводит к тем же результатам.
    В качестве иллюстрации рассмотрим определение двух классов, используя неформальный синтаксис публикаций:

    Class Mother { 'Раздел атрибутов Chromosome XX; 'Другие объявления свойств и методов .... } Class Father { 'Раздел атрибутов Chromosome XY; 'Другие объявления свойств и методов .... }

    В классы Mother и Father встроены объекты XX и XY ранее определенного класса Chromosome. Используя множественное наследование, можно создать новый класс, наследующий свойства и поведение его родителей:

    Class Child : Parents Mother, Father { 'Наследует свойства XX, XY и другие свойства и методы своих родителей 'Раздел атрибутов String NewProperty; 'Другие новые свойства и методы }

    Рассмотрим теперь создание класса, основанное на встраивании объектов:

    Class MyChild { 'Раздел атрибутов Father F; Mother M; String NewProperty; 'Другие новые свойства и методы }

    Вот объявление объектов этих классов:

    Child Son; MyChild Daughter;

    Объекты Son и Daughter обладают одинаковыми свойствами и поведением. И в этом случае мы будем говорить, что классы Child и MyChild подобны. Есть некоторая синтаксическая разница - у объекта Son свойства XX и XY доступны непосредственно, а у объекта Daughter они доступны через вложенные объекты F и M, так что в первом случае мы пишем Son.XX и Son.XY, а во втором - Daughter.F.XY и Daughter.M.XX.

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

    Если происходит переопределение родительских методов в производном классе, то в общем случае, используя встраивание, нельзя построить класс, подобный наследуемому. В нашем примере с классами Child и MyChild встраивание используется при построении класса MyChild как альтернатива наследованию.


    С другой стороны, в этих классах применяется, и "обычное" встраивание. Заметьте: свойства XX и XY в классах Child и MyChild - это встроенные объекты ранее определенного класса Chromosome. Так что этим примером мы демонстрируем обе ипостаси встраивания.

    Мы так подробно остановились на встраивании по одной простой причине: этот механизм лежит в основе объектной модели в Office 97 и Office 2000. Здесь нет наследования в истинном смысле этого слова, и хотя этот термин иногда и используется, но фактически всегда речь идет лишь о встраивании. Все приложения Office 2000 с объектной точки зрения построены одинаково. Всегда существует задающий приложение корневой объект Application (Word.Application, Excel.Application и т. д.), в который встроены все остальные объекты данного приложения.

    Когда в объект Application вложен объект X, говорят, что у объекта Application есть свойство X, и запись Application.X означает обращение к объекту X. Сам объект X может быть не менее сложен: в него могут быть встроены другие объекты, например объект Y, а в него в свою очередь - объект Z. Эта цепочка вложенности может быть продолжена. Для приложений Office 2000 типична ситуация, при которой нужно указать 6-7 уровней вложенности, чтобы добраться до объекта, свойство или метод которого будет использовано. Вложенный объект, который является свойством объекта, его содержащего, называют иногда участником (accessor).

    Длинные цепочки, возникающие при именовании объектов, являлись бы большим недостатком, если бы их пришлось выписывать вручную. Но вот пример того, как недостатки можно обращать в достоинства. Частью, используемой в Office 2000 всюду, где можно, технологии интеллектуальной поддержки (intellisense) является поддержка автоматического выписывания длинных имен. Как только после имени объекта ставится точка, автоматически раскрывается список с перечислением всех свойств этого объекта, так что остается только выбрать имя из списка, поставить точку и продолжить этот процесс. Если, поставив точку, Вы не видите списка, то это значит, что-то сделано неправильно, например, не задан класс объекта в операторе объявления или неверно понимается его структура, в любом случае это сигнал, заставляющий задуматься.

    Подводя итог, отметим, что для программиста большинство библиотек Office 2000 задают совокупность "толстых" объектов - объектов Application. В каждый такой корневой объект вложены другие объекты, устроенные не менее сложно. С методами, свойствами и событиями, происходящими в мире этих объектов, и работает программист.

    В заключение взгляните, как я представляю себе "толстый" объект Application.

    Встраивание вместо наследования
    Рис. 1.1.  Вложенность объектов

    Вторая группа терминальных свойств

    Объекты Application помимо свойств, заданных вложенными объектами, обладают "терминальными" свойствами. Их задают переменные языка программирования, типы которых непосредственно встроены в язык, - string, integer и т.д. Терминальные свойства также могут быть как специфическими, так и общими для всех приложений, определяя, например, свойства окна, в котором работает приложение. Терминальные свойства просты для понимания, мы не будем на них подробно останавливаться, ограничимся одним примером использования общих свойств. Приведем два макроса, один из которых изменяет размеры окна, предварительно запомнив текущие размеры и состояние окна. Второй из макросов восстанавливает прежние размеры.
    Пример 1.1.
    (html, txt)
    Эту пару макросов можно применять к трем приложениям Office 2000, поскольку в каждом из них объект Application обладает терминальными свойствами Left, Top, Height, Width, WindowState. Единственное, что придется поменять, - тип переменной StateW и задающую статус окна константу wdWindowStateNormal, так как у каждого из приложений свои имена констант и свои типы для них, хотя они и задают одинаковые значения. Заметьте: по имени константы можно определить ее тип и назначение. Так, константы Word начинаются префиксом "wd", а константы Excel - "xl".
    Следует отметить, что, Access продолжая выделяться, этих терминальных свойств еще не приобрел.
    Замечание.
    Все примеры этой книги мы старались проверить непосредственно на машине. С этой целью, как правило, для проверки соответствующего примера в тестовом документе создавалась командная кнопка, в ответ на щелчок которой вызывался соответствующий макрос, текст которого и приводится в книге. На рис. 1.10 показан такой тестовой документ, для проверки примеров этой лекции.
    Вторая группа терминальных свойств
    увеличить изображение
    Рис. 1.10.  Тестовый документ с командными кнопками

    Основы офисного программирования и язык VBA

    DocFour

    Документ 4


    Этот документ включает проект Four , дополняющий систему проектов One – Two – Three.
    Его цель - проверить возможности системы разнотипных документов.
    Нет возможности вручную включить ссылку на проект Excel из проекта в документе Word. Это связано с тем, что вызываются две разные версии VBA для документов Excel и документов Word. Программное включение ссылки также не проходит, по крайней мере, если это делать в лоб, хотя стоит заметить, что коллекция VBProjects содержит все открытые проекты, как Word так и Excel.
    Попробуем проверить гипотезу о  том, что прежде чем подключать ссылку, например, из Word на проект Excel, необходимо в документе Word подключить само приложение Excel.    
                                                                                       

    Документ и его программный проект

    От глобальных вопросов проектирования документов спустимся на землю и рассмотрим один единственный документ Office 2000. Сейчас нас будет интересовать, что собой представляет программный проект одного документа.
    Каждый вновь создаваемый документ обладает определенной архитектурой, поскольку строится на основе каркаса документов, заданного совокупностью библиотек Office 2000. Каркас каждого конкретного документа определяется, прежде всего, тем, в каком приложении создается документ, - корневым объектом Application. Частью архитектуры вновь создаваемого документа является и создаваемый по умолчанию программный проект. Здесь и в дальнейшем, если только не будет делаться специальных оговорок, под программным проектом понимается совокупность модулей. Модули, составляющие программный проект, могут быть следующих типов:
  • Модули, связанные с объектами приложения, реагирующими на события
  • Программные модули, создаваемые программистом, так называемые стандартные модули.
  • Модули классов, создаваемые программистом.
  • Модули макросов, создаваемые Macrorecorder.


  • Еще раз о "переиспользовании" модулей

    Пора, пожалуй, прервать плавный ход повествования и привести хоть небольшой фрагмент кода, иллюстрирующий высказанные в этой лекции утверждения. Мы отмечали роль стандартных модулей, одно из достоинств которых состоит в возможности их многократного использования. Рассмотрим следующую, часто возникающую на практике задачу. Пусть есть форма, содержащая два списка - элементы управления типа ListBox. При открытии формы пользователь может выбирать некоторые элементы в одном списке и переносить их в другой. Возникает вопрос, как организовать работу по переносу элементов, какие должны быть обработчики событий, где их разместить, где должна быть сосредоточена вся выполняемая работа по переносу элементов из списка в список в ответ на выбор пользователя. Решение кажется естественным и единственным, - все обработчики и вся обработка должна быть сосредоточена в модуле, связанном с формой. Действительно, для обработчиков событий это единственная возможность и поэтому кажется естественным, что и вся обработка должна быть сосредоточена в этом же модуле. Но, может быть, более эффективно, чтобы формальный обработчик события, находящийся в модуле формы, вызывал обычную процедуру с параметрами, которая может находиться в стандартном модуле общего назначения. Такая процедура может многократно использоваться разными формами, где есть списки и где возникает подобная задача.
    На рис. 2.3 показана одна из таких форм. В нашем тестовом примере эта форма открывается по ходу дела при работе с рабочей книгой документа Excel. Пользователь, работающий с этой формой, имеет возможность отобрать некоторые данные в одном списке и, затем, перенести их в другой список.
    Еще раз о
    Рис. 2.3.  Форма TwoListsForm документа Excel
    В форме есть два списка и 4 командные кнопки. Перенос элементов из одного списка в другой можно осуществлять тремя разными способами:
  • Двойным щелчком по элементу.
  • Выделить предварительно один или несколько элементов списка и затем щелкнуть командную кнопку со значком ">" ("<").
  • Для переноса всех элементов списка нет необходимости в их выделении, - достаточно щелкнуть командную кнопку со значком ">>" ("<<").


  • Направление переноса задают командные кнопки. Знак на кнопке меняется в зависимости от того, какой из списков выбран - левый или правый. Кнопка "OK" на форме переносит данные из списка на лист Excel, кнопка "Cancel" завершает работу с формой без переноса данных.

    Работу обработчиков событий, возникающих при работе с объектами формы, можно было бы без сомнения полностью описать в модуле, связанном с формой. Но мы поступили по-иному и создали стандартный модуль с именем ToolsMod, в который и вынесли содержательную часть обработки. Так что у нас появилось два модуля, - в одном находятся обработчики события, в другом - стандартном сосредоточен содержательный код, выполняющий обработку. Вот код модуля TwoLists, связанного с нашей формой, и содержащий обработчики событий:

    Пример 2.1.

    (html, txt)

    Модуль TwoListsForm, связанный с формой, содержит 9 обработчиков событий, возникающих при работе с самой формой - ее инициализации, так и при работе пользователя с объектами, населяющими форму. Каждый из обработчиков выполняет свои специфические задачи. Когда пользователь переключается на работу с элементами левого или правого списка, то в тот момент, когда список получает фокус, возникает событие Enter. Обработчик события Enter изменяет заголовок у соответствующих командных кнопок, подготавливая, тем самым, передачу данных в нужном направлении. Такое автоматическое изменение направления передачи позволяет уберечь пользователя от ошибочных действий. Но, обратите внимание на главное в этом примере. Для удобства пользователя ему предоставлены три разных способа передачи данных из одного списка в другой. Поскольку обмен данными может вестись в двух направлениях, то при лобовом программировании нужно было бы написать 6 различных макросов. Мы же свели задачу к двум процедурам, вызываемых с разными значениями параметров. Тексты этих процедур мы поместили в стандартный модуль с именем ToolsMod, предполагая возможность его дальнейшего использования. Вот эти тексты:

    Пример 2.2.


    (html, txt)

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

    Завершая разговор о переиспользовании стандартных модулей, предлагаем взглянуть на форму, взятую из совсем другого документа - документа Word.

    Еще раз о
    Рис. 2.4.  Форма TwoLists, взятая из документа Word

    Хотя форма и похожа на форму, связанную с рабочей книгой Excel, но, обратите внимание, списки в ней другие, они состоят из одного столбца, а не из двух, как в прошлом случае. Тем не менее, мы спокойно выполнили операции экспорта - импорта стандартного модуля из Excel в Word и без всяких изменений использовали его процедуры MoveSelectedItems и MoveAllItems. Конечно, думая о возможности многократного использования этих процедур, мы заранее побеспокоились о возможности работы с произвольным числом столбцов в списках.

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

    Замечание:

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

    Public Sub MoveAllItems(ByVal n As Byte, ByVal ListBox1 As Object, _

    ByVal ListBox2 As Object)

    Вы видите, что тип формальных параметров для ListBox1 и ListBox2 задан как Object и, следовательно, является нетипизированным указателем. Это не очень удобно, так как лишает нас подсказки в момент написания процедуры. Возникает вопрос, можно ли указывать точный тип для подобных формальных параметров. Ответ зависит от приложения. При работе в приложении Word разрешается указывать настоящий, правильный тип ListBox и при этом передача параметров производится корректно. В приложении Excel при написании процедуры также можно указать тип ListBox, но возникнет ошибка в момент передачи параметров.

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


    Но, обратите внимание на главное в этом примере. Для удобства пользователя ему предоставлены три разных способа передачи данных из одного списка в другой. Поскольку обмен данными может вестись в двух направлениях, то при лобовом программировании нужно было бы написать 6 различных макросов. Мы же свели задачу к двум процедурам, вызываемых с разными значениями параметров. Тексты этих процедур мы поместили в стандартный модуль с именем ToolsMod, предполагая возможность его дальнейшего использования. Вот эти тексты:

    Option Explicit Public Sub MoveSelectedItems(ByVal n As Byte, ByVal ListBox1 As Object, _ ByVal ListBox2 As Object) 'Перемещает выделенные элементы первого списка в конец второго 'с одновременным удалением данных из первого списка. 'Оба списка имеют n столбцов.

    Dim RowIndex1 As Byte, RowIndex2 As Byte, i As Byte, j As Byte

    'Выборочный обмен данными между списками: ListBox1 -> ListBox2 With ListBox1 RowIndex2 = ListBox2.ListCount RowIndex1 = 0 For i = 0 To .ListCount - 1 If .Selected(RowIndex1) Then 'Создается элемент нового списка и заполняется его первый столбец ListBox2.AddItem .List(RowIndex1) 'Заполняются остальные столбцы элемента списка For j = 1 To n - 1 ListBox2.Column(j, RowIndex2) = .Column(j, RowIndex1) Next j 'Перемещенный элемент удаляется из списка .RemoveItem RowIndex1 RowIndex2 = RowIndex2 + 1 Else RowIndex1 = RowIndex1 + 1 End If Next i End With End Sub

    Public Sub MoveAllItems(ByVal n As Byte, ByVal ListBox1 As Object, _ ByVal ListBox2 As Object) ' Перемещает все элементы первого списка в конец второго, ' возможно, не пустого списка с одновременным удалением данных из ' первого списка. ListBox1 -> ListBox2

    Dim RowIndex1 As Integer, RowIndex2 As Integer, i As Byte RowIndex2 = ListBox2.ListCount For RowIndex1 = 0 To ListBox1.ListCount - 1 With ListBox1 ListBox2.AddItem .List(0) For i = 1 To n - 1 ListBox2.Column(i, RowIndex2) = .Column(i, 0) Next i RowIndex2 = RowIndex2 + 1 'Перемещенный,- это всегда первый элемент,удаляется из списка .RemoveItem 0 End With Next RowIndex1 End Sub


    Public Sub MoveListToRange(ByVal n As Byte, List1 As Object, Dom As String) 'List1 - объект типа ListBox, состоящий из n столбцов. ' Его элементы переносятся в прямоугольную область активного листа, ' Dom - задает имя ячейки, расположенной в левом верхнем углу этой ' области.

    Dim myr As Range Dim i As Byte, j As Byte

    Set myr = Range(Dom) 'Цикл по числу элементов списка. For i = 0 To List1.ListCount - 1 'Цикл по числу столбцов списка. For j = 0 To n - 1 myr.Offset(j, i) = List1.Column(j, i) Next j Next i End Sub

    Public Sub ClearRange(Dom As String) 'Эта процедура очищает содержимое области листа рабочей книги, 'заданной ячейкой с именем Dom

    Dim myr As Range, Row As Byte, Col As Byte

    Set myr = Range(Dom) Col = 0: Row = 0 While myr.Offset(Row, Col) <> "" While myr.Offset(Row, Col) <> "" 'Чистка содержимого myr.Offset(Row, Col).ClearContents Col = Col + 1 Wend Row = Row + 1 Col = 0 Wend End Sub

    Пример 2.2.

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

    Завершая разговор о переиспользовании стандартных модулей, предлагаем взглянуть на форму, взятую из совсем другого документа - документа Word.

    Еще раз о
    Рис. 2.4.  Форма TwoLists, взятая из документа Word

    Хотя форма и похожа на форму, связанную с рабочей книгой Excel, но, обратите внимание, списки в ней другие, они состоят из одного столбца, а не из двух, как в прошлом случае. Тем не менее, мы спокойно выполнили операции экспорта - импорта стандартного модуля из Excel в Word и без всяких изменений использовали его процедуры MoveSelectedItems и MoveAllItems. Конечно, думая о возможности многократного использования этих процедур, мы заранее побеспокоились о возможности работы с произвольным числом столбцов в списках.


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

    Замечание:

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

    Public Sub MoveAllItems(ByVal n As Byte, ByVal ListBox1 As Object, _

    ByVal ListBox2 As Object)

    Вы видите, что тип формальных параметров для ListBox1 и ListBox2 задан как Object и, следовательно, является нетипизированным указателем. Это не очень удобно, так как лишает нас подсказки в момент написания процедуры. Возникает вопрос, можно ли указывать точный тип для подобных формальных параметров. Ответ зависит от приложения. При работе в приложении Word разрешается указывать настоящий, правильный тип ListBox и при этом передача параметров производится корректно. В приложении Excel при написании процедуры также можно указать тип ListBox, но возникнет ошибка в момент передачи параметров.

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

    Имя проекта

    Имя проекта используется не только для идентификации проекта в окне просмотра. Оно играет важную роль в тех случаях, когда документ является частью общей системы документов или используется, как компонент, вызываемый в других документах (COM объект). В последнем случае имя проекта идентифицирует компонент в реестре Windows. Во всех случаях проект можно рассматривать как компонент, отвечающий требованиям COM модели, Согласно принятой в компонентном программировании терминологии, проект экспонирует, то есть позволяет использовать, свои открытые Public - элементы. Имя проекта играет важную роль в этом процессе, - оно является именем библиотеки типов TypeLib. Эта библиотека содержит описание объектов и интерфейсов, которые поддерживает компонент. У имени проекта есть еще одна важная роль, оно используется также при построении полного имени открытых компонентов проекта. Всякий раз, когда в проекте A надо сослаться на Public переменную или Public метод проекта B, необходимо использовать полное имя открытого компонента:
    имя_проекта.имя_модуля.имя_открытого_компонента
    Заметьте, при именовании используется имя проекта, а не имя документа, содержащего проект.

    Как организуются ссылки между проектами

    Рассмотрим теперь технический вопрос, как организовать ссылку из проекта B на проект A. Для этого нужно сделать две вещи, во-первых, дать собственные имена проектам, во-вторых, - включить ссылку в проекте B на проект A. Имя нужно дать, по крайней мере, родительскому проекту A. О том, как это делается, мы уже рассказали в разделе "Свойства проекта" этой лекции. На рис. 2.2 показано соответствующее диалоговое окно.
    Задание ссылки выполняется также стандартным образом. В окне Редактора Visual Basic нужно выбрать соответствующий проект, в нашем примере проект B, и из меню Tools выбрать команду References. Если документ, содержащий проект A открыт, то в окне ссылок наряду с другими возможными для ссылок компонентами появится флажок с именем проекта A, останется только его включить.

    Модуль макросов

    Этот модуль создается автоматически при первом вызове Macrorecoder. В приложении Word все созданные Macrorecoder макросы размещаются в модуле с именем NewMacros. При создании макроса требуется указать, в модуль какого из проектов - Normal.dot или проекта, связанного с документом, должен быть записан макрос. В приложении Excel каждый новый макрос записывается в отдельный модуль. Это не всегда удобно и, зачастую, руками макросы объединяются в одном модуле, особенно, если все они решают некоторую общую задачу. Единственное отличие модулей макросов от стандартных модулей состоит в том, что они создаются системой, а не программистом. Поскольку это не столь существенно, то мы не будем в дальнейшем выделять этот тип модулей, и будем рассматривать их далее как стандартные модули.

    Модули классов

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

    Модули - обработчики событий

    Модули первого типа всегда связаны с объектами, реагирующими на события. Их главное назначение состоит в том, что они содержат обработчики таких событий. Объектов, имеющих события, довольно много, но только с некоторыми из них связываются модули. Попробуем с этим разобраться подробнее.
    Когда создается документ Word, то появляется один такой объект с именем ThisDocument, задающий сам документ. При создании документа Excel наряду с объектом ThisWorkbook, задающим рабочую книгу, появляется коллекция объектов Sheets, задающих каждую страницу этой книги. Все упомянутые объекты могут реагировать на события, они являются основными объектами, с ними и связываются модули. Но, заметьте, при создании презентации в приложении Power Point объект ThisPresentation, задающий презентацию, и связанный с ним модуль автоматически не появляется. В этот момент должен возникнуть вопрос, а почему? Дело в том, что объект класса Presentation не реагирует на события, следовательно, и модуль не может появиться. Это же касается и объектов класса Slide, - у них тоже нет событий.
    Есть и другие объекты, с которыми логично было бы связать модули, но по умолчанию этого не делается. В каждом из приложений, наряду с объектом, задающим сам объект - презентацию, документ, рабочую книгу, присутствует и корневой объект Application, задающий само приложение. Этот объект, как известно, также реагирует на события. Почему же нет модуля, ему соответствующего? Здесь есть одна тонкость. Создаваемые по умолчанию объекты Application не реагируют на события. На события реагируют объекты специального класса, - ApplicationWithEvents. Чуть позже мы рассмотрим, как создаются такие объекты и как появляются для них соответствующие модули.
    До сих пор мы говорили о модулях, создаваемых по умолчанию для новых документов. Но такие модули могут появляться и в процессе работы над существующим документом. Наиболее известным видом таких модулей являются модули форм (диалоговых окон), составляющие в проекте отдельную папку Forms.
    Всякий раз, когда Вы добавляете в проект новую форму, появляется модуль, связанный с этой формой. Есть и другие ситуации, при которых появляются новые модули проекта, например, при добавлении нового листа в книгу Excel, появляется и соответствующий ему модуль.

    Далеко не со всяким объектом, который может реагировать на события, связывается модуль. Для всех элементов управления (Controls), размещаемых в форме или непосредственно в документе Word, листе рабочей книги, слайде презентации, соответствующие обработчики появятся в модуле, связанном с "родительским объектом". Чуть выше мы сказали, что объекты класса Slide не имеют событий и потому с ними не связаны модули в момент создания презентации. Сейчас мы опровергнем это утверждение, уточним его. Если на слайде размещаются элементы управления - кнопки списки и прочее, то появляется модуль, связанный с этим слайдом, и обработчики событий, связанных с элементами управления размещаются в этом модуле.

    Есть еще одна группа объектов, имеющих обработчиков событий в обязательном порядке, но не имеющая "своих" модулей. Возможно, Вы уже догадались, что речь идет о командах меню, командных кнопках и других элементах управления, размещаемых на панелях - объектах класса CommandBar. Макросы, задающие обработку событий, связанных с этими элементами, помещаются в стандартные модули.

    Мои любимые поэты XIX века

                                                               

    Обмен информацией между документами

    Когда речь идет о системе документов, то один из основных вопросов - организация обмена информацией между отдельными документами системы. Когда речь идет о системе проектов, то, как правило, существует сравнительно небольшая по объему общая информация, которая используется во всех проектах. Сейчас мы и поговорим о том, как организовать такой общий информационный пул, содержащий глобальную информацию уровня системы проектов. Задача состоит в том, чтобы хранимые данные могли быть доступны во всех проектах для чтения и записи. Решить задачу можно по-разному, перечислим основные способы:
  • Выделим один из проектов, который будем называть основным, и в стандартном модуле этого проекта зададим множество Public переменных, в совокупности и определяющих общую для всей системы информацию. Систему документов спроектируем так, чтобы их проекты были связаны по ссылке, и корнем дерева был основной документ, хранящий глобальную информацию. В этом случае каждый проект будет иметь доступ к этой информации. Напомним, циклические ссылки между проектами не разрешены, отсюда следует, что нельзя часть информации поместить в проект А, часть в проект В и сделать эту информацию общедоступной для обоих проектов. Возможна только древесная организация хранения общей информации. Самая общая информация хранится в корневом проекте, в "дочерних" проектах хранится информация необходимая потомкам этих дочерей, и так до терминальных проектов.
  • Второй способ состоит в том, что общая для всех проектов информация размещается в виде списков Excel, в ячейках выделенной рабочей книги, играющей роль основного документа. Этот способ основан на том, что не только VBA обеспечивает взаимодействие проектов, возможно взаимодействие самих документов, ссылки между ними. Известно, что ссылки на ячейки книги Excel допустимы и широко распространены. Так что страницы Excel часто используются как удобное средство для передачи информации между документами. Чуть ниже на примере мы продемонстрируем этот способ передачи информации.
    Достоинство этого способа состоит и в том, что проекты могут и не быть связанными ссылками, более того, возможен взаимный обмен информацией.
  • Классическим способом хранения общей информации, особенно, в больших объемах, являются базы данных. Поэтому базу данных Access также можно использовать для организации информационного пула системы проектов.
  • Для полноты картины упомянем обычный файл.


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

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

    Обмен информацией между документами
    Рис. 2.6.  Открытие рабочей книги, содержащей ссылки

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


    На листе рабочей книги нашего примера расположена командная кнопка с надписью "ChooseBook". Эту кнопку пользователь должен нажать в тот момент, когда он стоит на распутье, и должен выбрать, с какой следующей книгой он хочет работать. Вот текст обработчика события "Click":

    Пример 2.3.

    (html, txt)

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

    Public Sub PrintGlobal() Dim MyrOne As Range, MyrTwo As Range, MyrThree As Range

    Debug.Print ("Работает процедура PrintOne") Debug.Print ("Печать глобальных переменных") Debug.Print (One & "-" & Two & "-" & Three)

    Debug.Print ("Допустимы ссылки на ячейки различных рабочих книг!") Set MyrOne = Range("[BookOne.xls]Sheet1!D1") Debug.Print MyrOne Set MyrTwo = Range("[BookTwo.xls]Sheet1!D1") Debug.Print MyrTwo Set MyrThree = Range("[BookThree.xls]Sheet1!D1") Debug.Print MyrThree MsgBox ("Everything is OK! Look at Immediate Window") End Sub

    Вот результаты отладочной печати в конце работы этой процедуры:

    Работает процедура PrintOne Печать глобальных переменных My value is One -My value is One -My value is One Допустимы ссылки на ячейки различных рабочих книг! BookOne BookTwo BookThree

    Всем глобальным переменным предварительно было присвоено одно и то же значение. Книги BookOne, BookTwo, BookThree.xls на первой странице в ячейке D1 хранят свои названия, их и печатает наша процедура, вызываемая в проекте одной из этих книг.


    Можно видеть, что доступ к информации не представляет трудностей.

    Public Sub PrintGlobal() Dim MyrOne As Range, MyrTwo As Range, MyrThree As Range

    Debug.Print ("Работает процедура PrintOne") Debug.Print ("Печать глобальных переменных") Debug.Print (One & "-" & Two & "-" & Three)

    Debug.Print ("Допустимы ссылки на ячейки различных рабочих книг!") Set MyrOne = Range("[BookOne.xls]Sheet1!D1") Debug.Print MyrOne Set MyrTwo = Range("[BookTwo.xls]Sheet1!D1") Debug.Print MyrTwo Set MyrThree = Range("[BookThree.xls]Sheet1!D1") Debug.Print MyrThree MsgBox ("Everything is OK! Look at Immediate Window") End Sub

    Вот результаты отладочной печати в конце работы этой процедуры:

    Работает процедура PrintOne Печать глобальных переменных My value is One -My value is One -My value is One Допустимы ссылки на ячейки различных рабочих книг! BookOne BookTwo BookThree

    Всем глобальным переменным предварительно было присвоено одно и то же значение. Книги BookOne, BookTwo, BookThree.xls на первой странице в ячейке D1 хранят свои названия, их и печатает наша процедура, вызываемая в проекте одной из этих книг.

    Окно кода

    Окно проекта согласовано с окном кода. Когда Вы выбираете некоторый модуль двойным щелчком в окне проекта, его код отображается в окне кода. Но окно кода отображает не только код модуля. Важную роль играют два раскрывающихся списка, расположенные в верхней части окна кода. Они играют особую роль для модулей, связанных с объектами, реагирующими на события. Ранее, мы уже говорили, что такие объекты, как главный документ, страницы Excel, слайды презентации, являются основными, в них может быть вложено большое число объектов, также реагирующих на события. На страницах Excel, например, можно размещать командные кнопки, списки и другие элементы управления. Поэтому модуль основного объекта связан фактически с большим числом объектов, реагирующих на события. Следовательно, нужно иметь возможность отображать структуру вложенных объектов.
    Левый раскрывающийся список окна кода показывает все объекты, выбранного модуля, позволяя понять, какие объекты вложены в соответствующий основной объект. На рисунке 2.1 зафиксирован момент выбора модуля, связанного с объектом Sheet2 ("ПостановкаЗадачи"). Можно видеть, что на странице расположено довольно много элементов управления. Если выбрать в левом раскрывающемся списке один из объектов, как показано на рисунке, то правый раскрывающийся список отобразит список всех возможных событий данного объекта. Если теперь в правом списке выбрать одно из возможных событий, то в окне кода автоматически будет создана заготовка обработчика этого события. Ее можно наполнить содержанием. Все выбранные таким образом события считаются активными, в правом списке событий они выделены полужирным шрифтом, при их возникновении будет послано сообщение объекту, обработчик будет выполняться, даже если он пустой, - заготовка не была наполнена содержанием.
    Конечно, при создании обработчиков событий вовсе не обязательно пользоваться такой технологией работы. Можно вообще не пользоваться заготовкой и самому полностью написать обработчик события, лишь бы выдерживались требования к имени обработчика, и, конечно же, нужно также помнить, что обработчики событий могут иметь и параметры. Поэтому мы рекомендуем пользоваться заготовками.
    Для стандартных модулей левый раскрывающийся список окна кода не представляет интереса, а правый содержит имена всех методов стандартного модуля и имя раздела объявлений. Выбор имени из списка позволяет немедленно перейти в соответствующее место окна кода.

    Окно проекта

    Давайте немного поговорим о том, как выглядит работа с модулями в среде Редактора VBE. Взгляните на рисунок 2.1, где показаны два основных окна редактора, о которых сейчас пойдет речь: окно проекта и окно кода.
    Окно проекта
    увеличить изображение
    Рис. 2.1.  Окно проекта и окно кода
    Окно проекта или, как еще говорят, Проводник Проекта (Project Explorer) отображает структуру проекта в привычном для всех Проводников виде - дерева раскрывающихся папок. В этом окне отображается программные проекты всех открытых документов. Так что, если Вы работаете с семейством документов, то Проводник покажет полностью программный проект всего семейства. Специальная папка References покажет взаимные связи проектов отдельных документов.
    Давайте разберемся в структуре проекта, показанного на рисунке 2.1. Этот проект связан с рабочей книгой Excel с именем MasterFNew. В данном случае мы имеем дело с единственным документом, хотя в окне показаны два проекта. Дело в том, что наш документ работает с Решателем (Solver). В Office 2000 Решатель представляет AddIn, ссылку на который надо включить в меню References, что и приводит к появлению соответствующего проекта в Проводнике. Заметьте, проект Solver закрыт паролем от просмотра, что не позволяет просмотреть его структуру и тем более код его модулей. Что же касается проекта нашего документа, то его структура раскрыта полностью. Он содержит:
  • 5 модулей, связанных с такими объектами, как рабочая книга, задающая сам документ, и четырьмя страницами этой книги. В окне отображаются названия этих страниц.
  • 3 модуля, связанных с формами - объектами класса Form.
  • 7 стандартных модулей. Точнее, шесть стандартных модулей и модуль Others, содержащий макросы. Обратите внимание, число стандартных модулей в этом проекте достаточно велико. Модуль Main играет роль основного модуля проекта и cодержит описание глобальных переменных всего проекта.
  • Папка References содержит ссылку на проект Solver.

  • Проект данного документа довольно типичен и содержит все типы модулей за исключением модулей классов.

    Организация системы документов

    Наше определение системы документов достаточно общее, - совокупность открытых документов составляет систему. О связях между документами ничего не говорится. Пользователь сам может решать, с каким документом он будет работать в следующий момент, и волен свободно переходить от одного документа к другому. Но документы в системе могут быть связаны. Рассмотрим связи двух видов:
  • Связи документов "по вызову".
  • Связи проектов документов "по ссылке"

  • Два документа A и B связаны по вызову, если в одном из документов, например A, вызывается документ B. Документ A называют родителем, а документ B, соответственно, потомком. Совокупность связанных по вызову документов образует дерево связанных документов. Корень дерева - это тот первичный документ, из которого и начинаются вызовы других документов. Связи в дереве направлены от корня к потомкам.
    Представив совокупность связанных по вызову документов в виде дерева, мы сознательно упростили картину, фактически, она гораздо сложнее, поскольку допустимы циклы. Вызов документа означает его открытие, если он не был еще открыт и его активизацию. Вполне допустимо, что несколько открытых документов в произвольном порядке и неоднократно становятся активными. Тем не менее, зачастую, особенно при проектировании полезно рассматривать древесную структуру организации связей по вызову.
    Проекты A и B связаны по ссылке, если один из проектов, например B, ссылается на проект A. Проект A называют родителем, а проект B, соответственно, потомком. Совокупность связанных по ссылке проектов образует дерево связанных проектов. Корень дерева - это тот первичный проект, на который ссылаются остальные проекты. Связи в дереве ссылок направлены от потомков к корню. Вообще говоря, проекты, связанные по ссылке, также могли бы организовывать более сложную структуру, чем дерево. Действительно, взаимные ссылки кажутся допустимыми. Но это не так. Помните, циклические ссылки проектов VBA документов не разрешены. Так что при разработке проекта системы документов Office 2000 исходите из древесной структуры организации связей между проектами. На рисунке 2.5 показаны три возможные структуры организации системы документов Office 2000.
    Организация системы документов
    Рис. 2.5.  Три структуры организации системы документов

    Выборочный обмен данными между n-

    Option Explicit
    Private Sub CommandButton1_Click() 'Обработчик события Click кнопки "> <" ' Выборочный обмен данными между n- колоночными списками: 'ListBox1 <--> ListBox2
    If CommandButton1.Caption = ">" Then Call MoveSelectedItems(ListBox1.ColumnCount, ListBox1, ListBox2) Else Call MoveSelectedItems(ListBox2.ColumnCount, ListBox2, ListBox1) End If End Sub
    Private Sub CommandButton2_Click() 'Обработчик события Click кнопки ">> <<" 'Перенос всех данных из одного n-колоночного списка 'в конец другого, возможно, не пустого списка: ListBox1 <--> ListBox2
    If CommandButton2.Caption = ">>" Then Call MoveAllItems(ListBox1.ColumnCount, ListBox1, ListBox2) Else Call MoveAllItems(ListBox2.ColumnCount, ListBox2, ListBox1) End If End Sub
    Private Sub ListBox1_DblClick(ByVal Cancel As MSForms.ReturnBoolean) 'Обработчик события DblClick левого списка ListBox1 (имеет параметры!) 'При двойном щелчке выбранный элемент одного n-колоночного списка 'переносится в конец другого списка
    Call MoveSelectedItems(ListBox1.ColumnCount, ListBox1, ListBox2) End Sub
    Private Sub ListBox2_DblClick(ByVal Cancel As MSForms.ReturnBoolean) 'Обработчик события DblClick правого списка ListBox2 'При двойном щелчке выбранный элемент одного n-колоночного списка 'переносится в конец другого списка
    Call MoveSelectedItems(ListBox2.ColumnCount, ListBox2, ListBox1) End Sub
    Private Sub CommandButton5_Click() 'Обработчик события Click кнопки "OK" 'Перенос данных из списка на лист Excel 'В область, заданную ячейкой с именем "Dom"
    Call MoveListToRange(ListBox2.ColumnCount, ListBox2, "Dom") Me.Hide End Sub
    Private Sub CommandButton6_Click() 'Обработчик события Click кнопки "Cancel" Me.Hide End Sub
    Private Sub ListBox1_Enter() 'Обработчик события Enter, возникающего при получении фокуса
    CommandButton1.Caption = ">" CommandButton2.Caption = ">>" End Sub
    Private Sub ListBox2_Enter() 'Обработчик события Enter, возникающего при получении фокуса
    CommandButton1.Caption = "<" CommandButton2.Caption = "<<" End Sub
    Private Sub UserForm_Initialize() 'Обработчик события Initialize формы TwoListsForm 'Заполнение списка ListBox1
    Dim MyArray(1 To 5, 1 To 2) As String
    MyArray(1, 1) = "Петров" : MyArray(1, 2) = "Музыкант" MyArray(2, 1) = "Сергеев" : MyArray(2, 2) = "Учитель" MyArray(3, 1) = "Гурина" : MyArray(3, 2) = "Актриса" MyArray(4, 1) = "Водкин" : MyArray(4, 2) = "Художник" MyArray(5, 1) = "Козина" : MyArray(5, 2) = "Геолог"
    ListBox1.ColumnCount = 2 : ListBox2.ColumnCount = 2 ListBox1.List() = MyArray End Sub
    Пример 2.1.
    Закрыть окно




    Option Explicit Public Sub MoveSelectedItems(ByVal n As Byte, ByVal ListBox1 As Object, _ ByVal ListBox2 As Object) 'Перемещает выделенные элементы первого списка в конец второго 'с одновременным удалением данных из первого списка. 'Оба списка имеют n столбцов.
    Dim RowIndex1 As Byte, RowIndex2 As Byte, i As Byte, j As Byte
    'Выборочный обмен данными между списками: ListBox1 -> ListBox2 With ListBox1 RowIndex2 = ListBox2.ListCount RowIndex1 = 0 For i = 0 To .ListCount - 1 If .Selected(RowIndex1) Then 'Создается элемент нового списка и заполняется его первый столбец ListBox2.AddItem .List(RowIndex1) 'Заполняются остальные столбцы элемента списка For j = 1 To n - 1 ListBox2.Column(j, RowIndex2) = .Column(j, RowIndex1) Next j 'Перемещенный элемент удаляется из списка .RemoveItem RowIndex1 RowIndex2 = RowIndex2 + 1 Else RowIndex1 = RowIndex1 + 1 End If Next i End With End Sub
    Public Sub MoveAllItems(ByVal n As Byte, ByVal ListBox1 As Object, _ ByVal ListBox2 As Object) ' Перемещает все элементы первого списка в конец второго, ' возможно, не пустого списка с одновременным удалением данных из ' первого списка. ListBox1 -> ListBox2
    Dim RowIndex1 As Integer, RowIndex2 As Integer, i As Byte RowIndex2 = ListBox2.ListCount For RowIndex1 = 0 To ListBox1.ListCount - 1 With ListBox1 ListBox2.AddItem .List(0) For i = 1 To n - 1 ListBox2.Column(i, RowIndex2) = .Column(i, 0) Next i RowIndex2 = RowIndex2 + 1 'Перемещенный,- это всегда первый элемент,удаляется из списка .RemoveItem 0 End With Next RowIndex1 End Sub
    Public Sub MoveListToRange(ByVal n As Byte, List1 As Object, Dom As String) 'List1 - объект типа ListBox, состоящий из n столбцов. ' Его элементы переносятся в прямоугольную область активного листа, ' Dom - задает имя ячейки, расположенной в левом верхнем углу этой ' области.
    Dim myr As Range Dim i As Byte, j As Byte
    Set myr = Range(Dom) 'Цикл по числу элементов списка. For i = 0 To List1.ListCount - 1 'Цикл по числу столбцов списка. For j = 0 To n - 1 myr.Offset(j, i) = List1.Column(j, i) Next j Next i End Sub
    Public Sub ClearRange(Dom As String) 'Эта процедура очищает содержимое области листа рабочей книги, 'заданной ячейкой с именем Dom
    Dim myr As Range, Row As Byte, Col As Byte
    Set myr = Range(Dom) Col = 0: Row = 0 While myr.Offset(Row, Col) <> "" While myr.Offset(Row, Col) <> "" 'Чистка содержимого myr.Offset(Row, Col).ClearContents Col = Col + 1 Wend Row = Row + 1 Col = 0 Wend End Sub
    Пример 2.2.
    Закрыть окно




    Private Sub CommandButton5_Click() ChooseBook End Sub
    Public Sub ChooseBook() 'Выбор книги, с которой будет работать пользователь Const PathDir As String = "e:\O2000\CD2000\Ch2\" Dim Answer As Variant Dim CurrentBook As String Dim Book As Workbook Dim Found As Boolean
    Answer = InputBox(prompt:= _ "Выберите книгу, с которой будете работать (1/2/3)", Default:=1) Select Case Answer Case 1 CurrentBook = "BookOne.xls" Case 2 CurrentBook = "BookTwo.xls" Case 3 CurrentBook = "BookThree.xls" Case Else CurrentBook = "I don't know such book" End Select MsgBox ("Your choose is " & CurrentBook) 'Проверяем есть ли уже книга в коллекции Found = False For Each Book In Workbooks If Book.Name = CurrentBook Then Found = True Exit For End If Next Book If Found Then 'Активизируем выбранную книгу Workbooks(CurrentBook).Activate ElseIf CurrentBook = "I don't know such book" Then MsgBox ("I don't know such book") Else 'Добавляем книгу в коллекцию Workbooks.Open PathDir & CurrentBook Workbooks(CurrentBook).Activate End If End Sub
    Пример 2.3.
    Закрыть окно




    Private Sub CommandButton1_Click() BookOne.Sheet1.ChooseBook End Sub
    Public Sub ChangeGlobal() ' Изменение значений глобальных переменных системы документов BookOne.ModuleOne.Two = "My value is Two " TwoThree = "My value is Two - Two " End Sub
    Public Sub PrintGlobal() Dim Number As Integer Debug.Print ("Работает процедура PrintTwo") Debug.Print ("Печать глобальных переменных") Debug.Print TwoThree 'Вызов процедур и функций проекта BookOne Number = BookOne.ModuleOne.PLus1(1) Debug.Print ("Это книга " & Number) ChangeGlobal BookOne.ModuleOne.PrintGlobal End Sub
    Пример 2.4.
    Закрыть окно



    Проект и область видимости

    Напомним, проект документа мы рассматриваем, как совокупность модулей разного типа. Компонентами каждого модуля являются переменные уровня модуля, описанные в разделе объявления модуля, и методы модуля.
    Первое правило видимости гласит:
    "Все компоненты модуля видимы в пределах этого модуля".
    Это означает, что любая переменная из раздела объявлений модуля является глобальной по отношению к методам модуля, она доступна в каждом из методов, если только внутри метода нет объявления локальной переменной с тем же именем. Общедоступность глобальных переменных облегчает передачу информации между методами модуля. Вместе с тем общедоступность и плохое, опасное качество. Каждый метод может изменить глобальную переменную. Поэтому глобальных переменных не должно быть много. За судьбой каждой из них надо внимательно следить.
    Каждый метод модуля также общедоступен внутри модуля и может быть вызван другими методами модуля. Понятно, это не касается обработчиков событий. Последние вызываются системой и не могут быть вызваны никакими другими методами модуля.
    Второе правило видимости гласит:
    "Область видимости компонент модуля расширяется на весь проект, если компонент объявлен со спецификатором Public".
    Каждый компонент модуля - переменная или метод может быть снабжен спецификатором области видимости, который имеет два возможных значения - Public и Private. Если задан спецификатор Public, то это означает, что компонент общедоступен в пределах всего проекта. Спецификатор Private делает компонент закрытым для других модулей проекта. Он видим только в своем родном модуле.
    Если при объявлении переменных модуля спецификатор области видимости опущен и указано только ключевое слово Dim, то такие переменные считаются закрытыми, - действует спецификатор Private. Для методов спецификатор области видимости можно опускать. В этом случае действует следующее правило. Все методы стандартных модулей имеют по умолчанию спецификатор Public и являются доступными во всем проекте. Методы модулей - классов и модулей, связанных с объектами, по умолчанию являются закрытыми и имеют статус Private.

    Есть еще одно важное правило, касающееся общедоступных компонент. Спецификатор Public еще не гарантирует, что имя компонента будет видимо вне модуля. Чтобы компонент был видимым вне модуля, следует использовать его полное имя, которое строится по обычным правилам построения сложных имен. Оно состоит из имен, разделенных точкой, - имени компонента, имени модуля и, возможно, имени проекта. Имя проекта может потребоваться в тех случаях, когда речь идет о системе взаимосвязанных документов. Внутри одного проекта его имя можно опускать, но, заметьте, нельзя опускать имя модуля для Public переменных модулей, связанных с объектами. Для них допустимы только полные имена. Вообще разумно не иметь Public переменных для таких модулей.

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

    Проектирование документов

    Мы уже не раз говорили об идеологии офисного программирования, - целью разработки является создание системы документов, а не создание программного проекта (приложения). Программный проект это часть документа, хранится вместе с документом и не может быть отделен от него. Совокупность программных проектов всех документов, входящих в некоторую систему, может рассматриваться как программный проект этой системы документов. Возможны два важных частных случая:
  • Документ (система документов) содержит "пустой" программный проект. В большинстве случаев такие документы создают конечные пользователи, которые могут не иметь понятия о программировании и вообще не знать, что существует возможность присоединения программного проекта к документам Office 2000. В этом весьма типичном случае каждый документ представляет стандартный, предоставляемый по умолчанию документ Office 2000, без всякой его дополнительной настройки.
  • Документ служит обложкой для программного проекта. Такие документы создаются программистами. Это тоже возможная ситуация, когда программист хочет написать свое типично программистское приложение. Ему не требуются возможности офисной среды, все, что ему нужно, - это привычный язык VBA. Он мог бы написать это приложение на VB или другом языке программирования, но предпочитает оставаться в офисной среде.

  • Основная цель этой лекции рассмотреть, как связаны между собой программные проекты системы документов, какова структура программного проекта одного документа, его основные компоненты.
    Но прежде, чем приступить к реализации этой цели, хочется рассмотреть хотя бы обзорно, некоторые, более общие вопросы, связанные с проектами и документами. Прежде всего, заметим, что " документ документу рознь". Одно дело, когда речь идет о настройке стандартного документа, придания ему некоторых дополнительных функций, совсем другое дело - разработка большой и сложной системы, затрагивающей многие службы офиса или предприятия. Считается, что Office 2000 - это масштабируемая среда разработки.
    Это означает, что она подходит для решения небольших, простых задач, так и для решения сложных задач. К простым задачам относится создание отдельных, возможно и сложных документов, создание сравнительно небольшой системы документов. Сложными задачами могут быть, например:

  • Разработка клиентской части "клиент - серверных" приложений, рассчитанных на работу большого числа пользователей с серьезной базой данных, такой как Microsoft SQL Server или Oracle. Заметим, что в Office Developer 2000 включены специальные средства разработки, облегчающие создание подобных приложений.
  • Разработка документов для совместного использования в Интернете. Во многом сложность этой задачи объясняется ее новизной. Опять - таки, в Office 2000 включены новые средства, облегчающие создание таких документов.
  • Разработка большой системы документов, обеспечивающей "полную" автоматизацию деятельности офиса.


  • Есть еще одно измерение в разработке документов Office 2000. Оно связано с разработкой документов специального вида. Сюда относится разработка: шаблонов, Мастеров (Wizards), компонент, расширяющих функциональные возможности других документов. Эти компоненты могут быть двух видов - AddIns и ComAddIns. Дадим краткую характеристику этим видам документов:

  • Шаблоны - это наиболее простой из специальных случаев. Шаблоны являются заготовками, на базе которых можно создать семейство близких документов. Шаблон задает общие свойства семейства и требуется лишь сравнительно небольшая настройка для получения конкретного документа. Как правило, рекомендуется создавать шаблоны и открывать документы на основе того или иного шаблона. Типичным и хорошо известным является шаблон Normal.dot, на базе которого открываются стандартные документы Word.
  • Мастера обеспечивают некоторый пошаговый процесс достижения цели. Они "ведут" пользователя шаг за шагом, пока конечная цель не будет достигнута. На каждом шаге, пользователь в процессе диалога с Мастером, задает информацию, необходимую для перехода к следующему шагу.


    Как правило, "хороший" Мастер облегчает и контролирует ввод данных, предоставляет необходимые справки, позволяет провести откат к предыдущему шагу, отменив ранее сделанные установки. Мастера широко используются в Office 2000 для решения тех или иных частных задач. Это средство становится привычным для любых офисных приложений. Мастера, которые решают общие задачи и могут быть полезными в разных документах, оформляются как компоненты.
  • AddIns являются компонентами, специфическими для того или иного приложения Office 2000. Один AddIn может использоваться, например, только в документах Word, другой - в документах Excel.
  • Com AddIns - то компоненты, которые могут использоваться в разных приложениях. Достаточно, чтобы эти приложения допускали использование Com - объектов, построенных на основе Com - модели компонентного программирования. Com AddIns отличается от ActiveX объектов тем, что с программной точки зрения представляет DLL - динамически загружаемую библиотеку, в то время как ActiveX - это исполняемые файлы. Заметим, что ранее эти компоненты не могли быть созданы в офисной среде, необходимо было использовать для их создания VC++, VB или другие языки программирования, допускающие создание подобных компонент. Теперь и в Office 2000 Developer включены средства, допускающие их разработку.


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

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

    Проектирование <=> Разработка <=> Отладка <=> Развертывание и Сопровождение <=> Модификация


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

  • Уделите проектированию самое пристальное внимание. Успех дела во многом определяется первым этапом. Нет смысла торопиться с переходом на последующие этапы, пока не составлены ясные и четкие спецификации. Ошибки этого этапа самые дорогие и плохо исправляемые. Для большого проекта, который ведет коллектив исполнителей, должны быть предусмотрены специальные средства ведения проекта.
  • Еще одно важное правило этапа проектирования. Помните о тех, для кого разрабатывается программный продукт. Идите "в люди", чтобы понять, что нужно делать. Даже студент, выполняющий учебную работу должен знать вкусы преподавателя, которому он собирается эту работу сдавать. Что уж говорить о серьезных проектах.
  • Не начинайте разработку "с нуля". Офисная среда в этом отношении уникальна, она предоставляет разработчику уже готовые каркасы документов, если хотите, в принудительном порядке. Кроме того, Вы легко можете использовать в новых разработках, как свой собственный инструментарий, так и средства специальных фирм, производящих в большом числе различные шаблоны, Мастеров, AddIns, DLL, ActiveX и Com AddIns компоненты. Работая над одним проектом, думайте о будущем, - создавайте компоненты, допускающие их использование в других проектах.
  • Следующий важный этап - это отладка. Человек (коллектив), занимающийся отладкой, это оппонент разработчика. Даже, если всю работу ведет один человек, на этапе отладки он должен стать другим, поменять цель, - стараться разрушить проект, найти ситуации, в которых проект ведет себя не так, как это предусмотрено замыслами разработчика. Помните, не бывает проектов без ошибок и каждая последняя найденная ошибка является предпоследней.
  • Говоря об отладке, следует обратить Ваше внимание на то, что, зачастую, не так важно исправить найденную ошибку, как точно ее специфицировать. Знаете ли Вы "Закон Чечако", который гласит, что у "новичков" любая система работает неправильно, - или зависает или дает неверные результаты.


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

    Замечание:

    Хочу привести одно из свежих впечатлений о процессе отладки. Будучи бета - тестером Office 2000, я поразился, сколь мощной является построенная Microsoft система отладки своих продуктов. Специальный узел в Интернете для тестеров, конференции для них, удобный для тестеров способ публикации своих отчетов о найденных ошибках, практически моментальная реакция разработчиков на каждую из найденных ошибок.

  • Говоря о последующих этапах, замечу только, что в Office 2000 входят специальные средства для создания собственного Setup' а, что Администратору Office 2000 предоставляются специальные средства для развертывания Office 2000 в сети. В общем, сопровождение обеспечивает долгую жизнь программному продукту, так что этим этапам необходимо уделять немалое внимание. Ну и, конечно, особый вопрос это обеспечение безопасности, надежности и защиты данных от несанкционированного доступа.


  • Система документов и ее проект

    До сих пор мы говорили о проекте одного документа. Давайте немного расширим рамки и поговорим о совокупности или системе документов, о том, как связаны между собой проекты такой системы.
    Оставим пока в стороне создание и использование AddIns, Com AddIns, DLL , ActiveX, Internet документов. Им будет посвящен особый разговор. Пока поговорим о более понятных случаях использования совокупности документов. Прежде всего, заметим, что корневой объект Application любого из приложения Office 2000 содержит коллекцию документов. Так что изначально предполагается возможность открытия в одном приложении нескольких документов и совместной работы с ними. Но совместная работа не ограничивается однотипными документами, принадлежащими одному приложению. Уже в первой лекции мы подробно, с примерами рассмотрели возможность создания и одновременной работы с несколькими объектами Application. Следовательно, всегда есть возможность открытия совокупности документов разного типа и совместной работы с ними, предоставляя пользователю возможность переключаться по ходу дела от одного документа к другому.
    Совместная работа с несколькими документами, возможно, разного типа типична для Office 2000. Программным проектом системы документов будем называть совокупность программных проектов всех документов, входящих в систему.
    Остается ответить на главный вопрос, - могут ли взаимодействовать между собой проекты отдельных документов. Можно ли из одного проекта вызывать процедуры стандартного модуля другого проекта, можно ли пользоваться объектами класса другого проекта? Можно ли иметь глобальные переменные уровня системы документов для передачи информации от одного документа к другому? На все эти вопросы существует один ответ, - Да! С программным проектом системы документов в Office 2000 достаточно просто работать, достаточно просто организовать нужные связи между программными проектами отдельных документов, нужно только уметь это делать.

    Система документов One - Two - Three

    Рассмотрим теперь систему документов из тех же трех книг BookOne, BookTwo, BookThree.xls, проекты которых связаны ссылками. Наша основная цель, - продемонстрировать на примере работу трех проектов с общей информацией и общими методами, вызываемыми во всех проектах.
    Вначале несколько слов об интерфейсе нашей системы документов. Мы сделали его достаточно простым. На первом рабочем листе каждой из трех книг расположены три командные кнопки: ChooseBook, ChangeGlobal, PrintGlobal.
    Система документов One - Two - Three
    Рис. 2.7.  Командные кнопки управления системой документов
    Когда пользователь щелкает первую из этих кнопок, соответствующий обработчик, открывает диалоговое окно, позволяет выбрать нужную книгу и активизирует ее. Вторая из кнопок отвечает за работу с общей информацией. В каждом из документов общая информация изменяется соответствующим образом. Обработчик третьей кнопки демонстрирует работу с общими методами. Перейдем теперь к деталям организации совместной работы трех документов и их проектов. Мы начали с того, что проектам дали имена, совпадающие с именами книг. Проект книги BookOne является в нашей системе основным, - он хранит глобальную информацию, общие методы и на него ссылаются остальные проекты системы. Глобальную информацию будем представлять тремя общими переменными, расположив их в стандартном модуле ModuleOne проекта BookOne:
    Option Explicit 'Глобальные переменные системы документов One - Two - Three Public One As String, Two As String, Three As String
    Три обработчика события Click для трех командных кнопок вызывают три общих метода ChooseBook, ChangeGlobal, PrintGlobal:
    Private Sub CommandButton5_Click() ChooseBook End Sub
    Private Sub CommandButton4_Click() ChangeGlobal End Sub
    Private Sub CommandButton3_Click() PrintGlobal End Sub
    Назначение процедуры ChooseBook и ее текст уже приведены. Заметим только, что эта общая процедура вызывается соответствующими обработчиками во всех трех модулях. Мы поместили ее не в стандартный модуль проекта BookOne, а в модуль, связанный с листом Sheet1 книги BookOne.
    Это, однако, ничуть не мешает ее вызову из других проектов. Процедура ChangeGlobal, помещена в стандартный модуль с именем ModuleOne. Она позволяет работать с глобальными переменными, и поскольку в каждом проекте работа с ними выполняется по-своему, то она не является общей процедурой. В каждом проекте есть свой вариант этой процедуры, который и вызывается обработчиком Click командной кнопки ChangeGlobal. Заметим, что когда речь идет об основном проекте, где описаны глобальные переменные, при обращении к ним не требуется использовать полные имена:

    Public Sub ChangeGlobal() One = "My value is One " Two = "My value is One " Three = "My value is One " End Sub

    В стандартный модуль ModuleOne мы поместили и процедуру PrintGlobal. Текст ее тоже уже приведен. Заметим, что в каждом проекте будет свой вариант этой процедуры, но в каждом из них на последнем этапе работы будет вызываться процедура PrintGlobal проекта BookOne. В стандартном модуле находится функция Plus1, которая будет вызываться из других проектов. Это очень простая функция, но она выполняет свою важную роль, демонстрируя передачу информации от проекта к проекту через аппарат формальных - фактических параметров процедур и функций. Вот ее текст:

    Public Function Plus1(ByVal X As Integer) As Integer Plus1 = X + 1 End Function

    Проект BookTwo ссылается на проект BookOne, использует его общие переменные и вызывает его методы - ChooseBook, PrintGlobal, Plus1. Он является хранителем общей информации для проектов BookTwo и BookThree:

    Option Explicit 'Глобальная переменная документов BookTwo, BookThree Public TwoThree As String

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

    Пример 2.4.

    (html, txt)

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


    Вот его тексты:

    Private Sub CommandButton1_Click() BookOne.Sheet1.ChooseBook End Sub

    Public Sub ChangeGlobal() ' Изменение значений глобальных переменных системы BookOne.ModuleOne.Three = "My value is Three " BookTwo.ModuleTwo.TwoThree = "My value: Two-Three" End Sub

    Public Sub PrintGlobal() Dim Number As Integer Debug.Print ("Работает процедура PrintThree") Number = BookOne.ModuleOne.Plus1(2) Debug.Print ("Это книга " & Number) ChangeGlobal BookTwo.ModuleTwo.PrintGlobal End Sub

    Приведем результаты отладочной печати, полученные при нажатии кнопки PrintGlobal в книге BookThree:

    Работает процедура PrintThree Это книга 3 Работает процедура PrintTwo Печать глобальных переменных My value: Two-Three Это книга 2 Работает процедура PrintOne Печать глобальных переменных My value is One -My value is Two -My value is Three Допустимы ссылки на ячейки различных рабочих книг! BookOne BookTwo BookThree

    В заключение приведем вид окна проектов, где отображены все проекты нашей системы.

    Система документов One - Two - Three
    увеличить изображение
    Рис. 2.8.  Три проекта One - Two - Three

    Стандартные модули

    Модули этого типа не создаются по умолчанию. Их создает программист явным образом. Каково их назначение? Можно ли без них обойтись? Сколько таких модулей следует создавать? Вот типичные вопросы, стоящие перед программистом в процессе работы над программным проектом. Постараемся сейчас на них ответить.
    Стандартные модули - это основной вид модулей. Большую часть всех процедур и функций, которые пишет программист, следует размещать в стандартных модулях. Разумно иметь не один большой стандартный модуль, а некоторое множество относительно небольших модулей. В один модуль следует помещать набор функций, совместно вызывающих друг друга, связанных общей темой, общей функциональной направленностью. В модулях первого типа, связанных с объектами, следует помещать только те процедуры и функции, которые непосредственно обрабатывают события. Все общие процедуры следует выносить в стандартные модули. Вот три основные причины, по которым следует создавать небольшие стандартные модули:
  • Эффективность. В соответствии с принятой по умолчанию стратегией вычислений в VBA - стратегией компиляции "по требованию" (on demand) при вызове какой-либо процедуры модуля происходит его трансляция и трансляция всех зависимых модулей, содержащих процедуры, вызываемые по ходу исполнения первоначально вызванной процедуры. Именно поэтому следует создавать небольшие модули, помещая в них процедуры так, чтобы было как можно меньше перекрестных межмодульных вызовов.
    Замечание:
    Опцию "On demand" можно включить или выключить на вкладке General в меню VBE Tools|Options. Если временная эффективность критична для Вас, и Вам требуется уменьшить время работы, то следует провести соответствующие эксперименты с опциями компиляции на этой вкладке, а также подобрать наилучшую структуру модулей проекта.
  • Переиспользование (Reusability). Если стандартный модуль содержит процедуры общего назначения, то велика вероятность его дальнейшего использования и в других документах. Заметьте, что стандартные модули легко экспортировать и импортировать.
    Стандартный модуль VBA легко встроить не только в любой другой документ Office 2000, но и в чистый VB, поскольку экспорт - импорт модулей между проектами VBA и VB происходит также как и внутри VBA.
  • Понимание и Читабельность. Небольшие модули, решающие определенную задачу, гораздо легче подаются чтению с пониманием смысла данных и процедур их обработки. Конечно, для достижения этой важной цели следует использовать и другие известные приемы - мнемонические имена переменных, комментарии, структуризацию текста модуля.


  • Итак, подводя итог, заметим, что разумно иметь набор стандартных модулей, располагая в них все содержательные процедуры обработки данных документа. И, конечно, если таких процедур много, то модули должны быть специализированными, объединяя под одной крышей данные и функционально связанные с ними процедуры и функции. Хотя все модули имеют одинаковый уровень иерархии, реально всегда выделяется головной модуль, в котором следует сосредоточить описание глобальных переменных, используемых всеми модулями документа. Во многих ситуациях стандартный модуль следует оформить как модуль класса.

    Структура модуля. Окно проекта и Окно кода

    Модули VBA имеют очень простую синтаксическую структуру. В отличие от большинства языков программирования здесь нет большого числа разделов, нет разделения на интерфейс и реализацию. Все сделано предельно просто. Каждый модуль вне зависимости от его типа имеет всего два раздела:
  • Раздел объявлений переменных уровня модуля. Этот раздел идет первым и автоматически отделяется чертой от раздела методов. Всегда можно добавить новое объявление переменной в этот раздел. Область действия таких переменных распространяется на весь модуль, но она может быть и расширена. Подробно об этом мы поговорим чуть позже.
  • Раздел методов модуля. В этом разделе располагаются процедуры и функции. С точки зрения синтаксиса ничего другого кроме процедур и функций в этом разделе быть не может. Конечно, есть, в том числе, и синтаксическая разница между макросом, методом - обработчиком события и, например, методом, представляющим процедуру с параметрами общего назначения. Тем не менее, метод это всегда либо процедура (Sub) либо функция (Function).

  • Но коль скоро зашла речь о классификации методов, то давайте уточним определения некоторых, часто встречающихся понятий. Начнем с понятия "макрос". Под макросом мы будем понимать любую процедуру без параметров. Отсутствие параметров это главный отличительный признак макроса. Чаще всего, о макросах идет речь в следующих контекстах:
  • Макрос - это результат работы Macrorecoder.
  • Макрос - это обработчик события элементов командной панели - команд меню, командных кнопок, всего того, что входит в коллекцию элементов объекта CommandBar.
  • Особый случай составляют макросы Access. Наше определение к ним не относится, и мы всегда будем специально оговариваться, когда речь будет идти о таких макросах.

  • Элементы командной панели имеют по существу одно событие и один метод, вызываемый в ответ на появление события. Этот метод, обрабатывающий событие, мы называем макросом. Но есть и другие объекты, у которых много событий и соответственно много методов, вызываемых в ответ на возникновение того или иного события. Такие методы мы называем обработчиками событий. Обработчики событий, как правило, макросы, но иногда они могут иметь параметры. Есть еще одно существенное ограничение, накладываемое на правило построения имени обработчика. Имя является двойным и составляется из имени объекта и имени события. Два имени разделяются (объединяются) знаком подчеркивания.

    Свойства проекта

    Проект имеет несколько терминальных свойств, которые можно задать в окне "Project Properties" (Свойства Проекта), оно выводится после щелчка правой кнопкой мыши по названию проекта или одной из его папок в окне проектов и последующего выбора из контекстного меню команды Project Properties.
    Свойства проекта
    Рис. 2.2.  Окно свойств проекта
    В окне две вкладки General и Protection. На вкладке General задается имя проекта, его описание, указываются файлы справки и константы условной компиляции, относящиеся ко всему проекту. На рисунке можно видеть, как заполнены некоторые из полей, задающие свойства проекта.

    Защита проекта

    Вкладка Protection позволяет защитить проект от просмотра и редактирования. В большинстве случаев при передаче документа пользователям, проект должен быть защищен как от несанкционированного просмотра его модулей, так и коррекции программного текста. Включив флажок "Lock project for viewing", Вы закрываете проект, его структура будет недоступна для просмотра, если неизвестен пароль. Мы описывали такую ситуацию ранее, когда говорили о проекте Solver, связанном с Решателем, вызываемым в Excel. Для всех нас, конечных пользователей, отсутствует возможность просмотреть реализацию модулей Решателя и тем более пытаться корректировать тексты. Чтобы получить доступ к закрытым проектам, нужно знать пароль проекта. Этот пароль и его подтверждение задается в соответствующих окнах вкладки Protection.

    Основы офисного программирования и язык VBA

    Что можно делать с записями?

    Итак, пусть определен пользовательский тип T и объявлены переменные этого типа, например X и Y. Что можно с ними сделать? Допустимо ли присвоение (X=Y) или сравнение (If (X=Y) Then Fun), допустимы ли операции над записями? Ответ: нет, за одним исключением - присвоение допустимо, все остальное нет, в том числе и сравнение. Так что можно написать:
    Петров = Козлов
    Но нельзя написать:
    If (Петров = Козлов) Then Debug.Print "Записи идентичны"
    Отсутствие разрешенных операций над записями не означает, что с ними нельзя работать. Главное, что определен прямой доступ к полям записи, и этого достаточно, - с полями можно работать, как с переменными. Наш пример (Sub UserType) демонстрирует работу с полями записей.

    Динамические массивы

    Динамические массивы VBA это мощное средство, отсутствующее в таких известных языках как, например, С++ и Паскаль. Массив считается динамическим , если при первоначальном объявлении не указывается его размерность, но она может быть определена и переопределена в последующем оператором ReDim. Размерность определяется динамически в той процедуре и в тот момент, когда она становится фактически известной. Обратите внимание, в этом операторе границы изменения индексов можно задать не только как константы, но и как выражения, зависимые от переменных.
    Если затем нужно изменить границы или размерность массива, Вы можете снова задать оператор переопределения ReDim и начать новый цикл работы с массивом. И еще одна "приятность" - можно сохранить все ранее полученные элементы и расширить массив, добавив новые элементы. Для этого надо просто задать ключевое слово Preserve при переопределении массива.
    Рассмотрим пример. На уровне модуля объявим глобальный динамический массив Vector:
    'Объявление динамического массива Public Vector() As Integer
    В момент объявления размерность динамического массива не указывается, соответственно не выделяется память. Все это произойдет позже, в процессе выполнения программы. Вот одна из возможных процедур, работающая с этим массивом Vector. Его размерность в момент работы с ним определяется в диалоге с пользователем. По ходу дела массив расширяется, сохраняя старые значения.
    Public Sub DynArray() 'Определяется фактическая размерность массива Vector Dim N As Byte, I As Byte N = InputBox("Введите число элементов вектора") ReDim Vector(1 To N) For I = 1 To N Vector(I) = 2 * I + 1 Next I ' Массив расширяется с сохранением ранее вычисленных элементов ReDim Preserve Vector(1 To 2 * N + 1) For I = N + 1 To 2 * N + 1 Vector(I) = 2 * I Next I 'А теперь печать Debug.Print "Элементы Vector:" & Chr(13) For I = 1 To 2 * N + 1 Debug.Print Vector(I) Next I End Sub
    При печати элементов этого массива будут напечатаны, как нетрудно понять вначале нечетные числа от 3 до 21, а затем четные от 22 до 42.
    Динамические массивы с успехом можно применять там, где необходимы динамические структуры данных, например списки, стеки, очереди.

    Константы

    Одними переменными не обойтись, - нужны и константы. Для каждого простого типа есть соответствующие ему константы. Синтаксис построения констант задается так, чтобы по ее значению однозначно можно было приписать ей тип. Поэтому в отличие от переменных константы могут не объявляться, не иметь имени и появляться там, где надо, заданные своими значениями.
    Но в VBA можно объявлять именованные константы, задавая в момент объявления значение константы и, возможно, ее тип. Вообще объявление константы во многом напоминает объявление переменной. Однако в этот момент задается значение, которое уже нельзя изменить. Рассмотрим синтаксис оператора Const, используемого
    [Public | Private] Const <имя константы> [As type] = <константное выражение>
    Вот пример определения классической константы:
    Public Const pi As Double = 3.141593
    Как и переменные, именованные константы можно объявлять на уровне процедуры или модуля. В первом случае используется только ключевое слово Const, во втором - дополнительно можно задать спецификаторы Public или Private, позволяющие объявить константу общей для всех модулей или закрытой. По умолчанию глобальные константы имеют статус Private. У локальных констант, объявленных в процедурах, областью видимости является процедура. Если не определить тип константы, он будет восстановлен по значению константного выражения. Но иногда лучше это делать самому.
    Встроенных констант огромное количество. Есть, например, встроенные константы, связанные с тем или иным приложением, например, Excel или Word. Обычно они имеют соответствующий префикс (xl, wd и т. д.). Но есть и встроенные константы самого VBA. Они позволяют задавать тип календаря, дни недели, цвет, клавиши и могут применяться в конкретной ситуации, скажем, при работе с окном сообщений. Большинство таких констант начинается с префикса vb, например:
    vbWhite, vbSunday, vbKeyEscape, vbOKOnly.

    Массивы

    Простейший и самый распространенный структурный тип - массив - упорядоченная совокупность данных одного типа. Порядок на элементах массива задается индексами его элементов. В VBA массивы могут быть одномерными и многомерными. Удивительно, сколь высока допустимая размерность массива, - число измерений доходит до 60. За всю свою многолетнюю практику не припомню случая использования более чем трехмерных массивов.
    Конечно, все, что было сказано о глобальных и локальных переменных, общих и закрытых, двух уровней объявления, - относится и к массивам. Появляется только единственное дополнение, связанное с необходимостью указать размерность массива и границы изменения индексов. Поэтому синтаксис объявления массивов несколько расширен, - после имени переменной в круглых скобках указывается список размерностей массива:
    {Dim | Private | Public | Static }<имя переменной> (<список размерностей>) [ As <имя типа>]
    Синтаксически каждое измерение в списке отделяется запятой и определяется заданием нижней и верхней границы изменения индексов. По историческим причинам в Бейсике нижняя граница была фиксирована и равна 0. Затем ввели специальную опцию OptionBase, позволяющую устанавливать эту границу равную 1 или 0. Подумав еще, разрешили задавать нижнюю и верхнюю границу, причем и та и другая могут быть выражениями при одном ограничении - это должны быть константные выражения, не содержащие переменных. Вот "классическое" объявление одномерного массива и работа с ним:
    Public Sub MyArray() Const LowBound As Integer = -5, HighBound As Integer = 5 Dim MyArr(LowBound To HighBound) As Byte Dim I As Integer Debug.Print "Элементы массива MyArr:" For I = LowBound To HighBound MyArr(I) = I + 6 Debug.Print MyArr(I) Next I End Sub
    При печати элементов этого массива будут напечатаны числа от 1 до 11 по числу его элементов.
    При объявлении массива всегда задавайте нижнюю границу, исходите из того, что граничная пара, задающая размерность, должна удовлетворять синтаксису:

    <граничная пара> ::= <константное выражение> To <константное выражение>

    Не используйте Option Base. Это ненадежное средство.

    Программист Бэдли (не "наш" программист), зная, что массивы положено объявлять, дал следующее объявление:

    Dim BadArr(10)

    Запись получилась короткой, он написал ее быстро и был горд собой. Могу подтвердить, Бэдли действительно быстро пишет программы, жаль только, что они не всегда правильно работают. Вот еще один пример его работы:

    'Option Base 0 Option Base 1 Dim Vector(10) Dim A1(5), A2(5) As Integer 'Бэдли добавил новое объявление массива Matrix 'При работе с матрицей ему удобно было, чтобы индексы начинались с 1, 'поэтому он изменил опцию Base, отменив предыдущее соглашение 'Ранее работавшая процедура War перестала работать Dim Matrix(10, 20) As Double

    Public Sub War() Dim i For i = 0 To 5 A1(i) = i: A2(i) = i + 5 Vector(i) = A1(i): Vector(i + 5) = A2(i) Next i End Sub

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

    Объявление переменных и констант простых типов

    В своем развитии Visual Basic прошел сложный путь - от бестипового языка с примитивным способом объявления переменных к структурированному языку с хорошо определенными типами данных и структурами управления. Но рудименты продолжают жить, и потому в языке одновременно существуют разные по идеологии способы определения данных. В частности, переменные можно объявлять или не объявлять, и тогда тип ей будет присвоен по умолчанию или по первой букве имени или по специальному символу объявления типа, которым может заканчиваться имя. Явно объявить переменную можно как в начале блока, так и в том произвольном месте, где возникла необходимость использовать новую переменную. Всеми этими рудиментами пользоваться не следует. Если в начало модуля вставить оператор Option Explicit (Опция "Явно"), то явное объявление переменных в этом модуле становится обязательным. Мы рекомендуем придерживаться следующей стратегии:
    Всегда используйте явное объявление типа. Всегда помещайте объявления переменных в начало блока. Включите в Редакторе VBA в меню Tools на вкладке Editor флажок Require Variable Declaration. В этом случае во всех модулях Ваших проектов будет автоматически появляться оператор Option Explicit. Тем самым, Вы принуждаете себя следить за соблюдением правил хорошего тона в программировании.
    При объявлении переменной определяется ее тип и область видимости - область, где имя переменной видимо и, значит, возможен доступ к ее значению. Важно понимать, что переменные можно объявлять на двух уровнях - уровне процедуры и уровне модуля . Для объявления переменных используются операторы Dim, Public, Private и Static. Первый можно использовать на обоих уровнях, Public и Private - на уровне модуля, Static - только на уровне процедуры.
    Переменные, объявленные на уровне процедуры, называются локальными по отношению к данной процедуре. Их областью видимости является только та процедура, в которой они объявлены. Переменные уровня модуля являются глобальными . Они объявляются в разделе Declarations, который есть у каждого модуля.
    Область видимости глобальных переменных может распространяться:

  • на все процедуры одного модуля, в котором они объявлены; такие глобальные переменные, называемые закрытыми ( Private), должны быть объявлены на уровне модуля либо оператором Private либо оператором Dim;
  • на весь программный проект - все процедуры всех модулей данного проекта; такие глобальные переменные, называемые открытыми ( Public), должны быть объявлены оператором Public.


  • Локальные переменные уровня процедуры могут быть объявлены оператором Static, что делает их статическими. У таких переменных увеличивается время жизни. Обычные локальные переменные рождаются при входе в процедуру, видимы только в ней и "умирают", выходя из процедуры. Это значит, что память под переменные отводится при входе в процедуру, а при выходе она освобождается. Область видимости статических переменных по-прежнему - процедура, но время жизни иное, так как у них не отбирается память при выходе, - она просто становится временно недоступной. Поэтому при повторном входе в процедуру статические переменные восстанавливают те значения, что у них были при последнем выходе. Статические переменные - это хранители информации между многократными вызовами одной и той же процедуры. Чтобы статические переменные имели смысл, необходима первоначальная инициализация переменных, - они должны иметь хоть какие-то значения уже при первом вхождении в процедуру. Вот как VBA инициализирует переменные в момент их объявления:

  • 0 - для численных значений;
  • пустая строка ("") - для строк переменной длины;
  • строка, содержащая нули, - для строк фиксированной длины;
  • Empty (значение, указывающее на отсутствие инициализации) - для типа Variant;
  • для массивов и записей (типа, определенного программистом), каждый элемент инициализируется в соответствии с указанными правилами.


  • Объявления по умолчанию

    Настоятельно рекомендуем: не используйте средств, о которых мы сейчас расскажем. Мы и приводим их в первую очередь для того, чтобы дать подобный совет. Все необъявленные явно переменные по умолчанию считаются объявленными и имеют тип Variant. В VBA есть старомодные средства Бэйсика, позволяющие не объявлять переменную явно, но устанавливать ее тип по первому или последнему символу имени переменной. Имена переменных VBA могут заканчиваться специальным символом, позволяющим установить тип этой переменной:
  • % - Integer;
  • & - Long;
  • ! - Single;
  • # - Double;
  • @ - Currency;
  • $ - String.

  • Есть еще одна возможность определения типа по первой букве имени. С этой целью в язык введена серия операторов DefType (по одному на каждый тип DefBool, DefInt и т. д.), определяющих свой диапазон букв для каждого типа. Если первая буква имени необъявленной переменной входит в тот или иной диапазон, ей приписывается соответствующий тип. Эти операторы устанавливаются на уровне модуля и действуют на все его процедуры.
    Концевой символ установления типа сильнее, чем DefType, а тот сильнее стандартного "умолчания" Variant. Но все они - архаизмы, не приличествующие современному стилю программирования.

    Правила именования

    Есть несколько простых правил, которые следует выполнять, чтобы быть цивилизованным программистом. Заметьте, можно быть умным, грамотным, но хорошо бы еще быть и цивилизованным. Многим из нас, я это отношу и к себе, не хватает цивилизации. Правила, о которых мы сейчас поговорим, касаются оформления текста программ. Большинство из них достаточно просто и по ходу дела мы о них неоднократно говорили. Вот основные:
  • Используйте комментарии.
  • Соблюдайте правила именования.
  • Структурируйте текст.
  • Стройте программы из модулей "подъемного" размера.

  • Сейчас мы поговорим об одном техническом, но практически важном вопросе, - как правильно давать имена константам, переменным и другим объектам в наших программах. Казалось бы, какая разница, - "хоть горшком называй, только в печь не сажай". Это, однако, не так. Имя играет большую роль в понимании программ, и я верю даже в некоторую мистику, с плохо названной переменной всегда случаются какие то истории.
    Правило написания имен переменных, которому мы обычно следовали в своих примерах и повседневной практике, состоит в следующем: имя должно отражать содержательный смысл, и состоит из одного или нескольких слитно написанных слов, каждое из которых начинается с большой буквы.
    Разработчики от Microsoft рекомендуют придерживаться более строгих правил. Имя должно отражать не только смысл, но и тип переменной и ее область действия. Поэтому имя должно состоять из префикса и собственного имени. Префикс также является составным, две его части отражают область действия и тип переменной. В идущих ниже таблицах показаны возможные значения префикса:

    Таблица 3.2. Префикс, задающий область действия Первая часть префиксаОбласть действия
    g Global - Весь проект
    m Module - Для Private переменных модуля отсутствует
    p Procedure - Для локальных переменных

    Таблица 3.3. Префикс, задающий тип переменнойВторая часть префиксаТип переменной
    str String
    int Integer
    byt Byte
    lng Long
    sng Single
    dbl Double
    cur Currency
    var Variant
    obj Object
    bln Boolean


    Вот несколько примеров правильно построенных имен: gstrOneWord, mintNumberOne, strAnswer, curSalary. Также как по значению константы можно восстановить ее тип, по правильно построенным именам можно однозначно восстановить их объявление. Сделаем это в нашем примере:

    Public gstrOneWord As String Private mintNumberOne As Integer Dim strAnswer As String Dim curSalary As Currency

    Согласно этим же рекомендациям имена констант следует строить только из заглавных букв. Если имя константы состоит из нескольких слов, то для их объединения используется знак подчеркивания, например: MY_DIRECTORY_PATH. Обратите внимание, при построении констант Office 2000 используется префикс, указывающий, какому из приложений принадлежит константа.

    Таблица 3.4. Префиксы констант Office 2000 ПриложениеПрефикс констант
    Accessac
    Excelxl
    FrontPagefp
    Officemso
    OfficeBinderbind
    Outlook ol
    Power Point pp
    Wordwd
    VBAvb
    Префиксы следует использовать и при именовании объектов, для форм обычно используется префикс frm, для командных кнопок - cmd и так далее. Следует понимать, что, если уж пользоваться префиксами, то обще употребительными, не следует заниматься самодеятельностью в этом вопросе. Мы закончим призывом, - "будьте цивилизованными".

    Объявление переменных. Вызов процедуры Start модуля Father:

                                                               

    Правильное объявление массива. Работа с ним:

                                                               

    Option Explicit Public Mx As

    ' Option Explicit Public Mx As Byte Private My As Integer
    Public Sub Mother1() 'Объявление статической переменной Static Count As Byte Count = Count + 1 Mx = Mx - 2: Fz = My + 2 Debug.Print "Mother: Статическая переменная Count =", Count 'Вызов процедуры Father другого модуля или заключительной - Finish If Fx < Mx Then Father1 Else Finish End Sub
    Public Sub Finish() 'Заключительная печать Debug.Print "Finish: Fx = ", Fx, "Fy =", Fy, "Fz =", Fz Debug.Print "Mx =", Mx, "My =", My, "Mz =", Mz 'Объявления разных типов и печать значений, полученных при объявлении Dim B As Byte, I As Integer, L As Long Dim Sng As Single, D As Double, C As Currency Dim SF As String * 7, SV As String, Dat As Date Dim O As Object, V Debug.Print "B =", B, "I=", I, "L=", L Debug.Print "Sng =", Sng, "D =", D; "C=", C Debug.Print "SF =", SF, "SV =", SV, "Dat=", Dat If O Is Nothing Then Debug.Print "Объект не определен" If V = Empty Then Debug.Print "Variant переменные не инициализированы" End Sub
    Пример 3.1.
    Закрыть окно




    'Option Explicit
    Public Mx As Byte
    Private My As Integer
    Public Sub Mother1()
    'Объявление статической переменной
    Static Count As Byte
    Count = Count + 1
    Mx = Mx - 2: Fz = My + 2
    Debug.Print "Mother: Статическая переменная Count =", Count
    ' Вызов процедуры Father другого модуля или заключительной - Finish
    If Fx < Mx Then Father1 Else Finish
    End Sub
    Public Sub Finish()
    'Заключительная печать
    Debug.Print "Finish: Fx = ", Fx, "Fy =", Fy, "Fz =", Fz
    Debug.Print "Mx =", Mx, "My =", My, "Mz =", Mz
    'Объявления разных типов и печать значений, полученных при объявлении
    Dim B As Byte, I As Integer, L As Long
    Dim Sng As Single, D As Double, C As Currency
    Dim SF As String * 7, SV As String, Dat As Date
    Dim O As Object, V
    Debug.Print "B =", B, "I=", I, "L=", L
    Debug.Print "Sng =", Sng, "D =", D; "C=", C
    Debug.Print "SF =", SF, "SV =", SV, "Dat=", Dat
    If O Is Nothing Then Debug.Print "Объект не определен"
    If V = Empty Then Debug.Print "Variant переменные не инициализированы"
    End Sub

    Работа с динамическим массивом:

                                                               

    Определение и работа с записями:

                                                               

    Простые типы данных.

    Как и всякий порядочный язык, VBA содержит все привычные встроенные простые типы данных: логические, арифметические и строковые:

    Таблица 3.1. Система простых типов языка VBAИмя типаВозможные значенияТребуемая память
    BooleanTrue, False2 байта
    Byte0…2551 байт
    Integer-32768 …+327672 байта
    LongПримерно: -2000 000 000…+2000 000 0004 байта
    DecimalПримерно 30 десятичных цифр. Можно указать число цифр после десятичной точки.12 байтов
    Single- 3,4E38 …-1,4 E-45 для отрицательных значений 1,4E-45 … 3,4E38 для положительных значений4 байта
    Double-1,7E308 … -4,9E-324 для отрицательных значений4,9E-324 … 1,7E308 для положительных значений8 байтов
    CurrencyДесятичные числа с фиксированной позицией запятой. Возможны 15 цифр до запятой и 4 после.8 байтов
    StringЕсть два вида строк: Строки фиксированной длины имеют до 216 символов. Строки переменной длины имеют до 231 символов.10 байтов +1 байт на символ в обычной кодировке и 2 байта в кодировке Unicode
    DateДаты изменяются в диапазоне от 1 января 100 г. до 31 декабря 9999 г.8 байтов
    ObjectСсылка на объект (указатель)4 байта
    VariantУниверсальный тип, значением которого могут быть данные любого из перечисленных выше типов, объекты, значения NULL и значения ошибок ERRORЗависит от контекста, но не менее 16 байтов

    Как видите, в VBA имеется не меньше простых типов, чем в других известных языках программирования. Сделаем несколько замечаний. Переменные типа Decimal нельзя объявлять так, как переменные других типов, - например, оператором Dim. Этот тип является одним из вариантов типа Variant и для его задания используется функция CDec. Тип Currency используется при денежных расчетах.
    Замечание:
    Иногда, тип Currency используют вместо вещественного типа Single для ускорения расчетов. Вычисления над данными этого типа идут быстрее, чем в случае, когда для проведения вычислений приходится привлекать сопроцессор.
    Особо стоит сказать о типе Variant. Такой универсальный тип позволяет превратить язык в бестиповый,- все данные могут иметь один тип (Variant).
    Конечно, это удобно (думать не надо!), а иногда и полезно, но чревато неприятными ошибками, да и память расходуется не эффективно. VBA провоцирует чрезмерное использование этого типа, так как позволяет, объявляя переменную, не указывать ее тип, и тогда по умолчанию он устанавливается как Variant. Правильно всегда объявлять тип переменной, например, пишите Byte, если Вы знаете, что возможные значения не выходят из этого диапазона. Контроль типов поможет избежать ошибок при выходе значения за возможные пределы. Особого внимания требует работа с массивами, - плата за неэффективное использование памяти здесь может быть чрезмерно высока.

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

  • Empty - переменная не была инициализирована;
  • NULL - данные ошибочны;
  • ERROR - значение содержит код ошибки, который может быть использован для ее обработки в программе;
  • Nothing - переменная типа Object ни на что не ссылается: связь между ней и конкретным объектом прервана или не установлена.


  • Раздел Declare

    Этот раздел появляется в тех случаях, когда модули проекта используют динамически присоединяемые библиотеки - DLL. Если DLL имеет библиотеку типов TypeLib и она доступна проекту, то нет необходимости описывать компоненты библиотеки, они будут найдены автоматически. Но если TypeLib недоступна или не определена, то в этой ситуации каждая из функций и процедур библиотеки, вызываемая в модуле, должна быть предварительно описана специальным оператором Declare. Вот его синтаксис:
    [Public | Private] Declare {Sub | Function} имя Lib "имя_библиотеки" _ [Alias "псевдоним"] [([параметры])] [As возвращаемый_тип]
    Здесь указывается имя библиотеки, имя процедуры или функции, возможный псевдоним, параметры и возвращаемое значение для функций. Подробно о DLL, операторе Declare с примерами на эту тему будет рассказано в одной из лекций этой книги.

    Раздел объявлений

    Итак, еще раз напомним, что объявления можно давать на двух уровнях - модуля и процедуры. На уровне модуля раздел объявлений идет первым и автоматически отделяется чертой от раздела методов. На уровне процедуры объявления и операторы могут быть перемешаны, требуется лишь, чтобы объявление переменной предшествовало ее использованию. Хорошим тоном считается и в процедурах иметь два четко выделенных раздела и все объявления размещать в начале процедуры, так чтобы они предшествовали исполняемой части процедуры - разделу операторов.
    В классически надежном языке программирования Паскаль, созданном Никласом Виртом, раздел объявлений имел четкую структуру и в свою очередь подразделялся на разделы, содержащие объявления констант, типов, процедур, переменных. В языке VBA такого формального разделения нет, поэтому возможно, что объявление переменных пользовательского типа T предшествует объявлению самого типа. Опять-таки наша рекомендация состоит в том, что программист должен самостоятельно поддерживать структуру раздела объявлений и разделять объявления констант, типов и переменных. Давайте четко выделим основные части раздела объявлений:
  • Раздел опций.
  • Раздел констант.
  • Раздел типов.
  • Раздел переменных.
  • Раздел Declare.


  • Раздел опций

    Опции являются указаниями для транслятора. Они могут задаваться только на уровне модуля и должны начинать раздел объявлений. Заметьте, это не пожелание, а синтаксическое требование. Опции задаются ключевым словом Option, после которого идет имя опции и возможно параметры. Часть из возможных опций мы уже рассмотрели по ходу дела. Сейчас же перечислим их состав полностью:
  • Explicit - Об этой опции мы уже подробно говорили. При ее указании транслятор требует, чтобы все переменные модуля были явно описаны. Правильно включить эту опцию раз и навсегда в опциях Редактора VBA.
  • Base - Эта опция имеет два значения: 0 и 1, указывающие нижнюю границу индекса массивов, задаваемую по умолчанию. Правильно не пользоваться этой опцией, а самому и всегда указывать нижнюю границу. К сожалению, нет флажка, который бы заставлял нас следовать этому разумному правилу.
  • Private - Эту опцию, достаточно поместить в один из модулей проекта, обычно, в главный модуль проекта, который неявно всегда выделяется программистом. При ее задании проект делается закрытым и недоступен для других проектов в системе документов.
  • Compare - Опция говорит транслятору, как он должен выполнять сравнение строк в процедурах модуля. Параметр опции может принимать одно из трех возможных значений:
    {Binary | Text | DataBase}

  • По умолчанию VBA применяет метод Binary, при котором строки сравниваются по внутренним кодам соответствующих символов. В Windows порядок сортировки определяется кодовой страницей. Вот часть типичного такого порядка:
    A < Z < a < z z < А < Я < а < я
    Метод сравнения Text основан на сравнении, не чувствительном к регистру, так что при сравнении заглавные и строчные буквы не различаются. Для тех же символов порядок при этом сравнении будет такой:
    (A = a) < (Z = z) (Z = z) < (А = а) < (Я = я)
    Метод DataBase допустим лишь при работе с Access. Сравнение при этом базируется на порядке, задаваемом локализацией той БД, для которой проводится сравнение.

    Разделы констант, типов и переменных

    О том, как объявляются константы, типы и переменные мы говорили достаточно подробно. Сейчас же отметим, что разумно создавать такие разделы явно, отделяя комментариями один раздел от другого. Желательно, чтобы эти разделы шли в описанном нами порядке. Тогда уже объявленные константы будут появляться при описании границ массивов, типы переменных будут предшествовать объявлению самих переменных.
    Для констант и типов ключевые слова Const и Type однозначно определяют описываемый объект. Для переменных ключевые слова могут быть разными. Заметим, что для переменных уровня модуля не целесообразно использовать ключевое слово Dim, лучше использовать спецификаторы Public или Private, явно указывающие область действия переменной.

    Синтаксис объявления простых переменных

    Объявление простых переменных имеет следующий синтаксис:
    {Dim | Private | Public | Static }<имя переменной> [ As <имя типа>] [, <имя переменной> [ As <имя типа>]]…
    Вначале идет имя оператора, а потом список объявлений переменных, где роль разделителя играет запятая. Каждое объявление связывает имя переменной с ее типом, заданным конструкцией As. Будьте внимательны, VBA неприятно отличается в этом вопросе от других языков программирования. Здесь, как обычно, одним оператором можно объявить произвольное число переменных, но следует в каждом объявлении указывать конструкцию As, иначе переменным без As будет приписан тип Variant. На наш взгляд, то, что одним As нельзя объявить список переменных одного типа, - некий синтаксический "прокол", приводящий, если не к серьезным ошибкам, то к излишнему и не предполагаемому употреблению типа Variant.
    Заметьте, есть и приятное отклонение, - имена переменных в VBA можно задать, используя русский алфавит.
    Приведем пример, где действуют модули Father и Mother, в каждом из которых объявлены глобальные (общие и закрытые) переменные. В каждом из модулей объявлены две процедуры, взаимно вызывающие друг друга. Отладочная печать в процедурах позволит проследить за изменением значений глобальных, локальных и статических переменных. Вот тексты модулей Father и Mother - объявления глобальных переменных и процедур:
    'Option Explicit Public Fx As Byte, Fz As Integer Private Fy As Integer
    Public Sub Start() 'Инициализация глобальных переменных Fx = 10: Fy = 11: Fz = 12 Mx = 20: My = 21: Mz = 22 Father1 End Sub
    Public Sub Father1() Dim Fz As Byte 'Локальная переменная Fx = Fx + 2 Fy = Mx - 2 Fz = 1 Debug.Print "Father1: Fx=", Fx, " Fy =", Fy, "Fz =", Fz 'Вызов процедуры другого модуля Mother1 End Sub
    Здесь мы приводим тексты модуля Mother:
    Пример 3.1.
    (html, txt)
    Запустив процедуру Start модуля Father, мы получили такие результаты отладочной печати:
    Father1: Fx= 12 Fy = 18 Fz = 1 Mother: Статическая переменная Count = 1 Father1: Fx= 14 Fy = 16 Fz = 1 Mother: Статическая переменная Count = 2 Father1: Fx= 16 Fy = 14 Fz = 1 Mother: Статическая переменная Count = 3 Finish: Fx =16 Fy = Fz = 2 Mx = 14 My = 0 Mz = B = 0 I= 0 L= 0 Sng = 0 D = 0 C= 0 SF = SV = Dat= 0:00:00 Объект не определен Variant переменные не инициализированы

    Дадим краткий комментарий:
  • Процедуры Father и Mother трижды взаимно вызывают друг друга. Статическая переменная Count подсчитывает число сделанных обращений.
  • Процедура Father печатает значения глобальных переменных Fx, Fy и локальной переменной Fz. Заметьте, локальное объявление "сильнее" глобального.
  • Процедура Finish печатает заключительные значения переменных Fx, Fy, Fz, Mx, My и Mz. Здесь печатается значение глобальной переменной Fz, а значения двух переменных - Fy и Mz - не определены. Дело в том, что Fy - закрытая переменная модуля Father, а Mz вообще не объявлялась. И все же ошибки не возникает, так как действуют объявления по умолчанию и обе эти переменные считаются объявленными в модуле Mother и по умолчанию имеют тип Variant. Именно здесь кроется причина возможных ошибок, во избежание которых мы и советовали включить оператор Option Excplicit. Будь это сделано, - появились бы предупреждающие сообщения. У нас эти опции написаны, но специально закомментированы для демонстрации эффекта их отсутствия. Если их включить, то ошибка сразу встретится в процедуре Start -- ведь переменные My и Mz не определены в модуле Father.
  • Вторая часть процедуры Finish носит самостоятельный характер, - она демонстрирует объявления всех простых типов и печать значений, получаемых переменными при объявлении. Заметьте, как проверяется неопределенность объекта и пустота значений переменных.


  • Типы данных

    Одни из первых вопросов, возникающих при изучении языка программирования: "Как в нем устроена система типов данных? Какие есть простые типы, как создаются сложные, структурные типы, есть ли возможность определения собственных типов, динамических типов, можно ли в нем определять классы - "настоящие" типы, где определяется не только область возможных значений и структура данных, но и операции над ними?" На эти вопросы применительно к VBA мы и постараемся ответить в этой лекции.
    Вначале договоримся о терминах. Когда мы говорим, что Т - это тип данных, то понимаем, что определение типа Т задает:
  • область возможных значений типа;
  • структуру организации данных;
  • операции, определенные над данными этого типа.

  • Исторически сложилось так, что, при определении типа опускают операции, разрешенные над ним, подразумевая их неявно. Например, определяя тип Integer, говорят, что он задает целые числа в некотором диапазоне. Конечно, где-то позже обязательно будет сказано, какие операции разрешены над целыми. В связи с развитием объектно-ориентированного программирования определение типа стали давать "по-настоящему" и включать в него все три компонента. Правда, теперь такие типы называются классами. Но для нас "класс" - синоним понятия "тип данных".
    Типы данных принято разделять на простые и сложные в зависимости от того, как устроены их данные. У простых (скалярных) типов возможные значения данных едины и неделимы. Сложные типы характеризуются способом структуризации данных, - одно значение сложного типа состоит из множества значений данных, организующих сложный тип.
    Есть и другие критерии классификации типов. Так, типы разделяются на встроенные типы и типы, определенные программистом (пользователем) . Встроенные типы изначально принадлежат языку программирования и составляют его базис. В основе системы типов любого языка программирования всегда лежит базисная система типов, встроенных в язык. На основе встроенных типов программист может строить собственные, им определенные типы данных.
    Но способы (правила) создания таких типов являются базисными, встроенными в язык.

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

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

    Замечание:

    Среди языков программирования выделяются два крайних случая, - строго типизированные языки и бестиповые языки. В первом случае, каждая переменная имеет строго фиксированный в момент объявления тип и он не может изменяться в процессе выполнения программы. Такие языки являются более надежными, поскольку обеспечивают жесткий контроль типов и позволяют обнаруживать ошибки еще на стадии компиляции программы. Классическим примером является язык Паскаль. Бестиповые языки это скорее языки с одним единственным типом. В таких языках одна и та же переменная по ходу программы может хранить данные фактически разных типов. Тип Variant языка VBA является примером такого обобщающего типа. Бестиповые языки обеспечивают более быстрое исполнение программ, предоставляют программистам большую гибкость, но плата за это высока, она достигается это за счет надежности программ. Одним из первых бестиповых языков был Lisp, где единственным типом данных был список. "Свежим" примером бестипового языка является HTML. "Хорошие" языки программирования, как, например, VBA допускают бестиповость в ограниченных пределах.

    Записи и тип, определенный программистом

    Наряду с массивами, представляющими объединение элементов одного типа, существует еще один способ создания сложного типа - запись (в языке Паскаль), или структура (в языке С). Запись представляет объединение элементов, каждый из которых может иметь свой тип. Элементы записи часто называют ее полями. С объектных позиций запись - частный случай класса, в котором заданы только свойства и не определяются методы и события. Это важный частный случай, и поэтому в "хорошем" языке должна быть возможность определения записей. Чтобы с записями было удобно работать, в языке надо предусмотреть средства для определения пользовательского типа. Тогда можно отделить процесс определения структуры записи - назвать это определением пользовательского типа, скажем Т, а затем в нужном месте и в нужном количестве объявлять переменные типа Т, представляющие собой конкретные записи.
    Определение каждой записи можно рассматривать, как определение дерева, с корнем которого связана сама запись, а с вершинами - потомками связаны поля этой записи. Поскольку каждое поле может быть элементом произвольного типа, в том числе записью, такие вершины имеют потомков. Благодаря вложенности запись может задавать структуру сколь угодно сложно построенного дерева, у каждой вершины которого может быть произвольное число потомков. Это статическая структура, полностью определяемая в момент объявления записи.
    Что же мы имеем в VBA? Формально здесь нет понятия запись или структура, или какого-либо эквивалента. Здесь есть только тип, определенный программистом. Но поскольку он позволяет определять запись, ничего больше и не нужно. Так что записи в VBA есть, но называются они типом, определенным программистом.
    При определении типа программист дает ему имя, что позволяет затем определять переменные этого типа обычным способом. Поскольку все элементы совокупности именованы, возможен прямой доступ по имени к каждому из элементов. Ниже мы будем называть переменные такого типа записями, а элементы записи - полями.
    Такая терминология общепринята и соответствует программистской традиции.

    Синтаксис определения типа в VBA достаточно прозрачен:

    [Private | Public] Type <имя типа> <имя элемента> [([<размерность массива>])] As <тип элемента> [<имя элемента> [([<размерность массива>])] As <тип элемента>] ... End Type

    Определение типа дается на уровне модуля и, если оно является закрытым (Private), распространяется на один модуль, а для общих (Public) типов - на все. Мы ограничимся довольно стандартным примером, в котором определяются два типа (записи): Fam и Person, где одно из полей Person имеет тип Fam.

    'Определение записей - пользовательских типов Type Fam firstName As String lastName As String End Type

    Type Person Fio As Fam Birthdate As Date End Type

    Эти объявления мы поместили в раздел объявлений модуля Father. Вот одна из его процедур, использующих эти типы данных:

    Public Sub UserType() Dim Петров As Person Dim Козлов As Person Петров.Fio.firstName = "Петр" Петров.Fio.lastName = "Петров" Петров.Birthdate = #1/23/1961# Козлов.Fio.firstName = Петров.Fio.firstName Козлов.Fio.lastName = "Козлов" Козлов.Birthdate = #7/21/1966# MsgBox (Петров.Fio.firstName & " " & Петров.Fio.lastName _ & " родился " & Петров.Birthdate) MsgBox (Козлов.Fio.firstName & " " & Козлов.Fio.lastName _ & " родился " & Козлов.Birthdate) End Sub

    Вот что будет напечатано в результате ее работы:

    Петр Петров родился 23.01.61 Петр Козлов родился 21.07.66

    VBA старается помочь при работе с записями, предоставляя возможность выбора полей записи из открывающегося списка. Пусть Вас не смущает форма записи дат рождения - при определении константы для дат можно задавать в более человечной форме. Например, здесь мы задавали их как #January 23, 1961# и #21 July, 66#, - они автоматически были преобразованы в формальный вид.

    Основы офисного программирования и язык VBA

    ActiveX-объекты

    Технология 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-объект этого класса уже создан, и тогда возвращается ссылка на активный объект заданного класса. Вот пример, в котором читается книга Excel, используя метод 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

    Что нового в классах "Office 2000"

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

  • Все эти возможности будут подробно рассмотрены в этой и следующей лекции. А начнем мы с примера создания собственного класса.

    Два конструктора класса Rational

    Вернемся теперь к нашему классу и определим для него конструктор по умолчанию и "настоящий" свой конструктор:
    Пример 4.2.
    (html, txt)
    Обратите внимание, конструктор по умолчанию, являясь обработчиком события Initialize, строился, как обычный обработчик события, используя стандартную заготовку. Он, естественно, является закрытым, поскольку вызывается только системой и не доступен для обычного вызова. Наш собственный обработчик открыт. Он выполняет довольно сложную работу и не сводится к простому присвоению свойств объекта. Это довольно типичная ситуация. По ходу дела пришлось написать вспомогательную функцию вычисления наибольшего общего делителя. Поскольку она носит служебный характер, то она закрыта и не доступна для вызова при работе с объектами класса Rational.

    Еще раз о понятии "класс"

    Класс является обобщением понятия типа данных и задает свойства и поведение объектов класса - экземпляров класса. Каждый объект принадлежит некоторому классу. Отношение между объектом и его классом такое же, как между переменной и ее типом. Класс - это объединение данных и обрабатывающих их процедур и функций. Данные называются также переменными класса, а процедуры и функции - методами класса. Переменные определяют свойства объекта, а совокупность их значений - состояние объекта. Наряду со свойствами и методами с классом связывается еще одно понятие - события. Каждый класс имеет определенный набор событий, которые могут возникать при работе с объектами класса, - чаще всего при определенных действиях пользователя, иногда, как результат действия системы. При возникновении события, связанного с тем или иным объектом, система посылает сообщение объекту, которое может быть обработано методом - обработчиком события, специально созданным при конструировании объекта. События обеспечивают большую гибкость при работе с объектами. Методы класса выполняются одинаково для всех объектов класса, а на события каждый объект реагирует индивидуально, поскольку имеет собственный обработчик события.
    "Программирование в классах" является основным способом работы современного программирования.
    За примерами далеко ходить не нужно. Так построена операционная система Windows, - окно являются ее основным объектом. Сам Office 2000 полностью построен на классах и работе с объектами этих классов, - здесь все, начиная от приложения и кончая отдельным символом, рассматривается как объект некоторого класса. Профессиональный прикладной программист, работающий в некоторой проблемной области и решающий разнообразные задачи из этой области, как правило, начинает с создания классов, описывающих специфику данной проблемной области. Затем уже решение тех или иных специальных задач он описывает в терминах работы с объектами данной проблемной области.
    VBA позволяет программисту создавать собственные классы. Синтаксически класс представляет отдельный модуль специального вида - модуль класса. Мы постараемся сейчас детально и на примерах разобраться во всех особенностях этой важной конструкции.

    Friend методы

    Методы класса, в том числе процедуры - свойства Get, Let и Set могут иметь описатель Friend наряду с описателями Public и Private. Каждый из них задает свою область видимости метода, область программы, где метод доступен для вызова. Напомним, Private делает метод видимым только внутри класса, Public - делает метод доступным и область действия может распространяться не только на тот проект, в котором определен данный класс, но и на другие проекты. Чтобы управлять этим процессом, расширяя или сужая область видимости Public объектов, применяют специальные спецификаторы, например, опцию Option Private, которая закрывает модуль от других проектов. Методы с описателем Friend называют дружественными . Описатель Friend, действуя только на один метод, распространяет его область видимости на проект, в котором описан класс с Friend методом. Но для других проектов дружественные методы не доступны, даже в том случае, когда там доступны Public методы. Так что метод дружит только со своим проектом и не более. Кто программировал на С++ и других языках, где есть наследование классов, знаком с дружественными методами, - там они дружат с классами - потомками родительского класса, в котором задан Friend метод. Вне семейства классов дружественные методы не доступны. На VBA роль семейства играет проект.

    Где и как следует создавать обработчики событий для экземпляров класса

    Задача состоит в том, что нужно определить модуль, в котором можно объявлять объекты With Events. В окне кода этого модуля должен быть доступен список событий класса, должна быть возможность создания заготовок обработчиков событий для каждого из объектов, наполнение этих заготовок соответствующим содержанием.
    Заметьте, создание этого модуля, создание обработчиков событий не имеет непосредственного отношения к созданию класса объектов с событиями. Это другая сторона проблемы, - это проблема того, кто использует класс объектов, кто создает экземпляры этих объектов и создает обработчики событий этих конкретных объектов. Вполне возможно, что вся эта работа будет выполняться в нескольких документах, других проектах, отличных от того проекта, где создан класс объектов с событиями.
    Модуль, в котором можно объявлять объекты With Events может быть либо модулем класса, либо модулем формы, но, заметьте, это не может быть стандартный модуль. В большинстве случаев предпочтение отдается модулю класса. Позже мы остановимся подробнее на ситуации, когда объекты с событиями объявляются в модуле формы. Предположим, что для решения некоторой задачи, нам потребовалось работать с двумя конкретными объектами класса Личность, - нашим старым приятелем и знакомой молодой девушкой. Для решения задачи создадим класс, который назовем "Личности" и поместим туда объявление двух объектов MyFriendOne и MyFriendTwo. Объекты объявлены со спецификатором WithEvents, указывающим на то, что они должны реагировать на события. Как только эти объявления появятся в разделе объявлений класса, сами объекты появятся в окне кода в списке объектов. Теперь, выбрав объект из списка, можно открыть список возможных событий объекта и создать вначале заготовку обработчика событий, а потом наполнить ее содержанием.
    Где и как следует создавать обработчики событий для экземпляров класса
    увеличить изображение
    Рис. 4.5.  Создание обработчиков событий
    Заметьте, эта технология работы нам уже хорошо знакома. Она использовалась при создании объектов Application With Events.
    Однако, использование New для создания ActiveX- объектов не всегда возможно. Не все приложения допускают такой способ подключения, и не все объекты доступны, - например, недоступны внутренние ActiveX-объекты, которые совместно могут храниться в одном и том же файле. Более универсальный способ создания ActiveX-объектов - вызов специальных функций CreateObject и GetObject. Как правило, вызов этих функций помещается в правую часть оператора Set. В результате его выполнения объектное выражение возвращает ссылку на созданный объект, которая и становится значением переменной.

    Синтаксис функции CreateObject таков:

    CreateObject(<класс>)

    Параметр класс - строка, задающая класс объекта, у которой две обязательные части: "имя_приложения.имя_класса"; ее вид совпадает с видом задания класса ActiveX-объектов в операторе объявления.

    Функция GetObject имеет дополнительный параметр:

    GetObject(<путь>, <класс>)

    Параметр путь задает полный путь к файлу, содержащему ActiveX-объект. Этот объект и активизируется. Второй параметр при этом необязателен. Но если файл содержит группу ActiveX-объектов разных классов, задать его необходимо. Можно опустить и первый параметр, если ActiveX-объект этого класса уже создан, и тогда возвращается ссылка на активный объект заданного класса. Вот пример, в котором читается книга Excel, используя метод 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

    В заключение, коротко опишем всю

    В заключение, коротко опишем всю картину создания класса и работы с объектами, реагирующими на события. Прежде всего, нужно создать сам класс объектов, в котором предусмотрен набор событий, возможных для объектов класса. В методах класса следует предусмотреть возможность возбуждения событий. На следующем этапе следует создать класс, в котором объявлены объекты WithEvents. В этом классе создаются обработчики событий для каждого экземпляра класса. Кроме этих объектов - двойников создаются и "реальные" объекты, как правило, в стандартном модуле. После того, как происходит связывание двойников, объекты могут реагировать на события. Всякий раз, когда объект вызывает метод класса, в котором возбуждается событие, это приводит к тому, что вызывается обработчик события. Можно передавать и получать информацию от обработчика событий.
    Одно из возможных применений этой технологии работы состоит в том, что классы с событиями могут выступать как упаковка для элементов управления. Цель такого класса состоит в расширении свойств элемента управления и, в частности, в добавлении новых событий, на которые мог бы реагировать этот элемент управления. Надеемся, что данное описание позволит Вам самостоятельно построить такое приложение. Заметим только, что в таких случаях используется модуль формы, для объявления объектов WithEvents и создания обработчиков событий этих объектов.

    Это документ под названием DocTwo

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

    Как создаются процедуры- свойства

    В классе 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 играют важную роль в определении большинства классов. Поэтому имеет смысл разобрать их точный синтаксис.

    Как создать класс с событиями

    Первый вопрос, который необходимо решить при создании класса с событиями, лежит вне программирования, прежде всего, нужно решить, каким должен быть набор событий у объектов класса. Если с объектом связан некоторый графический образ, то ситуация более или менее стандартна, всегда разумно иметь события 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, возбуждающий событие, - это не единственно возможное место для такого оператора. Вообще, одно и то же событие может возбуждаться в разных местах. В данном случае, может быть, имело смысл написать еще и специальный метод для возбуждения события, который вызывался бы пользователем в нужный момент.

    Классы, как упаковка

    К классу Rational, создание которого еще не закончено, мы еще вернемся. Пока же продолжим разговор о процедурах - свойствах, поскольку они являются важными компонентами класса. В этом разделе пойдет речь об одном из их довольно неожиданных применений, когда они позволяют выступать классу в роли привлекательной упаковки внутренних и служебных функций. Но обо всем по порядку.
    Построим класс Plan, в котором по существу есть только одно свойство, хранящее текущий месяц. Хранится это свойство, как целое, но для внешнего мира оно выглядит как обычное имя месяца. Вот как это реализовано.
    Пример 4.3.
    (html, txt)
    Обратите внимание, работу с закрытым свойством 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 функциям. Так что, подводя итог, отметим, что иногда классы используются как упаковка, расширяющая стандартные возможности и особую роль в этом процессе играют процедуры - свойства.

    Классы, объекты With Events и обработчики событий

    Объекты Application являются наиболее известными объектами, имеющими события, но появляющимися по умолчанию как объекты без событий. С этих объектов и начнем и, поскольку технология включения событий во всех случаях одинакова, достаточно рассмотреть задачу создания обработчиков событий для объектов Application. Для того чтобы изложение сделать конкретным, будем полагать, что целью является создание обработчиков событий объекта Excel.Application. По ходу дела нам надо решить две основные задачи:
  • Создать модуль, в котором можно строить обработчики событий:
  • Создать сам объект Excel.Application With Events и связать его с текущим Excel приложением.

  • Заметьте, тут есть нюанс, - можно эту работу выполнять в самом Excel, после того, как открыта одна из его рабочих книг. Но можно выполнить эту работу и в другом приложении, например, в документе Word, что позволит обрабатывать возникающие события для всех открывающихся книг Excel, в том числе и первой. Выберем второй из этих вариантов.

    Конструкторы и деструкторы. Стандартные события

    Следующим шагом в создании класса является разработка его конструкторов и деструктора. Напомним, что новый объект - экземпляр класса создается конструкцией New, например:
    Dim MyRationalNumber As New Rational
    В этот момент:
  • создается типизированный указатель MyRationalNumber,
  • создается новый объект - экземпляр класса Rational,
  • при создании объекта вызывается конструктор по умолчанию, инициализирующий этот объект, определяющий значения его свойств,
  • указатель связывается с объектом.

  • В языках объектного программирования, как, например, в языке C++ конструкторов может быть несколько, среди которых выделяется конструктор без параметров, являющийся конструктором по умолчанию. Остальные конструкторы имеют параметры, позволяющие задать свойства объекта в момент инициализации. Важно понимать роль конструкторов особенно в ситуации, когда создается "толстый" объект. В момент создания такого объекта будет вызвана цепочка конструкторов, начиная с самого внутреннего, создающего самый внутренний объект, затем будет создан охватывающий объект, пока не будет вызван внешний конструктор, создающий объект - оболочку. В таком конструкторе приходится задавать параметры для инициализации всех внутренних объектов.
    В VBA все проще. Во многом это объясняется тем, что здесь, в отличие от многих других языков программирования, есть разумная стратегия начальной инициализации переменных, - мы о ней говорили ранее. Поэтому здесь есть только конструктор по умолчанию - конструктор без параметров, да и тот часто не определяется, полагаясь на стандартную инициализацию. Конечно же, инициализировать объект "настоящими" значениями все равно придется в какой-то момент. Поэтому всегда для класса создаются свои конструкторы, синтаксически являющиеся методами, - их может быть несколько. В классе "Личность" таким конструктором с параметрами является метод InitPerson. Заметьте, мы могли бы определить еще один конструктор, который в отличие от первого, проводил бы полную инициализацию всех свойств, включая Отчество нашей личности.
    Деструктор вызывается автоматически при уничтожении объекта. В VBA нет динамического уничтожения объекта в момент, определенный программистом, объекты уничтожаются также как и переменные, при выходе из области их действия. Поэтому деструктор, как правило, не пишется.

    Методы

    Не будем повторяться. О методах было сказано уже достаточно. Любая процедура (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. Большую часть джентльменского его набора мы уже определили. Специфика этого класса определяется операциями над рациональными числами. Ограничимся четырьмя основными операциями - сложением, вычитанием, умножением и делением. Добавим к ним еще и метод печати дробей. Вот тексты этих методов, заканчивающих определение нашего класса:
    Пример 4.5.
    (html, txt)
    Замечание:
    Внутри класса при реализации операций над рациональными числами доступ к закрытым свойствам объекта, естественно, разрешен, и можно обращаться непосредственно к переменным 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

    Модуль класса с объектом WithEvents

    Модуль, в котором будут появляться обработчики событий объекта Excel.Application, синтаксически является модулем класса, который нужно создать. Этот класс очень прост по своей структуре и при создании он содержит единственное закрытое свойство - соответствующий объект Application, объявленный со спецификатором WithEvents. Так что в нашем примере все описание создаваемого класса, который назовем, например, ExcelWithEvents сводится к одной содержательной строке:
    Option Explicit 'Класс ExcelWithEvents Private WithEvents xlApp As Excel.Application
    Как только такое свойство определено в классе, в окне кода этого модуля появляется объект xlApp, обладающий событиями, тут же можно выбрать из списка соответствующее событие, создать заготовку и затем наполнить ее содержанием. Взгляните на соответствующий рисунок:
    Модуль класса с объектом WithEvents
    увеличить изображение
    Рис. 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 присутствуют в одном экземпляре, то не будем сейчас вдаваться в эти тонкости, позже мы еще вернемся к этому вопросу.

    Объект WithEvents

    Вторая проблема решается также довольно просто. Но здесь есть одна тонкость, которую следует понимать. Реально создаются два объекта, мы называем их двойниками. Один из них это обычный объект 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.

    Объекты и переменные

    Попробуем обобщить сведения об объектах и переменных. Начнем с того, что у них общего и что их различает. Если есть описания:
    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 Личность

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

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

  • объекты, чей класс определен пользователем в одном из модулей класса; например, объекты класса Личность, созданного в этой лекции;
  • объекты родового приложения (Excel, Word, PowerPoint), которому принадлежит проект и которые доступны по умолчанию;
  • 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 для изменения области, отведенной объекту.

    Свойства класса: имя, отчество, фамилию,

    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
    Пример 4.1.
    Закрыть окно




    ' Конструкторы класса 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
    Пример 4.2.
    Закрыть окно




    'Класс 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
    Пример 4.3.
    Закрыть окно




    ' Класс Группа Личностей 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
    Пример 4.4.
    Закрыть окно




    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
    Пример 4.5.
    Закрыть окно




    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
    Пример 4.6.
    Закрыть окно




    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
    Пример 4.7.
    Закрыть окно



    работа с классом Группа. Создание группы.

                                                               

    работа с классом Rational. Вычисления над рациональными числами.

                                                               

    работа по созданию объекта Excel Application With Events

                                                                [VB1]Эта глава подробно рассказывает о классах объектов.
    [VB2]Эта глава подробно рассказывает о классах объектов.

    Процедуры - свойства

    Для того чтобы можно было получить доступ к закрытым свойствам, предусмотрены специальные процедуры - свойства:
  • 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" и так далее. Мы не станем приводить подробный пример, подобную проверку мы осуществляли при работе со свойством Отчество в процедуре WhoIs класса " Личность. В следующих двух стратегиях используется только одна из процедур пары, либо Get, либо Let(Set). Пятая стратегия предполагает, что доступ к свойствам закрыт, и процедуры - свойства Get, Let(Set) в классе вообще не объявляются.
    Обратим внимание на одну важную особенность, когда свойство имеет тип Variant, его значениями могут быть как обычные данные (числа, строки), так и объекты. В этой ситуации необходимо реализовать все три процедуры, применяя Let или Set в зависимости от того, с чем мы имеем дело - объектом или переменной.

    Реальные объекты и инициирование событий

    Остался заключительный штрих в завершение разговора о том, как создаются классы и как ведется работа с объектами, реагирующими на события. Нужно еще рассказать о создании реальных объектов, двойники которых описаны в специальном модуле класса, в нашем примере - в классе Личности. Эти реальные объекты, обычно, находятся в стандартном модуле. В какой -то момент своей жизни, как правило, в самом начале появления, они связываются с двойниками и после этого могут реагировать на события. События инициируются в процессе работы с этими объектами, при вызове некоторых из их методов. Какие из методов класса зажигают события, - этот вопрос решается при проектировании класса, а в какой момент метод будет вызываться, - это прерогатива пользователя, работающего с объектами.
    Для класса Личность мы уже описали методы, зажигающие события. Поэтому нам осталось только привести описание стандартного модуля Примеры, в котором ведется работа с двумя объектами, названными FriendOne и FriendTwo:
    Пример 4.7.
    (html, txt)
    В этом модуле объявлены два объекта класса Личность и один объект класса Личности. Две процедуры - Знакомство и CallEvents вызываются в ответ на щелчки командных кнопок в соответствующих обработчиках событий. Первой должна вызываться процедура Знакомство, где объекты FriendOne и FriendTwo получают "нормальные" значения и происходит Знакомство с ними. Важно, что здесь же происходит связывание этих объектов с их двойниками. Обратите внимание, нам понадобился объект FOne класса Личности, чтобы вызвать его метод Connect и связать эти пары объектов. В процессе работы этих модулей будет вестись диалог с пользователем. На следующих рисунках показаны сообщения, выдаваемые обработчиками событий ИзменениеФамилии и ДеньРождения.
    Реальные объекты и инициирование событий
    Рис. 4.6.  Обработчик события ИзменениеФамилии объекта One
    Реальные объекты и инициирование событий
    Рис. 4.7.  Обработчик события ИзменениеФамилии объекта 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

    Пример 4.7.

    В этом модуле объявлены два объекта класса Личность и один объект класса Личности. Две процедуры - Знакомство и CallEvents вызываются в ответ на щелчки командных кнопок в соответствующих обработчиках событий. Первой должна вызываться процедура Знакомство, где объекты FriendOne и FriendTwo получают "нормальные" значения и происходит Знакомство с ними. Важно, что здесь же происходит связывание этих объектов с их двойниками. Обратите внимание, нам понадобился объект FOne класса Личности, чтобы вызвать его метод Connect и связать эти пары объектов. В процессе работы этих модулей будет вестись диалог с пользователем. На следующих рисунках показаны сообщения, выдаваемые обработчиками событий ИзменениеФамилии и ДеньРождения.

    Реальные объекты и инициирование событий
    Рис. 4.6.  Обработчик события ИзменениеФамилии объекта One

    Реальные объекты и инициирование событий
    Рис. 4.7.  Обработчик события ИзменениеФамилии объекта 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

    Семейство классов и процедуры - свойства

    Когда создается семейство классов, без свойств - участников не обойтись. В классах семейства, основанных на механизме встраивания, обязательно есть свойства, представляющие собой объекты. Эти свойства, как и терминальные, обычно, закрываются и для работы с ними используются процедуры - свойства Property Set и Property Get. Иногда приходится передавать и дополнительные параметры при работе с ними. Рассмотрим типичный пример. Ранее мы построили класс "Личность", теперь построим класс "Группа", содержащий группу личностей. При построении этого класса ограничимся минимальными средствами, необходимыми для демонстрации работы со свойствами:
    Пример 4.4.
    (html, txt)
    Массив объектов класса "Личность" является закрытым свойством класса "Группа". Процедуры - свойства 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
    Мы уже говорили о том, что классы могут выступать в роли привлекательной упаковки при работе со служебными функциями. Такую же "упаковочную" роль они могут играть и при работе со встроенными объектами. Свойствами такого класса могут быть, например, элементы управления, - списки, кнопки. Класс - упаковка позволит обращаться к методам и свойствам объекта, может быть в более привлекательной для конечного пользователя форме. Кроме того, в классе могут быть определены и дополнительные свойства и методы, расширяющие стандартные возможности.
    Без процедур - свойств можно обойтись, заменив их обычными методами класса. Правда, такая замена приведет, обычно, к некоторой потере эффективности. Но методы являются основным способом работы с данными (свойствами), определяя поведение объектов класса.

    Индекс элемента является дополнительным параметром. Заметьте, процедура 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

    Мы уже говорили о том, что классы могут выступать в роли привлекательной упаковки при работе со служебными функциями. Такую же "упаковочную" роль они могут играть и при работе со встроенными объектами. Свойствами такого класса могут быть, например, элементы управления, - списки, кнопки. Класс - упаковка позволит обращаться к методам и свойствам объекта, может быть в более привлекательной для конечного пользователя форме. Кроме того, в классе могут быть определены и дополнительные свойства и методы, расширяющие стандартные возможности.

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

    Синтаксис Let, Get и Set

    Оператор 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 для немедленного выхода из процедуры.


  • События собственных классов

    Возможность создавать события собственных классов - это новинка Office 2000. Ранее было возможным для классов иметь только два стандартных события - Initialize и Terminate, играющих роль конструктора и деструктора по умолчанию. Теперь можно в классе создать набор событий и для каждого из экземпляров класса написать обработчики любого из этих событий. Тем самым расширяются возможности созданных Вами объектов. Это существенное продвижение в сторону классики объектного программирования. Попробуем разобраться:
  • как создать класс с событиями,
  • как зажигаются события,
  • где и как следует создавать обработчики событий для экземпляров класса,
  • какие действия конечного пользователя (системы) при работе с объектом инициируют событие,
  • как в ответ на событие посылается сообщение объекту и вызывается обработчик события.


  • В мире объектов время от

    В мире объектов время от времени происходят события. Причиной их могут быть действия пользователя или реакция системы на ранее происшедшие события. При возникновении события, объекту с ним связанному, система посылает сообщение, при получении которого вызывается специальный метод - обработчик события. Эти методы, вызываемые специальным образом, и называются событиями. Итак, каждый класс имеет свойства, методы и события. Все объекты - экземпляры класса имеют одинаковый набор свойств, методов и событий, заданных определением класса. Методы у всех экземпляров класса одинаковы. Но, заметьте, обработчики событий у каждого экземпляра собственные! Впрочем, это же касается и значений свойств, - каждый экземпляр уникален. Все командные кнопки имеют метод Delete, который всегда выполняется одинаково, удаляя кнопку. Но щелчок по каждой кнопке, вызывающий событие Click, приводит к совершенно разным результатам. В этом сила событий, - поведение объектов становится уникальным.
    VBA позволяет работать с объектами Office 2000, которым несть числа, и собственными объектами, классы которых мы умеем определять. Большинство из объектов Office 2000 не имеет событий, например, прославленный объект Range имеет множество свойств и методов, но ни одно из событий с ним не связывается. С другой стороны все объекты, предназначенные для организации интерфейса с пользователем, - формы, кнопки, списки и прочие объекты обладают набором событий. Но не только интерфейсные объекты имеют события. Объекты, задающие документы Word и Excel, страницы рабочих книг Excel, объекты Application также имеют встроенный набор событий. Для большинства из них мы подробно рассмотрели, как создаются обработчики событий для конкретных экземпляров. Напомним, есть специальный тип модулей, связанных с объектами, имеющими события, - сюда относятся модули форм, документов и другие. В окне кода этого модуля можно выбрать конкретный объект, выбрать для него возможное событие, построить загот овку обработчика события и наполнить затем ее содержанием.
    Подробнее об этом сказано в предыдущей лекции, посвященной модулям. Нам осталось рассмотреть две серьезные задачи:
  • Есть важная группа объектов Office 2000, которая обладает встроенным набором событий. Однако эти объекты могут существовать в двух ипостасях, как объекты без событий и объекты с событиями With Events. В эту группу входят, например, объекты Application. По умолчанию эти объекты появляются как объекты, не имеющие событий, поэтому у них нет специального модуля, в окне кода которого можно было бы найти объект, найти список его возможных событий, создать обработчик события. Первая задача состоит в том, чтобы для таких стандартных объектов, классы которых обладают событиями, уметь создавать объекты с событиями (With Events), создавать для них модуль, в котором будут появляться заготовки обработчиков событий.
  • Для собственных объектов нужно решить более сложную задачу, - необходимо не только уметь строить обработчики событий, но предварительно в классе определить сам набор возможных событий.

  • К решению этих двух важных задач мы сейчас и приступим.

    Сокрытие свойств

    Возникает вопрос, почему свойства объявлены с описателем Private, а не Public Заметьте, таким же образом мы описывали свойства и в классе "Личность". Такова общепринятая практика объектно-ориентированного программирования. Если бы свойства имели атрибут Public, - были бы открытыми, тогда при работе с объектом, можно было бы иметь к ним прямой доступ, как при чтении, так и записи. Но такая свобода, как правило, недопустима. Вот возможные стратегии при работе со свойствами:
  • Чтение, запись (Read - Write).
  • Чтение, запись при первом обращении (Read, Write-once).
  • Только чтение (Read-only).
  • Только запись (Write-only).
  • Ни чтение, ни запись (Not Read -Not Write), - свойство закрыто.

  • Открытость свойств не позволяет реализовать только первую из стратегий. В нашем примере с классом Rational открытость свойств недопустима, поскольку числитель и знаменатель преобразуются скрытым от внешнего пользователя способом, позволяющим всегда задавать единственного представителя рационального числа.
    При задании свойств делайте их закрытыми.
    Для доступа к закрытым свойствам и реализации перечисленных выше стратегий работы с ними предложены специальные методы - свойства Property Let, Set и Get, примеры их мы видели в классе "Личность", а подробный разговор еще предстоит.

    Создание класса "Личность"

    В предыдущей лекции о типах данных мы ввели пользовательский тип Person. Там же мы показали, что прямой доступ к полям записи позволяет программисту свободно работать с данными этого типа. И все же Person в чем-то "ущербен"... А не хватает ему собственных операций: если бы наши переменные Петров и Козлов имели тип, например, Integer, можно было бы их сравнивать, складывать и умножать, присваивать значения и многое другое. Понятно, что умножение данных типа Person вряд ли разумно, но зато для него можно определить свои операции. Вот, например, какие операции можно определить для этого типа:
  • InitPerson- инициализация полей записи;
  • PrintPerson- печать полей записи;
  • CopyPerson(Source As Person) - копирование источника (записи Source) - аналог оператора присвоения;
  • WhoIs - более специфическая операция, с определенной достоверностью определяющая пол, анализируя имя и фамилию.

  • Всякий раз при определении пользовательского типа, так или иначе, но следует определить операции над данными этого типа. Естественно собрать определение типа и операции над ним в одном месте. Такое объединение представляет уже почти полное, с точностью до событий определение типа. Для типов, определенных подобным образом, введен новый термин - класс .
    Синтаксически классы в VBA оформляются в виде модуля класса . Поэтому начинать создание класса в Редакторе Visual Basic нужно с выбора в меню Insert пункта Class Module. Этот модуль имеет такую же структуру, как и стандартный модуль, о котором мы подробно уже рассказали. Модуль состоит из двух разделов - объявлений и методов. В первом из них естественным образом описываются свойства класса, а во втором - его методы. И здесь действуют спецификаторы области действия Public и Private. Public - свойства и Public -методы составляют интерфейс класса. Только к этим свойствам и методам можно обращаться при работе с объектами класса, объявленными в других модулях, где класс является видимым.
    Мы расширили ранее созданный тип Person до класса, названного нами Личность, путем добавления операций над данными класса.
    Часть из этих операций уже упомянута, другие ясны из контекста. Нам кажется разумным привести с самого начала полное описание класса, а потом уже, когда полная картина ясна, переходить к деталям. Создав модуль класса "Личность, мы поместили туда следующий текст:

    Пример 4.1.

    (html, txt)

    Прокомментируем этот довольно длинный текст.

  • Первое, что мы сделали, - перешли на русский язык при задании имен свойств и методов.
  • С точностью до имен все свойства класса Личность совпадают с полями типа 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.1.  Отчество Анны Керн

    Создание класса
    Рис. 4.2.  Кто Анна?


    Это попытка определить пол по имени и фамилии. Применяется примитивный, но обеспечивающий высокую достоверность для русских имен и фамилий алгоритм. По ходу дела потребовалось ввести вспомогательную функцию "ПоследняяБуква".
  • Метод 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.1.  Отчество Анны Керн

    Создание класса
    Рис. 4.2.  Кто Анна?

    Стандартные события Initialize и Terminate

    Говоря о конструкторах и деструкторах, мы не сказали главного. Роль конструктора по умолчанию в классах VBA играет обработчик события Initialize. Это общее для многих объектов событие, встречается, когда объект загружается, для объектов - экземпляров классов оно возникает при создании объекта. У обработчика этого события нет параметров, поэтому он играет роль конструктора по умолчанию, не имеющего параметров. Роль деструктора играет обработчик события Terminate. Он вызывается, когда все ранее установленные ссылки на экземпляр объекта получают значение Nothing или все указатели перестают существовать, выйдя из области своего определения. Заметьте, что при ненормальном завершении программы это событие не возникает. Обработчик этого события (деструктор) пишется значительно реже, поскольку в момент его вызова объект и так корректно будет уничтожен.

    Связывание объектов

    Особенностью рассматриваемой технологии работы с объектами, реагирующими на события, является то, что существует пара объектов - двойников. Один из них, реальный объект, объявляется как объект без событий, другой, его двойник, объявляется в специальном классе с описателем With Events. Для двойника создаются обработчики событий, но, чтобы все это заработало нужным образом, эту пару объектов нужно связать законными узами брака. Так мы поступали при работе с объектами Excel.Application и Excel.Application WithEvents. Также мы поступили в процедуре Connect с двумя парами объектов - двойников: MyFriendOne - FriendOne и MyFriendTwo - FriendTwo.

    и привели полное его описание,

    Мы начали эту лекцию с примера создания класса "Личность" и привели полное его описание, что позволило целиком охватить всю картину. Теперь разберем детали построения класса. Шаг за шагом построим еще один простой класс 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 'знаменатель

    Основы офисного программирования и язык VBA

    Абстрактные классы

    Виртуальный метод называется чистым, если в классе не определена его реализация. Класс называется абстрактным, если в нем объявлен один или более чистых методов. Из этого определения следует, что абстрактный класс не полностью определен, и потому работать с объектами абстрактного класса невозможно. Это, конечно, не означает, что нельзя объявлять объекты такого класса, это делать можно и нужно. Вся прелесть в том и состоит, что, перед тем как начать работать с таким родительским объектом, он связывается с одним из своих потомков - объектом, класс которого определен полностью. Абстрактные классы - важный механизм создания семейства классов. Как правило, абстрактные классы выступают в роли прародителей семейства классов, в котором многие классы имеют подобные свойства и поведение, отличаясь рядом деталей. Абстрактный класс позволяет задать общий интерфейс семейства, предопределяя, что все потомки должны уметь реагировать на вызов общих методов, хотя эта реакция может быть и разной. Благодаря свойствам, определенных абстрактным классом, все потомки позволяют задавать значения некоторых общих свойств. Итак, назначение абстрактного класса в том, что он задает общие свойства и общие виртуальные методы, которыми должны обладать его потомки. Именно потомки будут определять реализацию чистых виртуальных методов. Но эти методы предусмотрены и запроектированы на самом верхнем уровне.

    В двух предыдущих пунктах мы

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

    Наследование интерфейсов

    В Office 2000 на классах введено отношение "наследование интерфейса" . Здесь под интерфейсом понимается совокупность всех открытых (Public) свойств и методов класса. Пусть уже создан класс A, который будем называть родительским или базовым. Тогда при создании нового класса B, который будем называть классом-потомком, можно объявить, что потомок наследует интерфейс родительского класса. Заметьте, что это отношение в отличие от "классического" наследования не транзитивно, - потомок класса B не наследует интерфейс класса A - родителя класса B. Однако допускается множественное наследование интерфейсов, потомок может иметь несколько родителей, - наследовать интерфейсы нескольких классов. Говоря о родительском классе, следует отметить три возможности:
  • Родительский класс может быть полностью абстрактным классом, то есть все его открытые методы будут чистыми, не имеющими реализации.
  • Родительский класс может быть полностью определенным классом с заданной реализацией методов.
  • Зачастую, родительский класс является абстрактным классом, где часть методов, реализация которых предполагается одинаковой для большинства классов - потомков, задана, а некоторые методы являются чистыми.

  • Синтаксически, объявить о том, что класс наследует интерфейс другого класса, достаточно просто. Для этого достаточно в объявление класса поместить одну строчку:
    Implements имя_родительского_класса
    При появлении такой строки класс- потомок наследует интерфейс родительского класса. Это означает, что будут наследоваться все открытые методы, а для каждого открытого свойства, будет наследоваться пара процедур - свойств Property Get и Property Let. Сами свойства наследоваться не будут. Как только в раздел объявлений класса вставлена строка "Implements", в окне кода этого класса появится список методов родительского класса, реализацию которых предстоит написать. Для того чтобы задать реализацию этих методов, используется привычная техника первоначального создания заготовок методов с последующим наполнением их содержательным текстом.

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

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

  • Использовать обычную схему Copy - Paste, копируя реализацию родителя.
  • Реализовать наследование типичным для Office 2000 способом путем встраивания родительского объекта в класс - потомок. Тогда можно вызывать в нужном месте нужный метод родителя. Чуть позже мы рассмотрим проект "Люди и машины", где используем этот прием.


  • Еще одна важная особенность наследования интерфейса состоит в том, что интерфейс родителя не становится интерфейсом потомка. Как следствие этого факта, отсутствие транзитивности отношения "наследование интерфейсов". Синтаксически, это определяется двумя обстоятельствами:

  • Заготовки методов, строящиеся автоматически, используют спецификатор Private. Это означает, что наследуемые методы не являются открытыми методами потомка и, следовательно, не входят в его интерфейс и не передаются далее внукам родителя.
  • Еще одна особенность состоит в том, что имена наследуемых методов содержат имя родителя. Это также означает, что наследуемый метод не становится собственным методом, который можно передать далее своим потомкам


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


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

    'Class Father 'Свойства класса Public MyProperty As String

    'Методы класса

    Public Sub MyPureMethod() 'Чистый метод End Sub

    Public Sub MyRealMethod() MsgBox ("It's the Father") End Sub

    Класс Son, наследующий интерфейс класса Father, по контракту должен реализовать две процедуры - свойства и два его метода. Мы дали только формальную реализацию, - в созданные автоматически заготовки добавили комментарии. Лишь в реализацию метода Father_MyPureMethod класса Son вставлена строка текста. Кроме того, в класс добавлен новый метод, определяющий собственный интерфейс этого класса. Вот описание этого класса:

    Пример 5.1.

    (html, txt)

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

    Приведем теперь описание класса GrandSon - потомка классов Son и Father. Заметьте, класс явно определяет обоих своих родителей. Опять-таки мы ограничились формальной реализацией, определив лишь реализацию метода Father_MyRealMethod:

    Пример 5.2.

    (html, txt)

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

    Пример 5.3.

    (html, txt)

    В этом примере объект Grand класса Father связывается поочередно с объектами класса Father, Son, GrandSon и всякий раз вызываются методы родительского класса, унаследованные потомками.Обратите внимание на конструкцию TypeOf - Is, позволяющую определить текущий тип объекта. В процессе работы этой процедуры будут открываться диалоговые окна, уведомляющие, что мы встретились с объектами Father, Son, Son of his Father, GrandSon. При печати свойств лишь однажды будет напечатано значение Flat.

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


    Опять- таки мы ограничились формальной реализацией, определив лишь реализацию метода Father_MyRealMethod:

    Option Explicit

    Implements Father Implements Son

    Private Property Let Father_MyProperty(ByVal RHS As String) 'Реализация отложена

    End Property

    Private Property Get Father_MyProperty() As String 'Реализация отложена

    End Property

    Private Sub Father_MyPureMethod() 'Реализация отложена

    End Sub

    Private Sub Father_MyRealMethod() 'Реализация отложена MsgBox ("It's the GrandSon") End Sub

    Private Sub Son_SonNewMethod() 'Реализация отложена

    End Sub

    Пример 5.2.

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

    Public Sub Family()

    Dim F As New Father, S As New Son, GS As New GrandSon Dim Grand As Father, GrandS As Son

    Set Grand = F Grand.MyProperty = "Flat" Grand.MyRealMethod Grand.MyPureMethod Debug.Print Grand.MyProperty Set Grand = S Grand.MyProperty = "Flat" Grand.MyRealMethod Grand.MyPureMethod If TypeOf Grand Is Son Then Set GrandS = Grand: GrandS.SonNewMethod End If Debug.Print Grand.MyProperty

    Set Grand = GS If TypeOf Grand Is GrandSon Then Set GrandS = Grand: GrandS.SonNewMethod End If Grand.MyProperty = "Flat" Grand.MyRealMethod Grand.MyPureMethod Debug.Print Grand.MyProperty

    End Sub

    Пример 5.3.

    В этом примере объект Grand класса Father связывается поочередно с объектами класса Father, Son, GrandSon и всякий раз вызываются методы родительского класса, унаследованные потомками. Обратите внимание на конструкцию TypeOf - Is, позволяющую определить текущий тип объекта. В процессе работы этой процедуры будут открываться диалоговые окна, уведомляющие, что мы встретились с объектами Father, Son, Son of his Father, GrandSon. При печати свойств лишь однажды будет напечатано значение Flat.

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

    Наследование

    Объектно-ориентированное программирование дает возможность не только определить класс, задающий состояние и поведение объектов, но и организовать семейство классов с помощью важного отношения между классами, называемого отношением наследования.
    Суть наследования в том, что новый класс в момент его создания можно объявить наследником одного или нескольких базовых классов. Пусть уже определен класс A, и при определении новый класс В объявлен наследником класса А. В этом случае класс В наследует все открытые для потомков свойства и поведение класса А, - наследует его интерфейс. Это означает, что в классе В автоматически определены переменные и методы класса А, задающие интерфейс.
    Класс А называется базовым (родительским) по отношению к производному (порожденному) классу В. Очень часто, говоря о наследовании, используют терминологию "родительский класс" и "класс-потомок". Ввиду транзитивности наследования (транзитивность означает, что из утверждений "А родитель В" и "В родитель С" следует "А родитель С") каждый класс может иметь множество родителей (предков) и множество потомков, среди которых выделяют "непосредственных родителей" и "непосредственных потомков". Потомок транзитивно наследует свойства и поведение всех своих предков.
    В производном классе можно определить новые свойства и новое поведение, задав новые переменные и новые методы. Более того, в производном классе можно переопределить существующий метод базового класса.
    Наследование называется множественным, если производный класс может быть наследником нескольких базовых классов, т.е. иметь более одного непосредственного родителя. При обычном (не множественном) наследовании каждый производный класс наследует свойства только одного базового класса, т.е. имеет одного непосредственного родителя.
    Отношение наследования обычно представляют в виде графа, узлы которого соответствуют классам, и из узла A в узел B ведет дуга, если класс B является непосредственным наследником класса A.
    Этот граф изображает структуру совокупности классов с точки зрения наследования. Если наследование не множественное, то соответствующий граф является деревом. В корне дерева находится прародитель - класс, для которого все остальные классы являются его потомками-наследниками.

    Вот пример семейства классов, связанных отношением наследования:

    Наследование
    Рис. 5.1.  Дерево одиночного наследования

    Множественное наследование описывается ациклическим графом:

    Наследование
    Рис. 5.2.  Ациклический граф множественного наследования.

    Мы уже говорили, что производный класс наследует состояние и поведение базового класса. Это значит, что все открытые (Public) элементы базового класса доступны в производном классе. Но главное, конечно, в том, что потомок может пойти дальше своего родителя. Производный класс расширяет свойства и поведение базового класса. В производном классе можно:

  • объявить новые переменные
  • объявить новые методы
  • переопределить (перегрузить, перекрыть) методы базового класса.


  • Переопределение метода класса A в производном классе В, называемое также перегрузкой (overloading) или перекрытием (overriding), - это определение в классе B метода с именем, которое уже является именем некоторого метода класса А.

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

    Обертывание коллекции VBA

    Обратите внимание, наш только что приведенный пример ВводСписка, иллюстрирующий часто возникающую на практике задачу, невозможно реализовать, используя встроенный класс Collection.Причина этого уже пояснялась, - коллекция хранит ссылки на созданные вне нее элементы, она не обладает собственной памятью. Поэтому пользователь сам должен заботиться о создании и хранении элементов. Конечно, хотелось бы поручить эту задачу самой коллекции, как это делается в созданном динамическом классе СписокЛичностей. С другой стороны этот класс имеет значительно менее мощные методы AddFirst и AddLast в сравнении с методом Add класса Collection. Класс СписокЛичностей не позволяет создавать словари, он значительно менее эффективен, чем класс Collection в тех случаях, когда требуется обеспечить прямой доступ к элементам списка. Возникает вопрос, нельзя ли соединить достоинства двух подходов? Оказывается, можно. Для этого применяется стандартная при работе с классами технология обертывания. Суть ее в том, что стандартный объект, например, элемент управления встраивается в собственный класс. Методы класса могут вызывать методы объекта, сохраняя тем самым мощь стандартного объекта. С другой стороны можно добавить новые методы или расширить функциональность существующих. Мы продемонстрируем эту технологию, показав как можно строить собственные коллекции для объектов любого класса. Конечно, нам не удастся построить универсальный класс, расширяющий возможности класса Collection, но нам удастся показать, что для каждого собственного класса элементов, можно построить коллекцию, обладающую всеми свойствами обычной коллекции, но, вместе с тем, хранящую элементы в самой коллекции.
    Не будем изобретать новых примеров и построим класс КоллекцияЛичностей. Этот класс будет обладать всеми стандартными свойствами и методами класса Collection, дополненными методами класса СписокЛичностей. Он будет иметь:
  • Свойство Count - возвращает число элементов коллекции. Доступно только для чтения, имеет тип возвращаемого значения Long.
  • Метод AddPerson (item, key, before, after) - добавляет элементы в коллекцию.
    Первый параметр Item является обязательным и задает добавляемый элемент. Параметр key - необязателен, он задается, когда элементу ставится в соответствие ключ. Два последних необязательных параметра уточняют позицию вставки, - задают индекс или ключ элемента, перед или после которого добавляется новый элемент. Только один из этих параметров может быть задан. Если не задан ни один, элемент добавляется в конец коллекции. Заметьте, несмотря на внешнее сходство метод AddPerson отличается от метода Add класса Collection. Во-первых, параметр Item будет иметь строго фиксированный тип Личность. Во-вторых, есть различия в реализации. Коллекция будет хранить не ссылку на элемент Item класса Личность, а копию этого элемента.
  • Метод Remove(key) - удаляет элементы коллекции. Удаляется элемент с заданным ключом: заметьте, это может быть индекс элемента. После удаления происходит перенумерация элементов и уменьшается счетчик Count.
  • Метод Item(key) - возвращает значение элемента списка с заданным ключом. И здесь в роли ключа может выступать обычный индекс элемента.
  • Метод PrintList() - печатает элементы списка в его текущем состоянии.
  • Метод PrintHystory - печатает историю создания списка, - все его элементы, даже если они и были удалены впоследствии.


  • Наряду с открытыми свойствами и методами наш класс будет содержать и закрытые свойства и методы:

  • Свойство First - указатель на начало списка, хранящего элементы коллекции.
  • Событие Initialize - конструктор по умолчанию, инициализирует список.
  • Метод AddFirst - создает копии элементов и добавляет их в список, представляющий внутреннюю память коллекции.


  • Вот полное описание класса КоллекцияЛичностей, обертывающего класс Collection. Класс сохранил все мощные свойства стандартной коллекции и, к тому же, приобрел новые привлекательные свойства:

    Пример 5.11.

    (html, txt)

    Приведем комментарии к этому тексту:

  • Прежде всего, следует обратить внимание на то, что объект Persons класса Collection встроен, как свойство, в создаваемый класс. Этот объект инициализируется конструктором по умолчанию, - обработчиком события Initialize в тот момент, когда создается сам объект класса КоллекцияЛичностей.

    Полиморфизм семейства классов

    Два фактора обеспечивают полиморфизм:
  • Присваивание снизу - вверх.
  • Механизм позднего связывания.

  • Присваивание снизу - вверх от потомков к родителям обеспечивает возможность связать указатель родительского класса с объектом, принадлежащим любому из классов потомков. И в процедуру, формальный параметр которой является объектом родительского класса, можно передать в качестве фактического параметра объект - потомок. Позднее связывание гарантирует, что будет вызван виртуальный метод нужного класса, поскольку решение о вызове метода принимается динамически в тот момент, когда ясно, объект какого класса вызвал метод. Если в тексте процедуры стоит вызов X.VirtMethod, то по этому тексту ничего нельзя сказать о результате этого вызова, поскольку по первому свойству полиморфизма с X может быть связан объект любого из классов семейства, а благодаря второму свойству будет найден виртуальный метод именно этого класса.
    Заметьте, каждое из этих свойств полезно само по себе. Даже, если бы и не было, виртуальных методов, присваивание снизу - вверх дает возможность работать с массивами, списками, коллекциями, элементами которых могут быть любые объекты семейства. Появляется возможность использовать их общие свойства, анализировать при необходимости тип и учитывать индивидуальные особенности. Именно для решения этой задачи было введено наследование интерфейсов.
    Наследование интерфейсов позволяет организовать присваивание снизу - вверх и, как следствие, позволяет работать с группой элементов, каждый из которых может принадлежать различным классам семейства. Поскольку позднее связывание и ранее было характерно для VBA, то полиморфизм в Office 2000 реализован в полном объеме.
    Итак, подводя некоторый итог, еще раз отметим, что полиморфизм обеспечивает два важных достоинства при работе с семейством классов:
  • Возможность работать с группой объектов, принадлежащих разным классам.
  • Равенство прав по вызову, означающее, что потомки могут вызывать методы родителей, но и родители могут вызывать методы потомков.


  • Одним из классических приемов построения сложных систем, широко применяемое еще в недавнем прошлом, было построение их методом "раскрутки". Суть его состоит в том, что вначале строится ядро системы, содержащее базовые методы. Затем строится следующий слой системы, методы которого могут вызывать методы ядра. Этот процесс продолжается, строятся новые слои, пока система живет и развивается. Недостаток раскрутки состоял в том, что в нем не было равенства по вызову, внешние слои могли вызывать методы внутренних слоев, а обратное не допускалось. Позже для преодоления возникающих трудностей был создан специальный механизм обратного вызова, - "callback "функции.

    В языках программирования, где есть классы и наследование, сложная система может проектироваться как семейство классов. При этом крайне полезно, если семейство обладает таким гибким механизмом как полиморфизм, обеспечивающим, в частности, равенство по вызову между предками и потомками. Для реализации сложных систем может быть очень полезным и такое свойство языка, как множественное наследование, позволяющее организовать куда более сложные связи между объектами, чем в случае одиночного наследования. Мы уже говорили, что в Office 2000 наследование интерфейсов является множественным. Это позволяет одному и тому же объекту быть участником разных групп. Так бывает и в жизни, - один и тот же человек может быть членом Союза рыболовов и охотников, но он же входит и в Союз кинематографистов, кроме того, как личность он входит и в общую группу "homo sapiens".

    Давайте перейдем от слов к делу и посмотрим, как это все работает на более или менее содержательном примере.

    s the Son of his

    'Класс Son - Наследник класса Father Implements Father
    Private Property Let Father_MyProperty(ByVal RHS As String) 'Реализация отложена
    End Property
    Private Property Get Father_MyProperty() As String 'Реализация отложена
    End Property
    Private Sub Father_MyPureMethod() MsgBox ("It's the Son")
    End Sub
    Private Sub Father_MyRealMethod() 'Реализация отложена
    End Sub
    Public Sub SonNewMethod() MsgBox ("It' s the Son of his Father") End Sub
    Пример 5.1.
    Закрыть окно




    Option Explicit
    Implements Father Implements Son
    Private Property Let Father_MyProperty( ByVal RHS As String) 'Реализация отложена
    End Property
    Private Property Get Father_MyProperty() As String 'Реализация отложена
    End Property
    Private Sub Father_MyPureMethod() 'Реализация отложена
    End Sub
    Private Sub Father_MyRealMethod() 'Реализация отложена MsgBox ("It's the GrandSon") End Sub
    Private Sub Son_SonNewMethod() 'Реализация отложена
    End Sub
    Пример 5.2.
    Закрыть окно




    Public Sub Family()
    Dim F As New Father, S As New Son, GS As New GrandSon Dim Grand As Father, GrandS As Son
    Set Grand = F Grand.MyProperty = "Flat" Grand.MyRealMethod Grand.MyPureMethod Debug.Print Grand.MyProperty Set Grand = S Grand.MyProperty = "Flat" Grand.MyRealMethod Grand.MyPureMethod If TypeOf Grand Is Son Then Set GrandS = Grand: GrandS.SonNewMethod End If Debug.Print Grand.MyProperty
    Set Grand = GS If TypeOf Grand Is GrandSon Then Set GrandS = Grand: GrandS.SonNewMethod End If Grand.MyProperty = "Flat" Grand.MyRealMethod Grand.MyPureMethod Debug.Print Grand.MyProperty
    End Sub
    Пример 5.3.
    Закрыть окно




    Option Explicit 'Класс Машина ' Свойства класса Private Марка As String Private ДатаВыпуска As Date Private Цвет As String
    'Конструкторы класса
    Private Sub Class_Initialize() Марка = "Форд" ДатаВыпуска = "20.07.1925" Цвет = "Вишневый" End Sub
    Public Sub НоваяМашина(M As String, D As Date, C As String) Марка = M ДатаВыпуска = D Цвет = C End Sub
    'Методы класса Public Sub PrintDataCar() Debug.Print "Марка = ", Марка Debug.Print "ДатаВыпуска = ", ДатаВыпуска Debug.Print "Цвет = ", Цвет
    End Sub
    Public Property Get МаркаМашины() As String МаркаМашины = Марка End Property
    Public Property Get ЦветМашины() As String ЦветМашины = Цвет End Property
    Public Property Get ДатаВыпускаМашины() As Date ДатаВыпускаМашины = ДатаВыпуска End Property
    Пример 5.4.
    Закрыть окно




    Option Explicit 'Класс ВладелецМашины 'Наследует интерфейсы классов Личность и Машина Implements Машина Implements Личность
    'Свойства класса Private Сам As Личность Private ЕгоМашина As Машина
    Private Sub Class_Initialize() Set Сам = New Личность Set ЕгоМашина = New Машина End Sub
    'Реализация интерфейсов класса Личность Private Sub Личность_CopyPerson(You As Личность) Сам.CopyPerson (You) End Sub
    Private Sub Личность_InitPerson(ByVal FN As String, ByVal LN As String, ByVal DoB As Date) 'Инициализация личности Сам.InitPerson FN, LN, DoB End Sub
    Private Sub Личность_PrintPerson() 'Печать в отладочном окне Immediate Сам.PrintPerson End Sub
    Private Sub Личность_SayWhoIs() ' Вывод сообщения о поле и возрасте владельца машины Dim StrMsg As String StrMsg = "Думаю, Владелец машины марки: " & _ ЕгоМашина.МаркаМашины & " это - " If Сам.WhoIs Then If Year(Сам.ВашаДатаРождения) > 1967 Then StrMsg = StrMsg & "молодая девушка!" Else: StrMsg = StrMsg & "женщина!" End If Else If Year(Сам.ВашаДатаРождения) > 1967 Then StrMsg = StrMsg & "молодой человек!" Else: StrMsg = StrMsg & "мужчина!" End If End If MsgBox (StrMsg) End Sub
    Private Function Личность_WhoIs() As Boolean Сам.WhoIs End Function
    Private Property Let Личность_ВашаДатаРождения(ByVal NewValue As Date) Сам.ВашаДатаРождения = NewValue End Property
    Private Property Get Личность_ВашаДатаРождения() As Date 'Зажигает событие ДеньРождения 'в зависимости от значения текущей даты Личность_ВашаДатаРождения = Сам.ВашаДатаРождения End Property
    Private Property Let Личность_ВашаФамилия(ByVal NewValue As String) 'Зажигает событие ИзменениеФамилии Сам.ВашаФамилия = NewValue End Property
    Private Property Get Личность_ВашаФамилия() As String Личность_ВашаФамилия = Сам.ВашаФамилия End Property
    Private Property Let Личность_ВашеИмя(ByVal NewValue As String) Сам.ВашеИмя = NewValue End Property
    Private Property Get Личность_ВашеИмя() As String Личность_ВашеИмя = Сам.ВашеИмя End Property
    Private Property Let Личность_ВашеОтчество(ByVal NewValue As String) Сам.ВашеОтчество = NewValue End Property
    Private Property Get Личность_ВашеОтчество() As String Личность_ВашеОтчество = Сам.ВашеОтчество End Property
    'Реализация интерфейсов класса Машина Private Property Get Машина_ДатаВыпускаМашины() As Date Машина_ДатаВыпускаМашины = ЕгоМашина.ДатаВыпускаМашины End Property
    Private Property Get Машина_МаркаМашины() As String Машина_МаркаМашины = ЕгоМашина.МаркаМашины End Property
    Private Property Get Машина_ЦветМашины() As String Машина_ЦветМашины = ЕгоМашина.ЦветМашины End Property
    Private Sub Машина_PrintDataCar() ЕгоМашина.PrintDataCar End Sub
    Private Sub Машина_НоваяМашина(M As String, D As Date, C As String) ЕгоМашина.НоваяМашина M, D, C End Sub
    'Собственный интерфейс класса ВладелецМашины 'Public методы - интерфейс Владельца машины Public Sub InitCarOwner(FN As String, LN As String, DoB As Date, _ Marka As String, DB As Date, Color As String) 'Инициализация данных о хозяине и его машине Личность_InitPerson FN, LN, DoB Машина_НоваяМашина Marka, DB, Color End Sub
    Public Sub ConnectOwnerAndCar(pers As Личность, car As Машина) 'соединяет данные о хозяине и его новой машине Сам.CopyPerson pers Машина_НоваяМашина car.МаркаМашины, car.ДатаВыпускаМашины, _ car.ЦветМашины End Sub
    Public Sub PrintOwnerData() Личность_PrintPerson Debug.Print " владеет машиной: " Машина_PrintDataCar End Sub
    Пример 5.5.
    Закрыть окно




    Option Explicit ' Модуль Примеры Public FriendOne As New Личность Public FriendTwo As New Личность Public FriendThree As New Личность Public carOne As New Машина Public carTwo As New Машина Public carThree As New Машина Public OwnerOne As New ВладелецМашины Public OwnerTwo As New ВладелецМашины Public OwnerThree As New ВладелецМашины Public FOne As New Личности
    Public Sub Люди() 'Вызывается конструктор с параметрами 'и происходит знакомство с объектами FriendOne.InitPerson FN:="Станислав", LN:="Федотов", _ DoB:="21.05.39" FriendTwo.InitPerson FN:="Катя", LN:="Павлова", _ DoB:="22.03.79" FriendThree.InitPerson FN:="Остап", LN:="Бендер", DoB:="23.07.1910" FriendOne.PrintPerson FriendTwo.PrintPerson FriendOne.SayWhoIs FriendTwo.SayWhoIs 'Связывание с двойниками. 'Теперь объекты могут реагировать на события! FOne.Connect End Sub
    Public Sub Cars() 'Вызывается конструктор с параметрами carOne.НоваяМашина "Антилопа", "12.12.12", "Неопределенный" carTwo.НоваяМашина "Москвич", "12.11.98", "Морская волна" carThree.НоваяМашина "Jeep", "23.05.97", "Orange" End Sub
    Public Sub CarOwners() OwnerOne.ConnectOwnerAndCar FriendOne, carTwo OwnerTwo.ConnectOwnerAndCar FriendThree, carOne OwnerThree.InitCarOwner FN:="Юрий", LN:="Вегера", _ DoB:="21.08.34", Marka:="Газ69", DB:="20.01.76", Color:="Зеленый" OwnerOne.PrintOwnerData OwnerTwo.PrintOwnerData OwnerThree.PrintOwnerData End Sub Public Sub CallEvents() Dim DoB As Date 'Вызов методов приведет к возникновению событий! 'При замене фамилии возникнет событие ИзменениеФамилии 'Заметьте, не всегда фамилия будет изменена! FriendOne.ВашаФамилия = "Фидотов" FriendTwo.ВашаФамилия = "Волконская" 'При попытке узнать дату рождения 'может быть вызван обработчик события ДеньРождения. DoB = FriendOne.ВашаДатаРождения Debug.Print DoB DoB = FriendTwo.ВашаДатаРождения Debug.Print DoB FriendOne.PrintPerson FriendTwo.PrintPerson
    'События не наследуются Set FriendOne = OwnerTwo 'Нельзя связать теперь объект FriendOne с двойником 'FOne.Connect FriendOne.ВашаФамилия = "Воробьянинов" FriendOne.PrintPerson
    End Sub Public Sub Группа() Const SizeGroup = 6 Const SizeGarage = 6 Dim i As Byte Dim Group(1 To SizeGroup) As Личность Dim Гараж(1 To SizeGarage) As Машина
    Set Group(1) = FriendOne Set Group(2) = FriendTwo Set Group(3) = FriendThree Set Group(4) = OwnerOne Set Group(5) = OwnerTwo Set Group(6) = OwnerThree For i = 1 To SizeGroup Group(i).SayWhoIs Next i
    Set Гараж(1) = carOne Set Гараж(2) = carTwo Set Гараж(3) = carThree Set Гараж(4) = OwnerOne Set Гараж(5) = OwnerTwo Set Гараж(6) = OwnerThree For i = 1 To SizeGarage Гараж(i).PrintDataCar Next i End Sub
    Public Sub ЛюдиИМашины() Люди Cars CarOwners Группа PolyMorf FriendTwo PolyMorf OwnerTwo End Sub
    Public Sub PolyMorf(One As Личность) One.SayWhoIs End Sub
    Пример 5.6.
    Закрыть окно




    Станислав Федотов родился 21.05.39 Катя Павлова родилась 22.03.79 Станислав Федотов родился 21.05.39 владеет машиной: Марка = Москвич ДатаВыпуска = 12.11.98 Цвет = Морская волна Остап Бендер родился 23.07.1910 владеет машиной: Марка = Антилопа ДатаВыпуска = 12.12.12 Цвет = Неопределенный Юрий Алексеевич Вегера родился 21.08.34 владеет машиной: Марка = Газ69 ДатаВыпуска = 20.01.76 Цвет = Зеленый Марка = Антилопа ДатаВыпуска = 12.12.12 Цвет = Неопределенный Марка = Москвич ДатаВыпуска = 12.11.98 Цвет = Морская волна Марка = Jeep ДатаВыпуска = 23.05.97 Цвет = Orange Марка = Москвич ДатаВыпуска = 12.11.98 Цвет = Морская волна Марка = Антилопа ДатаВыпуска = 12.12.12 Цвет = Неопределенный Марка = Газ69 ДатаВыпуска = 20.01.76 Цвет = Зеленый
    Пример 5.7.
    Закрыть окно




    Sub TestOfCollection() 'Так объявляются объекты (переменные) типа Collection Dim MyCollection As New Collection 'Объявление обычных локальных переменных Dim i As Integer Dim N As Long 'Оператор With позволяет избежать многократного указания имени объекта With MyCollection N =.Count Debug.Print" Число элементов пустой коллекции =", N ' Добавление элементов в конец списка. 'Элементы имеют индексы, но не имеют ключа. .Add (2) .Add (4) .Add (6) 'Добавление нечетных элементов на свои места. 'Заметьте, как указывается позиция 'добавления c использованием параметров - before и after ' Добавляемые элементы имеют строковый тип и обладают ключом .Add" один"," first", 1 ' before (перед первым элементом) .Add" три"," third",, 2 'after (после второго) .Add" пять"," fifth",, 4 N =.Count Debug.Print" Число элементов после 6-и вызовов метода Add", N Debug.Print" Элементы коллекции:" ' Отладочная печать созданной коллекции из шести элементов. For i = 1 To MyCollection.Count Debug.Print MyCollection(i) Next ' Удаление 4-го и 5-го элементов по заданному индексу и ключу. .Remove 4 .Remove" fifth" N =.Count Debug.Print" Число элементов после двух вызовов метода Remove=", N Debug.Print" Элементы коллекции:" 'И снова печать коллекции, в которой теперь четыре элемента. For i = 1 To MyCollection.Count Debug.Print MyCollection(i) Next End With End Sub
    Пример 5.8.
    Закрыть окно




    Public Sub Collection() 'Создание и работа с коллекцией личностей Dim Личности As New Collection 'Работа с коллекцией, как со списком Dim Адам As New Личность Адам.InitPerson "Адам", "Первый Человек", #1/1/100# Личности.Add Адам Dim Ной As New Личность Ной.InitPerson "Ной", "Праведник", #1/1/100# Личности.Add Ной 'Работа с коллекцией, как с динамическим массивом Dim Шекспир As New Личность Шекспир.InitPerson "Вильям", "Шекспир", #4/23/1564# Личности.Add Item:=Шекспир, After:=2 Dim Гомер As New Личность Гомер.InitPerson "Гомер", "Великий Слепой", #1/1/100# Личности.Add Item:=Гомер, Before:=3 Личности(4).SayWhoIs 'Работа с коллекцией, как со словарем Dim Пушкин As New Личность Пушкин.InitPerson "Александр", "Пушкин", #6/6/1799# Личности.Add Item:=Пушкин, Key:="Гений" Dim Булгаков As New Личность Булгаков.InitPerson "Михаил", "Булгаков", #1/23/1891# Личности.Add Item:=Булгаков, Key:="Мастер" Debug.Print Личности("Гений").ВашаФамилия, " - это Гений!" Debug.Print Личности("Мастер").ВашаФамилия, " - это Мастер!" 'Печать всего списка Dim I As Byte For I = 1 To Личности.Count Личности(I).PrintPerson Next I End Sub
    Пример 5.9.
    Закрыть окно




    Option Explicit 'Определение класса СписокЛичностей ' Свойства Private First As ЭлементСпискаЛичностей Private Last As ЭлементСпискаЛичностей Public Count As Integer 'Методы Private Sub Class_Initialize() Set First = Nothing Set Last = Nothing Count = 0 End Sub
    Public Sub AddFirst(F As Личность) Dim Elem As New ЭлементСпискаЛичностей Dim Info As New Личность 'Создаем копию переменной F. В списке будем использовать копию, а не ссылку. Info.CopyPerson F Set Elem.Сам = Info Set Elem.Друг = First If First Is Nothing Then Set Last = Elem End If Set First = Elem Count = Count + 1 End Sub
    Public Sub PrintList() Dim P As ЭлементСпискаЛичностей Dim Q As Личность Set P = First While Not (P Is Nothing) Set Q = P.Сам Q.PrintPerson Set P = P.Друг Wend End Sub
    Public Sub AddLast(F As Личность) Dim Elem As New ЭлементСпискаЛичностей Dim Info As New Личность 'Создаем копию переменной F. В списке будем использовать копию, а не ссылку. Info.CopyPerson F Set Elem.Сам = Info Set Elem.Друг = Nothing If First Is Nothing Then Set First = Elem Else Set Last.Друг = Elem End If Set Last = Elem Count = Count + 1 End Sub
    Public Sub ClearList() 'Попытка освободить память не достигает успеха из-за отсутствия 'соответствующего оператора. Dim P As ЭлементСпискаЛичностей, R As ЭлементСпискаЛичностей Dim Q As Личность Set P = First While Not (P Is Nothing) Set Q = P.Сам 'Unload Q Set R = P Set P = P.Друг 'Unload R Wend 'Обнуление указателей Set First = Nothing Set Last = Nothing Count = 0 End Sub
    Пример 5.10.
    Закрыть окно




    Option Explicit
    'Определение класса КоллекцияЛичностей ' Свойства Private First As ЭлементСпискаЛичностей Private Count As Long 'Обратите внимание: встраиваем коллекцию Private Persons As Collection
    'Методы Private Sub Class_Initialize() Set First = Nothing Count = 0 'при создании объекта класса КоллекцияЛичностей создается 'и внутренняя коллекция для хранения его элементов Set Persons = New Collection End Sub
    Private Sub AddFirst(F As Личность) Dim Elem As New ЭлементСпискаЛичностей Dim Info As New Личность 'Создаем копию переменной F. В списке будем использовать копию, а не ссылку. Info.CopyPerson F Set Elem.Сам = Info Set Elem.Друг = First Set First = Elem Count = Count + 1 End Sub
    Public Sub PrintList() 'Печать текущего состояния списка Dim i As Long For i = 1 To Persons.Count Persons(i).PrintPerson Next i End Sub
    Public Sub AddPerson(Item As Личность, Optional key As String = "", _ Optional before As Long = 0, Optional after As Long = 0) Dim P As Личность 'Вначале добавляем элемент в линейный список, 'используя внутренний метод AddFirst AddFirst Item
    'Разбор случаев вызова Set P = First.Сам If key <> "" Then Persons.Add P, key ElseIf before <> 0 Then Persons.Add P,, before ElseIf after <> 0 Then Persons.Add P,,, after Else Persons.Add P End If Count = Persons.Count End Sub Public Property Get Количество() As Long Количество = Count End Property
    Public Sub Remove(key As Variant) Persons.Remove key Count = Persons.Count End Sub
    Public Function Item(key As Variant) As Личность Set Item = Persons.Item(key)
    End Function
    Public Sub PrintHystory() 'Печать всех элементов в порядке, обратном их добавлению в список. 'Печатаются все элементы, независимо от того, были ли они удалены. Dim P As ЭлементСпискаЛичностей Dim Q As Личность Set P = First While Not (P Is Nothing) Set Q = P.Сам Q.PrintPerson Set P = P.Друг Wend End Sub
    Пример 5.11.
    Закрыть окно




    Public Sub Великие() 'Создание и работа с коллекцией личностей Dim Личности As New КоллекцияЛичностей Dim ЭтоЛичность As Личность 'Работа с коллекцией, как со списком Dim Адам As New Личность Адам.InitPerson "Адам", "Первый Человек", #1/1/100# Личности.AddPerson Адам, "Первый" Dim Ной As New Личность Ной.InitPerson "Ной", "Праведник", #1/1/100# Личности.AddPerson Item:=Ной, after:=1
    'Работа с коллекцией, как с динамическим массивом Dim Шекспир As New Личность Шекспир.InitPerson "Вильям", "Шекспир", #4/23/1564# Личности.AddPerson Item:=Шекспир, after:=2 Dim Гомер As New Личность Гомер.InitPerson "Гомер", "Великий Слепой", #1/1/100# Личности.AddPerson Item:=Гомер, before:=3
    'Работа с коллекцией, как со словарем Dim Булгаков As New Личность Булгаков.InitPerson "Михаил", "Булгаков", #1/23/1891# Личности.AddPerson Item:=Булгаков, key:="Мастер" Dim Пушкин As New Личность Пушкин.InitPerson "Александр", "Пушкин", #6/6/1799# Личности.AddPerson Item:=Пушкин, key:="Гений"
    'Печать всего списка Личности.PrintList Debug.Print Личности.Количество 'Удаление элементов Личности.Remove "Первый" Личности.Remove 2 'Печать после удаления Личности.PrintList Debug.Print Личности.Количество 'Доступ к отдельным элементам по ключу Set ЭтоЛичность = Личности.Item("Гений") ЭтоЛичность.PrintPerson Set ЭтоЛичность = Личности.Item(2) ЭтоЛичность.PrintPerson End Sub
    Пример 5.12.
    Закрыть окно



    Проект "Люди и Машины"

    Проектирование семейства классов, как правило, начинают с проектирования схемы, задающей иерархию и связи в семействе, определения прародителей, определяющих базовые свойства и поведение. Приведем в качестве примера семейство классов, объектами которого будут Люди и машины. Вот одна из возможных схем, определяющая классы этого семейства и отношения между ними.
    Проект
    Рис. 5.3.  Схема семейства классов "Люди и Машины"
    Мы не станем давать полную реализацию этой схемы. Для наших целей достаточно ограничиться ее частичной реализацией, где будут два родителя, имеющие общего потомка. У нас уже построен класс Личность, - пусть он и будет одним из родительских классов. Другим базовым родительским классом пусть будет класс Машина, задающий машины. Потомком этих двух родительских классов будет класс ВладелецМашины. Он наследует интерфейсы класса Личность и класса Машина. Поэтому объекты этого класса смогут входить и обрабатываться в разных группах, - в группе личностей и в группе машин. В нашем примере родительские классы не будут абстрактными. Напомним, что определенный в предыдущей лекции класс Личность является классом с событиями, так что его объекты могут реагировать на некоторые события, происходящие с ними. Мы не будем повторять здесь его описание. Класс Машина будет достаточно простым. Вот его определение:
    Пример 5.4.
    (html, txt)
    У класса Машина имеется:
  • три закрытых свойства, обладающие статусом Read only,
  • два конструктора, - закрытый конструктор по умолчанию и открытый конструктор НоваяМашина,
  • один типичный для классов открытый метод, задающий печать свойств - PrintDataCar

  • Интерфейс класса составляют два открытых метода, они и будут наследоваться классом - потомком. Класс - потомок ВладелецМашины значительно больше унаследует от своего другого родителя - класса Личность. Вот определение класса ВладелецМашины:
    Пример 5.5.
    (html, txt)
    Прокомментируем этот довольно длинный текст. Вот на какие моменты следует обратить внимание:
  • Для того, чтобы реализовать полноценное наследование свойств и поведения родителей, а не только наследование интерфейсов, используется встраивание, - в классе определены свойства Сам класса Личность и свойство ЕгоМашина класса Машина.
  • В конструкторе по умолчанию Class_Initialize, вызываемом при создании объекта класса ВладелецМашины, объекты Сам и ЕгоМашина инициализируются, - будут вызваны их конструкторы по умолчанию класса Личность и класса Машина.
  • При наследовании интерфейсов класса Личность почти для всех методов наследовалось поведение родительского класса.
    Реализуется это достаточно просто - вызывается соответствующий метод объекта Сам.
  • Для метода SayWhoIs поведение переопределено. В данном случае при вызове метод учитывает специфику класса и сообщает некоторые данные, как о владельце, так и о его машине. Так что при вызове этого метода у просто личностей и у личностей, являющихся владельцами машин, результат будет различным.
  • Поскольку родительский класс допускает события, то и у потомка при вызове методов зажигаются соответствующие события.
  • Интерфейс класса Машина реализуется подобным образом. Полностью наследуется поведение родительского класса.
  • Собственный интерфейс класса довольно прост. У объектов этого класса нет новых дополнительных свойств, - все спрятано в свойствах встроенных объектов Сам и ЕгоМашина. Интерфейс составляют два конструктора и метод, осуществляющий печать данных. Обратите внимание, один конструктор InitCarOwner позволяет сконструировать новый объект класса по терминальным данным, характеризующим личность и его машину. Второй конструктор ConnectOwnerAndCar в качестве параметров использует ранее созданные объекты классов Личность и Машина, соединяя эти два объекта в один объект класса ВладелецМашины.


  • На рисунке 5.4 отражен один из моментов проектирования класса. Можно видеть, что в окне кода список объектов содержит объекты Class, Личность и Машина. При выборе объекта Class правый раскрывающийся список покажет список стандартных событий класса, а при выборе объектов Личность и Машина будет раскрываться список наследуемых методов.

    Проект
    увеличить изображение
    Рис. 5.4.  Проектирование класса ВладелецМашины

    Определение семейства классов дано и нам осталось продемонстрировать работу с различными объектами этих классов. Главное, что хотелось показать, это совместную работу с объектами разных классов. Начнем с программного текста:

    Пример 5.6.

    (html, txt)

    Дадим теперь комментарии к этому тексту:

  • В модуле Примеры вначале объявляются по три объекта классов Личность, Машина и ВладелецМашины.


    В методах Люди, Cars, CarOwners происходит инициализация этих объектов. Некоторые из этих личностей уже встречались в предыдущих примерах, но появился Остап Бендер и его знаменитая машина.
  • Обратите внимание на создание объектов Owner класса ВладелецМашины, они создаются разными конструкторами.
  • Основную работу выполняет метод Группа. Здесь создается массив элементов Group из трех объектов Friend и трех объектов Owner, принадлежащих разным классам. Тем не менее все эти объекты обрабатываются в едином цикле и для них вызывается метод SayWhoIs, который, как мы говорили ранее по-разному работает для объектов класса Личность и класса ВладелецМашины. Так что простой цикл по элементам Group демонстрирует реализацию полиморфизма.
  • В методе Группа создана и продемонстрирована работа с еще одной группой элементов, - массивом Garage. Эту группу составляют объекты класса Машина и объекты класса ВладелецМашины. Обратите внимание, одни и те же объекты Owner входят в обе группы элементов и могут быть обработаны нужным образом.
  • В процессе обработки массива Garage вызывается метод PrintDataCar, который унаследован объектами класса ВладелецМашины.
  • В модуле Примеры приведена полиморфная функция PolyMorf(One As Личность), с формальным параметром родительского класса Личность. Она дважды вызывается в процедуре ЛюдиИМашины с фактическими параметрами разных классов.
  • Процедура ЛюдиИМашины является процедурой, организующей всю работу, она поочередно запускает все перечисленные ранее процедуры. Сама она вызывается в обработчике события Click командной кнопки, встроенной в наш тестовый документ.


  • В процессе выполнения этой процедуры откроется серия диалоговых окон, где вначале будет сказано, кто такой Федотов и Катя, будет спрошено отчество Вегеры Юрия, затем опять будет рассказано о Федотове, Кате и Бендере, после чего появится информация о владельцах машин разных марок. На последнем этапе опять будет рассказано о Кате и владельце машины "Антилопа". Нам осталось привести результаты отладочной печати, появляющиеся при работе этой процедуры:


    Пример 5.7.

    (html, txt)

    Как мы уже отмечали, родительский класс Личность реагирует на события. Это не мешает наследовать его интерфейсы, но не означает, что потомки будут наследовать события своего родителя. Чтобы убедиться, что зажигание событий не мешает работе потомков и не влияет на их работу, мы включили процедуру CallEvents. В процессе ее работы возникают события у объектов родительского класса и они правильно обрабатываются. Но, заметьте, после того как объект FriendOne был связан с объектом - потомком FriendOwner, при смене фамилии соответствующее событие уже не возникало, так что ничто не помешало Остапу Бендеру сменить свою фамилию. По ходу работы этой процедуры в появляющихся диалоговых окнах будет сообщено об отказе смены фамилии Федотову и появится поздравление Кате Павловой в связи с замужеством. Приведем результаты отладочной печати при работе этой процедуры:

    21.05.39 22.03.79 Станислав Федотов родился 21.05.39 Катя Волконская родилась 22.03.79 Остап Воробьянинов родился 23.07.1910

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


    При выборе объекта Class правый раскрывающийся список покажет список стандартных событий класса, а при выборе объектов Личность и Машина будет раскрываться список наследуемых методов.

    Проект
    увеличить изображение
    Рис. 5.4.  Проектирование класса ВладелецМашины

    Определение семейства классов дано и нам осталось продемонстрировать работу с различными объектами этих классов. Главное, что хотелось показать, это совместную работу с объектами разных классов. Начнем с программного текста:

    Option Explicit 'Модуль Примеры Public FriendOne As New Личность Public FriendTwo As New Личность Public FriendThree As New Личность Public carOne As New Машина Public carTwo As New Машина Public carThree As New Машина Public OwnerOne As New ВладелецМашины Public OwnerTwo As New ВладелецМашины Public OwnerThree As New ВладелецМашины Public FOne As New Личности

    Public Sub Люди() 'Вызывается конструктор с параметрами 'и происходит знакомство с объектами FriendOne.InitPerson FN:="Станислав", LN:="Федотов", _ DoB:="21.05.39" FriendTwo.InitPerson FN:="Катя", LN:="Павлова", _ DoB:="22.03.79" FriendThree.InitPerson FN:="Остап", LN:="Бендер", DoB:="23.07.1910" FriendOne.PrintPerson FriendTwo.PrintPerson FriendOne.SayWhoIs FriendTwo.SayWhoIs 'Связывание с двойниками. 'Теперь объекты могут реагировать на события! FOne.Connect End Sub

    Public Sub Cars() 'Вызывается конструктор с параметрами carOne.НоваяМашина "Антилопа", "12.12.12", "Неопределенный" carTwo.НоваяМашина "Москвич", "12.11.98", "Морская волна" carThree.НоваяМашина "Jeep", "23.05.97", "Orange" End Sub

    Public Sub CarOwners() OwnerOne.ConnectOwnerAndCar FriendOne, carTwo OwnerTwo.ConnectOwnerAndCar FriendThree, carOne OwnerThree.InitCarOwner FN:="Юрий", LN:="Вегера", _ DoB:="21.08.34", Marka:="Газ69", DB:="20.01.76", Color:="Зеленый" OwnerOne.PrintOwnerData OwnerTwo.PrintOwnerData OwnerThree.PrintOwnerData End Sub Public Sub CallEvents() Dim DoB As Date 'Вызов методов приведет к возникновению событий! 'При замене фамилии возникнет событие ИзменениеФамилии 'Заметьте, не всегда фамилия будет изменена! FriendOne.ВашаФамилия = "Фидотов" FriendTwo.ВашаФамилия = "Волконская" 'При попытке узнать дату рождения 'может быть вызван обработчик события ДеньРождения.


    DoB = FriendOne.ВашаДатаРождения Debug.Print DoB DoB = FriendTwo.ВашаДатаРождения Debug.Print DoB FriendOne.PrintPerson FriendTwo.PrintPerson

    ' События не наследуются Set FriendOne = OwnerTwo 'Нельзя связать теперь объект FriendOne с двойником 'FOne.Connect FriendOne.ВашаФамилия = "Воробьянинов" FriendOne.PrintPerson

    End Sub Public Sub Группа() Const SizeGroup = 6 Const SizeGarage = 6 Dim i As Byte Dim Group(1 To SizeGroup) As Личность Dim Гараж(1 To SizeGarage) As Машина

    Set Group(1) = FriendOne Set Group(2) = FriendTwo Set Group(3) = FriendThree Set Group(4) = OwnerOne Set Group(5) = OwnerTwo Set Group(6) = OwnerThree For i = 1 To SizeGroup Group(i).SayWhoIs Next i

    Set Гараж(1) = carOne Set Гараж(2) = carTwo Set Гараж(3) = carThree Set Гараж(4) = OwnerOne Set Гараж(5) = OwnerTwo Set Гараж(6) = OwnerThree For i = 1 To SizeGarage Гараж(i).PrintDataCar Next i End Sub

    Public Sub ЛюдиИМашины() Люди Cars CarOwners Группа PolyMorf FriendTwo PolyMorf OwnerTwo End Sub

    Public Sub PolyMorf(One As Личность) One.SayWhoIs End Sub

    Пример 5.6.

    Дадим теперь комментарии к этому тексту:

  • В модуле Примеры вначале объявляются по три объекта классов Личность, Машина и ВладелецМашины. В методах Люди, Cars, CarOwners происходит инициализация этих объектов. Некоторые из этих личностей уже встречались в предыдущих примерах, но появился Остап Бендер и его знаменитая машина.
  • Обратите внимание на создание объектов Owner класса ВладелецМашины, они создаются разными конструкторами.
  • Основную работу выполняет метод Группа. Здесь создается массив элементов Group из трех объектов Friend и трех объектов Owner, принадлежащих разным классам. Тем не менее все эти объекты обрабатываются в едином цикле и для них вызывается метод SayWhoIs, который, как мы говорили ранее по-разному работает для объектов класса Личность и класса ВладелецМашины. Так что простой цикл по элементам Group демонстрирует реализацию полиморфизма.
  • В методе Группа создана и продемонстрирована работа с еще одной группой элементов, - массивом Garage.


    Эту группу составляют объекты класса Машина и объекты класса ВладелецМашины. Обратите внимание, одни и те же объекты Owner входят в обе группы элементов и могут быть обработаны нужным образом.
  • В процессе обработки массива Garage вызывается метод PrintDataCar, который унаследован объектами класса ВладелецМашины.
  • В модуле Примеры приведена полиморфная функция PolyMorf(One As Личность), с формальным параметром родительского класса Личность. Она дважды вызывается в процедуре ЛюдиИМашины с фактическими параметрами разных классов.
  • Процедура ЛюдиИМашины является процедурой, организующей всю работу, она поочередно запускает все перечисленные ранее процедуры. Сама она вызывается в обработчике события Click командной кнопки, встроенной в наш тестовый документ.


  • В процессе выполнения этой процедуры откроется серия диалоговых окон, где вначале будет сказано, кто такой Федотов и Катя, будет спрошено отчество Вегеры Юрия, затем опять будет рассказано о Федотове, Кате и Бендере, после чего появится информация о владельцах машин разных марок. На последнем этапе опять будет рассказано о Кате и владельце машины "Антилопа". Нам осталось привести результаты отладочной печати, появляющиеся при работе этой процедуры:

    Станислав Федотов родился 21.05.39 Катя Павлова родилась 22.03.79 Станислав Федотов родился 21.05.39 владеет машиной: Марка = Москвич ДатаВыпуска = 12.11.98 Цвет = Морская волна Остап Бендер родился 23.07.1910 владеет машиной: Марка = Антилопа ДатаВыпуска = 12.12.12 Цвет = Неопределенный Юрий Алексеевич Вегера родился 21.08.34 владеет машиной: Марка = Газ69 ДатаВыпуска = 20.01.76 Цвет = Зеленый Марка = Антилопа ДатаВыпуска = 12.12.12 Цвет = Неопределенный Марка = Москвич ДатаВыпуска = 12.11.98 Цвет = Морская волна Марка = Jeep ДатаВыпуска = 23.05.97 Цвет = Orange Марка = Москвич ДатаВыпуска = 12.11.98 Цвет = Морская волна Марка = Антилопа ДатаВыпуска = 12.12.12 Цвет = Неопределенный Марка = Газ69 ДатаВыпуска = 20.01.76 Цвет = Зеленый

    Пример 5.7.

    Как мы уже отмечали, родительский класс Личность реагирует на события.


    Это не мешает наследовать его интерфейсы, но не означает, что потомки будут наследовать события своего родителя. Чтобы убедиться, что зажигание событий не мешает работе потомков и не влияет на их работу, мы включили процедуру CallEvents. В процессе ее работы возникают события у объектов родительского класса и они правильно обрабатываются. Но, заметьте, после того как объект FriendOne был связан с объектом - потомком FriendOwner, при смене фамилии соответствующее событие уже не возникало, так что ничто не помешало Остапу Бендеру сменить свою фамилию. По ходу работы этой процедуры в появляющихся диалоговых окнах будет сообщено об отказе смены фамилии Федотову и появится поздравление Кате Павловой в связи с замужеством. Приведем результаты отладочной печати при работе этой процедуры:

    21.05.39 22.03.79 Станислав Федотов родился 21.05.39 Катя Волконская родилась 22.03.79 Остап Воробьянинов родился 23.07.1910

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

    Семейство классов и реализация интерфейсов

    В предыдущей лекции мы уже говорили об одной из важных новинок Office 2000, направленной на превращение VBA в полноценный объектно-ориентированный язык. Возможность создания событий для классов, определенных программистом, существенно расширяет их возможности. Теперь такие классы обладают всеми тремя компонентами, присущими "настоящим" классам, - свойствами, методами и событиями, теперь и их объекты могут индивидуально реагировать на происходящие события. Сейчас мы расскажем еще об одном важном нововведении - возможности реализовать в одном классе интерфейс другого класса. Эта новинка направлена на создание полноценного семейства классов, связанного наследованием интерфейсов и реализующего полиморфизм методов семейства классов. Нужно сразу сказать, что сделано только пол шага в нужном направлении, - можно говорить о реализации полиморфизма и частичной реализации наследования в его классическом понимании. Можно ожидать развития этого направления в последующих версиях Office 2000. Но и то, что уже сделано, представляет несомненный интерес. Понимание этого материала требует от читателя знания основ ООП, и, в первую очередь, понимание наследования и полиморфизма. Минимально необходимые сведения мы сейчас приведем.

    Создание динамических структур данных

    Говоря об объектах, мы не раз отмечали, что необходимо уметь работать не только с отдельными объектами, но и группами этих объектов. Конечно, можно во многих случаях использовать массив для представления группы объектов. Однако для решения многих, возникающих в программировании задач, необходимы более гибкие динамические структуры данных, позволяющие организовать сложные связи между элементами таких структур. Примеры таких структур хорошо известны, - это, например, списки, линейные и нелинейные, стеки, деки и очереди, деревья бинарные и сбалансированные. В современных языках программирования некоторые из таких структур стали такой же частью языка, как и массивы. Реализуется такая динамическая структура чаще всего в виде класса. Прекрасным примером является реализация самой системы Office 2000. Как мы не раз говорили, Office 2000 можно рассматривать как семейство классов объектов. Так вот, почти для каждого класса объектов, существует и класс, задающий коллекцию этих объектов. Классы - коллекции составляют почти половину классов Office 2000. Все они устроены похожим образом и имеют некоторый стандартный набор методов, позволяющих удалить или добавить новый элемент в коллекцию, получить элемент из коллекции, зная его порядковый номер или ключ элемента. Некоторые коллекции имеют специфические свойства и поведение, обусловленное спецификой самих элементов, хранящихся в коллекции. В ряде случаев, коллекции могут допускать хранение элементов разных классов. Обилие коллекций в Office 2000 первоначально раздражает, поскольку хотелось бы иметь единый класс со свойствами и методами, не зависящими от природы элементов, составляющих коллекцию. Но со временем ко всему привыкаешь и начинаешь принимать как должное, что у каждого класса объектов своя коллекция. Благодаря подсказкам по ходу написания программы и легко доступной справочной системе, не требуется держать в памяти специфику каждой коллекции.
    Частью языка VBA является класс Collection. Он существенно облегчает работу с динамическими структурами данных во многих типичных ситуациях. Он позволяет хранить в коллекции данные разных типов и имеет хорошо продуманные свойства и методы. Не менее важно и то, что VBA позволяет создавать и собственные динамические структуры данных сколь угодно сложно организованные. К их рассмотрению мы сейчас и приступаем.

    Создание собственных динамических классов

    VBA допускает создание динамических структур данных: списков, стеков, очередей, деревьев. Хоть мы и говорим о динамической структуре данных, речь фактически идет о создании динамических типов данных (классов), содержащих как данные, так и операции над ними.
    Рассмотрим классический пример и создадим линейный односвязный список. Это можно делать по-разному, но одно из самых разумных решений - определить три класса, задающие
  • информационную часть элемента списка;
  • структуру элемента списка;
  • сам список, в том числе и операции, над ним определенные.

  • Для определенности рассмотрим список, хранящий информацию о "личностях". Тогда можно считать первую часть нашей задачи решенной,- класс Личность уже создан, и мы его просто используем. Теперь создадим класс с именем ЭлементСпискаЛичностей, задающий структуру элемента (запись). Этот класс прост, поскольку элементы линейного односвязного списка состоят из двух полей - информационного и поля указателя соседнего элемента. Никакие методы над этим классом обычно не определяются. Его полное описание:
    'Класс ЭлементСпискаЛичностей содержит только два поля Public Сам As Личность Public Друг As ЭлементСпискаЛичностей
    В определении класса ЭлементСпискаЛичностей свойство Друг является объектом того же класса. VBA допускает такую рекурсию в определении - без нее списковую структуру не организовать.
    Осталось определить класс СписокЛичностей, задающий сам список. Главное на этом этапе - спроектировать операции над списком. Мы ограничимся набором традиционных методов:
  • AddFirst (F As Личность) - добавляет элемент в начало списка;
  • AddLast (F As Личность) - добавляет элемент в конец списка;
  • Initialize - конструктор по умолчанию, инициализирует список;
  • PrintList() - печатает содержимое элементов списка;
  • ClearList() - очищает список.

  • Методы диктуют и состав данных (свойств) класса СписокЛичностей. Нужны минимум два указателя на начало и конец списка: First и Last. Эти свойства разумно закрыть от внешнего использования. Мы добавим к ним открытую переменную Count, следящую за числом элементов списка.
    Приведем теперь описание свойств и методов этого класса:

    Пример 5.10.

    (html, txt)

    Теперь несколько замечаний по поводу реализации методов:

  • При добавлении нового элемента в методах AddFirst и AddLast вначале создается новый пустой элемент списка; поскольку в объявлении элемента используется спецификатор New. Создается также новый элемент для информационного поля, куда копируется информация, переданная при вызове методов Add, - здесь пригодился метод CopyPerson класса Личность. Заметьте, что в отличие от класса Collection, создается копия элемента, а не используется ссылка. Поэтому состояние элементов списка не зависит от внешних изменений. Изменять содержимое элементов списка можно только с помощью методов списка. Хотя мы и не спроектировали такие методы, понятно, что это нетрудно сделать.
  • При печати списка последовательно проходятся все его элементы, для каждого из них вызывается метод PrintPerson, определенный в классе Личность.
  • Особо остановимся на методе ClearList, в котором можно установить нулевые значения указателей (Nothing), но нельзя освободить память, занятую элементами списка. Мы специально делаем такую попытку, не приводящую к успеху, чтобы заострить Ваше внимание на этом вопросе. В большинстве языков программирования, позволяющих создавать динамические структуры данных, всегда есть пара взаимосвязанных операций над памятью: "Выделить память" и "Освободить память" (New-Delete или, например, Get-Free). Операции эти выполняются динамически, и выделение и освобождение памяти производится по требованию программиста при выполнении программы. В VBA дело обстоит не так,- здесь есть New, но нет Delete. Связано это, конечно, с особенностями VBA, который, не будучи классическим языком компиляторного типа, представляет нечто среднее между компилятором и интерпретатором. Поэтому здесь освобождение памяти происходит в соответствии с обычными для переменных правилами, определяющими их время жизни. Так что в VBA программист не должен заботиться об освобождении памяти, занятой его динамическими структурами, - это делается автоматически без его участия.


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


  • Приведем теперь достаточно простой пример, позволяющий построить список в процессе диалога с пользователем и в конце распечатать данные, хранящиеся в построенном списке:

    Public Sub ВводСписка() 'Создание списка в диалоге с пользователем Dim Личности As New СписокЛичностей Dim ЭтоЛичность As New Личность Dim Имя As String, Фамилия As String, Дата As Date If MsgBox("Начнем создавать список личностей?", vbYesNo) = vbYes Then Do ЭтоЛичность.ВашеИмя = InputBox("Введите имя личности") ЭтоЛичность.ВашаФамилия = InputBox("Введите Фамилию") ЭтоЛичность.ВашаДатаРождения = InputBox("Введите дату рождения ") Личности.AddLast ЭтоЛичность Loop Until MsgBox("Продолжим создавать список личностей?", vbYesNo) = vbNo Личности.PrintList End If End Sub

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

    Виртуальные методы и полиморфизм

    Как взаимодействуют между собой объекты разных классов, связанных отношением наследования? Возможны ли взаимные присваивания между объектами родительских классов и их потомками? Ответ на второй вопрос положителен лишь "наполовину". Совместимость по присваиванию обеспечивается лишь в одну сторону - от родителей к потомкам: объекту родительского класса может быть присвоено значение объекта-потомка - обратное недопустимо. Это и понятно: ведь потомок, сохраняя все свойства родителя, может иметь и другие свойства.
    Присваивание объекта объекту производится редко. Важнее, что совместимость по присваиванию имеет место в следующих случаях:
  • совместимость между формальным и фактическим параметром процедуры или функции. Формальный параметр может иметь тип базового класса, а фактический параметр - аргумент - при вызове функции может быть объектом любого производного класса.
  • совместимость указателей. Указатель может ссылаться на базовый класс, но при конструировании объекта в операторе "new" может быть вызван конструктор производного класса, что позволяет создать объект соответствующего класса. В более общей ситуации указателю базового класса может быть присвоен указатель производного класса.

  • Ниже мы покажем, что свойство совместимости по присваиванию в сочетании с возможностью определения виртуальных методов приводит к весьма полезным следствиям.
    Семейство классов может иметь виртуальные методы. Если какой-то метод базового класса объявлен как виртуальный с атрибутом virtual, эта характеристика сохраняется для всех классов-потомков. В случае переопределения в производном классе виртуального метода должно сохраняться число параметров метода и их типы, что гарантирует одинаковую форму вызова виртуального метода как производного, так и базового класса.
    Когда создается объект, в классе которого определены виртуальные методы, конструктор класса для этого объекта заносит строку в таблицу виртуальных методов. В ней содержатся ссылки на расположение виртуальных методов данного класса на этапе выполнения приложения, что позволяет реализовать механизм "позднего связывания" и при вызове объектом своего виртуального метода динамически найти по таблице и вызвать нужный метод.

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

    Принципиально виртуальный метод от не виртуального отличается механизмом позднего связывания , применяемым для организации вызова виртуального метода. Во многих языках программирования методы могут быть как виртуальными, так и, в большинстве случаев, не виртуальными. В языке VBA это не так, здесь практически все методы виртуальные и для них применяется механизм позднего связывания. Это и понятно, поскольку VBA по существу интерпретируемый язык, а интерпретация подразумевает позднее связывание. Поэтому, чтобы Вы почувствовали разницу между виртуальными и не виртуальными методами, позвольте привести один пример на языке Visual C++. Рассмотрим следующий пример. Пусть имеются базовый класс Base и производный от него класс Derived, в которых определены виртуальный метод VirtMethod и невиртуальный метод NonVirtMethod. В базовом классе определена также полиморфная функция Test:

    class Base { public: virtual void VirtMethod (); // печатает слово" Отец" void NonVirtMethod (); // печатает слово" Мать"

    void Test () { VirtMethod (); // вызов виртуального метода NonVirtMethod (); // вызов не виртуального метода } }

    class Derived: public Base { public: virtual void VirtMethod (); // печатает слово" Сын"

    void NonVirtMethod (); // печатает слово" Дочь" }

    Когда компилятор в базовом классе Base создает код для функции Test, то, анализируя вызов не виртуального метода NonVirtMethod(), он уже на этапе компиляции создаст обращение к методу NonVirtMethod класса Base.

    Совсем иначе он поступит, анализируя вызов виртуального метода VirtMethod. Для виртуального метода на этапе компиляции нельзя определить, метод какого класса следует вызывать. Это может быть метод класса Base или класса Derived. Возможно даже, что уже после того, как функция Test будет откомпилирована, программист определит новый класс - потомок класса Derived со своим виртуальным методом VirtMethod.


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

    // При создании объекта BaseItem вызывается конструктор класса Base. Base BaseItem; // При создании объекта DerivedItem вызывается конструктор класса Derived Derived DerivedItem; // Вызов функции Test BaseItem.Test(); // Будет напечатано "Отец" и "Мать" DerivedItem.Test(); // Будет напечатано "Сын" и "Мать".

    Функция Test является примером полиморфной функции. Заметьте: она имеет по умолчанию первым параметром указатель на объект базового класса (this). Продолжим рассмотрение нашего примера и покажем, как указатели обеспечивают полиморфизм:

    // Полиморфные указатели pItem1 и pItem2 ссылаются на базовый класс Base. // Вызывается конструктор и создается динамический объект класса Base. Base* pItem1 = new Base(); // Вызывается конструктор и создается динамический объект класса Derived Base* pItem2 = new Derived();

    // Будет вызван метод класса Base и напечатано слово "Отец". pItem1 ->VirtMethod(); // Будет вызван метод класса Derived и напечатано слово "Сын". pItem2 -> VirtMethod();

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

    Встроенный динамический класс Collection

    Коллекция в VBA - упорядоченная совокупность элементов, вообще говоря, разного типа. Этим коллекция отличается от массива, где объединяются только однотипные элементы. Все элементы индексированы, но некоторые могут иметь и ключ, связанный с элементом. Размер коллекции заранее не фиксируется и может динамически изменяться. Любой существующий элемент коллекции может быть удален, при этом не возникает "дыр" - элементы перенумеровываются, и непрерывность последовательности индексов не нарушается. Неприятным следствием этого факта, приводящего иногда к ошибке, является то, что индекс в отличие от ключа не является постоянной характеристикой элемента и может изменяться в процессе работы с коллекцией. Новый элемент может быть добавлен в произвольное место коллекции, - как перед некоторым элементом, так и после любого из элементов.
    У класса Collection одно свойство - Count и 3 метода: Add, Item, Remove. Рассмотрим их подробнее:
  • Свойство Count возвращает число элементов коллекции. Доступно только для чтения, имеет тип возвращаемого значения Long.
  • Метод Add (item, key, before, after) добавляет элементы в коллекцию. Первый параметр Item является обязательным и задает добавляемый элемент. Параметр key - необязателен, он задается, когда элементу ставится в соответствие ключ. Два последних необязательных параметра уточняют позицию вставки, - задают индекс или ключ элемента, перед или после которого добавляется новый элемент. Только один из этих параметров может быть задан. Если не задан ни один, элемент добавляется в конец коллекции.
  • Метод Remove(key) удаляет элементы коллекции. Удаляется элемент с заданным ключом: заметьте, это может быть индекс элемента. После удаления происходит перенумерация элементов и уменьшается счетчик Count.
  • Метод Item(key) возвращает значение элемента списка с заданным ключом. И здесь в роли ключа может выступать обычный индекс элемента.

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

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

    Пример 5.8.

    (html, txt)

    Приведем теперь результаты отладочной печати:

    Число элементов пустой коллекции = 0 Число элементов после 6-и вызовов метода Add = 6 Элементы коллекции: один 2 три 4 пять 6 Число элементов после двух вызовов метода Remove = 4 Элементы коллекции: один 2 три 6

    Подчеркнем еще раз основные свойства класса Collection:

  • Класс позволяет объединять в коллекцию элементы разных типов, хотя чаще применяются однотипные коллекции.
  • Класс объединяет в себе свойства линейного списка, динамического массива и структуры, называемой словарем , или отображением (map).
  • Это список, поскольку определена операция Add, позволяющая динамически добавлять элементы в конец списка.
  • Это динамический массив, поскольку все элементы индексированы и к ним возможен прямой доступ по индексу. С другой стороны, размер массива не фиксируется и динамически изменяется при добавлении и удалении элементов.
  • Это словарь, поскольку добавляемые элементы могут иметь ключ.


    Прямой доступ к элементам возможен как по индексу, так и по ключу. Под "прямым" здесь понимается доступ, основанный на применении функций расстановки (хэширования). Конкретный вид этих функций, определяющий эффективность поиска, - "секрет фирмы".
  • В классе определены методы Add, Item, Remove и свойство Count.


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

    Пример 5.9.

    (html, txt)

    В процессе работы этой процедуры в диалоговом окне появится сообщение о Шекспире, а в отладочном окне Immediate появятся следующие результаты:

    Пушкин - это Гений! Булгаков - это Мастер! Адам Первый Человек родился 01.01.100 Ной Праведник родился 01.01.100 Гомер Великий Слепой родился 01.01.100 Вильям Шекспир родился 23.04.1564 Александр Пушкин родился 06.06.1799 Михаил Булгаков родился 23.01.1891

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


    Более разумно было бы использование коллекций при работе с личностями, владельцами машин и самими машинами. Построим пример, в котором действует коллекция личностей:

    Public Sub Collection() 'Создание и работа с коллекцией личностей Dim Личности As New Collection 'Работа с коллекцией, как со списком Dim Адам As New Личность Адам.InitPerson "Адам", "Первый Человек", #1/1/100# Личности.Add Адам Dim Ной As New Личность Ной.InitPerson "Ной", "Праведник", #1/1/100# Личности.Add Ной 'Работа с коллекцией, как с динамическим массивом Dim Шекспир As New Личность Шекспир.InitPerson "Вильям", "Шекспир", #4/23/1564# Личности.Add Item:=Шекспир, After:=2 Dim Гомер As New Личность Гомер.InitPerson "Гомер", "Великий Слепой", #1/1/100# Личности.Add Item:=Гомер, Before:=3 Личности(4).SayWhoIs 'Работа с коллекцией, как со словарем Dim Пушкин As New Личность Пушкин.InitPerson "Александр", "Пушкин", #6/6/1799# Личности.Add Item:=Пушкин, Key:="Гений" Dim Булгаков As New Личность Булгаков.InitPerson "Михаил", "Булгаков", #1/23/1891# Личности.Add Item:=Булгаков, Key:="Мастер" Debug.Print Личности("Гений").ВашаФамилия, " - это Гений!" Debug.Print Личности("Мастер").ВашаФамилия, " - это Мастер!" 'Печать всего списка Dim I As Byte For I = 1 To Личности.Count Личности(I).PrintPerson Next I End Sub

    Пример 5.9.

    В процессе работы этой процедуры в диалоговом окне появится сообщение о Шекспире, а в отладочном окне Immediate появятся следующие результаты:

    Пушкин - это Гений! Булгаков - это Мастер! Адам Первый Человек родился 01.01.100 Ной Праведник родился 01.01.100 Гомер Великий Слепой родился 01.01.100 Вильям Шекспир родился 23.04.1564 Александр Пушкин родился 06.06.1799 Михаил Булгаков родился 23.01.1891

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

    Основы офисного программирования и язык VBA

    API Viewer

    Мы уже говорили об одной из основных проблем, возникающих при вызове Win32 API функций, - необходимо обеспечить корректное задание оператора Declare, описание типов данных и констант, для чего нужно уметь корректно транслировать описание этих объектов из языка C в язык VBA. Чтобы облегчить решение этой задачи, можно воспользоваться специальным инструментальным средством, называемым API Viewer. Это средство доступно в поставке Office 2000 Developer и в Microsoft Visual Basic. Этот обозреватель позволяет:
  • Загрузить прилагаемый текстовый файл Win32API.txt, содержащий информацию обо всех используемых при вызовах Win32 API константах, типах, функциях и операторах Declare.
  • Преобразовать этот файл в базу данных Access - Win32API.mdb файл, что позволяет ускорить работу, работать с формами и запросами к базе данных.
  • Получить корректную для VB и VBA форму записи операторов Declare.
  • Получить корректную для VB и VBA форму записи типов данных.
  • Получить корректную для VB и VBA форму записи констант.

  • Взгляните на начальный этап работы с обозревателем, где выбирается текстовый файл для просмотра. Обратите внимание, что обозреватель позволяет работать с текстовыми файлами, содержащими информацию о различных интерфейсах, в частности Win32 и Mapi интерфейсах:
    API Viewer
    Рис. 6.1.  Выбор файла Win32API для просмотра в API Viewer
    На следующем рисунке можно увидеть результаты нашей дальнейшей работы с API Viewer. Вначале из верхнего выпадающего списка API Types мы выбрали элемент Declares, в окне Available Items из списка поочередно выбрали интересующие нас функции Win32 API, - соответствующая этим функциям корректная форма записи оператора Declare появилась в окне Selected Items. На следующем шаге из верхнего списка был выбран элемент Types, из списка типов был выбран тип, в нашем примере MenuItemInfo, и корректное для VBA его определение появилось в окне Selected Items. Аналогично, можно получить корректное определение констант. Обычная технология Copy - Paste позволяет перенести информацию непосредственно в программу VBA.
    Взгляните, как выглядят окна обозревателя на этом этапе работы:

    API Viewer
    Рис. 6.2.  Получение корректной формы записи оператора Declare в окне API Viewer

    Хотя, несомненно, обозреватель полезен, у него есть один недостаток. Было бы полезнее, если бы выбрав имя функции из списка, вся необходимая для вызова этой функции информация, - оператор Declare, типы и константы, - была бы собрана в одном месте. Это, однако, не делается и потому необходимо обращаться к документации по соответствующей функции. Это может быть "Справочник программиста Win32", если он доступен для Вас. Возможно, проще найти эту информацию на сервере Microsoft, созданном для разработчиков: http://msdn.microsoft.com/developer/

    Для того чтобы получить справку по функции Win32 API, следует воспользоваться режимом Search. В окне поиска следует набрать имя нужной функции, предварительно задав требуемую область поиска. При задании области поиска необходимо, по крайней мере, включить флажки Platform SDK Documentation и Others SDK Documentation. При расширении этой области можно получить не только описание функций, но и статьи с примерами их использования. Заметьте, на этом сервере Вы всегда найдете много полезной информации.

    Два языка: C и VB. Различия при вызове функций

    Итак, чтобы корректно вызывать Win32 API функции на VBA, следует разбираться, чем отличаются вызовы функций в языках C и Visual Basic (VBA). Даже, если используется обозреватель API Viewer, который автоматически транслирует типы языка C в типы языка VBA, знание этих различий необходимо во многих ситуациях.

    Две кодировки ANSI и Unicode

    Win32 API функции могут существовать в двух вариантах, ориентированных на две кодировки - ANSI, когда символы строк кодируются одним байтом, и Unicode, предполагающей двухбайтную кодировку. По этой причине в DLL возможны три варианта для имен функций:
  • Имя. В этом случае кодировка не играет значения.
  • ИмяA. Окончание A означает, что функция использует ANSI кодировку.
  • ИмяW. Окончание W (от Wide) свидетельствует об Unicode кодировке.

  • Чтобы выбрать функцию в нужной кодировке, нужно правильно задать Alias имя функции. Заметьте, что в VBA вызываются функции Win32 в ANSI кодировке, поэтому следует выбирать имена с окончанием A в тех случаях, когда при вызове функции используются строки и кодировка имеет значение.

    Еще один пример работы с функцией EnumWindows

    Наш следующий пример является упрощенным вариантом предыдущего примера. Его целью является демонстрация возможности передать информацию в Callback процедуру EnumWindowsProc через параметр lParam. В нашем примере вместо трех коллекций будет использоваться только одна - коллекция описателей, именно она и будет передана стандартным способом через параметр lParam. Коллекция будет передана в качестве аргумента при вызове процедуры EnumWindows, а та, в свою очередь передаст его функции обратного вызова EnumWindowsProc. Приведем текст модуля, содержащего процедуры нашего примера:
    Пример 6.10.
    (html, txt)
    Дадим несколько комментариев:
  • В этом примере параллельно показаны оба способа передачи информации. Имена функций, оканчивающиеся на 1, связаны с передачей информации через параметр lParam.
  • В операторе Declare, описывающем функцию EnumWindows1, тип параметра lParam задан как Any, и в данном случае параметр передается по ссылке, а не по значению.
  • При описании Callback функции EnumWindowsProc1 для этого параметра указан уже конкретный тип Collection.
  • Аргумент HandleCol1 типа Collection передается при вызове EnumWindows в процедуре GetHandles1. Функция обратного вызова EnumWindowsProc1 заполнит эту коллекцию элементами.

  • Приведем результаты ее работы:
    Number of windows -184 Their handles: 131826 131824 131854 131868 36504034 2359854 65636 262764 65690 65626 3539122

    Функции API и вызов Callback функций

    Мы уже говорили о функциях обратного вызова, называемых Callback функциями. Для "многослойного" способа построения программных систем, характерного для программирования, функции внешнего слоя могут вызывать функции ядра без особых проблем. Однако паритета между ядром и внешним слоем нет. Вызов функций внешнего слоя из ядра затруднен. Чтобы как-то решить эту проблему и вводятся функции обратного вызова. Если функции ядра, в ответ на ее вызов из внешнего слоя, в свою очередь необходимо вызвать функцию внешнего слоя, то ядро диктует условия, каким должна удовлетворять вызываемая функция. Есть специальные механизмы, обеспечивающие вызов таких Callback функций, но во всех случаях заголовок вызываемой функции жестко фиксирован и известен ядру. Этот механизм Callback функций применяется и для обеспечения двусторонней связи между функциями VBA и функциями Win32 API, которым в процессе их работы требуется обратный вызов функций VBA.
    Заметьте, в предыдущих версиях VBA не было возможности явным образом работать с функциями Win32 API, требующими вызова Callback функций. Теперь такая возможность появилась, благодаря включению в язык возможности передачи указателя функции в качестве параметра процедур и функций. Явное введение в язык конструкции AddressOf, возвращающей указатель на функцию, дало возможность при вызове функции Win32 API передать ей в качестве аргумента имя Callback функции. Попробуем разобраться в деталях того, как вызываются функции Win32 API, требующие Callback функции для своей работы, как пишутся такие функции на VBA, как передается информация между функциями, - как это все, в конечном итоге, согласуется между собой. Начнем, прежде всего, с ответа на вопрос, а как узнать, что функция Win32 API требует для своей работы вызова Callback функции. Подсказку можно получить от обозревателя, если проанализировать оператор Declare, созданный API Viewer. Когда имя параметра начинается префиксом lp, а заканчивается окончанием Func, это означает, что соответствующий аргумент является ссылкой на имя Callback функции.
    К сожалению, обозреватель не содержит необходимой информации о том, каким должен быть заголовок функции обратного вызова, так что необходимо обращаться к документации по Win32 API или идти на сервер. Заметьте, документация, как правило, ориентирована на C программистов, поэтому необходимо самому корректно транслировать заголовок к виду, понимаемому VBA. Ошибки в задании типов аргументов, пропуск описателя ByVal могут дорого стоить. Пожалуй, одна из наиболее сложных задач при работе с Callback функцией состоит в том, чтобы найти ее описание, а затем, используя документацию, ориентированную на язык C/C++, корректно описать на VBA заголовок этой функции.

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

  • Определить, что функция Win32 API требует вызова Callback функции.
  • Найти документацию по этой функции, описывающую требования к заголовку этой функции. Если эта документация ориентирована на язык C/C++, то привести ее к виду, требуемому VBA.
  • Понять, как передать информацию об объектах VBA в Callback функцию.
  • Написать одну или несколько реализаций функций обратного вызова. Обращаем внимание, что функций обратного вызова может быть несколько. Имя функции не является жестко зафиксированным. Оно передается функции Win32 API как аргумент в момент вызова. Поэтому в зависимости от контекста одну и ту же функцию Win32 API можно вызывать с различными Callback функциями.
  • Вызвать функцию Win32 API, передав ей в момент вызова имя Callback функции и другие необходимые аргументы.


  • Функции перечисления Win32 API

    Группу функций Win32 API, требующих вызова Callback функций, составляют так называемые функции перечисления, - Enum функции. Эти функции позволяют перечислить в определенном порядке все объекты операционной системы заданной группы. Вызывая на каждом шаге перечисления функцию обратного вызова, функция Win32 API передает ей текущий объект группы в качестве аргумента. Функция обратного вызова уже может производить над этим объектом различные, но, естественно, допустимые действия. Тем самым у программиста появляется возможность работы с коллекцией объектов, возможность задать собственную обработку для каждого из объектов. К функциям перечисления относятся такие функции как: EnumWindows, EnumPrinters, EnumFontFamilies, EnumFonts, EnumPorts, EnumResourseNames и многие другие. В качестве примера, рассмотрим работу с одной из этих функций.

    Функции высших порядков и конструкция AddressOf

    Функцией (процедурой) высших порядков в программировании называют функцию (процедуру), один из формальных параметров которой имеет тип функции или процедуры. Введение в язык функций высших порядков существенно повышает выразительную силу языка программирования. Классическим примером процедуры высшего порядка является процедура вычисления интеграла, одним из параметров которой выступает подынтегральная функция. Типичным примером функций высших порядков являются функции Win32 API, требующие вызова Callback функций. Существует несколько способов введения функций высших порядков в язык программирования, среди которых лучшим, видимо, является способ, основанный на введении функционального типа. В этом случае можно объявлять переменные типа Func или Proc, а затем уже передавать такие переменные, как аргументы при вызове функции высшего порядка. Другой классический способ основан на работе с указателями. Такой типизированный указатель может хранить ссылку на функцию, - содержать ее адрес, и может быть передан в качестве аргумента при вызове функции высшего порядка. Именно этот способ и реализован в VBA. С этой целью в язык введена конструкция:
    AddressOf имя
    Параметр имя может быть именем процедуры, функции или процедуры - свойства (Property). В качестве результата возвращается ссылка на объект с указанным именем.
    К сожалению, введя долгожданную конструкцию AddressOf, разработчики остановились на пол пути. С ее помощью можно передать функции высшего порядка в качестве аргумента имя функции. Однако, по-прежнему, нельзя описать на VB или VBA функцию высшего порядка. Такие функции должны быть внешними, как функции Win32 API или функции собственноручно разработанной DLL на языке C/C++. Можно было бы предъявить и еще одно "законное" требование к этой конструкции. Было бы весьма полезно, если бы она позволяла получать ссылку на объект любого произвольного типа, не ограничиваясь только функциональным типом. В этом случае работа с указателями в языке VBA стала бы полноценной.
    В настоящее время на конструкцию AddressOf накладывается целый ряд ограничений:
  • Эта конструкция может быть использована только в выражении, задающем вызов функции высшего порядка. Здесь она используется как аргумент, непосредственно предшествуя имени процедуры, передаваемой в качестве фактического параметра. Заметьте, эту конструкцию нельзя использовать при описании функции или процедуры, из-за чего невозможно определить функцию высшего порядка средствами VBA.
  • Процедуры, функции и свойства, которые вызываются конструкцией AddressOf, должны быть в том же проекте, в котором помещено объявление и вызов функции высшего порядка. Вызываемые процедуры и функции должны быть расположены в стандартном модуле, они не могут находиться в модуле класса или в модуле формы.
  • Эту конструкцию можно использовать только для вызова собственных процедур и функций, - ее нельзя использовать для вызова внешних или стандартных функций.


  • Функции Win32 API для работы с таймером

    Кроме функций перечисления, требующих в процессе своей работы вызов Callback функций, другим известным примером является функция SetTimer, создающая таймер. Во многих приложениях возникает необходимость синхронизировать его работу в соответствии с регулярно поступающими сообщениями от таймера. Общая схема такова: таймер посылает сообщения приложению с заданным интервалом, в ответ приложение выполняет определенную работу, вызывая ту или иную функцию (Callback функцию). Класс таких диспетчерских приложений, регулярно обрабатывающих вновь поступившие заявки, весьма велик. При работе в приложении Access для этих целей введен специальный элемент управления - Timer. В приложениях Word или Excel такого элемента нет, но всегда можно воспользоваться соответствующими функциями Win32 API, чтобы создать один или несколько собственных таймеров и организовать работу приложения, реагирующего на их сообщения. Заметьте, несмотря на то, что физический таймер один, логических таймеров, посылающих приложению свои сообщения, может быть несколько.

    Функция EnumWindows

    Окна являются одним из основных объектов операционной системы. Обычно, программист и не подозревает, сколь много таких объектов существует в процессе работы его программы. Функция EnumWindows позволяет перечислить все такие объекты. В нашем примере мы в результате работы с окнами сформируем три коллекции, - описателей окон, имен классов окон, заголовков окон. Задачей функции обратного вызова будет формирование этих коллекций путем добавления очередного элемента коллекции при каждом вызове Callback функции.
    Начнем с описания функции EnumWindows в том виде, в каком оно представлено в документации Platform SDK:
    BOOL EnumWindows( WNDENUMPROC lpEnumFunc, // pointer to callback function LPARAM lParam // application-defined value );
    Функция EnumWindows перечисляет все окна верхнего уровня, передавая текущий описатель окна Callback функции, определенной в приложении. Функция не перечисляет дочерние окна - Child Windows . Функция выполняется, пока перечисление не будет закончено или Callback функция не вернет значение False. Ее параметры:
  • lpEnumFunc - указатель на определенную в приложении Callback функцию.
  • lParam - определенное приложением значение, передаваемое в Callback функцию.

  • Функция возвращает значение 0 в случае неуспеха и ненулевое значение при благоприятном исходе.
    Описание Callback функции EnumWindowsProc, полученное из той же документации, имеет вид:
    BOOL CALLBACK EnumWindowsProc( HWND hwnd, // handle to parent window LPARAM lParam // application-defined value );
    Функция EnumWindowsProc является Callback функцией, определенной в приложении, используемой при вызове функций Win32 API EnumWindows или EnumDesktopWindows. Она получает при вызове описатель окна верхнего уровня. Тип WNDENUMPROC определяет указатель на эту Callback функцию. Имя EnumWindowsProc является держателем места (placeholder) и должно быть замещено именем функции, определенной в приложении.
    Ее параметры:
  • hwnd - описатель окна верхнего уровня.
  • lParam - определенное приложением значение, данное в EnumWindows или EnumDesktopWindows.


  • Для продолжения перечисления функция возвращает значение True, для окончания - False.

    Как видите, представленные описания функций ориентированы на язык C/C++ и нуждаются в преобразовании для их использования в программах на VB/VBA. Используя обозреватель API Viewer, можно получить оператор Declare для функции EnumWindows. Вот как выглядит заголовок этой функции после соответствующей трансляции:

    Public Declare Function EnumWindows Lib "user32" _ (ByVal lpEnumFunc As Long, ByVal lParam As Long) As Long

    Заметьте, появились описатели ByVal, а все типы заменились в данном случае на тип Long. Поскольку API Viewer не помощник в деле преобразования описания Callback функций, то эту работу необходимо проделать самостоятельно. В результате, описание имеет вид:

    Public Function EnumWindowsProc(ByVal HandleW As Long, _ ByVal lParam As Long) As Long

    Заметим, формальная трансляция не вызывает затруднений, - описатель Callback следует опустить, имя можно дать произвольное, а трансляция типов в данном случае достаточно проста. Вместе с тем, с описанием типа параметра lParam не все так просто. Ведь он должен служить для передачи произвольной информации, поэтому теоретически допускается задание любого произвольного типа для этого параметра, например, этот параметр может быть объектом. В этом случае следует быть особо внимательным, так первая наша попытка передать функции параметр, отличный от типа Long, привела к критической ошибке и прекращении работы приложения. Как я уже говорил ранее, можно применить альтернативный способ и передавать информацию, пользуясь глобальными переменными.

    Перейдем теперь к описанию реализации нашего примера. В проекте тестового документа был создан модуль с именем "ОбратныйВызов". Вот текст раздела объявлений этого модуля:

    Option Explicit 'Операторы Declare вызываемых функций Win32 API

    Public Declare Function EnumWindows Lib "user32" _ (ByVal lpEnumFunc As Long, ByVal lParam As Long) As Long

    Public Declare Function GetWindowText Lib "user32" Alias "GetWindowTextA" _ (ByVal hwnd As Long, ByVal lpString As String, ByVal cch As Long) As Long


    Public Declare Function GetClassName Lib "user32" Alias "GetClassNameA" _ (ByVal hwnd As Long, ByVal lpClassName As String, ByVal nMaxCount As Long) As Long

    'Описание глобальных переменных, обеспечивающих связь 'с функцией обратного вызова EnumWindowsProc Public HandleCol As New Collection Public CaptCol As New Collection Public ClassNameCol As New Collection

    Помимо функции EnumWindows, основной для нашего примера, но о которой уже много говорилось, в разделе приведено объявление новой, ранее не упоминавшейся Win32 API функции GetClassName. Она похожа на функцию GetWindowText и возвращает по описателю окна имя класса этого окна. Функцию GetWindowText мы объявляем повторно, соответствующий оператор Declare есть в разделе объявлений другого модуля проекта, но, заметьте, в этом есть необходимость, поскольку в других модулях эта функция использовалась с различными псевдонимами. Раздел объявлений модуля содержит объявление трех глобальных переменных - трех коллекций, с которыми будет работать Callback функция EnumWindowsProc, формируя на каждом шаге своего вызова очередной элемент каждой из коллекций. Коллекции будут содержать соответственно описатели окон, заголовки окон и имена классов. Заметьте, что все окна имеют имя класса, но не все окна имеют заголовок, так что число элементов в коллекциях будет различным в процессе работы. Приведем теперь текст Callback процедуры EnumWindowsProc:

    Пример 6.7.

    (html, txt)

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


    Так обеспечивается связь с внешним миром.

    Чтобы закончить пример, нам осталось привести описание процедуры GetCaptions, вызывающей EnumWindows:

    Пример 6.8.

    (html, txt)

    Несколько комментариев к этой процедуре:

  • Главное, на что следует обратить внимание, это на операцию "AddressOf" в момент вызова Win32 API функции EnumWindows. В результате ее выполнения создается ссылка на Callback функцию (адрес расположения функции в памяти). Заметьте, на имя передаваемой функции не накладывается ограничений, поэтому в разных вызовах могут быть разные имена, что и позволяет иметь при необходимости несколько Сallback функций.
  • Мы передаем 0 в качестве значения второго параметра. Это своего рода заглушка, поскольку, как говорилось ранее, передача информации производится через глобальные переменные. В следующем примере мы продемонстрируем возможность передачи информации и через этот параметр.
  • После завершения работы процедуры перечисления начинается обработка коллекций, созданных в процессе ее работы. В данном случае обработка проста и сводится к печати числа элементов коллекции и первых десяти элементов каждой из коллекций. Возможно, было бы интересно посмотреть, сколько же объектов - окон существует в момент выполнения обычного приложения. Но полная распечатка всех имен классов и заголовков заняла бы несколько страниц текста. Ведь таких объектов несколько сотен.


  • Давайте познакомимся с начальными элементами коллекций, содержащих описатели, заголовки и имена классов объектов - окон, существующих в момент запуска процедуры GetCaptions:

    Пример 6.9.

    (html, txt)


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


  • Давайте познакомимся с начальными элементами коллекций, содержащих описатели, заголовки и имена классов объектов - окон, существующих в момент запуска процедуры GetCaptions:

    Число окон = 254 Описатели окон 3735790 131912 131888 131916 65684 40370412 917748 262866 852650 852668 131844 Число окон с заголовками =76 Заголовки окон Continue Microsoft Agent Microsoft Office Shortcut Bar Menu Parent Window NetDDE Agent Edit Microsoft Visual Basic - DocOne6 [running] - [ОбратныйВызов (Code)] Ch6 - Microsoft Word Edit Properties Microsoft Office Shortcut Bar Число окон, возвращающих класс =254 Имена классов окон OfficeTooltip tooltips_class32 ComboLBox tooltips_class32 tooltips_class32 AgentAnimBalloon AgentAnim tooltips_class32 tooltips_class32 tooltips_class32 tooltips_class32

    Пример 6.9.

    Функция KillTimer

    Поскольку одновременно могут существовать несколько таймеров, также как и при необходимости заменить один таймер другим, - по разным причинам возникает необходимость удаления уже не нужных таймеров. Для этого и используется функция Win32 API KillTimer. Вот ее стандартное описание:
    BOOL KillTimer( HWND hWnd, // handle to window that installed timer UINT nIDEvent, // timer identifier );
    Ее параметры:
  • hwnd - Описатель окна, ассоциированного с таймером, совпадающий по значению с соответствующим параметром функции SetTimer. Напомним, в VBA программах таймер не связывается с окном и значение этого параметр задается как NULL.
  • nIDEvent - Задает идентификатор таймера, который должен быть удален. В нашем случае, когда первый параметр равен NULL, его значение задается идентификатором, возвращенным в качестве результата по окончании работы функции SetTimer.

  • Если функция успешно завершает свою работу и удаляет таймер, то в качестве результата она возвращает ненулевое значение. В случае неуспеха возвращается значение 0.

    Функция обратного вызова TimerProc

    Функция TimerProc является Callback функцией, определенной приложением и вызываемой при обработке сообщений, поступающих от таймера. Ее определение имеет вид:
    VOID CALLBACK TimerProc( HWND hwnd, // handle of window for timer messages UINT uMsg, // WM_TIMER message UINT idEvent, // timer identifier DWORD dwTime // current system time );
    Ее параметры:
  • hwnd - Описатель окна.
  • uMsg - Указывает WM_Timer сообщение.
  • idEvent - Идентификатор таймера.
  • dwTime - Задает текущее системное время, возвращаемое функцией GetTickCount.

  • Заметьте, имя TimerProc является лишь держателем места. В конкретной ситуации необходимо будет определить одну или несколько Callback функций с подходящими именами. Поскольку вызов каждой из этих функций производится автоматически, то нет необходимости заботиться о корректной передаче аргументов в момент вызова. Необходимо лишь позаботиться о корректной трансляции приведенного определения, взятого из справочной системы Platform SDK, к виду, воспринимаемому в программах на VBA. Вот как выглядит возможное определение:
    Public Sub TimerProc(ByVal HandleW As Long, ByVal msg As Long, _ ByVal idEvent As Long, ByVal TimeSys As Long)
    Обратите внимание, мы транслировали функцию в процедуру, поскольку Callback функция TimerProc не возвращает значения. Все типы данных преобразованы в тип Long, в том числе UINT и DWORD. В данной ситуации нет причин для беспокойства о возможной некорректности передаваемых значений, поскольку их передачу обеспечивает сама система.

    Функция SetTimer

    Эта функция создает таймер, посылающий сообщения с заданным интервалом. Ее описание, которое можно найти на Platform SDK, имеет вид:
    UINT SetTimer( HWND hWnd, // handle to window for timer messages UINT nIDEvent, // timer identifier UINT uElapse, // time-out value TIMERPROC lpTimerFunc // pointer to timer procedure );
    Ее параметры:
  • hwnd - Описатель окна, которому будут посылаться сообщения таймера. В VBA программах таймер не связывается с окном и значение этого параметр задается как NULL.
  • nIDEvent - Задает идентификатор таймера. Его значение игнорируется, когда таймер не связан с окном, что имеет место в рассматриваемом нами случае.
  • uElapse - Задает интервал, с которым таймер будет посылать свои сообщения. Интервал задается в миллисекундах, так что значение 1000 соответствует одной секунде.
  • lpTimerFunc - Указатель на Callback функцию, которая будет вызываться всякий раз, когда обрабатывается сообщение WM_Timer, поступающее от таймера.

  • Если функция успешно завершает свою работу и создает таймер, то в качестве результата она возвращает уникальный идентификатор этого таймера, идентифицирующий его. Этот идентификатор запоминается и используется для уничтожения таймера при вызове Win32 API функции KillTimer. В случае неуспеха возвращается значение 0.
    Заметьте, функция SetTimer только создает таймер. В отличие от функций перечисления вызов Callback функции не происходит в ее теле. Вызов осуществляется более сложным путем. Созданный таймер посылает сообщения с заданным интервалом, сообщения, как обычно, поступают в очередь сообщений и в обработчике сообщения WM_Timer автоматически вызывается функция обратного вызова. Поскольку при обработке сообщений очереди могут происходить разные задержки, то вызываемая функция не всегда будет вызываться с заданным интервалом, - возможны задержки.
    Оператор Declare, задающий VBA описание этой функции имеет соответственно вид:
    Public Declare Function SetTimer Lib "user32" (ByVal hwnd As Long, _ ByVal nIDEvent As Long, ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long

    Характеристики окружения

    Наш следующий пример показывает, как получить, используя вызов Win32 API функций, следующие характеристики окружения:
  • Версию используемой операционной системы.
  • Тип процессора.
  • Общий и используемый объем физической и виртуальной памяти.

  • Функции, используемые в этом примере, принадлежат библиотеке Kernel32. Заметьте, что в отличие от большинства случаев в данном примере будут вызываться не только функции, но и процедуры, хранящиеся в библиотеке Kernel32. Начнем с описания констант, типов и операторов Declare, предшествующих вызову API функций:
    Пример 6.3.
    (html, txt)
    Для сбора информации об окружении - используемом процессоре, операционной системе, имеющейся памяти и других характеристиках компьютера используем три следующие функции (процедуры) Win32 API:
  • Функция GetVersionEx имеет в качестве единственного параметра, передаваемого по ссылке, структуру типа OSVERSIONINFO. В результате выполнения функции будут заполнены поля этой структуры, содержащие всю необходимую информацию об используемой версии операционной системы. Конечно, необходимо в раздел объявлений включить и объявление типа OSVERSIONINFO. Возвращаемый функцией результат позволяет проанализировать успешность ее выполнения.
  • Процедура GlobalMemoryStatus имеет единственный, передаваемый по ссылке параметр типа MEMORYSTATUS. В результате выполнения процедуры поля структуры заполняются собранной информацией об объеме физической и виртуальной памяти, общей и доступной для использования в текущий момент, размере слова и некоторых других характеристиках памяти.
  • Процедура GetSystemInfo имеет единственный, передаваемый по ссылке параметр типа SYSTEM_INFO. В результате выполнения процедуры поля структуры заполняются собранной системной информацией о типе процессора, числе процессоров, используемом размере страницы и других характеристиках компьютера. Нужно ли говорить, что для корректного вызова функции требуется объявление типа SYSTEM_INFO и констант, необходимых для анализа значений поля dwProcessorType, определяющего тип центрального процессора.

  • Приведем теперь процедуру, в которой вызываются указанные функции API, обрабатывается полученная информация, конечные результаты выводятся в окно отладки:
    Пример 6.4.
    (html, txt)
    Программа, по-видимому, не нуждается в особых комментариях, и мы ограничимся приведением результатов отладки, которые показывают мое рабочее окружение:
    Windows NT 4.0 (Build 1381)
    Процессор: Intel Pentium
    Число процессоров: 1
    Размер страницы: 4096
    Минимальный адрес приложения: 65536
    Максимальный адрес приложения: 2147418111
    Физическая память. Всего: 32 180K
    Физическая память. Доступно: 4 772K
    Виртуальная память. Всего: 2 097 024K
    Виртуальная память. Доступно: 1 982 872K
    Длина слова: 32
    Загрузка памяти: 0

    В результате выполнения функции будут заполнены поля этой структуры, содержащие всю необходимую информацию об используемой версии операционной системы. Конечно, необходимо в раздел объявлений включить и объявление типа OSVERSIONINFO. Возвращаемый функцией результат позволяет проанализировать успешность ее выполнения.
  • Процедура GlobalMemoryStatus имеет единственный, передаваемый по ссылке параметр типа MEMORYSTATUS. В результате выполнения процедуры поля структуры заполняются собранной информацией об объеме физической и виртуальной памяти, общей и доступной для использования в текущий момент, размере слова и некоторых других характеристиках памяти.
  • Процедура GetSystemInfo имеет единственный, передаваемый по ссылке параметр типа SYSTEM_INFO. В результате выполнения процедуры поля структуры заполняются собранной системной информацией о типе процессора, числе процессоров, используемом размере страницы и других характеристиках компьютера. Нужно ли говорить, что для корректного вызова функции требуется объявление типа SYSTEM_INFO и констант, необходимых для анализа значений поля dwProcessorType, определяющего тип центрального процессора.


  • Приведем теперь процедуру, в которой вызываются указанные функции API, обрабатывается полученная информация, конечные результаты выводятся в окно отладки:

    Public Sub WorkWithStatus() Dim res As Long 'Результат выполнения функции Dim msg As String ' Формируемое сообщение Dim verinfo As OSVERSIONINFO 'Информация об ОС и ее версиях Dim sysinfo As SYSTEM_INFO 'Системная информация Dim memstatus As MEMORYSTATUS 'Информация о статусе памяти

    verinfo.dwOSVersionInfoSize = Len(verinfo) res = GetVersionEx(verinfo) If res > 0 Then Select Case verinfo.dwPlatformId Case 0 msg = "Windows 32s " Case 1 msg = "Windows 95/98 " Case 2 msg = "Windows NT " End Select msg = msg & verinfo.dwMajorVersion & "." & verinfo.dwMinorVersion msg = msg & " (Build " & verinfo.dwBuildNumber & ")" & vbCrLf Debug.Print msg Else MsgBox ("Не могу получить версию операционной системы") End If


    ' определение типа процессора GetSystemInfo sysinfo msg = "Процессор: " Select Case sysinfo.dwProcessorType Case PROCESSOR_INTEL_386 msg = msg & "Intel 386" & vbCrLf Case PROCESSOR_INTEL_486 msg = msg & "Intel 486" & vbCrLf Case PROCESSOR_INTEL_PENTIUM msg = msg & "Intel Pentium" & vbCrLf Case PROCESSOR_MIPS_R4000 msg = msg & "MIPS R4000" & vbCrLf Case PROCESSOR_ALPHA_21064 msg = msg & "DEC Alpha 21064" & vbCrLf Case Else msg = msg & "(unknown)" & vbCrLf

    End Select Debug.Print msg msg = "Число процессоров: " & sysinfo.dwNumberOrfProcessors & vbCrLf Debug.Print msg msg = "Размер страницы: " & sysinfo.dwPageSize & vbCrLf Debug.Print msg msg = "Минимальный адрес приложения: " & sysinfo.lpMinimumApplicationAddress & vbCrLf Debug.Print msg msg = "Максимальный адрес приложения: " & sysinfo.lpMaximumApplicationAddress & vbCrLf Debug.Print msg

    ' Получение характеристик памяти GlobalMemoryStatus memstatus msg = "Физическая память. Всего: " & _ VBA.Format$(memstatus.dwTotalPhys \ 1024, "###,###,###") & "K" & vbCrLf Debug.Print msg

    msg = "Физическая память. Доступно: " & _ VBA.Format$(memstatus.dwAvailPhys \ 1024, "###,###,###") & "K" & vbCrLf Debug.Print msg

    msg = "Виртуальная память. Всего: " & _ VBA.Format$(memstatus.dwTotalVirtual \ 1024, "###,###,###") & "K" & vbCrLf Debug.Print msg msg = "Виртуальная память. Доступно: " & _ VBA.Format$(memstatus.dwAvailVirtual \ 1024, "###,###,###") & "K" & vbCrLf Debug.Print msg msg = "Длина слова: " & memstatus.dwLength & vbCrLf Debug.Print msg msg = "Загрузка памяти: " & memstatus.dwMemoryLoad & vbCrLf Debug.Print msg

    End Sub

    Пример 6.4.

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

    Windows NT 4.0 (Build 1381)

    Процессор: Intel Pentium

    Число процессоров: 1

    Размер страницы: 4096

    Минимальный адрес приложения: 65536

    Максимальный адрес приложения: 2147418111

    Физическая память. Всего: 32 180K

    Физическая память. Доступно: 4 772K

    Виртуальная память. Всего: 2 097 024K

    Виртуальная память. Доступно: 1 982 872K

    Длина слова: 32

    Загрузка памяти: 0

    Использование класса ВашТаймер

    Рассмотрим, как работать с классом ВашТаймер. Мы не стали изобретать ничего нового, - в тестовый документ добавили две кнопки Start1 и Finish1, которые работают также как и их тезки Start и Finish, но вызывают для этого методы и свойства объекта MyTimer класса ВашТаймер. В модуле, где объявлен соответствующий объект, находится и процедура обратного вызова TimerProc. Вот соответствующий текст этого модуля:
    Пример 6.13.
    (html, txt)
    Комментируя этот текст, следует заметить, что введение обертывающего класса облегчает работу с таймером. Единственной проблемой остается достаточно сложное и возможно непонятное конечному пользователю описание заголовка Callback функции TimerProc. Чтобы облегчить ее решение, можно, как это сделано в нашем примере, заготовку этой функции включить в описание класса в качестве комментария.
    В заключение приведем результаты эксперимента с нажатием кнопок Start1 и Finish1:
    Создан Таймер: Идентификатор =31711 Hi 1 Hi 2 Hi 3 Удален Таймер: Идентификатор =31711 Создан Таймер: Идентификатор =31704 Hi 4 Hi 5 Hi 6 Hi 7 Удален Таймер: Идентификатор =31704
    На этом мы закончим рассмотрение темы работы с функциями Win32 API.

    Классы как обертка вызовов функций Win32 API

    В предыдущих лекциях мы много говорили о классах и, в частности, рассматривали возможность применения класса в качестве упаковки тех или иных стандартных средств. Такой прием позволяет инкапсулировать в классе стандартные средства, - функции API, элементы управления, - скрыть многие детали работы с ними, а, зачастую, и предоставить конечному пользователю дополнительный сервис. Этот полезный прием целесообразно применять всякий раз, когда предполагается работа с функциями Win32 API. Все эти вопросы объявления функций, трансляции из одной формы описания в другую, описание многих параметров, не используемых по существу задачи, следует скрыть в описании класса, организовав интерфейс класса, более разумный с позиций конечного пользователя.
    Каждый из наших примеров работы с функциями Win32 API достоин лучшей упаковки и представления его в виде класса. Мы ограничимся одним последним примером и представим в виде класса работу с таймером. Пожалуй, этот пример наиболее интересен, поскольку он представляет особый случай из-за наличия Callback функции, которая, конечно же, не может быть стандартизована и должна быть задана конечным пользователем, в интересах которого и строится класс.

    Несколько слов об API, Win32, DLL

    API (Application Programming Interface - интерфейс прикладных программ) - это множество функций, организованных, обычно, в виде DLL. Функции API позволяют организовать интерфейс между прикладной программой и средой, в которой работает эта программа. Вызов функций API позволяет программе получать доступ к ресурсам среды и управлять ее работой. Как правило, API задает стандарт взаимодействия среды и прикладной программы.
    Win32 - это название интерфейса, ориентированного на 32-х разрядные приложения и реализованного на таких известных платформах как Windows 95, Windows 98, Windows NT, Windows CE. Функции, составляющие этот интерфейс, позволяют прикладной программе получать доступ к ресурсам операционной системы и управлять ее работой. Более ранние версии Windows используют интерфейс, известный как Win16. Конечно, не все функции, составляющие интерфейс Win32, реализованы в полной мере на всех платформах, так что вызов одной и той же функции под NT приведет к определенному результату, а под Windows 95 работает как вызов заглушки. Любое из приложений, работающее в среде Windows, прямо или косвенно вызывает функции, входящие в Win32 API.
    Функции, составляющие Win32 интерфейс, организованы в виде нескольких динамически подключаемых библиотек (DLL) и исполняемых файлов. Говоря о Win32 API, следует в первую очередь упомянуть три основные библиотеки:
  • Kernel32.dll. Эта библиотека предназначена для работы с объектами ядра операционной системы и ее функции позволяют управлять памятью и другими системными ресурсами.
  • User32.dll. Здесь сосредоточены функции для управления окнами - основным видом объектов операционной системы. Обработка сообщений, работа с меню, таймерами, все это выполняют функции этой DLL.
  • GDI32.dll. Эта библиотека, обеспечивающая графический интерфейс операционной системы (Graphics Device Interface). Функции управления выводом на экран дисплея, управления выводом принтера, функции для работы со шрифтами - все они входят в состав этой библиотеки.

  • Заметьте, Win API функции находятся не только в этих библиотеках. С другой стороны API функции не обязательно входят в состав Win32 интерфейса. Например, MAPI интерфейс (Messaging Application Programming Interface) составляют функции, предназначенные для обработки сообщений электронной почты, TAPI (Telephone API) - функции работы с телефонными сообщениями. MAPI, TAPI, также как и Win32 это некоторый набор функций, задающий определенный стандарт взаимодействия
    Как мы уже говорили, функции, образующие API, обычно, организованы в виде DLL - динамически подключаемых библиотеках. Одно из достоинств DLL состоит в том, что, сколько бы приложений (процессов) не работало с функциями одной и той же DLL, код DLL существует в единственном экземпляре.

    Об описателях языка C и объектах Windows

    В языке C особенно при работе с объектами Windows, широко используется тип Handle - задающий описатели объектов. Когда создаются объекты ядра операционной системы и такие основные объекты, как окна, - все они снабжаются описателем, имеющим тип Handle. Описатель представляет длинное целое и однозначно идентифицирует объект. Всегда, когда в функцию Win32 API необходимо передать такой объект, то реально передается его описатель. При трансляции описателей в тип языка VBA ему ставится в соответствие тип Long. При этом желательно понимать, что речь все-таки идет об описателях, создаваемых операционной системой, в момент создания соответствующего объекта. Соответствующие примеры будут даны чуть позже.

    Обработка ошибок, возникающих при вызове функций Win32 API

    Как мы уже говорили ранее, не бывает программ без ошибок. Если ошибка возникает при выполнении кода процедур и функций VBA, - ошибка периода выполнения (run time error), - то появляется окно сообщения об ошибке. Если ошибка периода выполнения появляется при работе функции Win32 API, то прерывания работы программы не происходит, окно сообщения об ошибке не появляется. Вместо этого функция возвращает значение 0 в качестве результата, свидетельствующее об ошибке периода выполнения. Тем не менее, большинство функций Win32 API сохраняют информацию о возникшей ошибке. Эту информацию можно получить стандартным способом, используя VBA объект Err. Свойство LastDLLErr этого объекта возвращает номер последней ошибки, возникшей в DLL. К сожалению, сам по себе номер мало что говорит. Необходимо знать описание ошибки, соответствующее этому номеру. Частично причину ошибки можно понять по имени константы, которую можно найти в уже неоднократно упоминавшемся файле Win32API.txt, используемом в API Viewer. Опять-таки, к сожалению, возможные значения констант приводятся независимо от функций, в которых они возникают. И, несмотря на то, что все такие константы начинаются со слова ERROR найти константу по ее значению не так то просто. Можно, конечно, воспользоваться возможностью создания базы данных по текстовому файлу и организовать специальный запрос, позволяющий найти имя константы по ее значению. Естественно, что лучше всего иметь полную информацию об используемых функциях Win32 API, включающую, в том числе, и сведения о возможных ошибках периода выполнения данных функций. Эту информацию можно найти, если под рукой есть подходящая литература, например, справочник программиста Win32, или поискать на упоминавшемся сервере Microsoft для разработчиков.
    Естественно, что пример ошибки времени выполнения в процессе работы DLL у нас уже под рукой. Нам и изобретать его не было необходимости. Как Вы помните, в последнем примере мы сетовали на возникновение подобной ошибки в процессе поиска описателя окна по его заголовку при вызове функции FindWindowW, работающей в Unicode кодировке.
    Давайте вернемся к этому примеру и попробуем обработать эту ошибку. В раздел объявлений ранее созданного модуля Unicode мы добавили объявление констант и функций и теперь он выглядит так:

    Option Explicit

    Public Const ERROR_INVALID_NAME = 123&

    'Объявление вызываемых функций Public Declare Function FindWindowA Lib "user32" _ (ByVal lpClassName As String, ByVal lpWindowName As String) As Long 'Функции в Unicode кодировке 'Тип string заменен на Any. Передача аргумента по ссылке Public Declare Function FindWindowW Lib "user32" _ (lpClassName As Any, lpWindowName As Any) As Long

    Public Declare Function GetWindowText Lib "user32" Alias "GetWindowTextW" _ (ByVal hwnd As Long, lpString As Any, ByVal cch As Long) As Long

    Public Declare Function SetWindowText Lib "user32" Alias "SetWindowTextW" _ (ByVal hwnd As Long, lpString As Any) As Long

    Public Declare Function GetActiveWindow Lib "user32" () As Long

    Public ArCapt() As Byte 'Объявление динамического массива

    Приведем теперь процедуру, в которой вызывается функция FindWindowW, приводящая к ошибке периода выполнения:

    Пример 6.6.

    (html, txt)

    Приведем результаты печати , появившиеся в окне отладки при выполнении этой процедуры:

    DocOne6 - Microsoft Word Не корректно задано имя при вызове Unicode FindWindowW функции! Microsoft Visual Basic - DocOne6 [running] - [Unicode (Code)] Не корректно задано имя при вызове Unicode FindWindowW функции!

    Прокомментируем теперь работу программы и полученные результаты:

  • Вначале мы попытались найти окно с заведомо существующим заголовком, - окно документа, содержащего тестовые примеры. В процессе работы функции Win32 API FindWindowW возникла ошибка периода выполнения, функция вернула нулевой результат. Ошибка была обработана, и как показывает константа ERROR_INVALID_NAME, причиной является ошибка в задании имени (передаваемый формат в виде массива байтов не годится для цели поиска и сравнения строк), о чем свидетельствует отладочная информация.
  • Далее проводится еще один эксперимент на ту же тему.Для активного окна находится заголовок, используя функцию GetWindowTextW, возвращающую строку в виде массива байтов. Тут же этот массив используется для поиска окна по заголовку. Однако ничего не помогает и снова при поиске окна возникает ошибка. Она обрабатывается, о чем выдается соответствующее сообщение.



  • Давайте вернемся к этому примеру и попробуем обработать эту ошибку. В раздел объявлений ранее созданного модуля Unicode мы добавили объявление констант и функций и теперь он выглядит так:

    Option Explicit

    Public Const ERROR_INVALID_NAME = 123&

    'Объявление вызываемых функций Public Declare Function FindWindowA Lib "user32" _ (ByVal lpClassName As String, ByVal lpWindowName As String) As Long 'Функции в Unicode кодировке 'Тип string заменен на Any. Передача аргумента по ссылке Public Declare Function FindWindowW Lib "user32" _ (lpClassName As Any, lpWindowName As Any) As Long

    Public Declare Function GetWindowText Lib "user32" Alias "GetWindowTextW" _ (ByVal hwnd As Long, lpString As Any, ByVal cch As Long) As Long

    Public Declare Function SetWindowText Lib "user32" Alias "SetWindowTextW" _ (ByVal hwnd As Long, lpString As Any) As Long

    Public Declare Function GetActiveWindow Lib "user32" () As Long

    Public ArCapt() As Byte 'Объявление динамического массива

    Приведем теперь процедуру, в которой вызывается функция FindWindowW, приводящая к ошибке периода выполнения:

    Public Sub WorkWithApiErr() Dim Res As Long Dim capt As String 'Заголовок Dim HandleW As Long 'Описатель окна

    'Поиск окна по заголовку capt = "DocOne6 - Microsoft Word" ArCapt = capt & vbNullChar Debug.Print ArCapt HandleW = FindWindowW(0&, ArCapt(0)) If HandleW > 0 Then 'OK Debug.Print HandleW Else: MsgBox ("Не могу корректно вызвать UniCode FindWindowW") If Err.LastDllError = ERROR_INVALID_NAME Then Debug.Print "Не корректно задано имя при вызове Unicode FindWindowW функции!" End If End If 'Еще один эксперимент: вначале получим заголовок активного окна, 'затем найдем окно по заголовку, работая в Unicode кодировке. HandleW = GetActiveWindow()

    'Получить заголовок окна ArCapt = VBA.String$(128, vbNullChar)

    Res = GetWindowText(HandleW, ArCapt(0), 128) If Res > 0 Then 'OK Debug.Print ArCapt Else: MsgBox ("не получен заголовок окна") End If

    Построение класса "ВашТаймер"

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

  • Применим эту общую схему для создания класса ВашТаймер. Начнем с проектирования его интерфейса. Естественно, целью класса является предоставление пользователю возможности создавать таймер, посылающий сообщения с заданным интервалом, и удалять его, когда необходимость в нем исчезнет. Пользователь не должен ничего знать о функциях Win32 API, об операторах Declare, преобразовании типов. Кажется естественным с этих позиций в интерфейс класса включить два метода: "СоздатьТаймер" и "УдалитьТаймер" и свойство "ИнтервалТаймера", доступное для чтения и записи. Методы не имеют параметров, что облегчает работу с ними. Чтобы созданный таймер посылал сообщения с заданным интервалом, необходимо предварительно установить подходящее значение свойства, но можно этого и не делать, - в этом случае будет использоваться значение по умолчанию.
    Рассматриваемый нами случай упаковки функций Win32 API особый, поскольку одна из этих функций требует вызова Callback функции. Заметьте, функции обратного вызова не должны принадлежать упаковке, - нашему классу. Они не являются стандартными средствами, это функции, создаваемые пользователем. Поэтому они должны находиться вне модуля класса, - в стандартном классе, созданном пользователем, там, где он будет создавать и объекты класса ВашТаймер. Чтобы не возникала соблазна поместить в класс функцию обратного вызова, такая возможность исключается синтаксически.
    Обратите, однако, внимание, что созданный класс предъявляет определенные требования к заголовку функции обратного вызова и даже диктует ее имя. Но обо всем по порядку и давайте вначале рассмотрим описание класса ВашТаймер:

    Пример 6.12.

    (html, txt)

    Некоторые комментарии к этому тексту:

  • О проектировании интерфейса класса мы уже говорили. Его составляют два метода, не имеющие параметров, - СоздатьТаймер и УдалитьТаймер, а также процедуры - свойства Property Get и Property Let ИнтервалТаймера, позволяющие взаимодействовать с закрытым свойством Интервал.
  • Закрытых свойств и методов больше. Закрытыми являются операторы Declare, описывающие функции Win32 API SetTimer и KillTimer, уже упомянутое свойство Интервал и свойство IdEv, хранящее идентификатор таймера, о котором конечный пользователь может и не знать.
  • Закрытыми являются конструктор класса по умолчанию и деструктор: Class_Initialize и Class_Terminate. В конструкторе инициализируется свойство Таймер, значение которого устанавливается по умолчанию, равным одной секунде. В деструкторе класса таймер уничтожается, если он не был удален до этого.
  • При создании таймера устанавливается ссылка на функцию обратного вызова с именем TimerProc. Процедура с таким именем и уже упоминавшимися требованиями к ее заголовку должна быть описана в стандартном модуле класса, созданным конечным пользователем.
  • В описание класса в качестве комментария вставлена заготовка функции обратного вызова, чтобы облегчить создание этой функции конечному пользователю.



  • Некоторые комментарии к этому тексту:

  • О проектировании интерфейса класса мы уже говорили. Его составляют два метода, не имеющие параметров, - СоздатьТаймер и УдалитьТаймер, а также процедуры - свойства Property Get и Property Let ИнтервалТаймера, позволяющие взаимодействовать с закрытым свойством Интервал.
  • Закрытых свойств и методов больше. Закрытыми являются операторы Declare, описывающие функции Win32 API SetTimer и KillTimer, уже упомянутое свойство Интервал и свойство IdEv, хранящее идентификатор таймера, о котором конечный пользователь может и не знать.
  • Закрытыми являются конструктор класса по умолчанию и деструктор: Class_Initialize и Class_Terminate. В конструкторе инициализируется свойство Таймер, значение которого устанавливается по умолчанию, равным одной секунде. В деструкторе класса таймер уничтожается, если он не был удален до этого.
  • При создании таймера устанавливается ссылка на функцию обратного вызова с именем TimerProc. Процедура с таким именем и уже упоминавшимися требованиями к ее заголовку должна быть описана в стандартном модуле класса, созданным конечным пользователем.
  • В описание класса в качестве комментария вставлена заготовка функции обратного вызова, чтобы облегчить создание этой функции конечному пользователю.


  • Работа с окнами

                                                   

    Работа с функциями в Unicode кодировке

                                                   

    Работа с функцией Api, вызывающей CallBack функцию

                                                   

    Типы Public Type RECT Left

    Option Explicit 'Константы Public Const SW_HIDE = 0 Public Const SW_SHOWNORMAL = 1 Public Const SW_SHOWMINIMIZED = 2 Public Const SW_SHOWMAXIMIZED = 3
    ' Типы Public Type RECT Left As Long Top As Long Right As Long Bottom As Long End Type
    'Функции Public Declare Function GetActiveWindow Lib "user32" () As Long
    Public Declare Function GetWindowRect Lib "user32" (ByVal hwnd As Long, _ lpRect As RECT) As Long
    Public Declare Function GetWindowText Lib "user32" Alias "GetWindowTextA" _ (ByVal hwnd As Long, ByVal lpString As String, ByVal cch As Long) As Long
    Public Declare Function FindWindow Lib "user32" Alias "FindWindowA" _ (ByVal lpClassName As String, ByVal lpWindowName As String) As Long
    Public Declare Function ShowWindow Lib "user32" (ByVal hwnd As Long, _ ByVal nCmdShow As Long) As Long
    Public Declare Function SetWindowText Lib "user32" Alias "SetWindowTextA" _ (ByVal hwnd As Long, ByVal lpString As String) As Long
    Пример 6.1.
    Закрыть окно




    Public Sub WorkWithWindows() Dim Res As Long ' Результат выполнения функции Dim HandleAW As Long 'Описатель активного окна Dim RectAW As RECT 'Структура, задающая прямоугольник окна Dim TextAW As String 'Заголовок активного окна Dim LenTextAW As Long 'Длина строки Dim HandleW As Long 'Описатель окна Dim TextW As String 'Заголовок окна
    'Получить описатель активного окна HandleAW = GetActiveWindow Debug.Print HandleAW
    'Получить прямоугольник, задающий положение активного окна Res = GetWindowRect(HandleAW, RectAW) Debug.Print Res If Res > 0 Then 'OK Debug.Print "Размеры окна: Left = ", RectAW.Left, " Top = ", _ RectAW.Top, " Right = ", RectAW.Right, " Bottom = ", RectAW.Bottom Else: MsgBox ("Не удалось получить размеры активного окна") End If 'Получить заголовок окна 'Предварительная набивка результирующей строки нулевыми символами TextAW = VBA.String$(255, vbNullChar) LenTextAW = VBA.Len(TextAW) Res = GetWindowText(HandleAW, TextAW, LenTextAW) Debug.Print Res If Res > 0 Then 'OK TextAW = VBA.Left(TextAW, VBA.InStr(1, TextAW, vbNullChar) - 1) Debug.Print TextAW Else: MsgBox ("Не удалось получить заголовок активного окна") End If
    'Поиск окна документа по его заголовку 'Возвращается описатель окна TextW = "DocOne6 - Microsoft Word" HandleW = FindWindow(vbNullString, TextW) If HandleW > 0 Then 'OK Debug.Print HandleW Else: MsgBox ("Не удалось найти окно с указанным заголовком" _ & vbCrLf & TextW) End If
    'Минимизация и нормализация окна документа Res = ShowWindow(HandleW, SW_SHOWMINIMIZED) If Res > 0 Then Debug.Print "Окно минимизировано" Res = ShowWindow(HandleW, SW_SHOWNORMAL) If Res > 0 Then Debug.Print "Окно в нормальном состоянии"
    'Изменение заголовка окна TextW = "Document1 - Microsoft Word" HandleW = FindWindow(vbNullString, TextW) If HandleW > 0 Then 'OK Debug.Print HandleW Else: MsgBox ("Не удалось найти окно с указанным заголовком" _ & vbCrLf & TextW) End If Res = SetWindowText(HandleW, "DocTwo6 - Microsoft Word") End Sub
    Пример 6.2.
    Закрыть окно




    Option Explicit 'Константы Public Const PROCESSOR_INTEL_386 = 386 Public Const PROCESSOR_INTEL_486 = 486 Public Const PROCESSOR_INTEL_PENTIUM = 586 Public Const PROCESSOR_MIPS_R4000 = 4000 Public Const PROCESSOR_ALPHA_21064 = 21064 ' Типы Type SYSTEM_INFO dwOemID As Long dwPageSize As Long lpMinimumApplicationAddress As Long lpMaximumApplicationAddress As Long dwActiveProcessorMask As Long dwNumberOrfProcessors As Long dwProcessorType As Long dwAllocationGranularity As Long dwReserved As Long End Type Type OSVERSIONINFO dwOSVersionInfoSize As Long dwMajorVersion As Long dwMinorVersion As Long dwBuildNumber As Long dwPlatformId As Long szCSDVersion As String * 128 End Type Type MEMORYSTATUS dwLength As Long dwMemoryLoad As Long dwTotalPhys As Long dwAvailPhys As Long dwTotalPageFile As Long dwAvailPageFile As Long dwTotalVirtual As Long dwAvailVirtual As Long End Type 'Операторы Declare Declare Function GetVersionEx Lib "kernel32" Alias "GetVersionExA" _ (LpVersionInformation As OSVERSIONINFO) As Long Declare Sub GlobalMemoryStatus Lib "kernel32" (lpBuffer As _ MEMORYSTATUS) Declare Sub GetSystemInfo Lib "kernel32" (lpSystemInfo As _ SYSTEM_INFO)
    Пример 6.3.
    Закрыть окно




    Public Sub WorkWithStatus() Dim res As Long ' Результат выполнения функции Dim msg As String ' Формируемое сообщение Dim verinfo As OSVERSIONINFO 'Информация об ОС и ее версиях Dim sysinfo As SYSTEM_INFO 'Системная информация Dim memstatus As MEMORYSTATUS 'Информация о статусе памяти
    verinfo.dwOSVersionInfoSize = Len(verinfo) res = GetVersionEx(verinfo) If res > 0 Then Select Case verinfo.dwPlatformId Case 0 msg = "Windows 32s " Case 1 msg = "Windows 95/98 " Case 2 msg = "Windows NT " End Select msg = msg & verinfo.dwMajorVersion & "." & verinfo.dwMinorVersion msg = msg & " (Build " & verinfo.dwBuildNumber & ")" & vbCrLf Debug.Print msg Else MsgBox ("Не могу получить версию операционной системы") End If
    ' определение типа процессора GetSystemInfo sysinfo msg = "Процессор: " Select Case sysinfo.dwProcessorType Case PROCESSOR_INTEL_386 msg = msg & "Intel 386" & vbCrLf Case PROCESSOR_INTEL_486 msg = msg & "Intel 486" & vbCrLf Case PROCESSOR_INTEL_PENTIUM msg = msg & "Intel Pentium" & vbCrLf Case PROCESSOR_MIPS_R4000 msg = msg & "MIPS R4000" & vbCrLf Case PROCESSOR_ALPHA_21064 msg = msg & "DEC Alpha 21064" & vbCrLf Case Else msg = msg & "(unknown)" & vbCrLf
    End Select Debug.Print msg msg = "Число процессоров: " & sysinfo.dwNumberOrfProcessors & vbCrLf Debug.Print msg msg = "Размер страницы: " & sysinfo.dwPageSize & vbCrLf Debug.Print msg msg = "Минимальный адрес приложения: " & sysinfo.lpMinimumApplicationAddress & vbCrLf Debug.Print msg msg = "Максимальный адрес приложения: " & sysinfo.lpMaximumApplicationAddress & vbCrLf Debug.Print msg
    ' Получение характеристик памяти GlobalMemoryStatus memstatus msg = "Физическая память. Всего: " & _ VBA.Format$(memstatus.dwTotalPhys \ 1024, "###,###,###") & "K" & vbCrLf Debug.Print msg
    msg = "Физическая память. Доступно: " & _ VBA.Format$(memstatus.dwAvailPhys \ 1024, "###,###,###") & "K" & vbCrLf Debug.Print msg
    msg = "Виртуальная память. Всего: " & _ VBA.Format$(memstatus.dwTotalVirtual \ 1024, "###,###,###") & "K" & vbCrLf Debug.Print msg msg = "Виртуальная память. Доступно: " & _ VBA.Format$(memstatus.dwAvailVirtual \ 1024, "###,###,###") & "K" & vbCrLf Debug.Print msg msg = "Длина слова: " & memstatus.dwLength & vbCrLf Debug.Print msg msg = "Загрузка памяти: " & memstatus.dwMemoryLoad & vbCrLf Debug.Print msg
    End Sub
    Пример 6.4.
    Закрыть окно




    Public Sub WorkWithUniFunc() Dim res As Long Dim Capt As String 'Заголовок 'Динамический массив байтов для передачи строки заголовка Dim HandleW As Long 'Описатель окна
    'Поиск окна по заголовку Capt = "Document1 - Microsoft Word" HandleW = FindWindowA(vbNullString, Capt) If HandleW > 0 Then 'OK Debug.Print HandleW Else: MsgBox ("FindWindowA не может найти окно с заголовком" & vbCrLf & Capt) End If 'Попытки использовать для поиска Unicode функцию 'FindWindowW не увенчались успехом ' ReDim ArCapt(0 To 2 * VBA.Len(Capt)) As Byte ' ArCapt = Capt & vbNullChar ' Debug.Print ArCapt ' HandleW = FindWindowW(0&, ArCapt(0)) ' If HandleW > 0 Then 'OK ' Debug.Print HandleW ' Else: MsgBox ("Не могу вызвать UniCode FindWindowW") ' End If 'Получить заголовок окна ArCapt = VBA.String$(128, vbNullChar) res = GetWindowText(HandleW, ArCapt(0), 128) If res > 0 Then 'OK Debug.Print ArCapt Else: MsgBox ("не получен заголовок окна") End If
    'Изменить заголовок окна Capt = "NewDoc" ArCapt = Capt & vbNullChar res = SetWindowText(HandleW, ArCapt(0))
    'Повторно получить заголовок окна ArCapt = VBA.String$(128, vbNullChar) res = GetWindowText(HandleW, ArCapt(0), 128) If res > 0 Then 'OK Debug.Print ArCapt Else: MsgBox ("не получен заголовок окна") End If End Sub
    Пример 6.5.
    Закрыть окно




    Public Sub WorkWithApiErr() Dim Res As Long Dim capt As String 'Заголовок Dim HandleW As Long 'Описатель окна
    'Поиск окна по заголовку capt = "DocOne6 - Microsoft Word" ArCapt = capt & vbNullChar Debug.Print ArCapt HandleW = FindWindowW(0&, ArCapt(0)) If HandleW > 0 Then 'OK Debug.Print HandleW Else: MsgBox ("Не могу корректно вызвать UniCode FindWindowW") If Err.LastDllError = ERROR_INVALID_NAME Then Debug.Print "Не корректно задано имя при вызове Unicode FindWindowW функции!" End If End If 'Еще один эксперимент: вначале получим заголовок активного окна, 'затем найдем окно по заголовку, работая в Unicode кодировке. HandleW = GetActiveWindow()
    'Получить заголовок окна ArCapt = VBA.String$(128, vbNullChar)
    Res = GetWindowText(HandleW, ArCapt(0), 128) If Res > 0 Then 'OK Debug.Print ArCapt Else: MsgBox ("не получен заголовок окна") End If
    ArCapt = VBA.Left(ArCapt, Res) HandleW = FindWindowW(0&, ArCapt(0)) If HandleW > 0 Then 'OK Debug.Print HandleW Else: MsgBox ("Не могу корректно вызвать UniCode FindWindowW") If Err.LastDllError = ERROR_INVALID_NAME Then Debug.Print "Не корректно задано имя при вызове Unicode FindWindowW функции!" End If End If End Sub
    Пример 6.6.
    Закрыть окно




    Public Function EnumWindowsProc(ByVal HandleW As Long, _ ByVal lParam As Long) As Long Dim TextW As String Dim LenTextW As Long Dim Res As Long
    'Добавить описатель в коллекцию HandleCol.Add HandleW
    'Получить заголовок окна. TextW = VBA.String$(255, vbNullChar) LenTextW = VBA.Len(TextW) Res = GetWindowText(HandleW, TextW, LenTextW) If Res > 0 Then 'Добавить заголовок в коллекцию TextW = VBA.Left(TextW, Res) CaptCol.Add TextW End If
    'Получить класс окна. TextW = VBA.String$(255, vbNullChar) LenTextW = VBA.Len(TextW) Res = GetClassName(HandleW, TextW, LenTextW) If Res > 0 Then 'Добавить имя класса в коллекцию TextW = VBA.Left(TextW, Res) ClassNameCol.Add TextW End If EnumWindowsProc = 1 End Function
    Пример 6.7.
    Закрыть окно




    Public Sub GetCaptions() ' Вызов Win32 API функции EnumWindows, 'вызывающей в свою очередь Callback функцию EnumWindowsProc Dim item As Variant Dim Res As Long
    Res = EnumWindows(AddressOf EnumWindowsProc, 0&)
    'Обработка глобальных переменных, определенных в 'результате совместной работы EnumWindows и EnumWindowsProc Debug.Print "Число окон = ", HandleCol.Count Debug.Print "Описатели окон" Res = 0 For Each item In HandleCol Debug.Print item Res = Res + 1 If Res > 10 Then Exit For Next item
    Debug.Print "Число окон с заголовками= ", CaptCol.Count Debug.Print "Заголовки окон" Res = 0 For Each item In CaptCol Debug.Print item Res = Res + 1 If Res > 10 Then Exit For Next item
    Debug.Print "Число окон, возвращающих класс = ", ClassNameCol.Count Debug.Print "Имена классов окон" Res = 0 For Each item In ClassNameCol Debug.Print item Res = Res + 1 If Res > 10 Then Exit For Next item End Sub
    Пример 6.8.
    Закрыть окно




    Число окон = 254 Описатели окон 3735790 131912 131888 131916 65684 40370412 917748 262866 852650 852668 131844 Число окон с заголовками =76 Заголовки окон Continue Microsoft Agent Microsoft Office Shortcut Bar Menu Parent Window NetDDE Agent Edit Microsoft Visual Basic - DocOne6 [running] - [ОбратныйВызов (Code)] Ch6 - Microsoft Word Edit Properties Microsoft Office Shortcut Bar Число окон, возвращающих класс =254 Имена классов окон OfficeTooltip tooltips_class32 ComboLBox tooltips_class32 tooltips_class32 AgentAnimBalloon AgentAnim tooltips_class32 tooltips_class32 tooltips_class32 tooltips_class32
    Пример 6.9.
    Закрыть окно




    Option Explicit
    Public Declare Function EnumWindows Lib "user32" _ (ByVal lpEnumFunc As Long, ByVal lParam As Long) As Long
    Public Declare Function EnumWindows1 Lib "user32" Alias "EnumWindows" _ (ByVal lpEnumFunc As Long, lParam As Any) As Long
    Public HandleCol As New Collection Public HandleCol1 As New Collection
    Public Function EnumWindowsProc(ByVal HandleW As Long, _ ByVal lParam As Long) As Long
    HandleCol.Add HandleW EnumWindowsProc = 1 End Function
    Public Function EnumWindowsProc1(ByVal HandleW As Long, _ lParam As Collection) As Long
    lParam.Add HandleW EnumWindowsProc1 = 1 End Function
    Public Sub GetHandles()
    Dim item As Variant Dim Res As Long
    Res = EnumWindows(AddressOf EnumWindowsProc, 0&)
    Debug.Print "Number of windows - ", HandleCol.Count Debug.Print "Their handles: " Res = 0 For Each item In HandleCol Debug.Print item Res = Res + 1 If Res > 10 Then Exit For Next item
    End Sub
    Public Sub GetHandles1()
    Dim item As Variant Dim Res As Long
    Res = EnumWindows1(AddressOf EnumWindowsProc1, HandleCol1)
    Debug.Print "Number of windows - ", HandleCol1.Count Debug.Print "Their handles: " Res = 0 For Each item In HandleCol1 Debug.Print item Res = Res + 1 If Res > 10 Then Exit For Next item
    End Sub
    Пример 6.10.
    Закрыть окно




    'Функции работы с таймером Public Declare Function SetTimer Lib "user32" (ByVal hwnd As Long, _ ByVal nIDEvent As Long, ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long
    Public Declare Function KillTimer Lib "user32" (ByVal hwnd As Long, ByVal nIDEvent As Long) As Long
    'Глобальная информация Public Counter As Long 'Счетчик числа вызовов Callback функции Public IdEv As Long
    Public Sub HowManyProc(ByVal HandleW As Long, ByVal msg As Long, _ ByVal idEvent As Long, ByVal TimeSys As Long) 'Функция обратного вызова. Вызывается при обработке сообщения WM_Timer, 'посылаемого таймером, созданным процедурой SetTimer
    Counter = Counter + 1 Debug.Print "Hi", Counter
    End Sub
    Public Sub Start()
    'Создает таймер, вызывая Win32 Api функцию SetTimer Counter = 0 IdEv = SetTimer(0&, 0&, 10000, AddressOf HowManyProc) If IdEv = 0 Then MsgBox ("Не удалось создать таймер!") Else Debug.Print "Создан Таймер: Идентификатор = ", IdEv End If
    End Sub
    Public Sub Finish() 'Удаляет таймер If IdEv > 0 Then Call KillTimer(0&, IdEv) Debug.Print "Удален Таймер: Идентификатор = ", IdEv IdEv = 0 End If
    End Sub
    Пример 6.11.
    Закрыть окно




    Option Explicit ' Класс ВашТаймер служит упаковкой функций WIN32 API работы с таймером 'Интерфейс класса будут составлять две функции: 'СоздатьТаймер, УдалитьТаймер и свойство ИнтервалТаймера
    'При работе с классом необходимо описать Callback функцию по следующему образцу:
    'Public Sub TimerProc(ByVal HandleW As Long, ByVal msg As Long, _ ' ByVal idEvent As Long, ByVal TimeSys As Long) ' 'Функция обратного вызова. Вызывается при обработке сообщения WM_Timer, ' 'посылаемого таймером, созданным процедурой SetTimer ' ' 'Поместите здесь свой код! ' 'End Sub
    'Функции Win32 API для работы с таймером Private Declare Function SetTimer Lib "user32" (ByVal hwnd As Long, _ ByVal nIDEvent As Long, ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long
    Private Declare Function KillTimer Lib "user32" (ByVal hwnd As Long, ByVal nIDEvent As Long) As Long
    'Свойства: Интервал - хранит значение интервала посылки сообщений Private Интервал As Long 'Идентификатор таймера Private IdEv As Long
    Public Sub СоздатьТаймер() 'Создает таймер, вызывая Win32 Api функцию SetTimer IdEv = SetTimer(0&, 0&, Интервал, AddressOf TimerProc) If IdEv = 0 Then MsgBox ("Не удалось создать таймер!") Else Debug.Print "Создан Таймер: Идентификатор = ", IdEv End If
    End Sub
    Public Sub УдалитьТаймер() 'Удаляет таймер If IdEv > 0 Then Call KillTimer(0&, IdEv) Debug.Print "Удален Таймер: Идентификатор = ", IdEv IdEv = 0 End If End Sub
    Public Property Get ИнтервалТаймера() As Long ИнтервалТаймера = Интервал End Property
    Public Property Let ИнтервалТаймера(ByVal NewValue As Long) Интервал = NewValue End Property
    Private Sub Class_Initialize() Интервал = 1000 End Sub
    Private Sub Class_Terminate() УдалитьТаймер End Sub
    Пример 6.12.
    Закрыть окно




    Option Explicit 'Модуль Таймер1 ' Глобальная информация Public Counter As Long 'Счетчик числа вызовов Callback функции Public MyTimer As New ВашТаймер
    Public Sub Start1() MyTimer.ИнтервалТаймера = 5000 MyTimer.СоздатьТаймер End Sub
    Public Sub Finish1() MyTimer.УдалитьТаймер End Sub
    Public Sub TimerProc(ByVal HandleW As Long, ByVal msg As Long, _ ByVal idEvent As Long, ByVal TimeSys As Long) 'Функция обратного вызова. Вызывается при обработке сообщения WM_Timer, 'посылаемого таймером, созданным процедурой SetTimer
    Counter = Counter + 1 Debug.Print "Hi", Counter
    End Sub
    Пример 6.13.
    Закрыть окно



    Пример создания, работы и удаления таймера

    В свое время в книге по языку Visual C++ , демонстрируя работу с таймером и соответствующими функциями Win32 API, мы разработали проект "Жизнь". В этом проекте моделируется известная компьютерная игра, где можно задать начальную конфигурацию "жизни". Затем эта конфигурация начинает жить, изменяя свое состояние по заданным правилам. Изменение состояния происходит в качестве ответной реакции на сообщения таймера. Другим подобным примером, по существу вариацией на эту же тему, является создание экранных заставок. Сейчас мы решили обойтись более простым примером, демонстрирующим суть проблемы, но не имеющим эффектной формы. В нашем тестовом примере есть две командные кнопки Start и Finish. В ответ на нажатие первой кнопки создается таймер, соответствующая ему Callback функция ведет подсчет числа ее вызов и уведомляет об этом, печатая значение счетчика в окне отладки. При нажатии кнопки Finish таймер удаляется. Кнопки можно нажимать многократно. Все процедуры обработки помещены в модуль Таймер. Вот его текст:
    Пример 6.11.
    (html, txt)
    Комментариев, приведенных в тексте, полагаем достаточно для понимания всех деталей. Приведем еще результаты печати, периодически появляющиеся в окне отладки. Следует только сказать, что дважды были поочередно нажаты кнопки Start и Finish:
    Создан Таймер: Идентификатор =32578 Hi 1 Hi 2 Hi 3 Удален Таймер: Идентификатор =32578 Создан Таймер: Идентификатор =32573 Hi 1 Hi 2 Hi 3 Hi 4 Удален Таймер: Идентификатор =32573
    Заметьте, в нашей реализации кнопки нужно нажимать поочередно, поскольку хранится только последнее значение идентификатора таймера, так что если подряд нажать несколько раз кнопку Start, то будет создано несколько таймеров, но при последующих нескольких нажатиях кнопки Finish будет удален только один, последний созданный таймер и печать в окне отладки будет продолжаться.

    Примеры работы с Win32 API функциями

    Разговор об особенностях вызова Win32 API функций еще не закончен. Тем не менее, пора прервать общее изложение и обратиться к примерам. Многое из того, что было сказано, целесообразно проиллюстрировать примерами работы с конкретными функциями Win 32 API. Каждый из наших примеров будет посвящен работе с некоторой группой функций.

    Работа с окнами

    Как мы уже говорили, окна - это один из основных объектов операционной системы. Функции для работы с ними находятся, в основном, в библиотеке User32. Из большого множества функций мы отобрали несколько функций, позволяющих продемонстрировать, как можно получать описатели окон, как, зная описатель, можно получать характеристики окон и как можно изменять характеристики окон. Начнем с приведения программного текста, а уж потом подробно прокомментируем его. Заметим, что для получения корректного описания операторов Declare, используемых типов данных и констант мы использовали API Viewer и описание функций, которое можно найти на уже упоминавшемся сервере Microsoft.
    В проекте нашего тестового документа был создан модуль "Окна" и в разделе его объявлений помещен следующий текст:
    Пример 6.1.
    (html, txt)
    Дадим краткую характеристику используемых функций:
  • GetActiveWindow возвращает описатель активного окна.
  • GetWindowRect получает в качестве входного параметра описатель окна hwnd и возвращает значения полей структуры Rect, переданной функции в качестве второго параметра lpRect. Заметьте, тип Rect должен быть предварительно определен. Обратите внимание, первый параметр передается по значению, а второй по ссылке. Передача по ссылке позволяет функции заполнить значениями поля переданной ей структуры. Значения этих полей задают координаты прямоугольника, определяющего положение окна на экране дисплея. Возвращаемый функцией результат будет равен нулю, если выполнение функции закончится неуспехом, например, при некорректном задании описателя.
  • GetWindowText по описателю окна возвращает его заголовок. Поскольку функция должна вернуть строку, то, как мы говорили ранее, ей передаются два параметра - lpString и cch, задающие строку и число символов, доступных для заполнения. Обратите внимание, здесь используется псевдоним с окончанием A, указывающим на использование кодировки ANSI. Опять-таки, результат, возвращаемый функцией, зависит от успеха операции.
  • FindWindow возвращает описатель окна.
    Функция ищет окно и возвращает в случае успеха описатель этого окна. Окно ищется либо по имени класса, заданного параметром lpClassName, либо по заголовку, заданному параметром lpWindowName. При вызове может быть задан только один из этих параметров, второй может быть указателем с неопределенным значением.. Поскольку параметр может быть либо указателем, имеющим тип Long, либо строкой, то в предыдущих версиях по этой причине для обоих параметров следовало указывать тип Any. Теперь указывается тип String, поскольку введена специальная константа vbNullString, формально имеющая тип String, но позволяющая передать указатель со значением Null.
  • ShowWindow позволяет сделать видимым или невидимым окно, заданное описателем, может также минимизировать, максимизировать или нормализовать его размеры. Соответствующее действие определяется вторым параметром, значения которого задаются соответствующей константой. В разделе объявлений модуля мы определили возможные значения этих констант, которые будут использованы в процессе вызова этой функции.
  • SetWindowText позволяет установить новый заголовок окна. Является функцией, парной к функции GetWindowText, но, поскольку здесь строка заголовка передается функции, то функция устроена проще, имеет два, а не три параметра и передавать ей можно константу, что и будет продемонстрировано в нашем примере.


  • Приведем теперь процедуру, в которой поочередно запускаются описанные здесь функции (В примерах используются документы: DocOne6, DocTwo6

    Пример 6.2.

    (html, txt)

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

    655706 1 Размеры окна: Left = 8 Top = 73 Right = 769 Bottom = 580 58 Microsoft Visual Basic - DocOne6 [running] - [Окна (Code)] 6684884 Окно минимизировано Окно в нормальном состоянии 191103490

    Дадим комментарии к работе этой процедуры:

  • Вначале, при вызове функции API GetActiveWindow был получен описатель активного окна.


    Заметим, что это было окно кода выполняемой процедуры. Значение этого описателя равно 655706.
  • Затем, при вызове функции GetWindowRect, был получен прямоугольник, определяющий положение окна на экране. Функция успешно завершила работу и вернула результат, равный 1 (истина). Как и в остальных случаях, по окончании выполнения функции мы проверили результат на успешность завершения.
  • На следующем шаге был получен заголовок активного окна. Как видно из распечатки заголовка, активным являлось выполняемое окно кода. Результат 58, который вернула функция GetWindowText, задает число символов результирующей строки. Обратите внимание, перед вызовом функции передаваемая ей строка была инициализирована нулевыми символами. После успешного завершения из строки был выделен результат, задающий заголовок. Его печать показывает, что активным в момент запуска функции было выполняемое окно кода. Для выделения заголовка из строки использовался нулевой символ, как признак окончания заголовка. Для решения этой задачи можно было использовать и число возвращаемых символов - значение, возвращаемое функцией.
  • Следующим шагом было получение описателя окна по заданному заголовку. В качестве такового был использован заголовок окна с тестовым документом. Функция FindWindow нашла такое окно и вернула его описатель, равный 6684884. Обратите внимание, первый параметр был задан константой vbNullString.
  • Окно, описатель которого был получен в последнем вызове, дважды перестраивалось, - вначале минимизировалось, затем нормализовалось, - при двух вызовах функции ShowWindow с различными значениями констант.
  • На последнем этапе снова вызывалась функция FindWindow для нахождения описателя вновь открытого документа. Затем при вызове функции SetWindowText был изменен заголовок этого окна. Заметьте, новое значение заголовка передавалось функции в виде обычной строковой константы.



  • 'Изменение заголовка окна TextW = "Document1 - Microsoft Word" HandleW = FindWindow(vbNullString, TextW) If HandleW > 0 Then 'OK Debug.Print HandleW Else: MsgBox ("Не удалось найти окно с указанным заголовком" _ & vbCrLf & TextW) End If Res = SetWindowText(HandleW, "DocTwo6 - Microsoft Word") End Sub

    Пример 6.2.

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

    655706 1 Размеры окна: Left = 8 Top = 73 Right = 769 Bottom = 580 58 Microsoft Visual Basic - DocOne6 [running] - [Окна (Code)] 6684884 Окно минимизировано Окно в нормальном состоянии 191103490

    Дадим комментарии к работе этой процедуры:

  • Вначале, при вызове функции API GetActiveWindow был получен описатель активного окна. Заметим, что это было окно кода выполняемой процедуры. Значение этого описателя равно 655706.
  • Затем, при вызове функции GetWindowRect, был получен прямоугольник, определяющий положение окна на экране. Функция успешно завершила работу и вернула результат, равный 1 (истина). Как и в остальных случаях, по окончании выполнения функции мы проверили результат на успешность завершения.
  • На следующем шаге был получен заголовок активного окна. Как видно из распечатки заголовка, активным являлось выполняемое окно кода. Результат 58, который вернула функция GetWindowText, задает число символов результирующей строки. Обратите внимание, перед вызовом функции передаваемая ей строка была инициализирована нулевыми символами. После успешного завершения из строки был выделен результат, задающий заголовок. Его печать показывает, что активным в момент запуска функции было выполняемое окно кода. Для выделения заголовка из строки использовался нулевой символ, как признак окончания заголовка. Для решения этой задачи можно было использовать и число возвращаемых символов - значение, возвращаемое функцией.
  • Следующим шагом было получение описателя окна по заданному заголовку.В качестве такового был использован заголовок окна с тестовым документом. Функция FindWindow нашла такое окно и вернула его описатель, равный 6684884. Обратите внимание, первый параметр был задан константой vbNullString.
  • Окно, описатель которого был получен в последнем вызове, дважды перестраивалось, - вначале минимизировалось, затем нормализовалось, - при двух вызовах функции ShowWindow с различными значениями констант.
  • На последнем этапе снова вызывалась функция FindWindow для нахождения описателя вновь открытого документа. Затем при вызове функции SetWindowText был изменен заголовок этого окна. Заметьте, новое значение заголовка передавалось функции в виде обычной строковой константы.


  • Соответствие между простыми типами данных

    Нижеследующая таблица описывает соответствие между некоторыми, простыми типами данных двух языков:

    Таблица 6.1. Соответствие между типами языков C и VBAC/C++ тип данныхВенгерская нотацияОписаниеТип языка VBA
    BOOL b8-и битное булево значение. Значение 0 эквивалентно False, ненулевое значение - True Boolean или Long
    BYTE ch8-и битное целое без знака Byte
    HANDLE h32 -х битное целое без знака, задающее описатель Windows -объектов Long
    int n2-х байтное целое со знаком Integer
    UINT u2-х байтное целое без знака Long
    DWORD dw4-х байтное целое без знака Long
    long l4-х байтное целое со знаком Long
    LP lp32-х битный указатель на C/C++ структуры, строки, функции или другие данные в памяти Long
    LPZSTR lpsz32-х битный указатель на C строку, завершаемую нулем Long

    При преобразовании данных между типами UINT и DWORD и типом Long могут возникнуть проблемы, если заданы некорректные значения этих данных. Понятно, что если параметр функции API объявлен как UINT, то возникнет ошибка при попытке передать через тип Long отрицательное значение или длинное целое, превосходящее значение, допустимое для типа UINT. Конечно, нужно быть или осторожным или ввести собственные типы данных, где все необходимые проверки будут выполняться.

    Строковые аргументы при вызове функций Win32 API

    Как мы уже говорили, строки передаются по ссылке, даже если у параметра указан описатель ByVal. Не возникает никаких проблем при передаче строки в функцию Win32 API. Передаваемый аргумент может быть произвольным строковым выражением, в том числе переменной типа String или строковой константой. Сложнее дело обстоит, если функция должна вернуть строку, в качестве результата. Прежде всего, напомним, что в функциях Win32 API тип возвращаемого значения никогда не является строкой, - это всегда целочисленное значение, чаще всего булево значение, указывающее на то, удачно ли завершилось выполнение функции. Поэтому, если нужно получить строку в качестве результата, то функции передается два параметра - строка и ее длина, описанные чаще всего следующим образом:
    ByVal lpResultStr As String, ByVal LenResultString As Long
    Поскольку результат будет формироваться непосредственно в области памяти, отведенной строке lpResultStr, то перед вызовом эта строка должна удовлетворять следующим условиям:
  • она должна быть строкой, завершаемой нулем. Чаще всего она вся состоит из нулевых символов.
  • Ее длина должна быть достаточной для того, чтобы вместить результирующую строку.
  • Параметр LenResultString должен указывать максимально допустимое число символов результирующей строки.

  • С учетом этих требований перед вызовом функций, возвращающих строковые значения, как правило, передаваемая функции строка инициализируется и набивается подходящим количеством нулевых символов. Напомним, нулевой символ задается константой vbNullChar. Обычно, это делается следующим образом:
    Const MaxSize = 255 As Long lpResultString = String$(MaxSize, vbNullChar) LenResultString = Len(lpResultString)
    После того, как функция благополучно завершит работу, приходится выделять из строки возвращенное значение. Если строка до вызова состояла сплошь из нулевых символов, то результат после вызова представляет ее префикс, заканчивающийся первым нулевым значением. В этом случае результат легко выделяется и это причина того, почему целесообразно строку инициализировать нулевыми символами.

    Структуры языка C и тип, определенный пользователем, в языке VBA

    В языке C можно определять записи - совокупность данных разного типа. Такие записи в языке C называются структурами. Обычно, вначале определяется соответствующий структурный тип, а затем имя этого типа используется при объявлении конкретных переменных. Структуры могут передаваться в качестве аргументов при вызове функций, в том числе при вызове Win32 API функций. В VBA, как известно, для задания подобного структурного типа используется конструкция Type…End Type, называемая типом, определенным пользователем. Переменные такого типа передаются вызываемой функции обычным способом: X As T, где T - имя пользовательского типа. Позже мы приведем пример вызова функции Win32 API, которой передается структура в качестве аргумента.

    Тип Any

    Иногда, параметр функции API может принимать значения различных типов. В этом случае в операторе Declare нельзя указать один конкретный тип этого параметра. Для решения подобной проблемы и был введен специальный тип Any. Если параметр имеет этот тип, то в период трансляции не проверяется правильность соответствия типов и аргумент может принимать значения любых типов. Конечно, нужно иметь в виду, что функция может принимать значения хоть и нескольких, но совершенно определенных типов. Поэтому при работе с типом Any на программиста возлагается вся ответственность за корректную передачу значения функции API. Альтернативой применения типа Any является задание для одной функции нескольких операторов Declare, - по одному на каждый возможный тип параметра. Тогда в каждой конкретной ситуации будет вызываться соответствующая функция с нужным типом параметра.

    VBA и Win32 API

    Работая на VBA, неявно всегда приходится иметь дело с функциями Win32 API, только вызов их упрятан в вызываемых VBA функциях или методах объектов Office 2000. Так, например, при работе с объектом Shape так или иначе будут вызываться функции GDI32, обеспечивающие работу с графикой, при работе c функциями VBA.Interaction, например GetSettings, SaveSettings и другими, будет вызываться соответствующие функции работы с реестром Windows, хранящиеся в библиотеках User32 и advapi32. Такой косвенный вызов имеет свои преимущества, обеспечивая определенную безопасность в работе VBA программ. Но в ряде случаев VBA программисту необходим доступ ко всем возможностям операционной системы, предоставляемым Win32 API интерфейсом. Естественно, в этом случае он понимает, что на него ложится большая ответственность в обеспечении корректного вызова функций, поскольку ошибки в вызове могут привести к непредвиденным отказам в работе программы.

    Void функции языка C

    В языке С формально нет процедур, есть только функции. В тех случаях, когда по существу речь идет о процедурах, вычисляющих не один скалярный результат, а имеющих несколько выходных параметров, как правило, используется функция, результат которой говорит об успешности выполнения процедуры. Если результат функции имеет значение True, то все выходные параметры благополучно вычислены, в противном случае следует проанализировать причину неуспеха. Типично для языка C то, что вызов функций является условием оператора If, - оператор If задает упаковку вызова функции, позволяя не только вызвать функцию, но и проверить корректность завершения ее работы. При трансляции заголовков таких функций в язык VBA, они естественно транслируются в функции VBA и для работы с ними можно сохранить стиль языка C.
    Однако в языке C используются и функции, не возвращающие результата. Результат таких функций задается описателем Void, по существу, они являются процедурами. И при трансляции их в VBA их и следует задавать в виде процедур.

    Вызов аргументов по ссылке ByRef и по значению ByVal

    В языке C++ основным способом передачи параметров является передача их по значению, в VBA - по ссылке. Конечно, в обоих языках применяются оба способа. Тем не менее, нужно понимать, что описатель ByVal очень часто будет встречаться при вызове Win32 API функций, значительно чаще, чем при вызове обычных VBA функций. Если используется API Viewer, то этот описатель автоматически будет появляться для тех параметров, где необходима подобная форма вызова. Следует обратить внимание на одно обстоятельство. Несмотря на то, что такой описатель может появляться и для строковых аргументов, строки всегда передаются в функции Win32 API по ссылке. Последнее обстоятельство также связано со спецификой работы со строками в функциях языка C, на которых следует остановиться подробнее.

    Вызов функций и оператор Declare

    Элементы ActiveX, COM объекты могут экспонировать свой интерфейс, - свои свойства и методы. Это означает, что они уведомляют, предоставляют информацию клиентам о своем интерфейсе. Технически это обеспечивается тем, что эти объекты, наряду с DLL, сопровождаются TypeLib - библиотекой типов, в которой содержится в требуемом виде информация об интерфейсе объекта. В этом случае, для того чтобы начать работу с объектом, достаточно подключить ссылку на эту библиотеку в меню Tools|References в среде редактора VBE. Эта возможность не раз обсуждалась, когда речь шла о вызове, например, приложения Excel в документах Word. Напомним, что приложения Office 2000 представляют собой ActiveX объекты, построенные на основе COM технологии. Они явно экспонируют свой интерфейс, именно поэтому нет проблем при работе с такими приложениями, вызовами свойств и методов их многочисленных объектов. Библиотеки, составляющие Win32 интерфейс, не сопровождаются библиотеками типов TypeLib. Поэтому необходимо самому программисту уведомить VBA о том, где найти и как следует вызывать ту или иную функцию Win32 API Вызову каждой функции должен предшествовать оператор Declare, описывающий эту функцию. Этот оператор и сама схема вызова библиотечных функций используется при работе с любыми DLL, а не только с теми, которые содержат Win32 API функции. В общем случае в DLL могут храниться как функции, так и процедуры. Два варианта вызова этого оператора соответствуют ссылке на процедуру и на функцию, возвращающую значение. Первый вариант:
    [Public | Private] Declare Sub имя Lib "имя-библиотеки" [Alias "псевдоним"] [([параметры])]
    Во втором случае его синтаксис:
    [Public | Private] Declare Function имя Lib "имя-библиотеки" [Alias "псевдоним"] [([параметры])] [As возвращаемый-тип]
    В этих вызовах ключевые слова и параметры имеют следующий смысл:
  • Ключевое слово Public используется, чтобы сделать объявляемую процедуру доступной всем модулям проекта; ключевое слово Private ограничивает доступ к объявленной процедуре лишь модулем, в котором она объявлена.
    Заметьте, в стандартном модуле можно использовать оба описателя, но в модуле класса разрешается использовать только описатель Private.
  • Ключевое слово Sub в первом случае означает, что речь идет о процедуре; альтернативный ключ Function во втором случае указывает на функцию, возвращающую значение, которое может быть использовано в выражениях.
  • Обязательный параметр имя является именем объявляемой процедуры или функции. Это имя используется при вызовах в VBA программах. Оно может совпадать или отличаться от того имени, под которым процедура (функция) хранится в DLL. Заметьте, для имен функций Win32 API в отличие от Win 16 имеет значение различие между прописными и строчными буквами.
  • После обязательного ключевого слова Lib должно следовать в кавычках имя-библиотеки, содержащей объявляемую процедуру.
  • Ключ Alias позволяет указать, что процедура имеет в DLL другое имя - "псевдоним", благодаря чему можно избежать коллизии имен в программе. Если первый символ параметра " псевдоним" не является признаком числа ( #), псевдоним понимается как имя входной точки DLL для данной процедуры; если же первый символ псевдонима - ( #), следующие за ним число должно задавать порядковый номер входной точки процедуры в DLL. Дело в том, что процедура может иметь несколько точек входа.
  • Необязательный список параметры задает список параметров, передаваемых процедуре при вызове.
  • As возвращаемый-тип во втором варианте оператора задает тип значения, возвращаемого функцией; им может быть любой из базисных типов VBA (не допускаются только строки фиксированной длины), тип объекта или определенный пользователем тип.
  • Список параметры - это список разделенных запятыми параметров процедуры; каждый элемент этого списка имеет вид:

    [Optional] [ByVal | ByRef] [ParamArray] переменная[()] [As тип]

  • Здесь ключ Optional означает, что данный параметр необязателен; при этом все следующие в списке параметры должны быть необязательными и сопровождаться этим же ключом Optional. Этот ключ нельзя применять, если среди параметров есть массив параметров ParamArray.
  • Ключевые слова ByVal и ByRef указывают на то, что параметр передается по значению или по ссылке; по умолчанию в VBA предполагается передача значения по ссылке (ByRef).
  • Ключевое слово ParamArray позволяет задать массив элементов типа Variant; этот параметр должен быть последним в списке и не должен перед собой иметь ключей ByVal, ByRef или Optional; такой массив позволяет передавать в процедуру произвольное (заранее неизвестное) число параметров.
  • Переменная - произвольное допустимое в VBA имя переменной; пустые скобки () после имени переменной означают, что соответствующий параметр - массив.
  • Необязательное определение As тип задает тип параметра, значения которого могут быть такими же, как и у описанного выше определения возвращаемый-тип.


    Задав произвольный тип (As Any), Вы исключите проверку при передаче параметра процедуре.


  • Вот пример задания оператора Declare для двух функций Win32 API:

    Private Declare Function CreateRectRgn Lib "gdi32" Alias "CreateRectRgn" _ (ByVal X1 As Long, ByVal Y1 As Long, ByVal X2 As Long, ByVal Y2 As Long) As Long Private Declare Function GetTempPath Lib "kernel32" _ Alias "GetTempPathA" (ByVal nBufferLength As Long, _ ByVal lpBuffer As String) As Long

    Синтаксически оператор Declare прост и понятен, нужно указать библиотеку, имя функции, под которым она будет вызываться в VBA программе, ее имя (псевдоним) под которым она записана в библиотеке и параметры функции в привычном синтаксисе. Однако реальная жизнь не так проста и в организации вызова функций API есть много подводных камней. Дело в том, что DLL служат средством межязыкового взаимодействия. Сама DLL может быть разработана на одном языке, а вызываться в другом. Тогда возникает проблема правильной передачи параметров, поскольку может не быть точного соответствия между типами данных двух используемых языков. Функции Win32 API разработаны в ориентации на синтаксис языка C и C++. Поэтому при записи оператора Declare требуется корректно указать типы параметров, так чтобы они соответствовали типам, используемым в языке C. Еще одна проблема состоит в том, что помимо оператора Declare, в ряде случаев необходимо предварительно описать требуемые типы данных и константы, необходимые в процессе вызова функции. Так что, прежде чем вызвать функцию из DLL, необходимо корректно задать оператор Declare, описать необходимые типы и константы, - все это может быть не столь простой задачей.

    Вызов функций Win32 API, работающих в Unicode кодировке

    Уже говорилось, что функции API, работающие со строками, вызываются в ANSI кодировке. Сейчас мы попытаемся объяснить причину этого факта, а, с другой стороны, покажем, как можно вызывать функции Win32 API, использующие Unicode кодировку. Заметим, что это может быть важным не столько для функций Win32 API, сколько для других внешних функций, которые могут существовать в кодировке Unicode и не иметь ANSI варианта. Начнем с объяснения ситуации, - почему в VBA внешние функции вызываются в кодировке ANSI. Следует понимать, что строки VBA хранятся в Unicode кодировке и передача строк при вызове внутренних функций внутри VBA происходит в кодировке Unicode. Однако VB и VBA предполагают, что внешний мир устроен по-другому и до сих пор использует кодировку ANSI. Поэтому всякий раз, когда вызываются внешние функции, при вызове происходит преобразование и строковая информация передается и возвращается в кодировке ANSI. По этой причине нельзя вызвать функцию в кодировке Unicode простым изменением псевдонима, задав у него окончание W. Покажем сейчас, как можно "обмануть" VBA, заставив его не выполнять указанных преобразований, что и позволит вызывать функции, корректно работающие в Unicode кодировке. Покажем также, что, как и всякий обман, не всегда все заканчивается благополучно. Тем не менее, с предлагаемым приемом полезно познакомиться. Решение задачи основывается на следующем:
  • При вызове функции вместо строки используется массив байтов, хранящий копию строки. Напомним, что внутри VBA строка хранится в Unicode кодировке, поэтому и массив байтов будет хранить строку в этой кодировке.
  • В операторе Declare необходимо тип String изменить на тип Any, что обеспечит отсутствие проверок и преобразований.
  • Если в операторе Declare для строкового параметра указан спецификатор ByVal, то его необходимо удалить или изменить на ByRef, явно указав передачу параметра по ссылке.

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

  • FindWindow, которая позволяет найти окно по его заголовку, вернув описатель окна в качестве результата,
  • GetWindowText, SetWindowText, позволяющие получить и установить новый заголовок окна.


  • Заметим сразу же, что нам удалось успешно вызвать и корректно работать с двумя последними функциями в Unicode кодировке. Однако этот прием не работает при вызове функции FindWindowW. Несмотря на все попытки, переданная для поиска строка заголовка не приводила к успешному завершению поиска. Но обо всем по порядку. Приведем вначале раздел объявлений модуля с именем Unicode, созданного для работы с этим примером:

    Option Explicit 'Объявление вызываемых функций в Unicode кодировке Public Declare Function FindWindowA Lib "user32" _ (ByVal lpClassName As String, ByVal lpWindowName As String) As Long 'Функции в Unicode кодировке 'Тип string заменен на Any. Передача аргумента по ссылке Public Declare Function FindWindowW Lib "user32" _ (lpClassName As Any, lpWindowName As Any) As Long

    Public Declare Function GetWindowText Lib "user32" Alias "GetWindowTextW" _ (ByVal hwnd As Long, lpString As Any, ByVal cch As Long) As Long

    Public Declare Function SetWindowText Lib "user32" Alias "SetWindowTextW" _ (ByVal hwnd As Long, lpString As Any) As Long

    Public ArCapt() As Byte 'Объявление динамического массива

    Все пояснения уже сделаны и поэтому приведем процедуру этого модуля, вызывающую функции API:

    Пример 6.5.

    (html, txt)

    Приведем результаты отладочной печати:

    327894 Document1 - Microsoft Word

    Document1 - Microsoft Word NewDoc

    Дадим краткие комментарии к тексту процедуры:

  • Работа процедуры начинается с вызова функции API FindWindowA, работающей в кодировке ANSI. Она успешно находит окно, заголовок которого задан переменной Capt. Функция возвращает его описатель.
  • На следующем шаге мы пытались решить эту же задачу, используя функцию API FindWindowW, работающую в кодировке Unicode. В тексте нашел отражение один из вариантов решения.


    К сожалению, поиск во всех случаях заканчивался неуспехом, хотя, как показал побайтный анализ в окне Watch и как показывает отладочная печать, массив байтов ArCapt содержит Unicode копию строки заголовка
  • Полученный описатель окна использовался при вызове Unicode варианта функции GetWindowTextW. Функция корректно работала, используя ссылку на переданный ей массив байтов ArCapt.
  • Затем, используя эту же технику, заголовок окна был изменен при вызове Unicode варианта функции SetWindowTextW.
  • Для контроля повторно была вызвана функция GetWindowTextW. Отладочная печать подтвердила корректность работы.


  • Неудача в вызове функции FindWindowW может быть вызвана разными причинами. Вероятнее всего, при выполнении поиска и проведения операций сравнения строк, применяемый способ не корректен, или, по крайней мере, требует дополнительных уточнений, возможно связанных с длиной строки. С другой стороны, сам прием является некоторой уловкой. Существует более честный способ, хотя, возможно, и более трудоемкий. Для решения задачи можно создать библиотеку типов TypeLib, содержащую описание функций Win32 API в Unicode кодировке, включить ссылку на эту библиотеку и вызывать функции без всяких уловок.

    Основы офисного программирования и язык VBA

    Цикл Do...Loop

    Повторяет блок операторов, пока заданное условие является истинным или пока оно не станет истинным.
    Синтаксис:
    Имеется четыре варианта синтаксиса этого цикла. В двух первых вариантах условие проверяется в начале цикла:
    Do [{While | Until} условие] тело цикла Loop
    В других двух вариантах условие проверяется в конце цикла:
    Do тело цикла Loop [{While | Until} условие]
    Здесь условие является числовым или строковым выражением со значениями True или False. Вообще оно необязательно. Значение Null условия трактуется как False. Тело цикла - это последовательность операторов, которая будет выполняться, пока условие остается истинным, если перед ним стоит ключевое слово While или пока оно остается ложным - в вариантах цикла с ключевым словом Until. Таким образом, циклы вида While условие эквивалентны циклам вида Until Not условие. Кроме того, в тело цикла может входить оператор Exit Do, выполнение которого сразу прекращает цикл и передает управление оператору, непосредственно следующему за Loop. В случае нескольких вложенных циклов Do … Loop оператор Exit Do завершает лишь самый внутренний цикл, в теле которого он расположен.
    Примеры.
    В нашем примере реализованы три варианта поиска по образцу с проверкой условия в начале цикла, в конце цикла и в середине цикла для варианта поиска по образцу с барьером:
    Пример 7.5.
    (html, txt)

    Цикл For Each...Next

    Повторяет заданную последовательность операторов для каждого элемента массива или набора.
    Синтаксис:
    For Each элемент In группа тело цикла Next [элемент]
    Здесь элемент - переменная, которая пробегает в качестве значений элементы коллекций или массива. Для коллекций элемент может быть переменной типа Variant, переменной типа Object или переменной (объектом) некоторого класса. В случае цикла по массиву элемент обязан быть переменной типа Variant. Группа - это имя набора объектов (чаще всего это коллекция объектов) или массива, для элементов которых выполняется цикл. Цикл не применим для массивов, тип элементов которых определен пользователем, так как такие элементы не могут быть значениями переменной типа Variant. В таких массивах можно использовать цикл вида For … Next. Тело цикла - последовательность операторов, выполняемая для каждого элемента набора или массива, - может содержать операторы Exit For, позволяющие прервать выполнение цикла и передать управление оператору, следующему за Next (обычно такой выход происходит при выполнении некоторого условия, проверяемого в операторе If…Then…Else). Указывать переменную элемент после ключевого слова Next не обязательно, но желательно.
    Примеры.
    В примере создается коллекция, число элементов которой и сами элементы выбираются случайным образом. Затем эта коллекция копируется в динамический массив, размерность которого увеличивается в процессе копирования. На последнем этапе массив распечатывается. Циклы типа For …Each прекрасно работают в подобных ситуациях:
    Public Sub ForEach1() Dim X As New Collection Dim Y() As Integer Dim item As Variant Dim i As Integer, Size As Integer
    'Инициализация коллекции Randomize Size = Int(21 * Rnd) For i = 1 To Size X.Add Int(11 * Rnd) Next i
    'Копирование коллекции в динамический массив Size = 1 For Each item In X ReDim Preserve Y(1 To Size) Y(Size) = item Size = Size + 1 Next item
    'Печать элементов динамического массива For Each item In Y Debug.Print item Next item End Sub
    Кроме рассмотренных управляющих операторов VBA содержит доставшиеся в наследство от прежних версий операторы перехода по метке GoTo, перехода по метке с возвратом GoSub...Return и условные операторы перехода по меткам On...GoSub и On...GoTo. Мы никогда не пользуемся этими операторами.

    Цикл While...Wend

    Повторяет выполнение последовательности операторов, пока заданное условие не станет ложным.
    Синтаксис:
    While условие тело цикла Wend
    Здесь условие и тело цикла такие же, как и для цикла Do…Loop. Только для этого вида цикла не предусмотрен оператор выхода Exit. Фактически цикл While…Wend - частный случай цикла Do…Loop - оставлен в языке для совместимости с предыдущими версиями.

    Изменение текущего диска: оператор ChDrive

    Синтаксис:
    ChDrive диск
    Обязательный параметр диск - это строка, задающая имя существующего диска, который должен стать текущим, например:
    ChDrive "D"
    сделает диск "D" текущим.

    Изменение текущего каталога (папки): оператор ChDir

    Синтаксис:
    ChDir путь
    Обязательный параметр путь - это строковое выражение, значение которого задает новый текущий каталог (папку). Если путь не содержит имени диска, меняется текущий каталог на текущем диске. Подчеркнем, что оператор ChDir меняет текущий каталог (папку), но не диск. Например, если текущим является диск С, команда:
    ChDir "D:\TMP"
    изменит текущий каталог на диске D, но диск C останется текущим.

    Копирование файлов: оператор FileCopy

    Синтаксис:
    FileCopy файл-источник, файл-результат
    Параметр файл-источник - строковое выражение, задающее имя копируемого файла, Файл-результат - строковое выражение, которое определяет имя результирующего файла. Оба имени могут включать имена дисков, каталогов или папок. Открытый в данный момент файл копировать нельзя.
    Пример:
    'Копирование файла FileCopy "Temp1\Example1.xls", "Temp2\Example1.xls"

    Моделирование ввода с клавиатуры: оператор SendKeys

    Посылает один или несколько кодов символов в текущее активное окно, как если бы соответствующие клавиши были нажаты.
    Синтаксис:
    SendKeys строка[, режим-ожидания]
    Параметр строка - строковое выражение, задающее последовательность посылаемых символов. Необязательный параметр режим-ожидания - выражение с булевым значением. Если оно False (по умолчанию), управление возвращается в процедуру сразу же после посылки кодов; True - посланная последовательность кодов должна быть обработана, прежде чем управление возвратится в процедуру.
    Большинство символов, набираемых на клавиатуре, входят в строку непосредственно. Например, чтобы послать последовательность из трех символов Y, E и S, возьмите в качестве строки " YES". Символы: +, ^, %, ~, скобки и некоторые другие следует помещать в фигурные скобки. Например, чтобы послать +, в параметр строка нужно поместить {+}. Для посылки кодов клавиш, не отображаемых на экране, имеются специальные коды. Некоторые представлены в таблице, остальные можно найти с помощью подсказки:

    Таблица 7.2. Коды клавиш, не отображаемых на экранеКлавишаКод
    BACKSPACE{BS}
    BREAK{BREAK}
    CAPS LOCK{CAPSLOCK}
    DEL{DEL}
    DOWN ARROW{DOWN}
    END{END}
    ENTER{ENTER} или ~
    ESC{ESC}
    HOME{HOME}
    INS{INS}
    LEFT ARROW{LEFT}
    PAGE DOWN{PGDN}
    PAGE UP{PGUP}
    RIGHT ARROW{RIGHT}
    TAB{TAB}
    UP ARROW{UP}
    Fn{Fn} (n=1,…, 12)

    Чтобы послать комбинацию клавиш, для Shift, Ctrl и Alt используются следующие коды (В примере используется документ DocTwo7:
    Shift - + Ctrl ^ Alt %
    Пример:
    В качестве примера рассмотрим открытие документа, требующего пароль при его открытии:
    Public Sub OpenDocWithPassword() 'Открытие документа с паролем SendKeys "+^" 'Переключение раскладки клавиатуры SendKeys "don" & "{'}" & "t know", False 'пароль "don't know" Documents.Open "e:\O2000\CD2000\Ch7\DocTwo7.doc" Documents("DocTwo7.doc").Activate
    End Sub
    Обратите внимание на два момента:
  • Прежде, чем послать сам пароль, происходит переключение клавиатуры на другую раскладку, для чего используется комбинация символов "Shift + Ctrl".
  • Символы клавиатуры посылаются в буфер ввода еще до того, как они потребуются. Если бы оператор Open предшествовал оператору SendKeys, то окно ввода пароля появилось бы до выполнения этого оператора. В данном же случае при открытии документа пароль автоматически появится в окне ввода, так что останется только щелкнуть по кнопке OK.


  • Операции с одним объектом. Оператор With

    Если в одном блоке программы предстоит выполнить несколько операций с одним объектом, то, чтобы не повторять многократно имя этого объекта, можно ввести оператор With.
    Синтаксис:
    With объект [операторы] End With
    Здесь объект - имя объекта или переменной, определенного пользователем типа, а операторы - последовательность операторов, которые могут действовать с указанным объектом. В этих операторах имена свойств и методов указанного объекта можно начинать с точки, опуская имя самого объекта.
    Допустим, пользовательский тип Person и переменная MyFriend определены так.
    Type Person Name As String Age As Integer Height As Single End Type
    Dim MyFriend As Person
    Тогда присвоение значений свойствам переменной MyFriend можно произвести с помощью оператора With:
    With MyFriend Name = "Сергей" Age= 35 .Height = 178.5 End With
    Подчеркнем, что в каждом блоке имя лишь одного объекта задается по умолчанию. При вложенности операторов With:
    With объект1 операторы1 With объект2 операторы2 End With
    End With
    в блоке операторы2 имя объекта1 нужно указывать полностью. Если же объект2 - элемент (подобъект) объекта1, то, использовав оператор With объект2, можно получить во внутреннем блоке сокращенный доступ к свойствам объекта "объект1. объект2".
    Например, если к типу Person добавить данные о встрече:
    Type Meeting Place As String Date As Date End Type
    Type Person Name As String Age As Integer Height As Single LastMeeting As Meeting End Type
    Dim NewAcquaintance As Person
    то задавать данные о новом знакомом NewAcquaintance можно, используя вложенные операторы With.
    With NewAcquaintance .Name = "Елена" .Age= 40 .Height = 168 With.LastMeeting .Place= "библиотека" 'этот оператор эквивалентен: 'NewAcquaintance. LastMeeting.Place="библиотека" .Date= #08/03/99# End With End With

    Оператор комментария

    Комментарии на исполнение программы не влияют, но необходимы как признак "хорошего стиля". Офисные программы используются многократно и не раз модернизируются в процессе своей жизни. Вы можете сэкономить на комментариях и написать и отладить небольшой модуль без них. Но уже через неделю никто, в том числе и автор, не сможет понять его действие и модифицировать нужным образом. Затраченные на это усилия и время намного превзойдут "экономию". Любая строка текста программы может заканчиваться комментарием. Комментарий в VBA начинается апострофом (') и включает любой текст, расположенный правее в строке. Обычно в комментариях описывают задачи, решаемые модулями, функции, выполняемые процедурами, смысл основных переменных (если он неясен из имен), алгоритмы работы процедур. Полезно также комментировать операторы вызовов внешних для данного модуля процедур, объясняя их действия.
    Другое применение комментарии находят при отладке программ. Если требуется исключить из программы некоторые операторы (например, вызовы еще не реализованных или сомнительных процедур), достаточно перед ними поместить апостроф. Например, при выполнении последовательности операторов
    x=x+z 'z=fun(z,x) y=y*x
    функция fun для вычисления нового значения z во второй строке не вызывается и не мешает проверить правильность значения y.
    В VBA имеется еще один способ выделения комментариев с помощью ключевого слова Rem. Такой комментарий (в отличие от комментария, начинающегося апострофом) должен отделяться от предыдущего оператора в строке двоеточием:
    weight= weight+z: Rem Увеличение веса value=weight*price 'Новая стоимость
    При отладке VBA программ часто приходится временно комментировать целые участки текста, иногда страницы текста. В этом случае закомментировать или снять комментарии довольно утомительное дело. В этих случаях следует пользоваться двумя полезными командами меню Edit: "Comment Block" и "UnComment Block". Они позволяют автоматически закомментировать или снять комментарий с выделенного блока. Я всегда использую настройку (Customize) и выношу на панель редактора VBE кнопки, выполняющие эти команды.

    Оператор Let

    С помощью этого оператора происходит "обычное" присвоение значения выражения переменной или свойству.
    Синтаксис:
    [Let] переменная = выражение
    Ключевое слово Let, как правило, опускается. Переменная является именем переменной или свойства; выражение задает значение, присваиваемое переменной. Его тип должен соответствовать типу переменной. Нарушение этого условия, например, при попытке присвоить числовой переменной строковое значение, приводит к тому, что при компиляции появится сообщение об ошибке. Переменным типа Variant можно присваивать значения разных типов, например, строковых или числовых выражений. Строковой переменной можно присваивать любое значение типа Variant, кроме Null. Числовой же переменной значение типа Variant можно присвоить, только если оно может быть преобразовано к числу.
    Оператор Let можно применять для присвоения одной переменной типа "запись" значения другой такой переменной. Заметьте, это возможно только, если обе они - переменные одного определенного пользователем типа.
    Примеры:
    Public Sub Assign1() Dim MyStr As String, MyInt As Integer
    Let MyStr = "Здравствуй, зайчик!" ' С ключевым словом Let MyInt = 5 ' Без него. Обычный вариант. Debug.Print MyStr, MyInt
    End Sub

    Оператор LSet

    Этот оператор служит для присвоения строк с одновременным выравниванием слева, а также для копирования записи одного определенного пользователем типа в запись другого типа. Его синтаксис:
    Lset СтрПеременная = СтрВыражение Lset переменная1 = переменная2
    Здесь ключевое слово LSet обязательно, СтрПеременная - имя строковой переменной, СтрВыражение - выражение строкового типа. Во втором варианте переменная1 - имя переменной некоторого определенного пользователем типа, в которую выполняется копирование, переменная2 - имя переменной, возможно, другого пользовательского типа, значение которой копируется.
    Результатом присвоения строк всегда является строка той же длины, что и у СтрПеременная. Если при этом СтрВыражение короче, добавляются пробелы справа; длиннее - лишние символы справа удаляются.
    При втором варианте оператора двоичное представление записи из участка памяти, отведенного переменной2 копируется в участок памяти, отведенный переменной1, - При этом данные не преобразуются в соответствии с типами элементов (полей) записи, и если типы соответствующих элементов обеих записей не совпадают, результат операции трудно предсказать (часто выдается сообщение о несоответствии типов). Поэтому этот вариант следует использовать, лишь тогда, когда типы всех соответствующих элементов записей совпадают и имеют одинаковый размер.
    Примеры.
    Public Sub Assign2() Dim Str1 As String, Str2 As String
    Str1 = "0123456789" ' Начальное значение Str2 = "abcd" Debug.Print Str1, Str2
    LSet Str2 = Str1 ' Результат - "0123" LSet Str1 = " Влево" ' Результат - " Влево Debug.Print Str1, Str2
    End Sub"
    Вот результаты отладочной печати:
    0123456789 abcd Влево 0123
    В следующем примере происходит корректное копирование одной записи в другую; типы соответствующих элементов в определенных пользователем типах MyType1 и MyType2 совпадают:
    Type MyType1 age As Integer cost As Long End Type
    Type MyType2 year As Integer pay As Long 'pay As Integer End Type
    Public Sub Assign3() Dim my1 As MyType1 Dim my2 As MyType2
    my1.age = 50 my1.cost = 45666
    my2.year = 1997 my2.pay = 22000 Debug.Print my1.age, my1.cost, my2.pay, my2.year
    LSet my2 = my1 ' после этого присвоения my2.year=50 и my2.pay=45666 Debug.Print my1.age, my1.cost, my2.pay, my2.year
    End Sub
    Вот результаты отладочной печати:
    50 45666 22000 1997 50 45666 45666 50
    Если изменить тип элемента pay в MyType2 на Integer, то будет напечатано значение my2.pay, равное 0.

    Оператор RSet

    Этот оператор присваивает значение строковой переменной с выравниванием справа:
    RSet СтрПеременная = СтрВыражение
    СтрПеременная - имя строковой переменной, СтрВыражение - выражение строкового типа. В отличие от LSet оператор RSet нельзя использовать для копирования переменных записей. Результатом присвоения строк всегда является строка той же длины, что и СтрПеременная. Если при этом СтрВыражение короче, добавляются пробелы слева, длиннее - лишние символы слева удаляются.
    Примеры:
    Public Sub Assign4() Dim Str1, Str2, Str3
    Str1 = "0123456789" ' Начальное значение Str2 = "abcd" Debug.Print Str1, Str2
    RSet Str2 = Str1 ' Результат - "0123" RSet Str1 = "Вправо " ' Результат - " Вправо " RSet Str3 = Str1 ' Результат - пустая строка "" Debug.Print Str1, Str2, Str3 End Sub
    Вот результаты отладочной печати:
    0123456789 abcd Вправо 0123

    Оператор Set

    Этот оператор применим при работе с объектами, устанавливает ссылку на вновь созданный или существующий объект. Его синтаксис:
    Set ОбПеременная = {[New] ОбВыражение| Nothing}
    ОбПеременная - имя переменной или свойства, New - необязательное ключевое слово, используемое для явного вызова операции создания нового экземпляра класса (объекта). Если ОбПеременная содержала ссылку на объект, при присвоении эта ссылка освободится. ОбВыражение может быть именем объекта (класса), другой переменной того же типа, функцией или методом, возвращающими объект соответствующего типа. Выполнение оператора Set с правой частью Nothing прерывает связь между ОбПеременной и объектом, на который она ссылалась. Если при этом на него не осталось других ссылок, ресурсы системы и память, выделенные под этот объект, освобождаются. В общем случае, если ключевое слово New не указано, новая копия объекта не создается, а ОбПеременная как значение получает ссылку на существующий объект. При этом может оказаться, что несколько переменных ссылаются на один объект и изменение этого объекта через одну из них влияет на все остальные.
    Примеры:
    Определим класс объектов Child:
    'Класс Child 'Свойства Public Age As Byte Public Name As String 'Другие свойства и методы пока не определены
    А теперь введем объекты этого класса:
    Пример 7.1.
    (html, txt)
    Вот результаты отладочной печати:
    Имя: Александр Возраст: 10 Имя: Мария Возраст: 7 Имя: Саша Возраст: 10 Имя: Маша Возраст: 7 Имя: Саша Возраст: 10 Имя: Маша Возраст: 7

    Оператор выбора Select Case

    Этот оператор производит разбор случаев и в зависимости от значения анализируемого выражения выбирает и исполняет одну из последовательностей операторов.
    Синтаксис.
    Select Case Выражение-тест [Case СписокВыражений-n [операторы-n]] [Case Else [ИначеОператоры]] End Select
    Выражение-тест должно присутствовать обязательно. Оно может быть произвольным выражением с числовым или строковым значением. СписокВыражений-n должен присутствовать в строке, начинающейся ключевым словом Case (Случай). Выражения в этом списке отделяются запятыми и могут иметь одну из форм:
  • выражение,
  • выражение-нижняя-граница To выражение-верхняя-граница,
  • Is оператор-сравнения выражение.

  • Первая форма задает отдельные значения, вторая и третья позволяют задавать сразу целые диапазоны (области) значений. Последовательность операторов операторы-n необязательна. Она будет исполнена, если соответствующий СписокВыражений-n является первым списком, сопоставимым с текущим значением Выражения-теста (т. е. он явно содержит это значение, либо оно попадает в один из заданных в списке диапазонов). После исполнения операторов последовательности операторы-n проверка на соответствие другим спискам выражений не производится, и управление передается на оператор, следующий за End Select. Необязательная последовательность ЕслиОператоры выполняется, если ни один из списков выражений несопоставим со значением Выражения-теста.
    Пример:
    Public Sub Case1() Dim Before As Integer Dim CurrentYear As Integer, Str As String
    ' Инициализация переменных: CurrentYear = 1999 Before = InputBox("Сколько лет тому назад?", "Когда", 10)
    Select Case CurrentYear - Before Case 1954 To 1969, 1971 To 1974, 1982, Is < 1970 Str = " Годы учебы" Case 1972 To 1989 Str = "Годы воспитания" Case Else Str = "Прочие годы" End Select Debug.Print Str
    End Sub
    Здесь, если перед выполнением выбора Before = 20, значением тестового выражения будет 1979, и будет работать второй вариант ("Годы воспитания"). При Before = 25 значение 1974 сопоставимо с двумя списками, но для исполнения будет выбран лишь первый вариант ("Годы учебы").
    Диапазоны значений можно задавать и для строк. При этом их значения считаются упорядоченными лексикографически. Например, возможен такой список выражений:
    Case "everything", "nuts" To "soup"
    Задаваемое им множество строк включает строку "everything" и все строки от "nuts" до "soup" (например, "onion").

    Операторы и строки

    При записи текста программ для упрощения чтения, отладки и модификации программы удобней каждый оператор располагать в отдельной строке текста. Следуйте правилу: "Один оператор - одна строка". Но, разрешается размещать на строке и несколько операторов, в отличие от общепринятого символа разделения операторов "точки с запятой", в VBA символом разделения двух операторов в одной строке служит двоеточие. Заметьте, некоторые операторы, например оператор If, могут стоять лишь на первом месте в строке. И по этой причине каждый оператор, как правило, следует начинать с новой строки, лишь иногда, разумно, группу операторов присваивания размещать в одной строке. Чаще возникает другая ситуация, - оператор слишком длинный и его текст не виден полностью на экране дисплея, что затрудняет чтение и понимание программы. В этом случае оператор следует продолжить в одной или нескольких строках. Чтобы продолжить (перенести) оператор на следующую строку, используется пара символов пробел-подчеркивание "_". Например,
    MyAddress = "дом: " & Number & "улица: " & Street _ & "город: " & City
    Перед оператором в строке может стоять метка - последовательность символов, начинающаяся с буквы и кончающаяся двоеточием ":". Метки можно размещать и в отдельных строках перед теми операторами, которые они должны помечать. Они нужны для операторов перехода типа GoTo, использование которых считается "дурным тоном" в структурном программировании. Но иногда без меток и переходов на них обойтись трудно - в частности, для указания входов в обработчики ошибок в некоторых процедурах.

    Операторы

    Большая часть материала этой и последующих лекций носит справочный характер. Для программистов, хорошо знакомых с VBA по предыдущим версиям, вполне достаточно беглого просмотра их содержания. Они будут обращаться к этим лекциям по мере необходимости. Конечно, для тех, кто впервые знакомится с этим языком, чтение этой и последующих лекций не только полезно, но и может предшествовать чтению предыдущих лекций, предполагающих хорошее знание основ языка VBA.
    VBA - операторный язык. Это значит, что его программы (модули) представляют последовательности операторов. Набор операторов VBA весьма обширен и не уступает в этом "большим" языкам вроде Паскаля и С. Группу декларативных операторов VBA, служащих для описания объектов, с которыми работает программа (типов, переменных, констант, объектов приложений и др.), мы уже рассмотрели. Операторы другой группы обеспечивают присвоение и изменение значений этих объектов, операторы третьей группы управляют ходом вычислений, четвертой - работой с каталогами и файлами и т.д. Часть операторов досталась VBA в наследство от предыдущих версий Бейсика и без них вполне можно обойтись при написании новых программ.

    Переименование каталогов (папок) и файлов: оператор Name

    Позволяет переименовывать каталоги (папки) и файлы и перемещать файлы.
    Синтаксис:
    Name СтароеИмя As НовоеИмя
    СтароеИмя и НовоеИмя - обязательные параметры, задающие старое и новое имя файла (каталога, папки). Они могут включать имя диска и путь. СтароеИмя должно быть именем существующего файла (каталога, папки), а НовоеИмя не должно быть именем уже существующего объекта. Оба имени должны использовать один и тот же диск. Если указанный новый путь существует и отличается от старого, оператор перемещает файл в новый каталог (папку) и, если требуется, переименовывает его. Каталоги и папки с помощью оператора Name перемещать нельзя, - лишь переименовывать. Перед переименованием файл должен быть закрыт.
    В этом примере файл переименовывается, а затем и перемещается в другой каталог:
    Public Sub DirsAndFiles() 'Сделать текущим диск F ChDrive "F" 'Переименование файла Name "Exam1E.xls" As "Example1.xls" 'Перемещение файла Name "Example1.xls" As "Temp1\Example1.xls" End Sub

    ссылки на существующие объекты

    Public Sub Assign5() Dim Children( 1 To 2) As Child Dim Boy As Child, Girl As Child
    'Создаем объекты Set Children(1) = New Child Set Children(2) = New Child 'Инициализируем их Children(1).Age = 10 Children(1).Name = "Александр" Children(2).Age = 7 Children(2).Name = "Мария" Debug.Print "Имя: ", Children(1).Name, "Возраст: ", Children(1).Age Debug.Print "Имя: ", Children(2).Name, "Возраст: ", Children(2).Age
    'Утанавливаем дополнительные ссылки на существующие объекты Set Boy = Children(1) Set Girl = Children(2) Boy.Name = "Саша" Girl.Name = "Маша" 'Изменились сввойства объектов Debug.Print "Имя: ", Children(1).Name, "Возраст: ", Children(1).Age Debug.Print "Имя: ", Children(2).Name, "Возраст: ", Children(2).Age
    'Удаляем одну из ссылок, но объект остается Set Children(1) = Nothing Set Children(2) = Nothing Debug.Print "Имя: ", Boy.Name, "Возраст: ", Boy.Age Debug.Print "Имя: ", Girl.Name, "Возраст: ", Girl.Age
    End Sub
    Пример 7.1.
    Закрыть окно




    Public Sub MinMax1(ByVal X As Integer, ByVal Y As Integer, _ Min As Integer, Max As Integer) 'Оператор If в одну строку If X > Y Then Max = X: Min = Y Else Max = Y: Min = X
    End Sub
    Public Sub MinMax2(ByVal X As Integer, ByVal Y As Integer, _ Min As Integer, Max As Integer) 'Оператор If в виде блока If X > Y Then Max = X Min = Y Else Max = Y Min = X End If
    End Sub
    Public Sub If1() Dim Max As Integer, Min As Integer
    Call MinMax1(2 + 3, 2 * 3, Min, Max) Debug.Print Max, Min Call MinMax2(2 + 3, 2 * 3, Min, Max) Debug.Print Max, Min End Sub
    Пример 7.2.
    Закрыть окно




    Public Sub BoyOrMan() Dim Boy As New Child Dim Person As New Man Dim SomeBody As Object Dim Answer As Integer
    Boy.Age = 10: Boy.Name = "Александр" Person.Age = 21: Person.Name = "Александр" Answer = InputBox("Введите число от 10 до 20", "Возраст", 15) If Answer > 15 Then Set SomeBody = Person Else: Set SomeBody = Boy End If
    If TypeOf SomeBody Is Child Then Debug.Print "Это мальчик!" ElseIf TypeOf SomeBody Is Man Then Debug.Print "Это Мужчина" Else Debug.Print "Не знаю, кто это" End If End Sub
    Пример 7.3.
    Закрыть окно




    Public Sub For1() Dim A(1 To 5, 1 To 5) As Integer Dim B(1 To 5, 1 To 5) As Integer Dim C(1 To 5, 1 To 5) As Integer Dim I As Integer, J As Integer, K As Integer Dim Res As String ' Инициализация матриц A и B случайными числами в интервале [-10, +10] VBA.Randomize For I = 1 To 5 For J = 1 To 5 'Получение случайного числа Rnd и преобразование в целое A(I, J) = Int(21 * Rnd) - 10 Next J Next I For I = 1 To 5 For J = 1 To 5 B(I, J) = Int(21 * Rnd) - 10 Next J Next I
    'Вычисление произведения матриц For I = 1 To 5 For J = 1 To 5 C(I, J) = 0 For K = 1 To 5 C(I, J) = C(I, J) + A(I, K) * B(K, J) Next K Next J Next I
    Res = "No" C(2, 2) = 0 'Проверка на нулевое значение For I = 1 To 5 For J = 1 To 5 If C(I, J) = 0 Then Debug.Print "Индексы: ", I, J Res = "Yes" Exit For End If Next J Next I Debug.Print Res End Sub
    Пример 7.4.
    Закрыть окно




    Public Sub Loop1() Const Size = 5 Dim X() As Integer Dim i As Integer Dim Found As Boolean Const pat = 7 'Инициализация случайными числами в интервале [1 - 10] ReDim X(1 To Size) Randomize For i = 1 To Size X(i) = Int(11 * Rnd) Next i
    'Поиск по образцу с проверкой в начале цикла i = 1: Found = False Do While (i <= Size) And (Not Found) If X(i) = pat Then Found = True Else: i = i + 1 End If Loop If Found Then Debug.Print "Найден образец!" Else: Debug.Print "Образец не найден!" End If
    'Поиск по образцу с проверкой в конце цикла i = 1: Found = False Do If X(i) = pat Then Found = True Else: i = i + 1 End If Loop Until Found Or (i = Size + 1) If Found Then Debug.Print "Найден образец!" Else: Debug.Print "Образец не найден!" End If
    'Поиск с барьером ReDim Preserve X(1 To Size + 1) X(Size + 1) = pat i = 1 Do If X(i) = pat Then Exit Do i = i + 1 Loop If i = Size + 1 Then Debug.Print "Образец не найден!" Else: Debug.Print "Образец найден!" End If End Sub
    Пример 7.5.
    Закрыть окно



    Присваивание

    Операторы присваивания - основное средство изменения состояния программы (значений переменных и свойств объектов). Мы уже многократно использовали их в примерах из предыдущих лекций. В VBA несколько видов операторов присваивания.

    Прочие операторы

    И еще несколько полезных операторов VBA, не попавших ни в одну из предыдущих групп.

    Работа с каталогами, папками и файлами

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

    Создание каталога (папки): оператор MkDir

    Синтаксис:
    MkDir путь
    Обязательный параметр путь - это строковое выражение, значение которого задает новый создаваемый каталог (папку). Если путь не содержит имени диска, каталог создается на текущем диске. В следующем примере на диске F создаются три новых каталога (папки):
    Public Sub Dirs() 'Сделать текущим диск F ChDrive "F" 'Создать 3 каталога на F MkDir "Temp1" MkDir "F:\Temp2" MkDir "F:Temp3" End Sub

    Удаление файлов: оператор Kill

    Синтаксис:
    Kill файл
    Параметр файл - строковое вырыжение, задающее имя удаляемого файла. Он может включать имя диска и путь по каталогам или папкам. Для удаления нескольких файлов можно использовать в образце имени файла символ '*' для обозначения произвольной последовательности букв и '?' - для обозначения одного символа. Нельзя удалять открытый в данный момент файл.
    Пример.
    Допустим, в текущем каталоге находятся файлы PROG.DOC, PROG.CPP и PROG.OBJ. Тогда оператор:
    Kill "PROG.*"
    удалит эти файлы с диска.

    Удаление каталога (папки): оператор RmDir

    Синтаксис:
    RmDir путь
    Аргумент путь - строковое выражение, задающее удаляемый каталог или папку. Нельзя удалять каталог или папку, содержащие файлы (иначе будет выдано сообщение об ошибке). Чтобы удалить все файлы из каталога (папки), используйте оператор Kill.
    RmDir "Temp3"
    удалит папку Temp3 с текущего диска.

    Управляющие операторы

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

    Установка атрибутов файла: оператор SetAttr

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

    Таблица 7.1. Атрибуты файлаИмя константыЗначениеОписание
    VbNormal 0Нормальный (по умолчанию)
    vbReadOnly 1Только для чтения
    VbHidden 2Скрытый
    VbSystem 4Системный файл
    VbArchive 32Файл изменен со времени последнего создания резервной копии

    Нельзя менять атрибуты открытого файла (иначе будет выдано сообщение об ошибке).

    Установка системного времени: оператор Time

    Синтаксис оператора Time:
    Time = время
    Пример:
    Time = #8:15:47 PM# ' Установка системного времени

    Установка системной даты: оператор Date

    Синтаксис:
    Date = дата
    Пример:
    Date = #April 29, 1999# ' Изменение системной даты.

    Звуковой сигнал: оператор Beep

    Подает звуковой сигнал через динамик компьютера.
    Синтаксис:
    Beep
    Частота и сила звука зависят от оборудования и могут меняться от компьютера к компьютеру. Обычно звуковой сигнал используется как реакция на какие-либо ошибки или исключительные ситуации, возникающие при выполнении программы.

    Основы офисного программирования и язык VBA

    Другие функции форматирования

    Функция Format является общей функцией применимой к произвольным выражениям. Остальные функции применимы к выражениям специального типа, предоставляя некоторые дополнительные возможности. Мы не станем их подробно рассматривать, ограничившись простым перечислением:
  • FormatCurrency - возвращает денежное выражение, используя денежный знак.
  • FormatDataTime - возвращает дату или время.
  • FormatNumber - возвращает выражение, отформатированное как число.
  • FormatPercent - возвращает выражение, заданное в процентах, с указанием знака процента.


  • Фильтрация элементов массива. Функция Filter

    Функция Filter является одним из вариантов поиска по образцу среди элементов массива. Здесь не требуется точного совпадения элемента и образца, достаточно, чтобы строка - образец содержалась в строке, заданной элементом массива. Поскольку совпадений может быть достаточно много, то результатом является массив отфильтрованных элементов. Некоторые детали будут пояснены при описании аргументов функции Filter. Вот ее синтаксис:
    Filter(sourcearray, match[, include[, compare]])
    Ее параметры имеют следующий смысл:
  • sourcearray - одномерный массив, элементы которого являются строками. Он может быть получен, например, как результат расщепления строки в массив.
  • match - образец поиска. Строка, вхождение которой ищется в каждом элементе исходного массива.
  • include - необязательный аргумент булевого типа. По умолчанию имеет значение True, означающее, что элементы, удовлетворяющие образцу, входят в результирующий массив. Если задано значение False, то результирующий массив составляется из элементов, не удовлетворяющих образцу.
  • compare - имеет обычный смысл.

  • В качестве результата возвращается массив отфильтрованных элементов.
    Наш пример будет представлять расширенный вариант уже приводившейся процедуры SplitAndJoin:
    Пример 8.3.
    (html, txt)
    Вот результаты отладочной печати:
    А это веселая птица - синица, которая часто ворует пшеницу, которая в темном чулане хранится в доме, который построил Джек которая часто ворует пшеницу, которая в темном чулане хранится в доме, который построил Джек А это веселая птица - синица

    Фильтрация, основанная на шаблоне. Функция WildFilter

    Во многих ситуациях есть необходимость производить фильтрацию элементов массива, основываясь на шаблоне, которому должны удовлетворять фильтруемые элементы. Здесь есть полная аналогия с тем, что есть потребность производить замену на основе шаблона, а не точного образца. Поэтому естественно наше желание расширить стандартные возможности функции Filter, тем более, что реализуется эта возможность достаточно просто. Рассмотрим реализацию нашей функции:
    Пример 8.5.
    (html, txt)
    Параметры этой функции сохраняют смысл стандартной функции Filter. Единственное отличие состоит в том, что теперь аргумент match задает настоящий шаблон, допускающий специальные символы. Чтобы быть справедливым, заметим, что теперь речь идет о точном соответствии шаблону, в то время, как в стандартной функции речь идет о вхождении шаблона в элемент. Так что обе функции имеют свои сферы применения. Реализация достаточно очевидна и основана на прямом использовании операции Like. Приведем тестовый пример, в котором вызывается наша функция:
    Public Sub testWildFilter() Dim Txt As String, resTxt As String Dim Artxt() As String, ResAr() As String
    Txt = "123, 5, 117, 7, 189" Artxt = Split(Txt, ", ") ResAr = WildFilter(Artxt, "1##") resTxt = Join(ResAr, ", ") Debug.Print resTxt
    ResAr = WildFilter(Artxt, "[3-57-9]") resTxt = Join(ResAr, ", ") Debug.Print resTxt
    End Sub
    Приведем результаты ее работы:
    123, 117, 189 5, 7

    Форматирование данных. Функции группы Format

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

    Функции проверки типов данных

    К этой группе относится функция TypeName, которая по имени переменной возвращает значение типа String, представляющее ее тип.
    Вызов имеет вид:
    TypeName(имяПеременной)
    где параметр имяПеременной представляет выражение типа Variant, определяющее любую переменную, за исключением переменной с определяемым пользователем типом.
    Строка, возвращаемая функцией TypeName, может быть любой из следующих: Byte, Integer, Long, Single, Double, Currency, Decimal, Date, String, Boolean, Error, Empty (если переменная не инициализирована), Null, Object, Unknown (тип неизвестен), Nothing (объектная переменная, не содержащая ссылки на объект).
    Для проверки типа имеется также набор функций с булевыми значениями. Они применяются к переменным или выражениям (часто имеющим тип Variant) и определяют, имеют ли те заданный тип или значение. В следующей таблице перечислены функции этой группы и указаны определяемые ими типы.

    Таблица 8.6. Функции проверки типовФункцияЧто проверяет
    IsArray(переменная) Является ли переменная массивом.
    IsDate(выражение) Может ли значение выражения быть преобразовано в значение даты.
    IsEmpty(переменная) Была ли инициализирована переменная.
    IsError(выражение) Представляет ли выражение значение ошибки.
    IsNull(выражение) Является ли результатом выражения пустое значение (Null).
    IsNumeric(выражение) Имеет ли выражение числовое значение. (Для выражений типа дата возвращает False).
    IsObject(переменная) Представляет ли переменная объектную переменную. Возвращает True, даже для переменной со значением Nothing.


    Функция Format.

    Функция возвращает строку, отформатированную в соответствии с указаниями, заданными при вызове. Ее синтаксис:
    Format(expression[, format[, firstdayofweek[, firstweekofyear]]])
    Ее параметры:
  • expression - любое правильное выражение.
  • Format - имя встроенного параметра или определение пользовательского формата. Если параметр опущен, то применяется формат, зависящий от типа первого аргумента
  • Firstdayofweek и firstweekofyear - их смысл был описан, когда мы рассматривали работу с датами

  • Приведем ряд примеров, в которых используется форматирование чисел на основе форматов, определяемых по умолчанию и пользователем:
    ? VBA.Format(55) 55 ? VBA.Format(5.5) 5,5 ? VBA.Format(-52.125, "##0.#0") -52,13 ? VBA.Format(-52.125, "000.##0") -052,125 ? VBA.Format(1152.125, "#,##0.#0") 1 152,13 ? VBA.Format(0.52125, "0.##0%") 52,125%
    Несколько примеров форматирования строк:
    ? VBA.Format("5.4") 05.04.99 ? VBA.Format("5,4") 5,4 ? VBA.Format("Мария", ">") МАРИЯ ? VBA.Format("Мария", "<") мария ? VBA.Format("Мария", "> Это ") Это МАРИЯ
    Даты форматируются обычно с использованием встроенных форматов, но можно применять и собственные определения форматов. Вот несколько примеров:
    ? VBA.Format(VBA.Time, "Long Time") 16:24:57 ? VBA.Format(VBA.Time, "Short Time") 16:25 ? VBA.Format(VBA.Time, "hh/mm/ss") 16.26.03 ? VBA.Format(VBA.Date, "Long Date") 9 Май 1999 г. ? VBA.Format(VBA.Date, "Short Date") 09.05.99 ? VBA.Format(VBA.Date, "yy/mm/dd") 99.05.09

    Функция InStrRev - поиск последнего вхождения подстроки

    Функция InStrRev симметрично дополняет функцию InStr, аналогично тому, как функция Right дополняет функцию Left. Эта функция ищет вхождение подстроки в строку, но начинает свою работу с правого конца строки. Ее использование может существенно ускорить работу, если заранее известно, что искомая подстрока находится где- то в конце строки - источника. Если вхождение искомой подстроки единственно, то обе функции дают один и тот же результат. При множественном вхождении функция InStr возвращает первое вхождение, в то время как InStrRev - последнее. Ее синтаксис:
    InstrRev(stringcheck, stringmatch[, start[, compare]])
    Ее параметры имеют тот же смысл, что и у функции InStr, но, заметьте, порядок их задания изменен. Необязательный параметр Start теперь задается третьим по счету. Когда он опущен, то по умолчанию, его значение равно "-1", и поиск начинается с последнего символа. Если вернуться к нашему последнему примеру, то для решения нашей содержательной задачи требовалось определить первое и последнее вхождение символа "\" в строке, задающей путь к файлу. Поэтому обе функции были весьма кстати. Нужно отметить, что это весьма типичная ситуация при разборах текста.

    Функция Replace - замена всех вхождений подстроки

    Это удивительно, что в наборе встроенных функций не было до сих пор функции Replace. Замена одной подстроки на другую - это одна из основных операций при работе со строками. Поэтому в арсенале практически каждого из программистов была своя версия функции Replace. Теперь можно пользоваться стандартной реализацией. Ее несомненным достоинством является то, что она позволяет заменить не только первое вхождение искомой подстроки, но и все такие вхождения, не требуя организации цикла. С другой стороны, можно ограничиться только заменой первого или нескольких первых вхождений. Рассмотрим синтаксис появившейся функции:
    Replace(expression, find, replace[, start[, count[, compare]]])
    Первый аргумент expression задает строковое выражение, результат которого определяет строку - источник, в которой осуществляется замена. Аргументы find и Replace задают заменяемую подстроку и ее новое значение. Аргумент Count определяет число замен. Обычно он равен 1, когда речь идет о замене первого вхождения, или опускается, - в этом случае его значение по умолчанию равно "-1", означающее замену всех вхождений. Аргумент compare имеет обычный смысл.
    В качестве примера приведем функцию Rep, которую мы постоянно используем в нашей работе в процессе работы над этой книгой. Все примеры в нашей книге проверены в реальной работе, они копируются из модулей VBA. При копировании программного текста в документ Word появляются последовательности пробелов, отражающие структурную запись процедур и функций. Но появление в документах Word двух пробелов подряд считается смертельным грехом. Заменять пробелы табуляцией можно и руками, но, конечно же, лучше написать макрос, решающий эту и другие задачи по приведению текста в форму, требуемую редакцией. Вот текст функции, используемой для этих целей:
    Public Sub Rep() 'Эта процедура преобразует выделенный программный текст 'Заменяя пробелы табуляцией и конец абзаца мягким концом строки Dim TxtRange As String
    TxtRange = Selection.Range.Text 'Замена пробелов: 4-х, 3-х и 2-х символом табуляции TxtRange = Replace(TxtRange, " ", vbTab) TxtRange = Replace(TxtRange, " ", vbTab) TxtRange = Replace(TxtRange, " ", vbTab)

    'Замена концов абзаца Selection.Range.Text = Replace(TxtRange, VBA.Chr(13), VBA.Chr(11))

    End Sub

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

    Public Function MyReplace(ByVal Expr As String, ByVal find As String, _ ByVal rep As String, Optional ByVal start As Long = 1, Optional ByVal count As Long = -1, _ Optional ByVal compare As VbCompareMethod = vbBinaryCompare) As String

    'Вызов стандартной функции Replace If start = 1 Then MyReplace = replace(Expr, find, rep, start, count, compare) Else MyReplace = VBA.Left(Expr, start - 1) & replace(Expr, find, rep, start, count, compare) End If

    End Function

    Вот результаты нескольких вызовов Replace и MyReplace в окне отладки:

    ? Replace("A+B *(D*B +B)","B","C",4,1) *(D*C +B) ? MyReplace("A+B *(D*B +B)","B","C",4,1) A+B *(D*C +B) ? MyReplace("A+B *(D*B +B)","B","C",4) A+B *(D*C +C)

    Функция Timer и хронометраж вычислений

    Функция Timer возвращает значение типа Single, представляющее число секунд, прошедших после полуночи. Эта функция широко используется при проведении хронометража вычислений в программах. Чтобы повысить эффективность выполнения VBA программ зачастую приходится прибегать к проведению хронометража, чтобы выявить наиболее критичные по времени выполнения участки программ, а затем уже применять специальные меры для ускорения вычислений. Приведем сейчас пример, в котором анализируется время, затрачиваемое для выполнения операций над арифметическими данными разных подтипов.
    Пример 8.7.
    (html, txt)
    Представим результаты вычислений в виде таблицы. Они представляют интерес сами по себе. Заметьте, что наибольшее время потребовалось для вычислений над самым коротким типом - Byte, а в целом вычисления над разными типами выполняются примерно с одинаковой скоростью.

    Таблица 8.5. Время вычислений над данными разных типовТип / Номер Эксперимента 12345
    Byte 0.3515625 0.359375 0.3320313 0.3320313 0.34375
    Integer 0.2421875 0.2382813 0.2421875 0.2421875 0.2382813
    Long 0.2890625 0.28125 0.28125 0.28125 0.28125
    Single 0.2929688 0.2695313 0.28125 0.28125 0.28125
    Double 0.3085938 0.2890625 0.2929688 0.2929688 0.28125


    Next i

    End Sub

    Пример 8.7.

    Представим результаты вычислений в виде таблицы. Они представляют интерес сами по себе. Заметьте, что наибольшее время потребовалось для вычислений над самым коротким типом - Byte, а в целом вычисления над разными типами выполняются примерно с одинаковой скоростью.

    Таблица 8.5. Время вычислений над данными разных типовТип / Номер Эксперимента 12345
    Byte 0.3515625 0.359375 0.3320313 0.3320313 0.34375
    Integer 0.2421875 0.2382813 0.2421875 0.2421875 0.2382813
    Long 0.2890625 0.28125 0.28125 0.28125 0.28125
    Single 0.2929688 0.2695313 0.28125 0.28125 0.28125
    Double 0.3085938 0.2890625 0.2929688 0.2929688 0.28125

    Математические функции

    Набор математических функций VBA достаточно стандартный. Перечислим их с краткими пояснениями:
  • Abs(число) - абсолютное значение числа.
  • Atn(число)- арктангенс (в радианах) аргумента, задающего тангенс угла.
  • Cos(число) - косинус угла. Аргумент число задает угол в радианах.
  • Exp(число) - экспонента, т. е. результат возведение числа e (основание натуральных логарифмов) в указанную степень.
  • Log(число) - натуральный логарифм числа.
  • Rnd[(число)] - результат представляет равномерно распределенное случайное число в интервале [0 - 1]. Если аргумент число не задан или больше нуля, то порождается очередное случайное число, если он равен 0, то результатом будет предыдущее случайное число, а если число меньше нуля, то всякий порождается одно и то же число, определяемое аргументом. Перед тем, как получить последовательность случайных чисел необходимо вызвать функцию Randomize для инициализации последовательности. Заметим, для формирования значения случайных чисел используется таймер. Чтобы получить целочисленную последовательность равномерно распределенных случайных чисел в интервале [Min - Max], следует использовать следующее преобразование
    Int((Max - Min +1)*Rnd)+ Min

  • В примерах предыдущей лекции мы неоднократно использовали эту функцию для инициализации массивов с числовыми данными.
  • Sgn(число) - знак числа (Если число больше нуля - 1, равно нулю - 0, меньше нуля - -1).
  • Sin(число) - синус угла. Аргумент число задает угол в радианах.
  • Sqr(число) - квадратный корень.
  • Tan(число) - тангенс угла. Аргумент число задает угол в радианах.

  • Во всех этих описаниях под аргументом функции число понимается числовое выражение.
    В справочной системе или в любом математическом справочнике Вы сможете найти указания, как на основе этого базиса, можно вычислить большое число других математических функций. Например, вычисление гиперболического синуса производится по формуле:
    HSin(X) = (Exp(X) - Exp(-X))/2
    а вычисление арккотангенса:
    Arccotan(X) = Atn(X) + 2*Atn(1)

    Некоторые встроенные функции

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

    Несколько модификаций встроенных функций

    Какие бы новые возможности не появлялись в системе, программист всегда будет создавать свой собственный инструментарий, расширяющий стандартные возможности. В предыдущих разделах мы уже приводили разработанные нами функции MyReplace, DelStr, являющиеся частью такого инструментария. Сейчас мы хотим привести еще несколько функций из нашего инструментального набора. Их приведение полезно и по той причине, что эти функции используют новые средства разбора строк. Мы приведем вариации стандартных функций Replace, Filter и Split.
    Несмотря на то, что уже один вариант функции Replace приведен, мы разработали еще два варианта. Оба они отражают часто возникающую необходимость производить замену, основанную не на точном совпадении, а на совпадении с шаблоном, как это делается в операции Like.Наши новые функции позволяют разные подстроки заменять одним и тем же новым содержимым. Нам не удалось написать универсальную и эффективную реализацию такой функции, поэтому мы ограничились двумя частными, но важными для практики случаями.

    Новые функции для работы со строками

    В VBA 2000, как мы уже говорили, добавлены полезные функции для работы со строками.

    Операции

    В любом языке программирования допустимы выражения. Нужно уметь выражаться корректно. Выражения строятся из переменных, констант, встроенных функций с использованием знаков операций и скобок. Запись выражения задает правило (алгоритм) вычисления его значения и его типа. Естественно, что тип и значения всех его переменных должны быть определены до момента вычисления выражения. Языки программирования различаются между собой тем, до какой степени они допускают автоматическое преобразование типов данных в процессе вычисления выражения. Считается, что язык является более надежным, предохраняющим от многих ошибок программиста, если он (язык) не допускает автоматического преобразования типов. Конечно, здесь необходим разумный компромисс. Так все языки допускают при вычислении выражения X+Y вещественный тип для переменной X и целочисленный тип для переменной Y, проводя автоматическое преобразование к вещественному типу и выполняя, затем уже, сложение вещественных чисел. Язык VBA в этом отношении занимает золотую середину. Он не столь строг, как классические языки, предложенные Никласом Виртом - Паскаль, Модула, Оберон, но и не допускает особых вольностей. Среди его встроенных функций есть большое число функций, предназначенных для явного преобразования типов, что позволяет программировать в лучших традициях надежных языков программирования, не доверяя неявным преобразованиям.
    Тема построения выражений обширна, достаточно сказать, что число встроенных функций измеряется не десятками, а сотнями. Подробное их описание не входит в нашу задачу, для этого есть справочная система, но общий обзор и некоторые детали мы постараемся привести.
    Приведем основные операции, которые можно выполнять над данными в языке VBA, классифицируя их по типу и приоритету:

    Таблица 8.1. Операции и их приоритетПриоритетАрифметическиеСравненияЛогическиеОписание некоторых операций
    1Возведение в степень - (^)Равенство - (=)Отрицание - (Not)При возведении в степень основание и показатель могут быть арифметическими выражениями любого типа. Результат имеет тип Double.
    2Унарный минус - (-)Неравенство - (<>)Конъюнкция - (And)
    3Умножение, Деление - (*, /)Меньше - (<)Дизъюнкция - (Or)
    4Деление нацело - (\)Больше - (>)Исключительное Или - (Xor)Деление нацело определено над целочисленными данными (применимо и к вещественным данным) и дает результат целого типа. Исключительное Или требует, чтобы один из операндов имел значение, отличное от True
    5Остаток от деления нацело - (mod)Меньше или равно - (<=)Эквивалентность - (Eqv)Операция mod определена над данными целого типа и возвращает результат целого типа - остаток от деления нацело.
    6Сложение, вычитание - (+,-)Больше или равно - (>=)Импликация - (Imp)Среди логических операций определена операция следования (импликация), ложная в единственном случае, когда посылка истинна, а заключение ложно.
    7Конкатенация строк - (&)Подобия - (Like), Равенство ссылок - (Is) Операция Like проверяет соответствие строки образцу. Операция Is, определенная над объектами, не проверяет равенство самих объектов, она проверяет совпадение ссылок. Ссылки должны задавать один и тот же объект.

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

    Определение текущей даты или времени.

  • Date - возвращает текущую дату.
  • Time - возвращает текущее время по часам компьютера.
  • Now - возвращает значение типа Variant (Date), содержащее текущую дату и время по системному календарю и часам компьютера.


  • Основные операции над строками

    В классической математике давным - давно определен набор основных операций над числовыми и булевыми данными. Строковой арифметикой серьезно стали заниматься с появлением компьютеров. Жесткий стандарт на эти операции еще не сложился. Как правило, во всех языках есть только одна операция, называемая конкатенацией строк, обозначаемая, обычно, символом "&". Все остальные основные операции реализуются с помощью встроенных функций, имена которых и их аргументы могут варьироваться от языка к языку. Более того, варьируется и сам набор этих операций. Минимально, помимо конкатенации необходимы еще две операции, первая из которых позволяет обнаружить индекс вхождения одной строки в другую, вторая - выделить из строки ее подстроку. VBA имеет достаточно мощный набор операций, который в Office 2000 существенно был расширен принципиально новыми возможностями. Рассмотрим вначале те операции, которые и ранее существовали в языке, а уж потом, чуть более подробно поговорим о новых возможностях.
    Функция Len(string) возвращает длину строки, заданной аргументом String. Заметьте, возвращается число символов строки, а не число байтов. Ранее мы уже говорили, что в Office 2000 для внутреннего представления строк используется Unicode кодировка, в которой каждый символ занимает два байта.
    Функция InStr определяет позицию (индекс) первого вхождения одной строки внутри другой строки. Синтаксис
    InStr([start, ]string1, string2[, compare])
    Необязательный аргумент start задает позицию, с которой начинается поиск (по умолчанию - с первого символа строки). Аргумент string1 задает строку, в которой выполняется поиск, а string2 - подстроку, вхождение которой ищется. Необязательный аргумент compare имеет тот же смысл, что и для функции StrComp. Возвращаемые значения определяются следующей таблицей.

    Таблица 8.3. Результат работы функции InStrУсловияЗначение функции InStr
    string1 - пустая строка 0
    string1 или string2 равны Null Пустое значение - Null
    string2 - пустая строка start
    Вхождение string2 не найдено в string1 0
    Вхождение string2 найдено в string1 Позиция обнаруженной подстроки
    start >Len(string2) 0


    Например, два вызова этой функции в окне отладки вернут следующие результаты:

    ? InStr(4, "XXпXXпXXПXXП", "П", 1) 6 ? InStr(4, "XXпXXпXXПXXП", "П", 0) 9

    Функция Left(string, length) выделяет в строке string указанное число length символов слева, позволяя выделить префикс строки.

    Функция Right(string, length) выполняет аналогичную операцию, выделяя символы справа, что позволяет получить суффикс (окончание) строки.

    Более универсальная функция Mid(string, start[, length]) позволяет выделить из строки string подстроку длины length, начиная с позиции start.

    Вот пример вызова этих функций непосредственно из окна отладки:

    ? VBA.Left("рококо",3) рок ? VBA.Right("рококо",3) око ? VBA.Mid("рококо",3,3) кок

    Функции LTrim(string), RTrim(string), Trim(string) возвращают копию строки, из которой удалены пробелы, находившиеся в начале строки (LTrim), в конце строки (RTrim) или в начале и конце строки (Trim).

    Функция String создает строку: содержащую заданное число повторяющихся символов.

    Синтаксис:

    String(number, character)

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

    Функции LCase(string) и UCase(string) возвращают копию строки, символы которой приведены к нижнему (Low) или верхнему регистру (Upper).

    Функции, возвращающие строки, существуют в двух вариантах, отличающихся именами. Функции, имена которых мы приводили, возвращают результат типа Variant. Во втором варианте имена функций оканчиваются знаком $, например, Mid$, UCase$. В этом случае результат возвращается типа String. Такие функции выполняются быстрее, но не могут корректно работать со строками, имеющими значение Null.

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

    Пример 8.2.

    (html, txt)

    Вот результаты отладочной печати после запуска процедуры MyPath:

    E O2000\VBA2000\Ch8\ Ch8.doc E:\O2000\VBA2000\Ch8\

    Обратите внимание, в функции AppPath мы использовали новую функцию InStrRev, что облегчило решение нашей задачи. К описанию вновь введенных функций мы и переходим.


    'Определяем полный путь к файлу, задающему активный документ Word Set MyDoc = ActiveDocument Path = MyDoc.FullName

    'Выделяем имя диска - первый символ полного пути Disk = VBA.Left(Path, 1)

    'Выделяем каталог, в котором хранится документ Start = VBA.InStr(1, Path, "\") Finish = VBA.InStrRev(Path, "\") Dir = VBA.Mid(Path, Start + 1, Finish - Start)

    'Выделяем имя файла FileName = VBA.Mid(Path, Finish + 1)

    'Возвращается результат - полный путь к каталогу AppPath = VBA.Left(Path, Finish) End Function

    Public Sub MyPath() Dim Path As String Dim Dir As String Dim Disk As String Dim FileName As String Path = AppPath(Disk, Dir, FileName) Debug.Print Disk, Dir, FileName, Path

    End Sub

    Пример 8.2.

    Вот результаты отладочной печати после запуска процедуры MyPath:

    E O2000\VBA2000\Ch8\ Ch8.doc E:\O2000\VBA2000\Ch8\

    Обратите внимание, в функции AppPath мы использовали новую функцию InStrRev, что облегчило решение нашей задачи. К описанию вновь введенных функций мы и переходим.

    Преобразование строки в массив. Функция Split

    Предполагается, что исходная строка состоит из элементов (подстрок), разделенных специальными символами - разделителями. Функция Split возвращает одномерный массив из элементов строки. Ее синтаксис:
    Split(expression[, delimiter[, limit[, compare]]])
    Ее параметры:
  • expression - строковое выражение, результат которого задает строку - источник, состоящую из элементов и разделителей.
  • delimeter - строка, задающая последовательность символов, используемых в качестве разделителя. Если этот параметр опущен, то по умолчанию предполагается, что в роли разделителя выступает пробел.
  • limit - необязательный параметр, позволяющий ограничить число возвращаемых элементов. По умолчанию его значение равно -1, означающее выделение всех элементов.
  • compare - необязательный параметр, имеющий стандартный смысл во всех операциях над строками. По умолчанию предполагается двоичное побитовое сравнение.

  • Приведем пример, в котором сложноподчиненное предложение разделяется на простые предложения:
    Public Sub SplitString() 'В этой процедуре сложное предложение разделяется на простые 'Объявляем динамический массив Dim Simple() As String, i As Byte
    'Размерность массива Simple устанавливается автоматически 'в соответствии с размерностью массива, возвращаемого функцией Split Simple = Split("А это пшеница, которая в темном чулане хранится в доме, " _ & "который построил Джек", ", ") For i = LBound(Simple) To UBound(Simple) Debug.Print Simple(i) Next i
    End Sub
    В результате работы этой процедуры создается массив из трех элементов:
    А это пшеница которая в темном чулане хранится в доме который построил Джек
    Обратите внимание, для использования результатов работы функции Split создается динамический массив строк. О размерности его нет нужды заботиться, поскольку в момент присвоения ему результата выполнения Split, он заполняется элементами и у него появляется нижняя и верхняя граница. Далее с этим массивом можно работать обычным образом.
    У функции Split, на наш взгляд, есть один недостаток. Хотя в роли разделителя может выступать последовательность символов, в нашем примере это пара символов "запятая, пробел", но нельзя использовать разные разделители элементов. В задачах подобного рода типичной является ситуация, когда элементы отделяются разными разделителями, возможно, окруженными пробелами. Позже мы покажем, как можно справиться с этой задачей.

    Преобразование типов данных

    Для приведения данных к нужному типу в VBA включен обширный набор функций: CBool, CByte, CCur, CDate, CDbl, CDec, CInt, CLng, CSng, CStr, CVar, CVErr, Fix, Int.
    Все они имеют следующий синтаксис:
    ИмяФункции(выражение)
    Обязательный аргумент выражение является любым строковым выражением или числовым выражением. В следующей таблице указаны типы возвращаемых значений и их диапазоны.

    Таблица 8.7. Функции приведения к типуФункцияТипДиапазон аргумента выражение
    CBool Boolean Любая допустимая строка или числовое выражение. Например, Cbool(0)= Cbool("00")= False, Cbool(1)=Cbool("-6")=True.
    CByte Byte От 0 до 255.
    CCur Currency От -922 337 203 685 477,5808 до 922 337 203 685 477,5807.Например, CCur("25,878755")= 25.8788, а CCur("25.878755") - ошибочный вызов, т. к. в строке отделять дробную часть должна запятая.
    CDate Date Любое допустимое выражение даты. Например, Cdate(32444.5)=28.10.88 12:00:00, Cdate("17/08/1972")=17.08.72, Cdate("6:45:28 PM")= 18:45:28.
    CDbl Double -4,94065645841247E-324 для отрицательных чисел; от 4,94065645841247E-324 до 1,79769313486232E308 для положительных чисел.
    CDec Decimal +/-7,9228162514264337593543950335. Минимальное ненулевое число 0,0000000000000000000000000001.
    CInt Integer От -32 768 до 32 767 с округлением дробной части.
    CLng Long От -2 147 483 648 до 2 147 483 647 с округлением дробной части.
    CSng Single От -3,402823E38 до -1,401298E-45 для отрицательных чисел; от 1,401298E-45 до 3,402823E38 для положительных чисел.
    CVar Variant Диапазон значений Double для числовых значений. Диапазон значений String для нечисловых значений.
    CStr String Возвращаемые значения функции CStr зависят от аргумента выражение. Например, CStr(True) = "Истина", CStr(122.344) = "122.344", CStr(#10/24/47#) = 24.10.47

    Если переданное в функцию значение аргумента выражение находится вне допустимого диапазона для соответствующего типа данных, возникает ошибка.
    Функция CVErr возвращает значение типа Variant с подтипом Error, содержащее код ошибки, указанный пользователем. Она используется для создания определяемых пользователем ошибок.
    Если дробная часть числа равна 0,5, то функции CInt и CLng всегда округляют число до ближайшего четного числа. Например, Cint(0,5)= 0, а Cint(1,5)= 2.
    Функции Fix и Int вычисляют целую часть числа (без округления). Они отличаются на отрицательных числах: Fix (-7.6) = -7, а Int(-7.6) = -8.

    Dim res As Byte Dim

    Public Sub LikeOperation() Const pat1 = "[A-Z]" Const pat2 = "[a-z]" Const pat3 = "[!a-z]" Const pat4 = "[3-5]" Dim res As Byte Dim Sym As String
    res = "Кук" Like "К[аоу]к" Debug.Print res
    res = "f" Like pat1 Debug.Print res res = "f" Like pat2 Debug.Print res res = "f" Like pat3 Debug.Print res
    res = "5" Like pat4 Debug.Print res Sym = "3" res = Sym Like pat1 & pat4 Debug.Print res res = Sym Like pat1 Or Sym Like pat4 Debug.Print res
    End Sub
    Пример 8.1.
    Закрыть окно




    Public Function AppPath( Disk As String, Dir As String, FileName As String) As String 'Эта Функция возвращает в качестве результата полный путь активного документа 'Ее параметры содержат компоненты этого пути - имя диска, каталог на диске и имя файла Dim MyDoc As Document Dim Path As String Dim Start As Byte, Finish As Byte
    'Определяем полный путь к файлу, задающему активный документ Word Set MyDoc = ActiveDocument Path = MyDoc.FullName
    'Выделяем имя диска - первый символ полного пути Disk = VBA.Left(Path, 1)
    'Выделяем каталог, в котором хранится документ Start = VBA.InStr(1, Path, "\") Finish = VBA.InStrRev(Path, "\") Dir = VBA.Mid(Path, Start + 1, Finish - Start)
    'Выделяем имя файла FileName = VBA.Mid(Path, Finish + 1)
    'Возвращается результат - полный путь к каталогу AppPath = VBA.Left(Path, Finish) End Function
    Public Sub MyPath() Dim Path As String Dim Dir As String Dim Disk As String Dim FileName As String Path = AppPath(Disk, Dir, FileName) Debug.Print Disk, Dir, FileName, Path
    End Sub
    Пример 8.2.
    Закрыть окно




    Public Sub SplitAndJoin() ' В этой процедуре сложное предложение разделяется на простые 'А затем после обработки строка восстанавливается 'Здесь же демонстрируется фильтрация элементов массива 'Объявляем динамический массив Dim Simple() As String, i As Byte Dim Simple1() As String, Res As String Dim Simple2() As String 'Размерность массива Simple устанавливается автоматически 'в соответствии с размерностью массива, возвращаемого функцией Split Simple = Split("А это пшеница, которая в темном чулане хранится в доме, " _ & "который построил Джек", ", ") 'Создаем новый массив ReDim Simple1(1 To UBound(Simple) + 2) Simple1(1) = "А это веселая птица - синица" Simple1(2) = "которая часто ворует пшеницу" For i = 3 To UBound(Simple1) Simple1(i) = Simple(i - 2) Next i 'Создаем строку из массива Simple1
    Res = Join(Simple1, ", ") Debug.Print Res 'Фильтрация элементов массива Simple2 = Filter(Simple1, "котор") Res = Join(Simple2, ", ") Debug.Print Res
    Simple2 = Filter(Simple1, "котор", False) Res = Join(Simple2, ", ") Debug.Print Res End Sub
    Пример 8.3.
    Закрыть окно




    Public Function WildReplace( ByVal expr As String, ByVal find As String, _ ByVal Rep As String, Optional ByVal delimiter As String = " ") As String
    'Эта функция применима в том частном случае, когда строка - источник, заданная аргументом Expr 'представляет совокупность элементов, отделяемых разделителями. 'Типичный пример - строка представляет совокупность слов, разделенных пробелами. 'Как и обычная функция Replace эта функция производит замену вхождений элемента (подстроки) новым значением 'Отличие от стандартной функции состоит в том, что заменяемый элемент (подстрока) find 'задается шаблоном. В результате разные элементы могут быть заменены на новое значение. 'Для этого частного случая WildReplace существенно расширяет стандартные возможности Replace
    'Алгоритм основан на том факте, что строка допускает разбор ее на элементы, 'а к элементам применима операция Like - сравнения с шаблоном.
    Dim Words() As String, i As Long, Res As String
    'Разбор строки на элементы Words = Split(expr, delimiter)
    'Сравнение элементов с шаблоном и замена в случае совпадения For i = LBound(Words) To UBound(Words) If Words(i) Like find Then Words(i) = Rep Next i
    'Сборка строки Res = Join(Words, delimiter) WildReplace = Res End Function
    Пример 8.4.
    Закрыть окно




    Public Function WildFilter(sourcearray() As String, ByVal match As String, _ Optional ByVal include As Boolean = True) As Variant
    'Эта функция фильтрует элементы исходного массива, проверяя их на соответствие шаблону match 'В отличие от стандартной функции Filter элементы проверяются на точное соответствие 'но шаблон match может включать специальные символы шаблона. 'Возвращается массив отобранных элементов. Если булев параметр include равен True 'то отбираются элементы, совпадающие с шаблоном, в противном случае - не совпадающие.
    Dim Sel() As String Dim i As Long, j As Long j = 1 If include Then For i = LBound(sourcearray) To UBound(sourcearray) If sourcearray(i) Like match Then ReDim Preserve Sel(1 To j) Sel(j) = sourcearray(i): j = j + 1 End If Next i Else For i = LBound(sourcearray) To UBound(sourcearray) If Not (sourcearray(i) Like match) Then Sel(j) = sourcearray(i): j = j + 1 End If Next i End If WildFilter = Sel End Function
    Пример 8.5.
    Закрыть окно




    Public Function WildSplit( expr As String, Optional ByVal delimiter As String = " ", _ Optional ByVal limit As Long = -1, Optional ByVal compare As VbCompareMethod = vbBinaryCompare) As Variant
    'Также, как и стандартная функция Split, эта функция расщепляет строку - источник expr 'на элементы, используя разделители, заданные аргументом delimiter 'Отличие состоит в том, что при выделении элементов предполагается, что разделителем 'может быть любой из символов множества delimeter, возможно, окруженный пробелами
    Dim Source As String, ResAr() As String Dim find As String, Rep As String Dim i As Long
    Source = expr find = VBA.Mid$(delimiter, 2) Rep = VBA.Left$(delimiter, 1) 'Заменяем в строке все разделители на один из них Source = CharSetReplace(Source, find, Rep)
    'Теперь используем стандартную функцию Split ResAr = Split(Source, Rep, limit, compare)
    'Удаляем пробелы For i = LBound(ResAr) To UBound(ResAr) ResAr(i) = VBA.Trim$(ResAr(i)) Next i WildSplit = ResAr End Function
    Пример 8.6.
    Закрыть окно




    Public Sub Speed() ' Эта программа выполняет замеры времени вычислений над данными разного типа
    Dim Start As Single, Finish As Single Dim i As Long, j As Long Dim bx As Byte, by As Byte, bz As Byte Dim ix As Integer, iy As Integer, iz As Integer Dim lx As Long, ly As Long, lz As Long Dim sx As Single, sy As Single, sz As Single Dim dx As Double, dy As Double, dz As Double
    For i = 1 To 5 'Внешний цикл для повторения замеров времени Start = Timer For j = 1 To 100000 bx = 99: by = 101 bz = by * (by - bx) \ by Next j Finish = Timer Debug.Print "Тип Byte: Время ", i, " = ", Finish - Start Debug.Print "bz = ", bz
    Start = Timer For j = 1 To 100000 ix = 99: iy = 101 iz = iy * (iy - ix) \ iy Next j Finish = Timer Debug.Print "Тип Integer: Время ", i, " = ", Finish - Start Debug.Print "iz = ", iz
    Start = Timer For j = 1 To 100000 lx = 99: ly = 101 lz = ly * (ly - lx) \ ly Next j Finish = Timer Debug.Print "Тип Long: Время ", i, " = ", Finish - Start Debug.Print "lz = ", lz
    Start = Timer For j = 1 To 100000 sx = 99: sy = 101 sz = sy * (sy - sx) / sy Next j Finish = Timer Debug.Print "Тип Single: Время ", i, " = ", Finish - Start Debug.Print "sz = ", sz
    Start = Timer For j = 1 To 100000 dx = 99: dy = 101 dz = dy * (dy - dx) / dy Next j Finish = Timer Debug.Print "Тип Double: Время ", i, " = ", Finish - Start Debug.Print "dz = ", dz
    Next i
    End Sub
    Пример 8.7.
    Закрыть окно



    Присваивание значений

    При присваивании значений переменным типа дата следует заключать дату в специальные ограничители "#" или задавать ее как строковую константу. При задании даты в ограничителях, например #9, May, 99 # она автоматически преобразуется к стандартному формату #5/9/99# (месяц/день/год). Вот пример некоторых действий над датами:
    Public Sub WorkWithDates()
    'Работа с датами Dim dat1 As Date, dat2 As Date, dat3 As Date
    'Присваивание дат dat1 = 12 dat2 = 9 / 5 / 99 dat3 = #9/5/1999# Debug.Print dat1, dat2, dat3 dat1 = "15/7/99" dat2 = #5/9/1999# dat3 = dat3 + 100 Debug.Print dat1, dat2, dat3 If dat3 > dat2 Then Debug.Print dat3 - dat2 Else Debug.Print dat2 - dat3 End If
    End Sub
    Приведем результаты выполнения этой программы:
    11.01.1900 0:26:11 05.09.99 15.07.99 09.05.99 14.12.99 219
    Прокомментируем результаты, поскольку некоторые из них могут вызывать недоумение. Остановимся на первых двух присваиваниях. Чтобы понять их, нужно вспомнить внутренне представление дат. В первом случае к начальной дате прибавляется 12 дней, отсюда и получается 11 января 1900 года. Во втором случае, поскольку выражение в правой части не заключено в ограничители, оно вычисляется как обычное арифметическое выражение, полученное дробное число воспринимается как время от начала суток, - оно и печатается. Обратите внимание, на третье присваивание, первое число воспринимается как номер месяца, для российских программистов здесь кроется источник возможных ошибок, поэтому лучше задавать даты, используя названия месяцев, которые автоматически будут преобразованы в соответствующий формат.
    Далее в программе продемонстрированы некоторые возможности выполнения операций над датами - прибавление целого числа дней, сравнение дат, вычитание дат. Заметьте, вычитание дат дает разницу между ними, выраженную в днях.

    Работа с числовыми данными

    Арифметика в VBA представлена достаточно полно, Напомним, что арифметический тип подразделяется на подтипы:
  • Byte, Integer, Long - для представления целочисленных данных.
  • Single, Double - для представления вещественных данных.
  • Decimal - для представления чисел в форме с фиксированной точкой, что важно, в частности, для финансовых вычислений.
  • Currency - специальный тип для представления денежных данных.
  • Variant - обобщенный тип, позволяющий хранить и обрабатывать данные разного типа.

  • Возможные арифметические операции мы уже упомянули.
    Пример работы с числовыми данными:
    Public Sub WorkWithArithmetic() Dim X As Integer, Y As Integer Dim U As Single, V As Single Dim Z As Double U = 15.8: V = -6.5 Z = U / V: X = CInt(U / V): Y = U \ V Debug.Print X, Y, Z, U, V, X \ Y, X Mod Y, U Mod V
    End Sub
    Вот результаты печати в окне отладки:
    -2 -2 -2,43076926011306 15,8 -6,5 1 0 4
    Заметим, что хотя целочисленные операции возможны над вещественными данными, применять их не следует, поскольку это один из тех случаев, когда выполняются внутренние преобразования, точная интерпретация которых сложна, что и может, в конечном итоге, служить источником программистских ошибок.
    Рассмотрим основные встроенные математические функции.

    Работа с датами и временем

    Для того, чтобы обеспечить программисту возможность корректно работать с датами и временем, VBA предоставляет специальный тип данных Date, хранящий дату и время. Над данными этого типа можно выполнять некоторые операции, но, конечно же, при работе с ними чаще всего используются специальные встроенные функции. Попытаемся коротко рассмотреть основные возможности работы с датами. Прежде всего, заметим, что возможный диапазон дат охватывает даты от 1.1.100 года до 1-го января 9999 года. Если говорить о внутреннем представлении дат, занимающих 4 байта памяти, то целая часть хранит число дней от некоторой начальной даты, дробная часть хранит время от полуночи. Начальной датой является 30-е декабря 1899 года. Благодаря такому внутреннему представлению сложение и вычитание целого числа воспринимается как прибавление или вычитание дней.

    Работа со строками

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

    Разбор строки, допускающей разные разделители ее элементов. Функция WildSplit

    Завершим обзор рассмотрения собственных инструментальных средств описанием функции WildSplit. Когда мы описывали стандартную функцию разбора строки на элементы, мы отмечали, что она, к сожалению, не применима в тех достаточно часто встречающихся ситуациях, когда элементы разделяются разными разделителями. Наша функция в какой то мере пытается восполнить существующий пробел. Она разделяет строку на элементы, которые могут в строке разделяться разными символами, представляющими разделители элементов, возможно, окруженные пробелами.
    Алгоритм реализации прост. Он использует ранее написанную функцию замены CharSetReplace, заменяя все разделители одним. После этого становится возможным применить стандартную функцию разбора. На заключительном этапе у элементов убираются пробелы слева и справа. Следует отметить и недостаток этой функции, - преобразование необратимо и последующее применение Join не позволяет восстановить исходную строку в первозданном виде. Приведем текст этой функции:
    Пример 8.6.
    (html, txt)
    Приведем также тестовую процедуру, в которой выполняется разбор строки функцией WildSplit:
    Public Sub testWildSplit() Dim Txt As String, Res As String Dim Items() As String
    Txt = "a * b - (c+d)/v" Items = WildSplit(Txt, "+-*/") Res = Join(Items) Debug.Print Res End Sub
    Вот результаты ее работы:
    a b (c d) v
    На этом мы закончим наш затянувшийся разговор о работе со строковыми данными.

    Разбор строки. Функции Split, Join и Filter

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

    Сборка элементов массива в строку. Функция Join

    Функция Join решает обратную задачу, - она восстанавливает строку по ее элементам, хранящимся в массиве, добавляя разделители в момент их объединения. Ее синтаксис:
    Join(sourcearray[, delimiter])
    Параметры ее понятны без особых пояснений. Заметим, что если необязательный аргумент delimiter опущен, то элементы разделяются пробелами. В качестве примера, приведем обратную сборку сложного предложения:
    Public Sub SplitAndJoin() 'В этой процедуре сложное предложение разделяется на простые 'А затем после обработки строка восстанавливается 'Объявляем динамический массив Dim Simple() As String, i As Byte Dim Simple1() As String, Res As String 'Размерность массива Simple устанавливается автоматически 'в соответствии с размерностью массива, возвращаемого функцией Split Simple = Split("А это пшеница, которая в темном чулане хранится в доме, " _ & "который построил Джек", ", ") 'Создаем новый массив ReDim Simple1(1 To UBound(Simple) + 2) Simple1(1) = "А это веселая птица - синица" Simple1(2) = "которая часто ворует пшеницу" For i = 3 To UBound(Simple1) Simple1(i) = Simple(i - 2) Next i 'Создаем строку из массива Simple1
    Res = Join(Simple1, ", ") Debug.Print Res End Sub
    Вот результат отладочной печати:
    А это веселая птица - синица, которая часто ворует пшеницу, которая в темном чулане хранится в доме, который построил Джек

    Сравнение с образцом

    Мощным и весьма полезным средством при работе с текстами является операция Like, задающая сравнение с образцом. Необходимость нахождения в наборе всех строк, удовлетворяющих некоторому шаблону (образцу), возникает в самых разнообразных задачах. VBA позволяет решать ее в одну операцию. Приведем таблицу специальных символов, допустимых при задании образца.

    Таблица 8.2. Специальные символы, используемые при задании шаблона СимволыИнтерпретацияПримеры
    * Любой текст - произвольное число символов Шаблону Agent* соответствуют все тексты, начинающиеся со слова Agent. Строки Agent007 и Agent Майор Пронин удовлетворяют шаблону.
    ? Один любой символ Шаблону К?к удовлетворяют, в частности строки Кок и Кук.
    # Любая цифра от 0 до 9 Шаблону Agent### соотвествуют 1000 различных строк, среди которых и Agent007, но, конечно же, не Agent Майор Пронин.
    [множество_символов] Любой символ, принадлежащий множеству Задать множество можно с помощью перечисления и интервалов. Шаблону К[аоу]к удовлетворяют слова "Как", "Кок", "Кук". Чувствительность к регистру зависит от установки опции Option Compare.
    [!множество_символов] Любой не принадлежащий множеству символ Шаблону [!а-я] удовлетворяет символ, не являющийся буквой русского алфавита.

    Приведем пример работы с операцией Like:
    Пример 8.1.
    (html, txt)
    Вот результаты отладочной печати:
    255 0 255 0 255 0 255
    Обратите внимание на последние два результата, демонстрирующие некорректный и корректный способы работы с объединением множеств проверяемых символов.

    Сравнение строк

    Обычные операции сравнения применимы и к строковым данным. Мы уже говорили ранее о том, что интерпретация этих операций зависит от установки опции Option Compare.
  • Если эта опция установлена как Text, то сравнение на "больше - меньше" представляет лексикографическое сравнение, когда строки сравниваются по их расположению в словаре. Заметьте, это сравнение не чувствительно к регистру, так что большие и малые буквы не различаются. Программисты, конечно, понимают, что сравнение строк означает сравнение кодов их символов, так что лексикографический порядок определяется кодировкой символов алфавита.
  • Если эта опция установлена как Binary, то сравнение идет побитно. В этом случае сравнение естественно, чувствительно к регистру.
  • При работе со строками в Access по умолчанию применяется сортировка, заданная на строках базы данных Access. Заметьте, при создании модуля в Access по умолчанию вставляется опция Option Compare Database. Конечно, эта опция применима только при работе в Access.

  • Если нужно локально переопределить вид сравнения, заданный опцией для всего модуля, то можно использовать встроенную функцию StrComp, которая возвращает результат сравнения строк. Ее синтаксис:
    StrComp(string1, string2[, compare])
    Аргументы string1 и string2 - сравниваемые строки. Необязательный аргумент compare указывает способ сравнения строк: значение по умолчанию 0 используется, чтобы выполнить двоичное сравнение, 1 задает посимвольное сравнение без учета регистра.
    Если string1 меньше чем string2, то результат равен -1, если строки равны, то - 0, если вторая меньше, то - 1, если хоть одна из строк имеет значение Null, то результат также равен Null.

    Удаление подстроки

    Удаление вхождений подстроки в строку также является одной из основных операций над строками. Хотя специальной встроенной функции DelStr не появилось, но написать ее реализацию, имея Replace, совсем просто. Удалить подстроку эквивалентно замене ее пустой подстрокой. Вот текст, написанной нами функции DelStr:
    Public Function DelStr(ByVal Expr As String, ByVal find As String, _ Optional ByVal start As Long = 1, Optional ByVal count As Long = -1, _ Optional ByVal compare As VbCompareMethod = vbBinaryCompare)
    'Вызов функции MyReplace с пустой строкой для замены DelStr = MyReplace(Expr, find, "", start, count, compare) End Function
    Приведем результаты ее вызовов в окне отладки:
    ? DelStr("T* + U** - V*","*") T + U - V ? DelStr("T* + U** - V*","*",5) T* + U - V ? DelStr("T* + U** - V*","*",5,1) T* + U* - V*
    Заметьте, при определении DelStr мы предпочли пользоваться собственной модификацией MyReplace, поскольку, как нам кажется, она в большей степени отвечает сути дела.

    Встроенные функции для работы с датами

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

    Вычисления над датами

    Функция DateAdd предназначена для добавления или вычитания указанного временного интервала из значения даты. Заметьте, с ее помощью можно задавать временной интервал не только в днях, но и в месяцах, годах и так далее.
    DateAdd(interval, number, date)
    Аргумент interval - строка, указывающая тип добавляемого временного интервала, number - число временных интервалов, на которое следует изменить дату, date - дата, к которой добавляется указанный временной интервал. Допустимые значения аргумента interval: приведены в следующей таблице.

    Таблица 8.4. Возможные временные интервалыЗначениеОписание
    yyyy Год.
    Q Квартал.
    m Месяц.
    Y День года.
    D День месяца.
    w День недели.
    ww Неделя.
    H Часы.
    N Минуты.
    S Секунды.

    Для примера, приведем два вызова этой функции в окне отладки:
    ? DateAdd("m", 1, "31-янв-95") 28.02.95 ? DateAdd("m", -1, "31-янв-95") 31.12.94
    Функция DateDiff предназначена для определения времени, прошедшего между двумя датами. Например, с помощью этой функции можно вычислить число дней между двумя датами или число недель между текущей датой и концом года. Синтаксис:
    DateDiff(interval, date1, date2[, firstdayofweek[, firstweekofyear]])
  • Аргумент interval задает тип временного интервала при вычислении разности между датами date1 и date2, - его возможные значения те же, что и для функции DateAdd,
  • date1 и date2 - две даты, разность между которыми следует вычислить.
  • firstdayofweek - константа, указывающая первый день недели (по умолчанию считается, что неделя начинается с воскресенья).
  • firstweekofyear - константа, указывающая первую неделю года (по умолчанию первой неделей считается неделя, содержащая 1 января).

  • Приведем примеры вызова этой функции в окне отладки:
    ? DateDiff("m", "18.10.55", "31-янв-95") 471 ? DateDiff("Y", "18.11.97", "01.01.97") -321
    Функция DatePart предназначена для определения указанного компонента даты. Например, с помощью этой функции можно определить день недели или текущий час.
    Синтаксис:

    DatePart(interval, date[,firstdayofweek[, firstweekofyear]])
  • Аргумент interval задает тип возвращаемого временного интервала,
  • date - дата, подлежащая обработке.
  • Необязательные аргументы Firstdayofweek и Firstweekofyear имеют тот же смысл, что и для функции DateDiff.


  • Примеры вызова:

    ? DatePart("ww", "02.09.89") 35 ? DatePart("w", "02.09.89") 7

    Функция DateSerial позволяет вычислить значение даты типа Variant (Date) по ее компонентам, - году, месяцу и дню. Ее синтаксис:

    DateSerial(year, month, day)

    Значение каждого аргумента должно лежать в соответствующем диапазоне: 100 -- 9999 для года, 1 - 31 для дней и 1 - 12 для месяцев. Можно также использовать для аргументов числовые выражения для описания относительной даты.

    Примеры:

    ? DateSerial(1997 - 25, 10 - 2, 18 - 1) 17.08.72 ? DateSerial(Year(Now) - 3,Month(Now) - 2, Day(Now) - 1) 08.03.96

    Функция DateValue переводит аргумент-строку в дату. В отличие от функции преобразования CDate, функция DateValue правильно обрабатывает допустимые даты, содержащие полные или краткие названия месяцев.

    Ее синтаксис:

    DateValue(date)

    Аргумент date может задавать как дату, так и время. Возможные разные форматы задания даты, в том числе и с названиями месяцев:

    Примеры:

    ? DateValue ("25.04.1997") 25.04.97 ? DateValue ("04.25.1997") 25.04.97 ? DateValue ("25 апреля 1997") 25.04.97 ? DateValue ("25 - апр.-97") 25.04.97 ? DateValue ("Апрель, 25, 97") 25.04.97 ? DateValue ("25/04/1997") 25.04.97 ? DateValue ("25.04") 25.04.99

    Последнее равенство связано с тем: что при отсутствии года, функция DateValue использует текущий год по системному календарю компьютера.

    Функции Day(date), Month(date), и Year(date) являются, в некотором смысле, обратными к двум предыдущим. Они по аргументу-дате определяют номер дня, месяца и года. Функция Weekday(date, [firstdayofweek]) возвращает значение типа Variant (Integer), содержащее целое число, представляющее день недели.


    Второй аргумент задает первый день недели (по умолчанию - воскресенье). Значение 2 соответствует понедельнику.

    Функции Hour(время), Minute(время) и Second(время) по аргументу, являющемуся числовым или строковым выражением, представляющим время, возвращает содержащее целое число, которое представляет, соответственно, часы, минуты и секунды в значении времени.

    Функция TimeSerial(hour, minute, second) вычисляет результат Variant (Date), содержащее значение времени, соответствующее указанным часу, минуте и секунде.

    Функция TimeValue(время) возвращает значение типа Variant (Date), содержащее время. Аргумент время обычно задается строковым выражением, представляющим время от 0:00:00 (12:00:00 A.M.) до 23:59:59 (11:59:59 P.M.) включительно. Задавать его нужно корректно, чтобы не возникало ошибок, как в последних двух примерах:

    ? Year(Now) 1999 ? Month(now) 5 ? Day(Now) 9 ? Hour(Now) 11 ? Minute (Now) 57 ? Second(Now) 28 ? TimeSerial(Hour(Now), Minute(now), Second(Now)) 11:59:00 ? TimeValue("12:30 PM") 12:30:00 ? TimeValue("12.30") 0:00:00 ? TimeValue("12,5") 0:00:00

    Замена, основанная на шаблоне. Функция WildReplace

    Важным частным случаем, когда легко реализовать замену, основанную на шаблоне, представляют строки, допускающие разбор их на элементы. В этом случае алгоритм замены понятен, - строка разбирается на элементы, используя новые возможности, предоставляемые функцией Split; затем к каждому элементу применяется операция Like для сравнения с шаблоном; на последнем шаге применяется Join для восстановления строки. Приведем текст нашей функции:
    Пример 8.4.
    (html, txt)
    Параметры этой функции имеют тот же смысл, что и для стандартной функции Replace. Существенное отличие состоит в том, что специальные символы шаблона допустимы в строке поиска find. Приведем теперь тестовую процедуру, осуществляющую вызов функции WildReplace:
    Public Sub testWildReplace() Dim Inf As String, SecretInf As String Inf = "Agent001, Agent007, Агент Майор Пронин, Agent008" SecretInf = WildReplace(Inf, "Agent###", "Agent***", ", ") Debug.Print SecretInf End Sub
    Вот результаты ее работы:
    Agent***, Agent***, Агент Майор Пронин, Agent***

    Замена разных символов строки. Функция CharSetReplace

    Другим важным случаем, допускающим простую реализацию, является случай, когда замене подлежат вхождения отдельного символа, а не целой подстроки, как в общем случае. Наша функция производит замену разных символов из заданного множества на один и тот же символ. Алгоритм основан на обычном линейном просмотре и посимвольном сравнении. Сама замена производится путем выделения и конкатенации.
    Эта функция понадобилась нам для реализации нового варианта функции расщепления строки, хотя она полезна и сама по себе. Приведем ее текст:
    Public Function CharSetReplace(ByVal expr As String, ByVal find As String, _ ByVal Rep As String)
    'Эта функция применима в том частном случае, когда в строке - источнике Expr 'требуется произвести замену символов, входящих в множество, заданное аргументом find 'на символ (последовательность символов), заданных аргументом rep
    Dim Source As String Dim i As Long, Sym As String Dim N As Long
    N = Len(expr) Source = expr For i = 1 To N Sym = VBA.Mid$(Source, i, 1) If InStr(1, find, Sym) Then Source = VBA.Left$(Source, i - 1) & Rep & VBA.Mid$(Source, i + 1) End If Next i CharSetReplace = Source End Function
    Приведем теперь тестовую процедуру, организующую вызов функции CharSetReplace:
    Public Sub testCharSetReplace() Dim Text As String Text = "test123" Text = CharSetReplace(Text, "1234567890", "*") Debug.Print Text Text = "Шла кошка по крыше" Text = CharSetReplace(Text, "аоеиуыяюэ", "*") Debug.Print Text End Sub
    В результате ее работы в окне отладки появляются следующие данные:
    Шл* к*шк* п* кр*ш* Agent***, Agent***, Агент Майор Пронин, Agent***

    Основы офисного программирования и язык VBA

    Аргументы, являющиеся массивами

    Аргументы процедуры могут быть массивами. Процедуре передается имя массива, а размерность массива определяется встроенными функциями LBound и UBound. Приведем пример процедуры, вычисляющей скалярное произведение векторов:
    Public Function ScalarProduct(X() As Integer, Y() As Integer) As Integer 'Вычисляет скалярное произведение двух векторов. 'Предполагается, что границы массивов совпадают.
    Dim i As Integer, Sum As Integer Sum = 0 For i = LBound(X) To UBound(X) Sum = Sum + X(i) * Y(i) Next i ScalarProduct = Sum End Function
    Оба параметра процедуры, передаваемые по ссылке, являются массивами, работа с которыми в теле процедуры не представляет затруднений, благодаря тому, что функции LBound и UBound позволяют установить границы массива по любому измерению. Приведем программу, в которой вызывается функция ScalarProduct:
    Public Sub TestScalarProduct() Dim A(1 To 5) As Integer Dim B(1 To 5) As Integer Dim C As Variant Dim Res As Integer Dim i As Integer
    C = Array(1, 2, 3, 4, 5) For i = 1 To 5 A(i) = C(i - 1) Next i C = Array(5, 4, 3, 2, 1) For i = 1 To 5 B(i) = C(i - 1) Next i Res = ScalarProduct(A, B) Debug.Print Res End Sub

    Деревья поиска

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

    Функции с побочным эффектом

    В классическом варианте все аргументы функции являются входными параметрами, и единственный результат вычисления функции - это ее возвращаемое значение., примером является функция Cube. Чаще всего, однако, используются функции с побочным эффектом, то есть такие функции, которые помимо получения значения функции изменяют значения некоторых результирующих параметров, передаваемых функции по ссылке. Например,
    Public Function SideEffect(ByVal X As Integer, ByRef Y As Integer) As Integer SideEffect = X + Y Y = Y + 1 End Function
    Public Sub TestSideEffect() Dim X As Integer, Y As Integer, Z As Integer X = 3: Y = 5 Z = X + Y + SideEffect(X, Y) Debug.Print X, Y, Z X = 3: Y = 5 Z = SideEffect(X, Y) + X + Y Debug.Print X, Y, Z End Sub
    Вот результаты вычислений:
    3 6 16 3 6 17
    Как видите, обращаться с функциями, обладающими побочным эффектом, следует с осторожностью. В нашем примере результат вычисления суммы трех слагаемых зависит от порядка их записи. Это противоречит основным принципам математики. Более того, следует понимать, что результат вычисления непредсказуем, поскольку VBA может для увеличения эффективности изменять порядок действий при вычислении арифметического выражения. Поэтому лучше не использовать в выражении вызов функции с побочным эффектом, изменяющей значения входящих в него переменных.

    Использование именованных аргументов

    В предыдущих примерах фактические параметры вызова процедуры или функции располагались в том же порядке, что и формальные параметры в ее заголовке. Это не всегда удобно, особенно если некоторые аргументы необязательны (Optional). VBA позволяет указывать значения аргументов в произвольном порядке, используя их имена. При этом после имени аргумента ставятся двоеточие и знак равенства, после которого помещается значение аргумента (фактический параметр). Например, вызов рассмотренной выше процедуры-функции MyFunc может выглядеть так:
    Myfunc Age:= 25, Name:= "Alex", Newdate:= DateOfArrival
    Удобство такого способа особенно проявляется при вызове процедур с необязательными аргументами, которые всегда помещаются в конец списка аргументов в заголовке процедуры. Пусть, например, заголовок процедуры ProcEx:
    Sub ProcEx(Name As String, Optional Age As Integer, Optional City = "Москва")
    Список ее аргументов включает один обязательный аргумент Name и два необязательных: Age и City, - причем для последнего задано значение по умолчанию "Москва". Если при вызове этой процедуры второй аргумент не требуется, то при вызове, не использующем именованных параметров, сам параметр опускается, но, выделяющая его запятая, должна оставаться:
    ProcEx "Оля",,"Тверь"
    Вместо этого можно использовать вызов с именами аргументов:
    ProcEx City:="Тверь", Name:="Оля"
    в котором не требуется заменять пропущенный аргумент запятыми и соблюдать определенный порядок следования аргументов. Если некий необязательный аргумент не задан при вызове, вместо него подставляется значение, определенное пользователем по умолчанию, если и такое не определено, подставляется значение, определенное по умолчанию для соответствующего типа. Например, при вызове:
    ProcEx Name:="Оля"
    в качестве значения аргумента Age в процедуру передастся 0, а в качестве аргумента City - явно заданное по умолчанию значение "Москва".
    Как процедура "узнает", передан ли ей при вызове необязательный аргумент? Для этого можно воспользоваться функцией IsMissing.
    Она по имени аргумента возвращает логическое значение True, когда значение аргумента не передано в процедуру, и False, если аргумент задан. Но это все работает только в том случае, если параметр имеет тип Variant. Для всех остальных типов данных полагается, что в процедуру всегда передано значение параметра, явно или неявно заданное по умолчанию. Поэтому, если такая проверка необходима, то параметр должен иметь тип Variant. Отметим также, что для массива аргументов ParamArray функция IsMissing всегда возвращает False, и для установления его пустоты нужно проверять, что верхняя граница индекса меньше нижней.

    Рассмотрим функцию от двух аргументов, второй из которых необязателен:

    Function TwoArgs(I As Integer, Optional X As Variant) As Variant If IsMissing(X) Then ' если 2-ой аргумент отсутствует, то вернуть 1-ый. TwoArgs = I Else ' если 2-ой аргумент есть, то вернуть их произведение TwoArgs = I * X End If End Function

    Вот результаты нескольких вызовов этой функции в окне отладки:

    ? TwoArgs(5,7) 35 ? TwoArgs(5.5) 6 ? TwoArgs(5, 5.5) 27,5 ? TwoArgs(5, "6") 30

    Класс BinTree

    Класс BinTree содержит одно свойство - объект класса TreeNode, задающий корень дерева и группу операций над элементами дерева. Одним из основных методов класса является метод SearchAndInsert, который, по существу, реализует две операции - поиска элемента в дереве по заданному ключу и вставки элемента в дерево. Заметьте, что вставка должна быть реализована так, чтобы выполнялось основное условие дерева поиска: для каждой вершины ее ключ больше всех ключей вершин левого поддерева и меньше всех ключей вершин правого поддерева. Такая структура обеспечивает эффективное выполнение операций поиска, так как в этом случае для поиска требуется просмотр вершин, лежащих только на одной ветви дерева, ведущей от корня к искомому элементу. Этот метод обеспечивает создание класса, позволяя, начав с корня, вставлять элемент за элементом. Кроме этого метода мы определим методы для удаления элементов и группу методов обхода дерева. Все методы реализованы с использованием рекурсивных алгоритмов. Приведем описание класса:
    Пример 9.4.
    (html, txt)
    Все методы класса довольно подробно прокомментированы, однако хотелось бы подчеркнуть некоторые моменты:
  • Начнем с общего замечания, связанного с реализацией рекурсивных алгоритмов. Рекурсия это мощный инструмент, полезный при решении многих задач по обработке данных. Для тех, кто не привык писать рекурсивные программы, мы рекомендуем внимательно разобрать реализацию приведенных методов класса. Каждое рекурсивное определение содержит некоторый базис, позволяющий найти решение в простейшем случае без использования рекурсии, а затем вся задача сводится к нескольким подобным задачам, но меньшей размерности. Если число задач, к которым сводится исходная задача, не меньше двух, то можно заведомо говорить, что рекурсивное решение намного проще не рекурсивного алгоритма и использование рекурсии оправданно. Для пояснения этих общих утверждений обратимся к примеру. В нашем классе приведены три метода обхода бинарного дерева: PrefixOrder, InfixOrder, PostfixOrder.
    Написать не рекурсивный алгоритм, который обходил бы все узлы дерева некоторым заданным образом не так то просто. Другое дело рекурсивное определение. Действительно базисное решение очевидно, - когда дерево пусто, то ничего и делать не надо. Если же оно не пусто, то у нас есть корень дерева, а у него два потомка, которые в свою очередь являются деревьями. Поэтому для обхода всего дерева достаточно посетить корень, а затем обойти (рекурсивно) оба поддерева. Меняя порядок посещения корня и поддеревьев, получаем три различных способа обхода дерева. Заметим, именно благодаря тому, что сама структура данных рекурсивна, рекурсивные алгоритмы естественным образом описывают решения задач по обработке таких данных. Рекурсивные определения просты и понятны, но напоминают некоторый фокус. Наиболее сложно воспроизвести вычисления, выполняемые рекурсивным алгоритмом.
  • Для простоты в методе SearchAndInsert мы совместили две операции поиска элемента по заданному ключу и вставки нового элемента. Если в дереве найден элемент с заданным ключом, то предполагается, что речь идет о поиске и возвращается информация из информационного поля этого элемента. Если в дереве нет элемента с таким ключом, то создается новый узел дерева. Заметьте, что наше решение не позволяет производить замену элемента, а в процессе поиска не уведомляет об отсутствии элемента с заданным ключом
  • Удаление элемента из дерева поиска осложняется тем, что нужно поддерживать структуру дерева поиска. В тех случаях, когда нужно удалить элемент, у которого есть два потомка, вызывается специальная процедура ReplaceAndDelete. Эта процедура ищет кандидата, который мог бы заменить удаляемый элемент, сохраняя структуру дерева.
  • Недостатком деревьев поиска является то, что они могут быть плохо сбалансированы и могут иметь относительно длинные ветви. Так, если при создании дерева поиска, ключи будут поступать в отсортированном порядке, то дерево будет представлено одной ветвью. Работа с этой структурой данных предполагает, что при создании и добавлении элементов в дерево ключи поступают в случайном порядке, хорошо перемешанные.Эта структура особенно применима в тех случаях, когда в процессе работы над данными широко используются все операции - поиск, вставка и удаление.
  • Мы не стали писать реализацию этого класса, оперирующего с данными, хранящимися в списках Excel или базе данных Access, поскольку это выходит за рамки этой лекции.



  • End Sub

    Public Sub ReplaceAndDelete(q As TreeNode) ' Заменяет узел на самый правый If Not (root.right.root Is Nothing) Then root.right.ReplaceAndDelete q Else 'Найден самый правый q.key = root.key: q.info = root.info Set root = root.left.root End If

    End Sub

    Пример 9.4.

    Все методы класса довольно подробно прокомментированы, однако хотелось бы подчеркнуть некоторые моменты:

  • Начнем с общего замечания, связанного с реализацией рекурсивных алгоритмов. Рекурсия это мощный инструмент, полезный при решении многих задач по обработке данных. Для тех, кто не привык писать рекурсивные программы, мы рекомендуем внимательно разобрать реализацию приведенных методов класса. Каждое рекурсивное определение содержит некоторый базис, позволяющий найти решение в простейшем случае без использования рекурсии, а затем вся задача сводится к нескольким подобным задачам, но меньшей размерности. Если число задач, к которым сводится исходная задача, не меньше двух, то можно заведомо говорить, что рекурсивное решение намного проще не рекурсивного алгоритма и использование рекурсии оправданно. Для пояснения этих общих утверждений обратимся к примеру. В нашем классе приведены три метода обхода бинарного дерева: PrefixOrder, InfixOrder, PostfixOrder. Написать не рекурсивный алгоритм, который обходил бы все узлы дерева некоторым заданным образом не так то просто. Другое дело рекурсивное определение. Действительно базисное решение очевидно, - когда дерево пусто, то ничего и делать не надо. Если же оно не пусто, то у нас есть корень дерева, а у него два потомка, которые в свою очередь являются деревьями. Поэтому для обхода всего дерева достаточно посетить корень, а затем обойти (рекурсивно) оба поддерева. Меняя порядок посещения корня и поддеревьев, получаем три различных способа обхода дерева. Заметим, именно благодаря тому, что сама структура данных рекурсивна, рекурсивные алгоритмы естественным образом описывают решения задач по обработке таких данных. Рекурсивные определения просты и понятны, но напоминают некоторый фокус.


    Наиболее сложно воспроизвести вычисления, выполняемые рекурсивным алгоритмом.
  • Для простоты в методе SearchAndInsert мы совместили две операции поиска элемента по заданному ключу и вставки нового элемента. Если в дереве найден элемент с заданным ключом, то предполагается, что речь идет о поиске и возвращается информация из информационного поля этого элемента. Если в дереве нет элемента с таким ключом, то создается новый узел дерева. Заметьте, что наше решение не позволяет производить замену элемента, а в процессе поиска не уведомляет об отсутствии элемента с заданным ключом
  • Удаление элемента из дерева поиска осложняется тем, что нужно поддерживать структуру дерева поиска. В тех случаях, когда нужно удалить элемент, у которого есть два потомка, вызывается специальная процедура ReplaceAndDelete. Эта процедура ищет кандидата, который мог бы заменить удаляемый элемент, сохраняя структуру дерева.
  • Недостатком деревьев поиска является то, что они могут быть плохо сбалансированы и могут иметь относительно длинные ветви. Так, если при создании дерева поиска, ключи будут поступать в отсортированном порядке, то дерево будет представлено одной ветвью. Работа с этой структурой данных предполагает, что при создании и добавлении элементов в дерево ключи поступают в случайном порядке, хорошо перемешанные. Эта структура особенно применима в тех случаях, когда в процессе работы над данными широко используются все операции - поиск, вставка и удаление.
  • Мы не стали писать реализацию этого класса, оперирующего с данными, хранящимися в списках Excel или базе данных Access, поскольку это выходит за рамки этой лекции.


  • Класс TreeNode

    Как следует из определения бинарного дерева, каждый его узел содержит ссылки на левого и правого сына. Кроме того, предполагается, что информация, хранящаяся в узле, сопровождается ключом. Несколько упрощая ситуацию, можно предложить следующее описание класса TreeNode, задающее описание узла бинарного дерева поиска:
    'Class TreeNode 'Элемент дерева
    Public key As String Public info As String Public left As New BinTree Public right As New BinTree

    Классификация процедур

    Процедуры VBA можно классифицировать по нескольким признакам: по способу использования (вызова) в программе, по способу запуска процедуры на выполнение, по способу создания кода процедуры, по месту нахождения кода процедуры в проекте.
    Процедуры VBA подразделяются на подпрограммы и функции. Первые описываются ключевым словом Sub, вторые - Function. Мы очень редко используем термин подпрограмма, характерный для VBA, и вместо него используем термин процедура, более распространенный в программировании. Иногда, правда, это может приводить к недоразумениям, поскольку в зависимости от контекста под процедурой понимается как подпрограмма, так и функция. Различие между этими видами процедур скорее синтаксическое, так как преобразовать процедуру одного вида в эквивалентную процедуру другого вида совсем не сложно. В языке С/С++, как известно, есть только функции и нет процедур, по крайней мере формально.
    По способу создания кода процедуры делятся на обычные, разрабатываемые "вручную", и на процедуры, код которых создается автоматически генератором макросов (MacroRecoder); их называют также макро-процедурами или командными процедурами, поскольку их код - это последовательность вызовов команд соответствующего приложения Office 97. И это разделение в известной степени условно, так как довольно типичны процедуры, каркасы которых, созданные генератором макросов, затем изменяют и дописывают вручную.
    По способу запуска процедур на выполнение можно выделить в отдельную группу процедуры, запускаемые автоматически при наступлении того или иного события, - мы называем их процедурами обработки событий.
    По положению в проекте различаются процедуры, находящиеся в специальных программных единицах - стандартных модулях, модулях классов и модулях, связанными с объектами, реагирующими на события.
    Еще один специальный тип процедур - процедуры-свойства Property Let, Property Set и Property Get. Они служат для задания и получения значений закрытых свойств класса.
    Главное назначение процедур во всех языках программирования состоит в том, что при их вызове они изменяют состояние программного проекта, - изменяют значения переменных (свойства объектов), описанных в модулях проекта. У процедур VBA сфера действия шире. Их главное назначение состоит в изменении состояния системы документов, частью которого является изменение состояния самого программного проекта. Поэтому процедуры VBA оперируют, в основном, с объектами Office 2000. Заметьте, есть два способа, с помощью которых процедура получает и передает информацию, изменяя тем самым состояние системы документов. Первый и основной способ состоит в использовании параметров процедуры. При вызове процедуры ее аргументы, соответствующие входным параметрам получают значение, так процедура получает информацию от внешней среды, в результате работы процедуры формируются значения выходных параметров, переданных ей по ссылке, тем самым изменяется состояние проекта и документов. Второй способ состоит в использовании процедурой глобальных переменных и объектов, как для получения, так и для передачи информации.

    Конструкция ParamArray

    Иногда, когда в процедуру следует передать только один массив, для этой цели можно использовать конструкцию ParamArray. Следующая процедура PosNeg подсчитывает суммы поступлений Positive и расходов Negative, указанные в массиве Sums:
    Sub PosNeg(Positive As Integer, Negative As Integer, ParamArray Sums() As Variant) Dim I As Integer Positive = 0: Negative = 0 For I = 0 To UBound(Sums()) ' цикл по всем элементам массива If Sums(I) > 0 Then Positive = Positive + Sums(I) Else Negative = Negative - Sums(I) End If Next I End Sub
    Вызов процедуры PosNeg может иметь такой вид:
    Public Sub TestPosNeg() Dim Incomes As Integer, Expences As Integer
    PosNeg Incomes, Expences, -20, 100, 25, -44, -23, -60, 120 Debug.Print Incomes, Expences End Sub
    В результате переменная Incomes получит значение 245, а переменная Expences - 147. Заметьте, преимуществом использования массива аргументов ParamArray является возможность непосредственного перечисления элементов массива в момент вызова.
    Однако такое использование массива аргументов ParamArray не исчерпывает всех его возможностей. В более сложных ситуациях передаваемые аргументы могут иметь разные типы. Мы приведем сейчас пример, в котором, во-первых, действуют объекты Office 2000, а, во-вторых, используется передача параметров через массив аргументов ParamArray.

    Описание и создание процедур

    Ранее мы говорили, что в офисном программировании целью работы является создание системы документов. С системой документов связан программный проект, представляющий совокупность программных проектов, связанных с каждым отдельным документом. Проекты разных документов связаны между собой и могут иметь общие данные и общие процедуры и функции, доступные из разных проектов. Каждый проект состоит из модулей разного типа. Каждый модуль содержит объявления переменных, процедур и функций. Процедуры и функции являются минимальными модульными конструкциями, из которых строится программный проект.
    Процедура (функция) - это программная единица VBA, включающая операторы описания ее локальных данных и исполняемые операторы. Обычно в процедуру объединяют регулярно выполняемую последовательность действий, решающую отдельную задачу или подзадачу. Особенность процедур VBA в том, что они работают в мощном окружении Office 97 и могут использовать в качестве элементарных действий большое количество встроенных методов и функций, оперирующих с разнообразными объектами этой системы. Поэтому структура управления типичной процедуры прикладной офисной системы довольно проста: она состоит из последовательности вызовов встроенных процедур и функций, управляемой небольшим количеством условных операторов и циклов. Ее размеры не должны превышать нескольких десятков строк. Если в Вашей процедуре несколько сотен строк, это, скорее всего, значит, что задачу, решаемую процедурой, можно разбить на несколько самостоятельных подзадач, для решения каждой из которых следует написать отдельную процедуру. Это облегчит понимание программы и ее отладку. Разумеется, эти замечания носят неформальный методологический характер, поскольку сам VBA никак не ограничивает ни размер процедуры, ни сложность ее структуры управления.

    Пользовательские функции, принимающие сложный объект Range

    Известно, что в Excel объект Range может представлять несмежную область и являться объединением нескольких интервалов ячеек. Иначе говоря, один объект Range может задавать несколько массивов рабочего листа. Можно ли такой объект передать пользовательской функции и, если да, как его обрабатывать? Ответ: "можно", хотя соответствующий формальный параметр следует описывать особым образом. Процедуры и функции VBA допускают произвольное число параметров, это достигается за счет того, что один, последний по счету формальный параметр может иметь спецификатор ParamArray. В этом случае данный параметр задает фактически массив параметров с произвольным числом элементов. Именно эта техника и применяется для передачи в пользовательскую функцию сложного объекта Range, представляющего не один, а произвольное число массивов. У такой функции последний параметр должен иметь спецификатор ParamArray и быть массивом типа Variant.
    Рассмотрим предыдущую задачу, немного усложнив ее, полагая, что при проверке кандидата на "медианность" используется произвольное число массивов. Вот как выглядит функция, решающая эту задачу:
    Пример 9.2.
    (html, txt)
    Комментируя работу этой функции, отметим:
  • Эта функция может (и будет) вызываться как из процедур VBA, так и из формул рабочего листа Excel.
  • Формально функция по-прежнему имеет два параметра Cand и M. Правда, теперь они поменялись местами, и параметр M стал последним. Фактически у этой функции теперь произвольное число параметров, поскольку параметр M, сохранив тип Variant, стал теперь массивом. Спецификатор ParamArray подчеркивает, что это специальный массив с произвольным числом элементов.
  • Для работы с массивом M используется цикл типа For Each. В цикле выделяется очередной элемент Elem типа Variant, а дальше используется уже знакомый по функции IsMediana алгоритм проверки элемента Cand.
  • Разбор случаев делается независимо для каждого из элементов массива M.

  • Демонстрацию использования этой функции начнем с ее вызова в процедуре VBA, которая передает ей целочисленный массив элементов:

    Public Sub TestIsMedianaForAll() Const Size = 7 Dim Mas( 1 To Size) As Integer Dim Cand As Integer Dim i As Integer Dim Res As Integer 'Инициализация массива целыми в интервале 1-20 Debug.Print TypeName(Mas) Randomize For i = 1 To Size Mas(i) = Int(Rnd * 21) Next i Cand = Int(Rnd * 21) Res = IsMedianaForAll(Cand, Mas) Debug.Print "Массив:" For i = 1 To Size Debug.Print Mas(i) Next i Debug.Print "Кандидат:", Cand Debug.Print "Результат:", Res End Sub

    Приведем результаты выполнения этой процедуры:

    Integer() Массив: 9 1 14 11 11 18 0 Кандидат: 12 Результат: -3

    А теперь покажем, как эта функция вызывается в формулах рабочего листа Excel На том же рабочем листе, где мы проводили эксперименты с формулами, вызывающими функцию IsMediana, мы записали еще несколько формул, вызывающих функцию IsMedianaForAll. Заметьте, ее аргумент может иметь значительно более сложный вид, чем при вызове функции IsMedina.

    Пользовательские функции, принимающие сложный объект Range
    Рис. 9.2.  Вызов функции IsMedianaForAll, допускающей сложные объекты Range

    Проанализируем четыре сделанных вызова:

  • =IsMedianaForAll(7;M;N). В этом вызове наш кандидат - число 7 - проверяется по отношению к объединению двух массивов рабочего листа, заданных своими именами M и N. Формальных параметров у функции два, а фактических при вызове задается три. Два последних можно рассматривать как сложный объект Range, представляющий несмежную область ячеек и объединение вектора M и матрицы N. С программистской точки зрения, можно полагать, что передается массив с произвольным числом элементов, где каждый из них в свою очередь является массивом. Такой фактический параметр является допустимым значением формального параметра нашей функции, имеющего спецификатор ParamArray.
  • =IsMedianaForAll(4,5;N;M)). В этом вызове мало нового в сравнении с предыдущим. Изменен порядок следования массивов N и M, изменен кандидат, - им стало число 4.5, не входящее ни в один из массивов. Как показывает результат, это число является медианой объединенных массивов.
  • =IsMedianaForAll(7; {4;7;2}; {9;12;5}).Здесь в роли аргументов выступают массивы, заданные в виде констант, заключенных в фигурные скобки. Фактическое значение параметра M в этом случае представляет массив из двух элементов, каждый из которых в свою очередь является массивом.
  • =IsMedianaForAll(7; {4;7;2}; {9;12;5}; M). Ситуация в этом вызове сложнее, так как число аргументов возросло, но, что более важно, среди них есть как массивы - константы, так и массив рабочего листа - вектор M. Тем не менее, все работает правильно.



  • Dim Val As Variant For Each Val In Elem If Val > Cand Then Pos = Pos + 1 ElseIf Val < Cand Then Neg = Neg + 1 End If Next Val ElseIf TypeName(Elem) = "Integer()" Then 'Это настоящий массив целых VBA, для которого 'определены функции границ. For i = LBound(Elem) To UBound(Elem) If Elem(i) > Cand Then Pos = Pos + 1 ElseIf Elem(i) < Cand Then Neg = Neg + 1 End If Next i Else MsgBox ("При вызове IsMedianaForAll один из аргументов" _ & "не является массивом или объектом Range!") End If Next Elem IsMedianaForAll = Pos - Neg End Function

    Пример 9.2.

    Комментируя работу этой функции, отметим:

  • Эта функция может (и будет) вызываться как из процедур VBA, так и из формул рабочего листа Excel.
  • Формально функция по-прежнему имеет два параметра Cand и M. Правда, теперь они поменялись местами, и параметр M стал последним. Фактически у этой функции теперь произвольное число параметров, поскольку параметр M, сохранив тип Variant, стал теперь массивом. Спецификатор ParamArray подчеркивает, что это специальный массив с произвольным числом элементов.
  • Для работы с массивом M используется цикл типа For Each. В цикле выделяется очередной элемент Elem типа Variant, а дальше используется уже знакомый по функции IsMediana алгоритм проверки элемента Cand.
  • Разбор случаев делается независимо для каждого из элементов массива M.


  • Демонстрацию использования этой функции начнем с ее вызова в процедуре VBA, которая передает ей целочисленный массив элементов:

    Public Sub TestIsMedianaForAll() Const Size = 7 Dim Mas(1 To Size) As Integer Dim Cand As Integer Dim i As Integer Dim Res As Integer 'Инициализация массива целыми в интервале 1-20 Debug.Print TypeName(Mas) Randomize For i = 1 To Size Mas(i) = Int(Rnd * 21) Next i Cand = Int(Rnd * 21) Res = IsMedianaForAll(Cand, Mas) Debug.Print "Массив:" For i = 1 To Size Debug.Print Mas(i) Next i Debug.Print "Кандидат:", Cand Debug.Print "Результат:", Res End Sub

    Приведем результаты выполнения этой процедуры:

    M As Variant, Cand As

    Public Function IsMediana( M As Variant, Cand As Variant) As Integer 'Дан массив M и элемент Cand. В качестве результата возвращается 'разность между числом элементов массива M, больших и меньших Cand. Dim i As Integer, j As Integer Dim Pos As Integer, Neg As Integer Pos = 0: Neg = 0 'Анализ типа параметра M If TypeName(M) = "Range" Then For i = 1 To M.Rows.Count For j = 1 To M.Columns.Count If M.Cells(i, j) > Cand Then Pos = Pos + 1 ElseIf M.Cells(i, j) < Cand Then Neg = Neg + 1 End If Next j Next i IsMediana = Pos - Neg ElseIf TypeName(M) = "Variant()" Then 'TypeName is "Variant()" 'Это массив, но не совсем настоящий, для него не определены, 'например, функции границ: LBound, UBound. Dim Val As Variant For Each Val In M If Val > Cand Then Pos = Pos + 1 ElseIf Val < Cand Then Neg = Neg + 1 End If Next Val IsMediana = Pos - Neg ElseIf TypeName(M) = "Integer()" Then 'Это настоящий массив целых VBA, для которого 'определены функции границ. For i = LBound(M) To UBound(M) If M(i) > Cand Then Pos = Pos + 1 ElseIf M(i) < Cand Then Neg = Neg + 1 End If Next i IsMediana = Pos - Neg Else MsgBox ("При вызове функции:IsMediana(M,Cand)" _ & "- M не является массивом или объектом Range!") End If End Function
    Пример 9.1.
    Закрыть окно




    Public Function IsMedianaForAll(Cand As Variant, ParamArray M() As Variant) As Integer ' Эта функция осуществляет те же вычисления, что и функция IsMediana 'Важное отличие состоит в том, что аргумент M может быть задан сложным объектом 'Range как объединение массивов. Dim i As Integer, j As Integer Dim Pos As Integer, Neg As Integer Pos = 0: Neg = 0 Dim Elem As Variant 'Теперь M - это массив параметров, а Elem - его элемент. For Each Elem In M 'Анализ типа параметра Elem If TypeName(Elem) = "Range" Then For i = 1 To Elem.Rows.Count For j = 1 To Elem.Columns.Count If Elem.Cells(i, j) > Cand Then Pos = Pos + 1 ElseIf Elem.Cells(i, j) < Cand Then Neg = Neg + 1 End If Next j Next i ElseIf TypeName(Elem) = "Variant()" Then 'TypeName is "Variant()" 'Это массив, но не совсем настоящий, для него не определены, 'например, функции границ: LBound, UBound. Dim Val As Variant For Each Val In Elem If Val > Cand Then Pos = Pos + 1 ElseIf Val < Cand Then Neg = Neg + 1 End If Next Val ElseIf TypeName(Elem) = "Integer()" Then 'Это настоящий массив целых VBA, для которого 'определены функции границ. For i = LBound(Elem) To UBound(Elem) If Elem(i) > Cand Then Pos = Pos + 1 ElseIf Elem(i) < Cand Then Neg = Neg + 1 End If Next i Else MsgBox ("При вызове IsMedianaForAll один из аргументов" _ & "не является массивом или объектом Range!") End If Next Elem IsMedianaForAll = Pos - Neg End Function
    Пример 9.2.
    Закрыть окно




    Public Sub TestRecursive() 'Сравнение по времени рекурсивной и нерекурсивной реализации факториала. Dim i As Long, Res As Long Dim Start As Single, Finish As Single 'Рекурсивное вычисление факториала Start = Timer For i = 1 To 100000 Res = Fact(12) Next i Finish = Timer Debug.Print "Время рекурсивных вычислений:", Finish - Start
    'Нерекурсивное вычисление факториала Start = Timer For i = 1 To 100000 Res = Fact1(12) Next i Finish = Timer Debug.Print "Время нерекурсивных вычислений:", Finish - Start End Sub
    Пример 9.3.
    Закрыть окно




    Option Explicit
    'Класс BinTree 'Бинарным будем называть дерево, у которого каждая вершина имеет 'одного или двух потомков, называемых левым и правым сыном (поддеревом). 'В дальнейшем будем полагать, что узел нашего дерева содержит 'информационное поле info и поле ключа - key. 'Деревом поиска (двоичным или лексикографическим деревом) будем называть 'бинарное дерево, в котором ключ каждой вершины больше ключа, хранящегося 'в корне левого поддерева, и меньше ключа, хранящегося в корне правого поддерева. 'Рассмотрим операции над деревом поиска: поиск, включение, удаление элементов 'и обход дерева. Все операции сохраняют структуру дерева поиска.
    Public root As TreeNode
    Public Sub PrefixOrder() 'Префиксный обход дерева (корень, левое поддерево, правое)
    If Not (root Is Nothing) Then With root Debug.Print "key: ",.key, "info: ",.info .left.PrefixOrder .right.PrefixOrder End With End If
    End Sub
    Public Sub InfixOrder() 'Инфиксный обход дерева (левое поддерево, корень, правое)
    If Not (root Is Nothing) Then With root .left.InfixOrder Debug.Print "key: ",.key, "info: ",.info .right.InfixOrder End With End If
    End Sub
    Public Sub PostfixOrder() 'Постфиксный обход дерева (левое поддерево, правое, корень)
    If Not (root Is Nothing) Then With root .left.PostfixOrder .right.PostfixOrder Debug.Print "key: ",.key, "info: ",.info End With End If
    End Sub
    Public Sub SearchAndInsert(key As String, info As String) 'Если в дереве есть узел с ключом key, 'то возвращается информация в этом узле - работает поиск 'Если такого узла нет, то создается новый узел и его поля 'заполняются информацией, - работает вставка. 'Вначале поиск If root Is Nothing Then ' элемент не найден и происходит вставка Set root = New TreeNode root.key = key: root.info = info ElseIf key < root.key Then 'Поиск в левом поддереве root.left.SearchAndInsert key, info ElseIf key > root.key Then 'Поиск в правом поддереве root.right.SearchAndInsert key, info Else 'Элемент найден - возвращается результат поиска info = root.info End If
    End Sub
    Public Sub DelInTree(key As String) 'Эта процедура позволяет удалить элемент дерева с заданным ключом 'Удаление с сохранением структуры дерева более сложная операция, 'чем вставка или поиск. Причина сложности в том, что при удалении 'элемента остаются два его потомка, которые необходимо корректно 'связать с оставшимися элементами, чтобы не нарушить структуру дерева поиска. 'В программе анализируются три случая: 'Удаляется лист дерева (нет потомков - нет проблем), 'Удаляется узел с одним потомком (потомок замещает удаленный узел), 'Есть два потомка. В этом случае узел может быть заменен одним из двух 'возможных кандидатов, не имеющих двух потомков. 'Кандидатами являются самый левый узел правого подддерева и 'самый правый узел левого поддерева. 'Мы производим удаление в левом поддереве.
    Dim q As TreeNode If root Is Nothing Then Debug.Print "Key is not found" ElseIf key < root.key Then 'Удаляем из левого поддерева root.left.DelInTree key ElseIf key > root.key Then 'Удаляем из правого поддерева root.right.DelInTree key Else 'Удаление узла Set q = root If q.right.root Is Nothing Then Set root = q.left.root ElseIf q.left.root Is Nothing Then Set root = q.right.root Else 'есть два потомка q.left.ReplaceAndDelete q End If Set q = Nothing End If
    End Sub
    Public Sub ReplaceAndDelete(q As TreeNode) 'Заменяет узел на самый правый If Not (root.right.root Is Nothing) Then root.right.ReplaceAndDelete q Else 'Найден самый правый q.key = root.key: q.info = root.info Set root = root.left.root End If
    End Sub
    Пример 9.4.
    Закрыть окно




    Public Sub WorkwithBinTree() Dim MyDict As New BinTree Dim englword As String, rusword As String 'Создание словаря
    MyDict.SearchAndInsert key:="dictionary", info:="словарь" MyDict.SearchAndInsert key:="hardware", info:="аппаратура, аппаратные средства" MyDict.SearchAndInsert key:="processor", info:="процессор" MyDict.SearchAndInsert key:="backup", info:="резервная копия" MyDict.SearchAndInsert key:="token", info:="лексема" MyDict.SearchAndInsert key:="file", info:="файл" MyDict.SearchAndInsert key:="compiler", info:="компилятор" MyDict.SearchAndInsert key:="account", info:="учетная запись"
    'Обход словаря MyDict.PrefixOrder
    'Поиск в словаре englword = "account": rusword = "" MyDict.SearchAndInsert key:=englword, info:=rusword Debug.Print englword, rusword
    'Удаление из словаря MyDict.DelInTree englword englword = "hardware" MyDict.DelInTree englword
    'Обход словаря MyDict.PrefixOrder
    End Sub
    Пример 9.5.
    Закрыть окно



    Работа со словарем

    Используем класс BinTree для работы со словарем. В нашем примере работы с классом будет создаваться словарь, в нем будет осуществляться поиск и удаление элементов. Вот текст процедуры, выполняющей эти операции:
    Пример 9.5.
    (html, txt)
    Приведем результаты ее работы:
    key: dictionary info: словарь key: backup info: резервная копия key: account info: учетная запись key: compiler info: компилятор key: hardware info: аппаратура, аппаратные средства key: file info: файл key: processor info: процессор key: token info: лексема account учетная запись key: dictionary info: словарь key: backup info: резервная копия key: compiler info: компилятор key: file info: файл key: processor info: процессор key: token info: лексема
    Обратите внимание, процедура обхода дерева в префиксном порядке печатает слова из словаря не в том порядке, в каком он создавался. Это и понятно, поскольку дерево создается, как лексикографическое дерево поиска. Взгляните, как выглядит дерево поиска нашего словаря после его первоначального создания.
    Работа со словарем
    увеличить изображение
    Рис. 9.3.  Лексикографическое дерево, задающее словарь

    Рекурсивные процедуры

    VBA допускает создание рекурсивных процедур, т. е. процедур, при вычислении вызывающих самих себя. Вызовы рекурсивной процедуры могут непосредственно входить в ее тело, или она может вызывать себя через другие процедуры. В последнем случае в модуле есть несколько связанных рекурсивных процедур. Стандартный пример рекурсивной процедуры - функция-факториал Fact(N)= N!. Вот ее определение в VBA:
    Function Fact(N As Integer) As Long If N <= 1 Then ' базис индукции. Fact = 1 ' 0! =1. Else ' рекурсивный вызов в случае N > 0. Fact = Fact(N - 1) * N End If End Function
    Так как каждый вызов процедуры требует накладных расходов, эффективнее для факториала итеративная программа:
    Function Fact1(N As Integer) As Long Dim Fact As Long, i As Integer Fact = 1 ' 0! =1. If N > 1 Then ' цикл вместо рекурсии. For i = 1 To N Fact = Fact * i Next i End If Fact1 = Fact End Function
    Приведем процедуру, оценивающую время исполнения рекурсивного и не рекурсивного варианта:
    Пример 9.3.
    (html, txt)
    Вот результаты вычислений, приведенные для двух запусков тестовой процедуры:
    Время рекурсивных вычислений: 6,238281 Время нерекурсивных вычислений: 2,304688 Время рекурсивных вычислений: 6,25 Время нерекурсивных вычислений: 2,253906
    Как видите, в данном случае не рекурсивный вариант работает почти в три раза быстрее. Кроме проблем со временем исполнения, рекурсивные процедуры могут легко исчерпать и стековую память, в которой размещаются аргументы каждого рекурсивного вызова. Поэтому избегайте неконтролируемого размножения рекурсивных вызовов и заменяйте рекурсивные алгоритмы на итеративные там, где использование рекурсии по существу не требуется.
    Польза от рекурсивных процедур в большей мере может проявиться при обработке данных, имеющих рекурсивную структуру (скажем, иерархическую или сетевую). Основные структуры данных (объекты) Office 97 вообще-то не являются рекурсивными: один рабочий лист Excel не может быть значением ячейки другого, одна таблица Access - элементом другой и т.д. Но данные, хранящиеся на рабочих листах Excel или в БД Access, сами по себе могут задавать "рекурсивные" отношения, и для их успешной обработки следует пользоваться рекурсивными процедурами. Мы рассмотрим сейчас класс, для работы с двоичными деревьями поиска. Деревья представляют рекурсивную структуру данных, поэтому и операции над ними естественным образом определяются рекурсивными алгоритмами.

    Синтаксис процедур и функций

    Описание процедуры Sub в VBA имеет такой вид.
    [Private | Public] [Static] Sub имя([список-аргументов]) тело-процедуры End Sub
  • Ключевое слово Public в заголовке процедуры используется, чтобы объявить процедуру общедоступной, т. е. дать возможность вызывать ее из всех других процедур всех модулей любого проекта. Если модуль, в котором описана процедура, содержит закрывающий оператор Option Private, процедура будет доступна лишь модулям своего проекта. Альтернативный ключ Private используется, чтобы закрыть процедуру от всех модулей, кроме того, в котором она описана. По умолчанию процедура считается общедоступной.
  • Ключевое слово Static означает, что значения локальных (объявленных в теле процедуры) переменных будут сохраняться в промежутках между вызовами процедуры (используемые процедурой глобальные переменные, описанные вне ее тела, при этом не сохраняются).
  • Параметр имя - это имя процедуры, удовлетворяющее стандартным условиям VBA на имена переменных.
  • Необязательный параметр список-аргументов - это последовательность разделенных запятыми переменных, задающих передаваемые процедуре при вызове параметры. Заметьте, что аргументы или, как мы часто говорим, формальные параметры, задаваемые при описании процедуры, всегда представляют только имена (идентификаторы). В то же время при вызове процедуры ее аргументы - фактические параметры могут быть не только именами, но и выражениями.
  • Последовательность операторов тело-процедуры задает программу выполнения процедуры. Тело процедуры может включать как "пассивные" операторы объявления локальных данных процедуры (переменных, массивов, объектов и др.), так и "активные" - они изменяют состояния аргументов, локальных и внешних (глобальных) переменных и объектов. В тело могут входить также операторы Exit Sub, приводящие к немедленному завершению процедуры и передаче управления в вызывающую программу. Каждая процедура в VBA определяется отдельно от других, т. е. тело одной процедуры не может включать описания других процедур и функций.


  • Рассмотрим подробнее структуру одного аргумента из списка-аргументов.
    [Optional] [ByVal | ByRef] [ParamArray] переменная[()] [As тип] [= значение-по-умолчанию]
  • Ключевое слово Optional означает, что заданный им аргумент является возможным, необязательным, - его необязательно задавать в момент вызова процедуры. Для таких аргументов можно задать значение по умолчанию. Необязательные аргументы всегда помещаются в конце списка аргументов.
  • Альтернативные ключи ByVal и ByRef определяют способ передачи аргумента в процедуру. ByVal означает, что аргумент передается по значению, т. е. при вызове процедуры будет создаваться локальная копия переменной с начальным передаваемым значением и изменения этой локальной переменной во время выполнения процедуры не отразятся на значении переменной, передавшей свое значение в процедуру при вызове. Передача по значению возможна только для входных параметров, которые передают информацию в процедуру, но не являются результатами. Для таких параметров передача по значению зачастую удобнее, чем передача по ссылке, поскольку в момент вызова аргумент может быть задан сколь угодно сложным выражением. Заметим, что входные параметры, являющиеся объектами, массивами или переменными пользовательского типа, передаются по ссылке, что позволяет избежать создание копий. Выражения над такими аргументами все равно недопустимы, поэтому передача по значению теряет свой смысл.
  • ByRef означает, что аргумент передается по ссылке, т. е. все изменения значения передаваемой переменной при выполнении процедуры будут непосредственно происходить с переменной-аргументом из вызвавшей данную процедуру программы. В VBA по умолчанию аргументы передаются по ссылке (ByRef). Это не совсем удобно для программистов, привыкших к другим языкам (например, Паскалю или С), где по умолчанию аргументы передаются по значению. Поэтому при описании процедуры рекомендуем явно указывать способ передачи каждого аргумента. Отметим также одну интересную особенность, которую не следует использовать, но которую следует учитывать, - VBA допускает, чтобы фактическое значение аргумента, передаваемого по ссылке, было константой или выражением соответствующего типа.


    В таком случае этот аргумент рассматривается как передаваемый по значению, и никаких сообщений об ошибке не выдается, даже если этот аргумент встречается в левой части присвоения.
  • Процедура VBA допускает возможность иметь необязательные аргументы, которые можно опускать в момент вызова. Обобщением такого подхода является возможность иметь переменное, заранее не фиксированное число аргументов. Достигается это за счет того, что один из параметров (последний в списке) может задавать массив аргументов, - в этом случае он задается с описателем ParamArray. Если список-аргументов включает массив аргументов ParamArray, ключ Optional использовать в списке нельзя. Ключевое слово ParamArray. может появиться перед последним аргументом в списке, чтобы указать, что этот аргумент - массив с произвольным числом элементов типа Variant. Перед ним нельзя использовать ключи ByVal, ByRef или Optional.
  • Переменная - это имя переменной, представляющей аргумент.
  • Если после имени переменной заданы круглые скобки, то это означает, что соответствующий параметр является массивом.
  • Параметр тип задает тип значения, передаваемого в процедуру. Он может быть одним из базисных типов VBA (не допускаются только строки String c фиксированной длиной). Обязательные аргументы могут также иметь тип определенной пользователем записи или класса. Если тип аргумента не указан, то по умолчанию ему приписывается тип Variant. Ну и, конечно же, в этом мощь VBA, тип может быть одним из типов Office 2000.
  • Для необязательных (Optional) аргументов можно явно задать значение-по умолчанию. Это константа или константное выражение, значение которого передается в процедуру, если при ее вызове соответствующий аргумент не задан. Для аргументов типа объект (Object) в качестве значения по умолчанию можно задать только Nothing.

  • Синтаксис определения процедур-функций похож на определение обычных процедур:
    [Public | Private] [Static] Function имя [(список-аргументов)] [As тип-значения] тело-функции End Function
    Отличие лишь в том, что вместо ключевого слова Sub для объявления функции используется ключевое слово Function, а после списка аргументов следует указать параметр тип-значения, определяющий тип возвращаемого функцией значения.


    В теле функции должен быть использован оператор присвоения вида:
    имя = выражение
    Здесь, в левой части оператора стоит имя функции, а в правой - значение выражения, задающего результат вычисления функции. Если при выходе из функции переменной имя значение явно не присвоено, функция возвращает значение соответствующего типа, определенное по умолчанию. Для числовых типов это 0, для строк - строка нулевой длины (""), для типа Variant функция вернет значение Empty, для ссылок на объекты - Nothing.
    Чтобы немедленно завершить вычисления функции и выйти из нее, в теле функции можно использовать оператор:
    Exit Function
    Основное отличие процедур от функций состоит в способе их использования в вызывающей программе. Следующая функция Cube возвращает аргумент, возведенный в куб:
    Function cube(ByVal N As Integer) As Long cube= N*N*N End Function
    Вызов этой функции может иметь вид
    Dim x As Integer, y As Integer y = 2 x = cube(y+3)
    Мы уже говорили, что любую функцию можно преобразовать в эквивалентную ей процедуру. Когда функция преобразуется в процедуру, то появляется дополнительный параметр, необходимый для задания результата. Так что у эквивалентной процедуры Cube1 два аргумента:
    Sub cube1(ByVal N As Integer, ByRef C As Long) C= N*N*N ' получение результата в переменной, заданной по ссылке End Sub
    Ее можно использовать для такого же возведения в куб:
    cube1 y+3, x
    Эта взаимозаменяемость отнюдь не означает, что безразлично, какой вид процедур использовать в программе. Если бы выражение, в котором участвует функция, было сложнее, например,
    x = cube(y)+sin(cube(x))
    то его вычисление с помощью процедуры Cube1 потребовало бы выполнения нескольких операторов и ввода дополнительных переменных:
    cube1 y,z cube1 x,u x=z+ sin(u)

    Создание процедур обработки событий

    VBA является языком, в котором, как и в большинстве современных объектно-ориентированных языков, реализована концепция программирования, управляемого событиями (event driven programming). Здесь нет понятия программы, которая начинает выполняться от Begin до End. В противоположность этому есть множество объектов Office 2000, представляющих документы и их компоненты, каждый из которых может реагировать на события. Пользователи системы документов и операционная система могут инициировать события в мире объектов. В ответ на возникновение события операционная система посылает сообщение соответствующему объекту. Реакцией объекта на получение сообщения является вызов процедуры - обработчика события. Задача программиста сводится к написанию обработчиков событий для объектов. Заметьте, все обычные процедуры и функции VBA, о которых мы говорили выше, вызываются прямо или косвенно из процедур обработки событий, если только речь не идет о режиме отладки. Именно эти процедуры являются спусковым крючком, приводящим к последовательности вызовов обычных процедур и функций.
    С каждым из объектов Office 2000 связан набор событий, на которые он может реагировать. Процедуры обработки этих событий располагаются в модулях, связанных с объектами, реагирующими на события. Для кнопок меню, у которых есть только один обработчик события, соответствующая процедура может находиться в стандартном модуле или модуле макросов. Office 2000 позволяет при описании собственных классов, создаваемых программистом, задать определенный набор событий. Обработчики событий таких объектов создаются по определенной технологии. Все эти вопросы были уже подробно рассмотрены в предыдущих лекциях. Не будем сейчас повторяться и для напоминания ограничимся лишь простым примером. Вот, например, заготовка для события Close документа Word:
    Private Sub Document_Close() End Sub
    Дополнив ее:
    Private Sub Document_Close() Dim I As Integer
    For I = 1 To 5 ' 5 раз подается Beep ' звуковой сигнал Next I End Sub
    мы получим процедуру, которая будет 5 раз подавать звуковой сигнал при закрытии документа. Эта процедура находится в модуле, связанном с объектом ThisDocument, и вызывается в момент закрытия документа.

    Создание процедуры

    Здесь мы рассмотрим создание процедур, текст которых пишется вручную. Чтобы создать новую процедуру, нужно:
  • открыть в окне проектов "Проект-(VBA)Project" (Project Explorer) папку с модулем (формой, документом, рабочим листом и т. п.), к которому требуется добавить процедуру, и, щелкнув этот модуль, открыть окно редактора с кодами процедур модуля;
  • перейти в редактор, набрать ключевое слово (Sub, Function или Property), имя процедуры и ее аргументы; затем нажмите клавишу Enter, и VBA поместит ниже строку с соответствующим закрывающим оператором (End Sub, End Function, End Property);
  • написать текст процедуры между ее заголовком и закрывающим оператором.

  • Как правило, следует "автоматизировать" работу, вызвав диалоговое окно "Вставка процедуры" (Insert Procedure). Последовательность действий в этом случае такая:
  • выбрать в меню Вставка (Insert) команду Процедура (Procedure);
  • в поле Имя (Name) появившегося окна "Вставка процедуры" (Insert Procedure) ввести имя процедуры.
  • указать в группе кнопок-переключателей Тип (Type) тип создаваемой процедуры: Подпрограмма (Sub), Функция (Function) или Свойство (Property);
  • указать в группе кнопок-переключателей "Область определения" (Scope) вид доступа к процедуре: Общая (Public) или Личная (Private);
  • пометить, если нужно, флажок "Все локальные переменные считать статическими" (All Local Variables as Statics), чтобы в заголовок процедуры добавился ключ Static;
  • щелкнуть кнопку OK - в окне редактора появится заготовка процедуры, состоящая из ее заголовка (без параметров) и закрывающего оператора;
  • добавить параметры в заголовок процедуры и написать текст процедуры между ее заголовком и закрывающим оператором.


  • Вызовы функций

    Оформление вызова функции зависит от того, требуется ли использовать ее значение в вызывающей процедуре. Если Вы хотите передать вычисляемое функцией значение в переменную или применить его в выражении правой части оператора присвоения, то вызов пользовательской функции имеет тот же вид, что и вызов встроенной функции, например, sin(x). При вызове указывается имя функции, а после него идет заключенный в круглые скобки список фактических параметров. Например, если заголовок функции MyFunc:
    Func Myfunc(Name As String, Age As Integer, Newdate As Date) As Integer
    использовать ее значение можно с помощью вызовов:
    val= Myfunc("Alex",25, "10/04/97")
    или
    x = sqrt(Myfunc("Alex",25, "10/04/97")) + x
    Если же значение, вычисляемое функцией, нас не интересует и нужно воспользоваться лишь ее побочными эффектами, вызов функции может иметь ту же форму, что и вызов процедуры Sub. Например:
    Myfunc "Alex",I, "10/04/97"
    или:
    Call Myfunc(Myson, 25, DateOfArrival)

    Вызовы процедур Sub

    Вызов обычной процедуры Sub из другой процедуры можно оформить по-разному. Первый способ:
    имя список-фактических-параметров
    где имя - имя вызываемой процедуры, а список-фактических-параметров - список аргументов, передаваемых процедуре; он должен соответствовать списку аргументов, заданному в описании процедуры. Задать этот список можно разными способами. В простейшем случае в нем перечисляются через запятую значения передаваемых процедуре аргументов в том же порядке, что и в списке аргументов из заголовка процедуры.
    Может оказаться, что в одном проекте несколько модулей содержат процедуры с одинаковыми именами. Для различения этих процедур нужно при их вызове указывать имя процедуры через точку после имени модуля, в котором она определена. Например, если каждый из двух модулей Mod1 и Mod2 содержит определение процедуры ReadData, а в процедуре MyProc нужно воспользоваться процедурой из Mod2, этот вызов имеет вид:
    Sub Myproc() ... Mod2.ReadData ... End Sub
    Если требуется использовать процедуры с одинаковыми именами из разных проектов, добавьте к именам модуля и процедуры имя проекта. Например, если модуль Mod2 входит в проект MyBook, тот же вызов можно уточнить так:
    MyBooks.Mod2.ReadData
    Второй способ вызова процедур связан с использованием оператора Call. В этом случае вызов процедуры выглядит так:
    Call имя(список-фактических-параметров)
    Обратите внимание на то, что в этом случае список-фактических-параметров заключен в круглые скобки, а в первом случае - нет. Попытка вызывать процедуру без оператора Call, но с заданием круглых скобок является источником синтаксических ошибок особенно для разработчиков с большим опытом программирования на Паскале или С, где списки параметров всегда заключаются в скобки. Следует обратить внимание на одну важную и, пожалуй, неприятную особенность вызова процедур VBA. Если процедура VBA имеет только один параметр, то она может быть вызвана без оператора Call и с использованием круглых скобок, не сообщая об ошибке вызова. Это было бы не так страшно, если бы возвращался правильный результат.
    К сожалению, это не так, проиллюстрируем сказанное примером:

    Public Sub MyInc(ByRef X As Integer) X = X + 1 End Sub

    Public Sub TestInc() Dim X As Integer X = 1 'Вызов процедуры с параметром, заключенным в скобки, 'синтаксически допустим, но работает не корректно! MyInc (X) Debug.Print X

    'Корректный вызов MyInc X Debug.Print X

    'Это тоже корректный вызов Call MyInc(X) Debug.Print X

    End Sub

    Вот результаты ее работы:

    1 2 3

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

    Если же процедура имеет более одного параметра, то попытка вызвать ее, заключив параметры в круглые скобки и не предварив этот вызов ключевым словом Call, приводит к синтаксической ошибке. Вот простой пример:

    Public Sub SumXY(ByVal X As Integer, ByVal Y As Integer, ByRef Z As Integer) Z = X + Y End Sub

    Public Sub TestSumXY() Dim a As Integer, b As Integer, c As Integer a = 3: b = 5 'SumXY (a, b, c) 'Синтаксическая ошибка SumXY a, b, c Debug.Print c

    End Sub

    В этом примере некорректный вызов процедуры SumXY будет обнаружен на этапе проверки синтаксиса.

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

    Пусть, например, процедура CompVal c 4 аргументами, которая в зависимости от положительности z возвращает в переменной y либо увеличенное, либо уменьшенное на 100 значение x и сообщает об этом в строковой переменной w, определена следующим образом.


    Sub CompVal(ByVal x As Single, ByRef y As Single, _ ByVal z As Integer, ByRef w As String)

    If z > 0 Then ' увеличение y = x + 100 w = "increase" Else ' уменьшение y = x - 100 w = "decrease" End If End Sub

    Рассмотрим процедуру TestCompVal, в которой несколько раз вызывается процедура CompVal:

    Sub TestCompVal() Dim a As Single Dim b As Single Dim n As Integer Dim S As String

    n = 5: a = 7.4 ' значения параметров CompVal a, b, n, S ' 1-ый вызов Debug.Print b, S

    CompVal 7.4, b, 5, S ' 2-ой вызов Debug.Print b, S

    CompVal 0, 0, 0, S ' 3-ий вызов Debug.Print b, S

    CompVal 0, 0, 0, "В чем дело?" ' 4-ый вызов Debug.Print b, S End Sub

    В результате выполнения этой процедуры будут напечатаны следующие результаты:

    107,4 increase 107,4 increase 107,4 decrease 107,4 decrease

    Первые два вызова корректны. Следующие два вызова хотя и допустимы в языке VBA, но приводят к тому, что параметры, переданные по ссылке, не меняют своих значений в ходе выполнения процедуры и, по существу, вызов ByRef по умолчанию заменяется вызовом ByVal. Конечно, было бы лучше, если бы эта программа выдавала ошибки на этапе проверки синтаксиса.

    Задача о медиане

    Для массива M и элемента Cand вычислить разность между числом элементов массива M, больших и меньших Cand.
    Это вариация задачи о медиане - "среднем" элементе - массива. Медиану можно определить, например, таким алгоритмом: упорядочив массив, взять элемент, находящийся в середине. Есть и более эффективные алгоритмы. Но мы решили ограничиться более простой задачей - проверкой на "медианность". Заметим: если все элементы массива M различны и число их нечетно, то для медианы искомая в задаче разность равна 0. В общем случае, значение разности является мерой близости параметра Cand к медиане массива M. Но займемся программистскими аспектами этой задачи. У функции, ее реализующей, на входе - массив, а на выходе - скаляр. Мы хотели бы, чтобы эта функция могла вызываться в формулах рабочего листа, а в качестве фактического параметра ей могли быть переданы как объект Range, так и массив Visual Basic. Вот как мы реализовали эту функцию, назвав ее IsMediana:
    Пример 9.1.
    (html, txt)
    Прокомментируем работу функции IsMediana.
  • Функция IsMediana может (и будет) вызываться как из процедур VBA, так и из рабочих формул листа Excel. Обратите внимание, она работает с объектами Office 2000 - Range, Cells, Rows и другими.
  • Функции, чьи аргументы имеют универсальный тип Variant, целесообразно строить по принципу разбора случаев. Алгоритм обработки зависит от типа фактического параметра, задаваемого в момент вызова.
  • Стандартная функция TypeName(V) возвращает в качестве результата конкретный тип параметра V.
  • Работа функции IsMediana(M,Cand) начинается с вызова TypeName(M). Далее разбираются четыре возможных случая: M - объект Range, M - массив типа Variant(), M - настоящий целочисленный массив VBA, M имеет любой другой тип.
  • В первом случае функция IsMediana вызывается в формуле рабочего листа Excel и в качестве фактического параметра ей передается объект Range - интервал ячеек этого листа. Следовательно, функция TypeName возвратит строку "Range" в качестве результата.
    При обработке этого случая организуется цикл по числу строк и столбцов объекта Range, используя свойство Cells этого объекта.
  • Во втором случае обработка основана на том, что функции передан массив типа Variant(). Это возможно, когда при вызове нашей функции в формуле рабочего листа ей передается константа, задающая массив. Ниже мы приведем примеры подобного вызова. Для таких массивов не определены функции границ UBound и LBound. Поэтому обработка в этом случае основана на использовании цикла For Each.
  • В третьем случае функция получает при вызове обычный массив VBA и обработка идет стандартным для массивов способом. Мы приведем пример вызова нашей функции из обычной процедуры VBA, передающей в момент вызова целочисленный массив.
  • В четвертом случае, когда наш параметр не является ни массивом, ни объектом Range, в качестве результата по умолчанию выдается 0. Но выдается также и окно сообщений с предупреждением о возникшей ситуации.

  • Начнем с того, что приведем процедуру VBA, вызывающую нашу функцию. Вот ее текст:
    Public Sub TestIsMediana() Const Size = 7 Dim Mas(1 To Size) As Integer Dim Cand As Integer Dim i As Integer Dim Res As Integer 'Инициализация массива целыми в интервале 1-20 Debug.Print TypeName(Mas) Randomize For i = 1 To Size Mas(i) = Int(Rnd * 21) Next i Cand = Int(Rnd * 21) Res = IsMediana(Mas, Cand) Debug.Print "Массив:" For i = 1 To Size Debug.Print Mas(i) Next i Debug.Print "Кандидат:", Cand Debug.Print "Результат:", Res End Sub
    Вот результаты ее работы:
    Массив: 3 8 14 0 3 8 2 Кандидат: 2 Результат: 4
    В данном варианте вызове анализ типа переданного параметра показал, что он является обычным массивом, соответственно был выбран третий вариант обработки, не требующий работы с объектами Office 2000.
    Теперь покажем, что эту же функцию можно вызывать в формулах рабочего листа Excel, передавая ей в момент вызова объекты Range в разной форме, а также массивы, заданные константой - массивом. Посмотрим, как это выглядит на экране, и разберем примеры нескольких различных вызовов функции IsMediana в формулах рабочего листа:


    Задача о медиане
    увеличить изображение
    Рис. 9.1.  Вызов функции IsMediana в формулах рабочего листа
    На рабочем листе мы сформировали два массива: вектор M, вытянутый в виде столбца, и прямоугольную матрицу N. Вектор M записан в ячейках C6:C11, матрица N - в F5:I6. В ячейки E8:E15 мы поместили формулы, вызывающие функцию IsMediana. Они не являются формулами над массивами, несмотря на то, что параметром может быть массив рабочего листа. Важно, что результат - скаляр. Если бы результат, возвращаемый функцией, был массивом, формулу следовало бы вызывать как формулу над массивами. Для скалярного результата это не так.
    В двух первых вызовах функции IsMediana (в ячейках E8, E9) передается в качестве параметров имя массива рабочего листа "M" и разные кандидаты: 7 и 8. Они оба годятся на роль медианы этого массива. В следующих двух вызовах проверяются кандидаты на медиану массива N. Как видите, оба кандидата 4 и 3 одинаково близки к медиане. Следующие два вызова в ячейках E12 и E13 демонстрируют возможность указания непосредственно диапазона ячеек в момент вызова, что позволяет, например, работать с частью массива. В следующем вызове вообще не используются в качестве входных данных элементы рабочего листа. Входным параметром M является массив - константа, заключенный в фигурные скобки, а его элементы разделяются символом ";". В этих случаях фактический параметр уже не является объектом Range, а имеет тип массива с элементами Variant. Поэтому и функция IsMediana будет работать по-другому, в отличие от предыдущих вызовов. Разбор случаев в зависимости от результата, возвращаемого функцией TypeName, приведет к выбору второго варианта. Наконец, вызов, записанный в формуле из ячейки E15, демонстрирует случай, когда входной параметр M - обычное число и, следовательно, не является ни объектом Range, ни массивом. Как следствие, разбор случаев в функции IsMediana приводит к четвертому варианту и появлению на экране окна сообщений.


    Следующие два вызова в ячейках E12 и E13 демонстрируют возможность указания непосредственно диапазона ячеек в момент вызова, что позволяет, например, работать с частью массива. В следующем вызове вообще не используются в качестве входных данных элементы рабочего листа. Входным параметром M является массив - константа, заключенный в фигурные скобки, а его элементы разделяются символом ";". В этих случаях фактический параметр уже не является объектом Range, а имеет тип массива с элементами Variant. Поэтому и функция IsMediana будет работать по-другому, в отличие от предыдущих вызовов. Разбор случаев в зависимости от результата, возвращаемого функцией TypeName, приведет к выбору второго варианта. Наконец, вызов, записанный в формуле из ячейки E15, демонстрирует случай, когда входной параметр M - обычное число и, следовательно, не является ни объектом Range, ни массивом. Как следствие, разбор случаев в функции IsMediana приводит к четвертому варианту и появлению на экране окна сообщений.

    Основы офисного программирования и язык VBA

    Циклы

    Когда требуется уменьшить размеры используемой памяти, нужно начинать с чистки массивов. Когда требуется уменьшить время выполнения программы, нужно начинать с чистки циклов. Особо тщательно нужно оптимизировать выполнение самых внутренних циклов. Каждая миллисекунда, сэкономленная на выполнении внутреннего цикла, может обернуться сэкономленными часами на выполнении всей программы. Вот несколько советов, полезных при работе с циклами:
  • Все вычисления, которые могут быть сделаны вне цикла, должны быть вынесены из него.
  • Предусматривайте возможность досрочного завершения цикла, когда решение задачи уже получено.
  • Аккуратно работайте в цикле с элементами массивов. Старайтесь избегать лишнего вычисления индексных выражений и обращений к элементам массива. Часто введение дополнительных переменных позволяет существенно ускорить работу с элементами массивов.
  • При работе с коллекциями объектов Office 2000 используйте цикл For Each вместо обычного цикла For. В большинстве случаев это приводит к существенному выигрышу во времени исполнения цикла.

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

    Директива #const

    Эта директива позволяет задать константы условной компиляции. Ее синтаксис:
    #Const constname = expression
    Имя константы строится по обычным правилам, а выражение в правой части может содержать только литералы (символьные константы) и другие константы условной компиляции, соединенные знаками арифметических и логических операций за исключением операции Is. Эти константы, являются флажками и используются в операторе #If

    Доказательство правильности программ

    Мы уже говорили, что отладка, основанная на построении системы тестов, не может доказать правильность программы. Поэтому в теоретическом программировании были предприняты большие усилия по разработке методов доказательства правильности программ, такие же строгие, как и методы доказательства правильности теорем. На практике эти методы не получили широкого распространения по двум причинам. Во-первых, построить доказательство правильности программы сложнее, чем написать саму программу. Во-вторых, ошибки в доказательстве столь же возможны, как и в самой программе. Тем не менее, знание основ доказательства правильности программ должно быть частью образования программиста. Умение строго доказывать правильность простых программ помогает программисту лучше понять, как следует разрабатывать корректно работающие, сложные программы. Не ставя целью сколь либо полный обзор этого важного направления, остановимся лишь на самом понятии правильности программы. Действительно, мы многократно использовали этот термин, но что значит правильно (корректно) работающая программа? Вот одно из возможных определений. Пусть P(X,Y) - программа, с заданными входными данными X и результатами Y. Предикат Q(X), определенный на входных данных, будем называть предусловием программы P, а предикат R(X,Y), связывающий входные и выходные переменные будем называть постусловием программы P. Будем также предполагать, что в ходе своей работы программа не меняет своих входных переменных X.
    Программа P(X,Y) корректна по отношению к предусловию Q(X) и постусловию R(X,Y), если из истинности Q(X) до начала выполнения программы следует, что, будучи запущенной, программа завершит свою работу и по ее завершению будет истинным предикат R(X,Y). Условие корректности записывают в виде триады (триады Хоора) - Q(X) {P(X,Y)} R(X,Y)
    Уже из этого определения становится ясно, что говорить о правильности следует не вообще, а по отношению к заданным спецификациям, например, в виде предусловия и постусловия. Доказать правильность триады для сложных программ, как уже говорилось, довольно сложно. Один из методов (метод Флойда) состоит в том, что программа разбивается на участки, размеченные предикатами - Q1, Q2, …QN, R. Первый из предикатов представляет предусловие программы, последний - постусловие. Тогда доказательство корректности сводится к доказательству корректности последовательности триад:
    Q1{P1}Q2; Q2{P2}Q3; …QN{PN}R
    Нетрудно видеть, что введение Assert - утверждений является отражением метода Флойда. Хотя использование этих утверждений не предполагает проведения строгого доказательства, но это практически реализуемая попытка в этом направлении. Все что может быть сделано для повышения надежности программы, должно быть сделано.

    Функция CvErr

    При работе с процедурами стандартного модуля есть еще один способ для возврата кодов ошибок, определенных пользователем. Для этой цели можно использовать функцию CVErr, возвращающую значение типа Variant с подтипом Error, которое содержит код ошибки, указанный пользователем. В вызывающей процедуре с помощью булевой функции IsError можно проверить, является ли возвращенное значение ошибкой. В следующем примере генерируется ошибка 1999, если аргумент функции Func1 является нечисловым.
    Function Func1(Number As Variant) As Variant If IsNumeric(Number) Then ' Вычисление корректного результата. Func1 = Number * Number Else 'аргумент некорректен Func1 = CVErr(1999) ' возвращает код ошибки End If End Function
    Проверять корректность работы Func1 можно так.
    Пример 10.6.
    (html, txt)
    Приведем результаты вычислений:
    Результат : 144 Ошибка #: Error 1999 аргумент : двенадцать

    #If … Then … #Else директива

    Оператор If условной компиляции или директива If имеет синтаксис, похожий на обычный оператор If:
    #If expression Then statements [#ElseIf expression-n Then [elseifstatements]] [#Else [elsestatements]] #End If
    Особенность состоит в том, что выражения состоят из констант периода компиляции и литералов, объединенных знаками операций. Все выражения вычисляются, поэтому все константы периода компиляции должны быть определены. Если выражение ложно, то соответствующие операторы не компилируются и не увеличивают тем самым размер программы. Основное назначение директивы условной компиляции в том, чтобы скомпилировать одну и ту же программу для различных платформ, для различных версий, для того, чтобы убрать отладочный код из заключительной версии программы.
    Приведем простой пример использования средств условной компиляции:
    'Константа conDebug включает или выключает отладочную печать #Const conDebug = True '#Const conDebug = False Public Sub TestDebug() #If conDebug Then Debug.Print "Привет!" #Else MsgBox ("Привет!") #End If End Sub

    Искусство отладки

    Прелесть работы программиста во многом связана с отладкой. Почему программисты нарушают известные им требования, - не задают комментарии, не описывают детально суть решаемой задачи и не следуют другим полезным советам. Чаще всего, причина в нетерпении, им хочется скорее посмотреть, как же работает программа, увидеть результаты ее работы. Отладка - это некоторый детективный процесс. Вновь созданную программу мы подозреваем в том, что она работает не корректно. Презумпция невиновности здесь не работает. Если удается предъявить тест, на котором программа дает неверный результат, то доказано, что наши подозрения верны. Втайне мы всегда надеемся, что программа заработает правильно с первого раза. Но цель тестирования другая, - попытаться опровергнуть это предположение. И только потом, исправив все выявленные ошибки, получить корректно работающую программу. К сожалению, отладка не может гарантировать, что программа корректна, даже если все тесты прошли успешно. Отладка может доказать некорректность программы, но она не может доказать ее правильности.
    Искусство тестера состоит в том, чтобы создать по возможности полную систему тестов, проверяющую все возможные ветви вычислений. Поясним это на самом простом примере. Пусть программа находит сумму первых N элементов массива X, содержащего M элементов. Кроме "нормального" теста, проверяющего ситуацию, в которой 1M. Но это простой случай, а циклы обычно вложенные, и внутри них производится разбор случаев, внутри которых свои циклы.
    Ранее мы упоминали закон "чечако" - новичок может подвесить любую систему. Этому есть объяснение, по незнанию, он задаст одно из мало вероятных сочетаний входных данных (работая в визуальной среде, нажмет самую неподходящую для данной ситуации кнопку). Поэтому тестер, ведущий отладку, должен уметь встать и на позицию новичка, система тестов должна гарантировать, что программа корректно работает не только в "нормальных ситуациях", но и имеет "защиту от дурака" и не приведет к зацикливанию или останову в крайних, мало вероятных ситуациях.
    Сложность отладки заключается и в том, что, обнаружив и исправив ошибку, вы получаете новую программу, для которой процесс отладки нужно начинать заново, снова пропустив все тесты. Известно, что в программах встречаются заколдованные места, - исправление одной ошибки ведет к появлению новой. В таких случаях лучшим выходом бывает поиск другого, принципиально иного решения задачи.

    Класс и обработка ошибок

    Мы уже говорили, что важная часть работы программиста по отладке программы состоит в том, чтобы предохранить работающую программу от прерывания ее работы при возникновении внутренних ошибок. Не менее важно, особенно при работе с объектами пользовательских классов, предусмотреть возможность появления исключительных ситуаций, генерировать и соответственно обрабатывать собственные ошибки класса. В качестве примера приведем класс Day, у которого есть два свойства - дата и температура на эту дату. Методы класса, (их реализацию мы не приводим) исходят из того, что летом должно быть жарко, а зимой - холодно. Нарушение этого условия приведет к неверным результатам. Поэтому в состав класса включен метод CheckDay, проверяющий корректность задания свойств. В случае несоблюдения требуемых условий метод генерирует собственные ошибки класса. Вот описание нашего класса:
    Пример 10.4.
    (html, txt)
    Приведем теперь процедуру, работающую с объектами, этого класса:
    Пример 10.5.
    (html, txt)
    Заметьте, эта процедура, работающая с объектом myday класса Day, построена по всем правилам, - в ней есть охраняемый блок. При возникновении ошибки, а она действительно возникает из-за некорректного задания свойств объекта, производится захват ошибки, управление передается предусмотренному обработчику ошибки. В обработчике пользователю разъясняется суть ситуации, приведшей к ошибке, после чего он вводит корректные данные. Взгляните, как выглядит окно для диалога с пользователем на этом этапе:
    Класс и обработка ошибок
    Рис. 10.17.  Окно диалога с пользователем
    После того, как пользователь задал нормальную летнюю температуру, процедура нормально завершила свою работу. В окне проверки напечатаны следующие результаты:
    09.08.99 25

    Приведем теперь процедуру, работающую с объектами, этого класса:

    Public Sub WorkWithDay() 'Работа с объектами класса Day Dim myday As New Day Dim Msg As String 'Охраняемый блок On Error GoTo ErrorHandler

    myday.Сегодня = "9.8.99" myday.Температура = -15 myday.CheckDay Debug.Print myday.Сегодня, myday.Температура Exit Sub ErrorHandler: If Err.Number = vbObjectError + 513 Then Msg = vbCrLf & "Введите температуру сегодняшнего дня " _ & myday.Сегодня & vbCrLf & " Учтите, она должна быть положительной" myday.Температура = InputBox(Err.Source & vbCrLf & Err.Description & Msg, "CheckDay", 15) ElseIf Err.Number = vbObjectError + 514 Then Msg = vbCrLf & "Введите температуру сегодняшнего дня " _ & myday.Сегодня & vbCrLf & " Учтите, она должна быть отрицательной" myday.Температура = InputBox(Err.Source & vbCrLf & Err.Description & Msg, "CheckDay", -15) End If Resume End Sub

    Пример 10.5.

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

    Класс и обработка ошибок
    Рис. 10.17.  Окно диалога с пользователем

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

    09.08.99 25

    Математические операции

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


  • Метод Assert

    Его синтаксис:
    Debug.Assert булево_выражение
    Всякий раз, когда при выполнении программы управление получает метод Assert, вычисляется значение булевого выражения и, если оно истинно, выполнение программы продолжается обычным образом. Если же значения выражения ложно, то происходит останов выполнения и программа переходит в состояние прерывания и, как обычно, начинается отладка и выяснение причин, повлекших ложность высказывания, заданного Assert выражением.
    Заметьте, Assert - выражения подобны одному из типов контрольных выражений, которые также приводят к прерыванию, когда приобретают значение False. Синтаксис и выполнение отдельного оператора Debug.Assert, совершенно понятны. Однако есть смысл поговорить о том, какую роль играют Assert - утверждения в процессе отладки. Идея такова: программа разбивается на участки, каждый из которых начинается и завершается Assert - утверждением. Предполагается, что программист может задать в виде булевого выражения утверждение, которое должно быть истинно в этой точке программы. В типичном случае Assert - утверждения вставляются в начало и конец каждой процедуры и функции. В начале процедуры эти утверждения задают условия, которым должны удовлетворять исходные данные. В конце работы процедуры булевы выражения описывает требования, предъявляемые к результатам. Если начальные и конечные утверждения истинны, то полагается, что процедура была коррект но вызвана и корректно завершила свою работу. Конечно же, чтобы использовать эту технику в полной мере следует иметь опыт доказательства правильности программ. Приведем пример:
    Public Function Fact2(ByVal N As Integer) As Integer 'Функция спроектирована для вычисления факториалов чисел, не больших 7
    Debug.Assert (N >= 0) And (N < 8)
    If (N = 0) Or (N = 1) Then ' базис индукции. Fact2 = 1 ' 0! =1. Else ' рекурсивный вызов в случае N > 0. Fact2 = Fact2(N - 1) * N End If
    Debug.Assert Fact2 <= 5040
    End Function
    Заметьте, говорить о том, правильно или неправильно работает функция Fact2, вычисляющая факториал, не имеет особого смысла, если не упоминать интервал возможных значений ее входного параметра. В узком интервале значений, для которого она и была спроектирована, она работает корректно. Вне этого интервала использовать ее не следует. Assert - утверждение в начале этой функции приостановит выполнение, если будет сделана попытка использовать эту функцию вне тех пределов, для которых она гарантирует корректную работу. Если бы этих утверждений не было, то попытка вызвать эту функцию с отрицательным N или N, большим 7, привела бы к останову выполнения программы.

    Метод Clear

    Его синтаксис:
    Err.Clear
    Этот метод используется для явной очистки значений свойств объекта Err после завершения обработки ошибки. Автоматическая очистка свойств Err происходит также при выполнении операторов:
  • оператора Resume любого вида;
  • Exit Sub, Exit Function, Exit Property;
  • оператора On Error любого вида.


  • Метод Print

    Его синтаксис:
    Debug.Print список_выражений
    Метод позволяет во время выполнения программы напечатать значения выражений из списка выражений в окне проверки Immediate. Трудно представить программу в период отладки, в которой бы многократно не использовался бы метод Print объекта Debug. Во всех наших примерах демонстрировалось его применение. Мы не будем останавливаться на некоторых деталях синтаксиса и управления выводом, при желании познакомиться с ними можно обратиться к справке.

    Метод Raise

    Метод Raise генерирует ошибку выполнения. У него двоякое назначение. Во-первых, он используется для моделирования стандартных, внутренних ошибок. Необходимость в этом возникает, например, при отладке обработчиков соответствующих ошибок. Но его главное назначение состоит в возбуждении собственных ошибок, когда в результате проделанного анализа обнаружена исключительная ситуация. Его синтаксис:
    Err.Raise number, source, description, helpfile, helpcontext
    Параметры метода имеют тот же смысл, что и соответствующие свойства объекта Err. Обязателен лишь параметр Number. При моделировании внутренних ошибок только этот параметр указывается при вызове метода. При возбуждении собственных ошибок разумно задавать все параметры, для того, чтобы были определены свойства объекта Err.
    Параметр Number имеет тип Long и определяет код ошибки. Коды ошибок (как внутренних, так и определяемых пользователем) лежат в диапазоне 0-65535. При этом коды от 0 до 512 зарезервированы за системными ошибками VBA. При возбуждении собственных ошибок при задании параметра Number к ее собственному коду необходимо добавлять константу vbObjectError + 512, что позволяет системе отличать внутренние и пользовательские ошибки.
    Перед вызовом метода Raise для возбуждения собственной ошибки полезно предварительно очистить объект Err методом Clear. Если Вы не зададите некоторые параметры в вызове Raise, в качестве их значений используются текущие значения соответствующих свойств объекта Err.

    Модель управления ошибками в языке VBA.

    В языке VBA обработка ошибок сосредоточена на уровне процедуры (функции). В каждой процедуре может быть выделен один или несколько охраняемых блоков, с каждым из которых связывается свой обработчик ошибки. Если во время работы охраняемого блока возникла ошибка (исключение), то нормальный ход выполнения процедуры приостанавливается, управление ее работой перехватывается и передается обработчику ошибки. Стандартный объект Err содержит информацию об ошибке. Поэтому в обработчике ошибки имеется возможность обработать возникшую ситуацию, исправить ее, запросив, например, у пользователя дополнительные данные, и принять правильное решение о дальнейшем ходе выполнения программы. В некоторых случаях, когда устранена причина ошибки или ее последствия, управление может быть возвращено в охраняемый блок, так что вычисления будут продолжены. В некоторых случаях работа программы приостанавливается с выдачей пользователю вразумительного объяснения причин, приведших к невозможности дальнейшего выполнения пр ограммы.
    Давайте разберемся, что значит "возникла ошибка"? Точнее следует говорить возбуждена (raise) ошибка. Кто обнаруживает ошибку? Обнаружение исключительной ситуации и возбуждение ошибки может быть сделано самой операционной системой (VBA) или исполняемой процедурой. Ошибки, возбуждаемые операционной системой, могут быть следствием аппаратных прерываний, например, из-за деления на ноль, вычисления корня из отрицательного числа, но это могут быть и ошибки, программно обнаруживаемые операционной системой, например, при попытке открыть несуществующий файл. Все эти ошибки будем называть системными или внутренними ошибками VBA. Все они тщательно классифицированы и каждая из них однозначно идентифицируется своим номером. Другую группу ошибок составляют собственные или пользовательские ошибки, возбуждение которых предусматривает программист. Например, при работе с объектом пользовательского класса программист может и должен предусмотреть специальную процедуру Check, которая проверяет правильность задания свойств объекта.
    Если обнаруживается, что свойства объекта заданы некорректно, так что выполнение операций над ним приведет к неверным результатам, то возбуждается собственная ошибка. Конечно, также как и для стандартных ошибок, ее тип должен быть полностью определен, задан ее номер и другие параметры. Возможно, программное обнаружение исключительных ситуаций и возбуждение собственных ошибок это наиболее важная и наиболее трудная часть программистской работы по управлению ошибками. Заметим, что какие бы ошибки не возбуждались, - внутренние или пользовательские, в момент возбуждения ошибки заполняются свойства объекта Err, так что он содержит всю информацию о последней возникшей ошибке.

    Синтаксически охраняемый блок окружен специальными операторами On Error. В начале блока оператор On Error задает метку обработчика ошибки охраняемого блока. Обработчик ошибок, как правило, завершается специальным оператором Resume, который задает точку в процедуре, которой передается управление после завершения обработки ошибки. Приведем схему процедуры с тремя охраняемыми блоками:

    Пример 10.2.

    (html, txt)

    Такова общая, достаточно простая схема обработки ошибок (исключений) в языке VBA. Стоит обратить внимание на то, что ситуация все же не столь проста, как может показаться с первого взгляда. Дело в том, что любой охраняемый блок может содержать вызовы процедуры процедур и функций. Поэтому реальная ситуация обычно такова, - один из операторов охраняемого блока запускает цепочку вызовов процедур и функций, каждая из которых имеет свои охраняемые блоки и свои обработчики ошибок. Ошибка может произойти на каком-то шаге в одной из вызванных процедур. Какие обработчики будут вызываться и в каком порядке, об этом поговорим чуть позже. Чтобы разобраться с деталями, вначале стоит подробно рассмотреть возможности используемых средств - операторов On Error, Resume и объекта Err с его свойствами и методами.


    ErrHandler3: ' 3-ий обработчик ошибок ... Resume RepeatPoint 'переход к строке, с которой возобновляется 'выполнение после обработки ошибки в 3-ей части End Sub

    Пример 10.2.

    Такова общая, достаточно простая схема обработки ошибок (исключений) в языке VBA. Стоит обратить внимание на то, что ситуация все же не столь проста, как может показаться с первого взгляда. Дело в том, что любой охраняемый блок может содержать вызовы процедуры процедур и функций. Поэтому реальная ситуация обычно такова, - один из операторов охраняемого блока запускает цепочку вызовов процедур и функций, каждая из которых имеет свои охраняемые блоки и свои обработчики ошибок. Ошибка может произойти на каком-то шаге в одной из вызванных процедур. Какие обработчики будут вызываться и в каком порядке, об этом поговорим чуть позже. Чтобы разобраться с деталями, вначале стоит подробно рассмотреть возможности используемых средств - операторов On Error, Resume и объекта Err с его свойствами и методами.

    Написание надежных программ

    Ошибки неизбежно сопровождают всякую сколь-нибудь сложную программу. Это утверждение может вызвать раздражение у начинающего программиста, но человек более опытный посчитает его само собой разумеющимся и сосредоточится на средствах, предлагаемых программным окружением для борьбы с этим неизбежным злом. Самые неприятные, дорогостоящие ошибки это те, что допущены при определении основных задач и целей приложения, при проектировании структуры его управления и потоков передачи данных, а также ошибки, связанные с неверной реализацией алгоритмов. Часто они не проявляются непосредственно в виде сбоев в работе программы, а обнаруживаются после довольно длительного использования приложения и требуют для своего исправления существенных изменений в проекте и программе. Их корни могут иметь как объективную природу (сложность решаемых задач), так и субъективную (непонимание заказчиком того, что ему надо). Как бороться с такими ошибками? Выделим два известных подхода к этой проблеме. Первый из них состоит в повышении ур овня языков и систем программирования, чтобы разработчик мог оперировать при создании системы понятиями предметной области, для которой она создается. Другой подход связан с идеей быстрого прототипирования, т. е. создания на ранней стадии разработки системы ее работающего прототипа, в ходе экспериментов с которым заказчик может уточнить свои требования. Оба эти подхода нашли отражение в Office 2000. Первый реализован в объектно-ориентированном подходе к построению как самой инструментальной системы, так и создаваемых в ней приложений. Развитие в VBA возможностей пользовательских модулей классов, в частности введение полиморфизма, - еще один шаг в направлении повышения уровня языка. Благодаря классам, можно вводить в программы объекты, представляющие те или иные понятия предметных областей. Реализации второго из указанных подходов существенно способствует визуальный стиль программирования, принятый во всех приложениях Office 2000. Создавая собственные меню и диалоговые окна, можно быстро спроектировать интерф ейс системы и передать пользователю (тестерам) действующий ее прототип для оценки соответствия его требованиям.
    Затем завершить реализацию с учетом замечаний и уточнений, возникших у тестеров при работе с прототипом. Это позволит избежать многих ошибок, неизбежных при разработке системы без контактов с ее будущими пользователями.

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

  • Проследите, чтобы все флажки на вкладке Editor из меню Tools|Options были включены. Автоматическая проверка синтаксиса в процессе написания программ, подсказка о значениях переменных, подсказка о параметрах функции, - все подключаемые свойства крайне полезны. Особое внимание обращаем на флажок "Require Variable Declaration", при включении которого в каждый модуль вставляется опция Option Explicit, принуждающая явно объявлять все переменные. С этим включенным флажком у языка VBA становится одним недостатком меньше.
  • При объявлении переменных старайтесь указать точный тип переменной и объекта. Избегайте объявлений типа Variant и Object. В этом случае на Вашей стороне будет контроль типов, что позволит избежать многих возможных ошибок.
  • При объявлении процедур явно указывайте описатели ByRef и ByVal, помните об особенностях передачи аргументов по ссылке в VBA.
  • Не забывайте о разумных размерах модулей и процедур, старайтесь создавать процедуры, модули и компоненты, допускающие переиспользование.
  • Наконец, еще раз напомним, что хорошие спецификации залог того, что программа допускает возможность изменения в процессе жизненного цикла, и что она будет корректно работать у конечного пользователя.Поэтому комментарии в тексте программы, хорошая справочная система, - все это важнейшие факторы, повышающие надежность программ.


  • Написание надежных программ
    Рис. 10.1.  Флажки вкладки Editor

    Объект Debug и его методы

    Объект Debug специально сконструирован для проведения отладки. Он имеет два метода Print и Assert.

    Объект Err

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

    Таблица 10.1. Описание свойств объекта ErrСвойствоЗначение
    Number Номер (код) ошибки. Это свойство по умолчанию.
    Source Строковое выражение, представляющее источник, в котором возникла ошибка. При ошибках в стандартном модуле оно содержит имя проекта. При ошибках в модуле класса свойство Source получает имя вида проект.класс. Конечно, хотелось бы, чтобы Source указывал источник возникновения ошибки более точно, хотя бы с точностью до имени процедуры, а лучше бы до оператора. Однако, этого пока не сделано.
    Description Строка с кратким описанием ошибки, если такая строка для кода, указанного в Number, существует. Для собственных ошибок значение этого свойства следует задавать.
    HelpFile Полное имя (включая диск и путь) файла справки VBA. Опять таки для собственных ошибок следует подготовить справочную систему и задавать путь к ней в этом свойстве.
    HelpContext Контекстный идентификатор файла справки, соответствующий ошибке с кодом, указанным в свойстве Number.
    LastDLLError Содержит системный код ошибки для последнего вызова DLL. Значение свойства LastDLLError доступно только для чтения. В лекции, посвященной работе с функциями Win32 API, подробно рассматривалось использование этого свойства.

    Рассмотрим пример, в котором возникает ошибка периода выполнения. Обработчик ошибки выдает сообщение о ней, используя свойства объекта Err. Затем в обработчике устраняется причина возникновения ошибки и управление возвращается оператору, инициировавшему запуск процедуры, приведшей к ошибке. Вся эта ситуация демонстрируется на примере работы с уже известной функцией fact2, вычисляющей корректно значение факториала для ограниченного диапазона значений входного параметра.
    Public Function Fact2(ByVal N As Integer) As Integer 'Функция спроектирована для вычисления факториалов чисел, не больших 7 #If conDebug Then Debug.Assert (N >= 0) And (N < 8) #End If

    If (N = 0) Or (N = 1) Then ' базис индукции. Fact2 = 1 ' 0! =1. Else ' рекурсивный вызов в случае N > 0. Fact2 = Fact2(N - 1) * N End If

    #If conDebug Then Debug.Assert Fact2 <= 5040 #End If

    End Function

    Заметьте, поскольку флаг отладки (conDebug) уже отключен, то Assert - утверждения не работают. Приведем процедуру, вызывающую функцию fact2 первый раз корректно, второй - нет, что приведет к ошибке, ее перехвату и исправлению ситуации:

    Пример 10.3.

    (html, txt)

    Вот как выглядит окно сообщения, выведенное в обработчике ошибки.

    Объект Err
    Рис. 10.16.  Сообщение, сформированное в обработчике ошибки

    Заметьте, после выдачи сообщения процедура нормально завершает свою работу и в окне проверки Immediate появятся следующие результаты:

    5 600 7 25200

    Объект Err специально спроектирован для работы на этапе обнаружения и исправления ошибок периода выполнения. Он заменил ранее существовавшие функцию и оператор Err. Для совместимости с ними свойство Number реализовано, как свойство по умолчанию и его можно не указывать. Если в борьбе с ошибками на этапе отладки важную роль играет объект Debug, то не менее важна роль объекта Err при борьбе с ошибками периода выполнения. Также как и объект Debug, объект Err имеет всего два метода - Clear и Raise. Рассмотрим их подробнее.

    Объявление переменных

    Для уменьшения размеров требуемой памяти и ускорения выполнения операций над данными:
  • При работе с большими массивами данных, размер которых, как правило, может изменяться, используйте динамические массивы или динамические структуры данных.
  • Объявляйте переменные в строгом соответствии с их возможным типом. Не используйте без крайней необходимости тип Variant. Работа с Variant требует больше памяти и дополнительных преобразований.
  • При задании типа, как правило, используйте тип с более узкой областью определения, достаточной для представления возможных значений. Здесь могут быть исключения, специфические для VBA. Так переменные типа Integer преобразуются к типу Long, так что разумнее задавать сразу тип Long вместо типа Integer.
  • Операции с плавающей точкой выполняются более долго, поскольку требуют обращения к сопроцессору. Поэтому тип Single и Double следует использовать только в случае действительной необходимости. Иногда, можно воспользоваться типом Currency для выполнения подобных действий. В этом случае потребуется больше памяти, но не будет обращений к сопроцессору с плавающей точкой.
  • При работе с объектами следует вводить переменные соответствующего объектного типа. Они хранят ссылки на объекты, а работа со ссылками выполняется значительно быстрее, чем с самими объектами.
  • При объявлении объектных переменных избегайте объявления Object, указывайте явный тип объекта. Типы Object и Variant весьма полезные типы, но применять их нужно только в случае действительной необходимости.

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

    Обработчики ошибок и вложенные вызовы процедур

    К каждой процедуре может быть подключено несколько обработчиков ошибок. Этот факт был специально отражен в схеме процедуры, когда в этой лекции описывалась модель управления ошибками. При возникновении ошибки в процедуре только один из них может быть активным. Активным обработчиком является тот обработчик, который связан с охраняемым блоком, в теле которого возникла ошибка. Заметьте, несмотря на то, что подключенных обработчиков в процедуре может быть несколько, в момент возникновения ошибки в ней может не быть активного обработчика, если ошибка возникла вне охраняемых блоков.
    В процессе вычислений одни процедуры могут вызывать другие. Поэтому в момент возникновения ошибки в стеке вызовов процедур могут находиться несколько процедур: C1, C2, …Cn. Каждая из этих процедур может иметь активный обработчик ошибок. Какой же из них будет применяться для обработки ошибки? Рассмотрим применяемую стратегию обработки. Итак, пусть есть непустой стек вызовов C1, C2, …Cn, где C1 это самый внешний вызов, а Cn - самый внутренний вызов. Обработка начинается подъемом по стеку вызовов. Если в Cn имеется активный обработчик ошибки, то он и получает управление, если его нет, то в стеке проверяется следующий по порядку вызов. Если ни один из вызовов C1 - Cn не имеет активного обработчика, то выполняется стандартная обработка с выдачей сообщения об ошибке и снятия приложения. Пусть Ck - это первый, найденный в стеке вызов, для которого сущест вует активный обработчик, и который, как было сказано, получает управление. Обработчик Ck имеет две возможности:
  • Обработать ошибку.
  • Передать обработку ошибки обработчику, выше стоящему в стеке вызовов.

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

    Окна наблюдения

    Мы хотим теперь подробнее остановиться на окнах отладчика, позволяющих следить за состоянием вычислительного процесса. Чтобы сделать изложение конкретным, обратимся к примеру предыдущей лекции, в котором показана работа с бинарным деревом, задающим словарь. Единственное сделанное добавление связано с введением в модуле глобальной переменной GlobeVar. Приведем текст отлаживаемой процедуры:
    Пример 10.1.
    (html, txt)

    Окно контрольных выражений - Watch

    Привычное назначение окна Watch в той или иной среде программирования состоит в том, что оно является окном наблюдений и позволяет следить за состоянием некоторых переменных и выражений программы. Теперь, во многом, эта функция перешла к окну Locals. Тем не менее, и окно Watch используется для этой же цели. В отличие от окна Locals, выражения, появляющиеся в этом окне, не связаны с контекстом выполняемой процедуры. Программист сам формирует список наблюдаемых переменных и выражений, при этом всегда можно точно указать область определения наблюдаемой переменной.
    Наблюдение за состоянием выражений перестало быть главной функцией окна Watch. Выражения, хранящиеся в этом окне, являются контрольными выражениями (может быть, точнее было бы - контролируемыми выражениями). Контрольными могут быть любые арифметические, логические или строковые выражения. Для заданных контрольных выражений отладчик позволяет следить за:
  • Значением контрольного выражения. В этом случае контрольное выражение - это обычное наблюдаемое выражение.
  • Моментом изменения значения контрольного выражения. В этом теперь основное назначение контрольных выражений. Всякий раз, когда контрольное выражение меняет свое значение, выполнение приостанавливается на операторе, следующим за оператором, изменившим контрольное выражение. Вот пример, когда такое использование контрольных выражений крайне полезно. В сложных системах обмен информацией между модулями может осуществляться через общий пул данных - общие глобальные переменные. В этом случае бывает трудно понять, какая из процедур меняет неподходящим образом значение общедоступной глобальной переменной. Без контрольного выражения поймать такую ошибку крайне трудно.
  • Моментом, когда контрольное выражение, принимает значение True. Это вариация предыдущего случая и можно указать много ситуаций, когда обнаружение такого факта, приостанавливающего вычисления весьма полезно при отладке.

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

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

    Окно контрольных выражений - Watch
    Рис. 10.9.  Окно добавления контрольного выражения

    В поле "Expression" вводится новое контрольное выражение. В нашем примере - это переменная info, в общем случае это может быть любое выражение. В этом окне задается контекст, в котором выражение будет регулярно вычисляться по завершении каждого оператора. Контекст определяет процедуру, модуль и проект. В нашем примере мы указали все процедуры модуля BinTree текущего проекта. Как уже говорилось, чем шире контекст, тем больше проводится дополнительных вычислений по ходу отладки. Кроме контекста задается один из трех возможных типов контрольного выражения. Выбранный нами тип "Break When Value Changes" позволяет прервать вычисления всякий раз, когда в заданном контексте изменится значение контрольной переменной rusword. Добавленное контрольное выражение появляется в окне "Контрольные значения" (Watch).

    Просмотреть значение и добавить новое контрольное выражение можно в окне быстрого просмотра Quick Watch. Для этого достаточно выделить выражение и нажать упоминавшуюся ранее инструментальную кнопку панели Debug, выполнить команду меню или нажать комбинацию горячих клавиш - Shift +F9. Вот как выглядит это окно

    Окно контрольных выражений - Watch
    Рис. 10.10.  Окно быстрого просмотра контрольного выражения

    Щелкнув по кнопке Add в этом окне, мы добавили переменную MyDict в окно Watch. Сравнивая окно Watch с окном Locals, отметим, что к трем колонкам Expression, Value и Type добавился еще один столбец Context, в котором задается контекст контрольного выражения.


    Взгляните, как выглядят эти окна после того, как мы добавили три контрольных выражения:

    Окно контрольных выражений - Watch
    увеличить изображение
    Рис. 10.11.  Окна отладчика после добавления контрольных выражений

    Еще один вид информации о текущем состоянии вычисления представлен стеком вызовов процедур. Имя каждой вызванной процедуры помещается на вершину этого стека в момент ее вызова и убирается из него сразу после завершения этого вызова. Просмотреть стек вызовов можно, нажав клавиши Ctrl+L или выбрав инструментальную кнопку или команду "Call Stack…" в меню View. Чаще всего, в окно "Стек вызова" попадают из окна Locals, щелкнув в нем имеющуюся для этой цели кнопку "…".

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

    Окно контрольных выражений - Watch
    увеличить изображение
    Рис. 10.12.  Окна отладчика в момент прерывания

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

    Окно контрольных выражений - Watch
    Рис. 10.13.  Окно стека вызовов

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

    Разговор о средствах отладки будет не полным, если не рассмотреть специальный объект Debug.

    Окно локальных переменных - Locals

    Наиболее простой и эффективный способ следить за состоянием вычислений заключается в том, чтобы во время отладки включить окно локальных переменных (Locals). Напоминаем, для этого есть соответствующая инструментальная кнопка панели Debug и команда меню View. В этом окне будут отображены все описанные в текущей процедуре переменные, их значения и типы. Обновление окна происходит автоматически при каждом переключении из режима выполнения в режим прерывания, а также при перемещении по стеку вызовов процедур. Первая переменная в этом окне - это специальная переменная с именем модуля, которая показывает все глобальные переменные, описанные на уровне текущего модуля. Для модуля класса первая переменная имеет имя Me и показывает свойства класса. Окно разбито на три столбца для имени переменной, ее значения и типа. Таким образом, окно позволяет следить за всеми локальными переменными текущей выполняемой процедуры и частью ее глобальных переменных, тех, которые описаны в текущем модуле. Отметим также, что объекты и массивы могут быть свернуты и развернуты, используя обычную технику знаков "+" и "-".
    Давайте начнем отладку нашего примера, поставим первую точку прерывания после предварительного формирования словаря на операторе, запускающем префиксный обход дерева. Запустим программу на выполнение, и после ее останова на точке прерывания исследуем окно локальных переменных. Эта ситуация показана на следующем рисунке.
    Окно локальных переменных - Locals
    увеличить изображение
    Рис. 10.6.  Окно локальных переменных процедуры стандартного модуля
    Заметьте, в окне Locals показана глобальная переменная модуля Examples и все локальные переменные исполняемой процедуры WorkWithBinTree. Объект MyDict класса BinTree на рисунке частично раскрыт. Как видите, окно позволяет тщательно проанализировать достаточно сложную структуру этого объекта. В окне Locals можно не только наблюдать за значениями переменных, но и изменять эти значения, редактируя поле Value.
    Давайте нажмем клавишу F8 для выполнения следующего шага вычислений.
    Тем самым мы начнем исполнять метод PrefixOrder класса BinTree, вызванный объектом MyDict. Выполним в пошаговом режиме несколько операторов и остановимся после первой отладочной печати. Заметьте, что поскольку теперь исполнение идет в другом контексте, - исполняется модуль класса, то окно Locals полностью обновится и будет теперь задавать состояние исполняемого метода класса. Взгляните, как это выглядит на рисунке:

    Окно локальных переменных - Locals
    увеличить изображение
    Рис. 10.7.  Окно локальных переменных метода класса

    Кроме показа переменных окно Locals позволяет проанализировать текущее состояние стека вызовов. Для этого в правом верхнем углу окна есть специальная кнопка, щелчок которой дает тот же эффект, что соответствующая инструментальная кнопка меню Debug. Продемонстрируем ее использование чуть позже, а сейчас обращаем внимание, что результаты исполнения оператора Debug.Print появились в окне Immediate, к рассмотрению которого мы и переходим.

    Окно проверки - Immediate

    В этом окне появляется вся отладочная информация, поступающая в результате выполнения методов Print и Assert объекта Debug. В этом основное назначение этого окна. Об объекте Debug мы еще поговорим особо, а сейчас рассмотрим другие возможности, которые предоставляет окно проверки. Еще одно назначение этого окна состоит в том, что оно представляет блокнот или калькулятор, в котором можно производить какие-либо дополнительные вычисления. Во-первых, в этом окне можно выполнять любые операторы, допустимые в контексте процедуры, выполнение которой было приостановлено. Отсюда, в частности, следует, что в окне можно изменять значения доступных переменных. Во-вторых, в этом окне можно выполнять запросы на вычисление выражений. Запрос начинается со знака "?", после которого печатается запрашиваемое выражение. После нажатия Enter в окне появляется значение выражения. В предыдущих лекциях мы неоднократно пользовались запросами для вычисления значений р азличных функций. Окно позволяет изменять и значения глобальных переменных модуля. Взгляните, как выглядит окно проверки, в котором выполняются некоторые вычисления в состоянии прерывания процедуры. В частности, в этом окне дважды присваивалось новое значение глобальной переменной и выдавался запрос на получение ее значения. Здесь же был вызван на исполнение метод PostfixOrder объекта root. В этом же окне появились результаты его работы, напечатанные оператором Debug.
    Окно проверки - Immediate
    увеличить изображение
    Рис. 10.8.  Вычисления в окне проверки

    Оператор On Error

    Имеется три варианта синтаксиса этого оператора:
    On Error GoTo строка On Error Resume Next On Error GoTo 0
    Рассмотрим подробно каждый из трех вариантов:
  • Оператор On Error GoTo строка используется, как заголовок охраняемого блока. Его обязательный аргумент строка является либо меткой строки или номером строки, задающей начало обработчика ошибки. Заметьте, обработчик ошибки - это фрагмент кода, расположенный в той же процедуре, что и охраняемый блок. Если в охраняемом блоке возбуждается ошибка, то управление покидает охраняемый блок и передается на указанную строку, запуская, тем самым, обработчик ошибок, начинающийся в этой строке.
  • Оператор On Error Resume Next также используется, как заголовок охраняемого блока. В этом случае с охраняемым блоком обработчик ошибок не связан. Точнее, он состоит из одного оператора Resume Next, включенного непосредственно в оператор On Error. При возникновении ошибки в охраняемом блоке, управление перехватывается и передается оператору, следующему за оператором, приведшему к ошибке. Конечно, такая ситуация разумна только в том случае, когда вслед за оператором, при выполнении которого потенциально возможна ошибка, программист помещает оператор, анализирующий объект Err, и в случае ошибки принимает меры по ее устранению. Это довольно типичная ситуация, когда обработка возможной ошибки заранее предусмотрена и встроена в процедуру.
  • Оператор On Error GoTo 0 является закрывающей скобкой, - он завершает охраняемый блок. Выполнение оператора On Error GoTo 0 приводит также к "чистке" свойств объекта Err аналогично методу Clear этого объекта. Синтаксис оператора трудно признать удачным, фраза GoTo 0 только сбивает с толку, поскольку 0 не рассматривается как номер строки, даже если строка с номером 0 существует. Неудачным решением является и то, что этот оператор можно опускать, если охраняемый блок завершается вместе с самой процедурой. Лучше бы иметь завершающую структурную скобку, как это сделано в VBA для всех управляющих структур.
    В процедурах, состоящих из нескольких охраняемых блоков, применение этого оператора обязательно. Прежде чем объявить новый охраняемый блок, нужно отключить текущий оператором On Error GoTo 0.


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

    Оператор On Error
    Рис. 10.14.  Стандартное сообщение об ошибке периода выполнения

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

    Оператор On Error
    Рис. 10.15.  Другой вид окна сообщения об ошибке периода выполнения

    Оператор Resume

    Оператор Resume должен быть последним выполняемым оператором обработчика ошибок. Он определяет точку процедуры, которой передается управление по завершении обработки ошибки. У него также три варианта синтаксиса:
    Resume [0] Resume Next Resume строка
    В первом случае выполнение программы продолжается с повторного выполнения оператора, вызвавшего ошибку. Заметим, если это был оператор, инициировавший вызов процедур и функций, приведших в конечном итоге к ошибке, то управление передается этому оператору. Другими словами, управление всегда передается оператору охраняемого блока, явно или неявно инициировавшему ошибку. Вариант Resume (или Resume 0) естественно использовать, когда ошибка вызвана вводом неверных данных пользователем, а в обработчике ошибок у него запрашиваются новые правильные данные. Например, если в основной процедуре пользователь ввел имя несуществующего файла, в обработчике можно запросить новое имя и продолжить выполнение с повторения оператора открытия файла. Таким образом, этот вариант используется, если в обработчике ошибок устранена причина ошибки. В этом случае можно надеяться, что повторное исполнение оператора, ранее инициировавшего ошибку, теперь завершится благополучно.
    В варианте Resume Next после обработки ошибки выполнение процедуры продолжается с оператора, следующего за оператором, инициировавшим ошибку. Не будем повторяться, и здесь речь идет об операторах охраняемого блока, а не тех вызванных процедур, где реально произошла ошибка. Если в первом варианте обработчик ошибки должен устранить причину ошибки, то в этом варианте обработчик устраняет последствия ошибки, выполняя, по существу, работу оператора, приведшего к ошибке. Еще одна возможная ситуация применения этого варианта, о которой мы уже упоминали, состоит в том, что следующий оператор сам анализирует причину ошибки и задает способы ее устранения.
    В третьем варианте параметр строка задает метку строки или номер строки данной процедуры, на которую передается управление после обработки ошибки. Этот вариант используется в разных ситуациях, например, причина ошибки устранена, но необходимо повторить часть вычислений, предшествовавших появлению ошибки. Возможно, также, что устранены последствия ошибки, но действия надо продолжить не со следующего оператора, а с определенного участка процедуры. Третий вариант задает общую конструкцию, и мог бы использоваться во всех трех случаях.

    Оптимизация программ

    Результаты работы с системой документов должны удовлетворять заданным спецификациям. Эти результаты должны быть получены в заданное время. Точный прогноз погоды на завтра требуется сегодня, получить его послезавтра становится бессмысленным. Поэтому программисту постоянно приходится заботиться не только о корректности работы свих программ, но и об их эффективности. И здесь успех, во многом, определяется решениями, принимаемыми на самом верхнем уровне разработки проекта. Самое важное решение связано с выбором подходящей среды программирования. Здесь нужно точно учитывать перспективы развития разрабатываемой системы и выбираемой среды программирования, насколько средства, выбранные Вами сегодня, смогут удовлетворять Вашим завтрашним потребностям. Другой разрез, для многих задач не менее важный, состоит в правильном выборе структур данных и алгоритмов их обработки. Нет ничего лучше хорошего алгоритма. Он, как правило, определяет эффективность решения задач. Важную роль в повышении эффективности играет и переис пользование. Ранее созданные, тщательно проверенные и эффективные компоненты, могут определить и эффективность вновь решаемой задачи. Рассмотрение всех этих вопросов, главных источников эффективности работы системы в целом, выходит за рамки данной книги. Мы остановимся сейчас на деталях, каждая из которых в отдельности не способна существенно повысить эффективность решения. При добыче золота, можно пытаться найти самородок, а можно промывать руду, добывая песчинку за песчинкой. Так и при разработке программ, песчинки, повышающие эффективность, могут принести в результате ощутимый эффект.
    Прежде, чем начать рассмотрение отдельных элементов, еще пару слов о измерениях, которые выполняет программист, для повышения эффективности своей программы. Известен эффект "bottle neck", - узких мест программы, которые могут быть причиной ее не эффективного поведения. Чтобы получить представление о том, есть ли в программе такие узкие места, требующие первостепенного вмешательства и переписывания, чаще всего требуется построение полного профиля программы. Причем и здесь важна представительная система тестов. Опять таки, мы не будем сейчас рассматривать средства построения профиля. Отметим только, что в ряде случаев можно для проведения измерений использовать функцию Timer.По ходу изложения мы несколько раз демонстрировали ее применение.

    Ошибки периода выполнения и их обработка

    Итак, отладка программы завершена, последняя найденная ошибка исправлена. Теперь программа должна быть передана пользователю. Значит ли это, что в ходе работы пользователя с программой не будут возникать ошибки? Обязательно, будут! Нужно предпринять специальные меры, чтобы появление этих ошибок не приводило к неприятным последствиям. Если этого не сделать, то при возникновении ошибки на экране появляется сообщение, как правило, мало что говорящее пользователю и программа завершает свою работу. Как правило, все это сопровождается нелестными высказываниями пользователя в адрес разработчика. Заметьте, ошибки этого периода (run time errors) могут быть и в правильно работающей программе. Они могут возникать из-за неверных действий самого пользователя, не знающего спецификаций. Обычно ошибки связаны с вводом данных несоответствующих типов или вводом значений, выходящих за пределы допустимого диапазона. Пользователь может пытаться открыть несуществующий файл, или файл, который он необдуманно удалил. При работе пользователя, например, в Access может быть сделана попытка открытия несуществующей таблицы или формы. В общем, у пользователя есть масса возможностей нарушить спецификации, особенно, если они не четко сформулированы. Но не стоит обольщаться, многие ошибки на совести программиста. Правильно считать, что во всех случаях виноват программист. В его задачу входит обнаружение и обработка всех исключительных ситуаций, возникающих в процессе работы программы. Сейчас мы и переходим к рассмотрению самого понятия исключительной ситуации и о тех средствах, которые есть в VBA для их обработки.
    Исключительная ситуация или исключение (exception) возникает при выполнении программы и делает ее дальнейшее выполнение невозможным или нецелесообразным ввиду неопределенности, непредсказуемости или неправильности дальнейшего результата вычислений. Управление исключениями (exception handling) включает специально предусмотренное обнаружение исключений, перехват управления выполнением программы при возникновении исключения и передачу этого управления специальному разделу программы - обработчику исключения.
    Определения, которые мы дали, носят общий характер и применимы к любому языку программирования. Следует сказать, что стандарт на исключения, их классификацию, способы обработки, еще не сформировался. Например, в языке Visual C++ обработка исключений значительно изощренней, чем в языке VBA. Следует сказать, что в VBA, к сожалению, не используется общепринятый термин исключение, вместо него используется термин ошибка. Суть дела от этого не меняется. Давайте начнем с рассмотрения общей схемы управления ошибками (исключениями) в языке VBA.

    Отладка

    Занимаясь программированием уже многие годы, я не только теоретически, но и на собственном опыте осознал, что программы, которые мы, программисты, разрабатываем, относятся к средствам повышенной опасности. В простых ситуациях ошибки в программах, могут стать источником разочарований и огорчений отдельного человека. В серьезных ситуациях ошибки чреваты катастрофой. На людей, создающих программы, возлагается ответственность, если хотите, то и моральная ответственность за надежность и правильность работы их творений. И хотя программа программе рознь не нужно думать, что сегодня можно состряпать что либо на авось, а завтра, когда наступит время серьезной программы, тогда и придет черед надежному программированию. Нет, программировать надежно нужно всегда.
    Можно ли создать надежную программную систему? Вспоминая опыт собственной работы, могу сказать, что в наиболее ответственных случаях, когда речь шла об экспериментах, связанных с космосом, решение заключалось в том, что программа создавалась независимо двумя коллективами, начиная от разработки алгоритма, кончая системой тестов. Только после того, как обе системы правильно работали на всех предъявленных обеими сторонами тестах, программа принималась в эксплуатацию. Это были шестидесятые годы. Сегодня мы живем в другом мире, с другими возможностями. Вот цитата из письма, которое я, как бета-тестер Office 2000 получил от команды, занимающейся отладкой этой системы:
    "...Another added feature to our beta program will be the privilege to nominate other beta testers. Over our beta program we receive over 500,000 requests to participate on the Office beta program. This upcoming beta we are going to allow you, our top beta tester to add your co worker, friend or neighbor to our program".
    Вряд ли здесь необходим точный перевод. Речь идет о том, что одной из привилегий лучших бета - тестеров будет возможность рекомендовать тестеров для участия в новой программе тестирования. И делается это потому, что возникла проблема отбора тестеров. Заметьте, поступило 500000 заявок на участие в тестировании программного продукта. Следует заметить, что отношение к тестерам и их работе самое серьезное. Могу сказать, что ни один из посланных мной отчетов не остался без внимания. Конечно, при такой коллективной и независимой отладке можно в гораздо большей степени надеяться на надежность программ.
    Поговорим сейчас о том, что должен делать каждый из программистов, работающих в среде Office 2000, чтобы создать надежный продукт и уменьшить число возможных ошибок, не надеясь на постороннюю помощь. Мы рассмотрим три темы:
  • Как написать, по возможности, надежную программу?
  • Как вести отладку? Средства отладки Office 2000.
  • Ошибки периода исполнения и их обработка.


  • Панель отладки и команды меню

    Как и во многих других случаях, интерфейс отладчика VBA избыточен, - к одним и тем же инструментальным средствам можно добраться по-разному. В зависимости от привычек можно использовать панель Debug с инструментальными кнопками, можно использовать команды из меню Debug и View, можно использовать горячие клавиши. На следующем рисунке показана панель Debug (Отладка). Заметьте, на этой панели больше кнопок, чем в стандартном варианте, предлагаемом по умолчанию. Используя режим настройки (Customize) я вынес на эту панель дополнительные кнопки, задающие инструменты отладки.
    Панель отладки и команды меню
    Рис. 10.3.  Панель отладки с инструментальными кнопками
    Кнопки этой панели соответствуют командам меню "Отладка" (Debug) и меню "Вид" (View), которые представлены на следующих рисунках:
    Панель отладки и команды меню
    Рис. 10.4.  Команды меню View
    Панель отладки и команды меню
    Рис. 10.5.  Команды меню Debug
    На рисунках для большинства команд меню показаны соответствующие им кнопки и комбинация горячих клавиш, выполняющих вызов тех же инструментальных средств. Дадим краткое описание этих инструментальных средств:
    Панель отладки и команды меню Первая из этих кнопок включает и выключает состояние проектирования. Вторая запускает проект на компиляцию. Запуск на компиляцию стоит делать чаще, не дожидаясь завершения проектирования. Так можно раньше выявить некоторые ошибки.
    Панель отладки и команды меню Первая из этих кнопок запускает программу на выполнение. Более точно это означает следующее. Если курсор стоит в любом месте некоторой процедуры, то на выполнение запускается эта процедура. Если активна некоторая форма, то запускается эта форма, в противном случае запускается макрос. Вторая кнопка этой группы прерывает выполнение и переводит программу в состояние прерывания. Третья кнопка приостанавливает выполнение и производит сброс проекта,- чистятся все стеки, так что происходит переход в состояние проектирования.
    Панель отладки и команды меню Эти две кнопки позволяют работать с точками прерывания. Точки прерывания играют важную роль в процессе отладки. Чаще всего, для понимания поведения программы достаточно проанализировать состояние вычислений в некоторых ее точках, например, после завершения очередного цикла.
    Точки прерывания ставятся в таких контрольных точках программы, после чего программа запускается на выполнение. Достигнув первой по ходу вычисления точки прерывания, выполнение приостанавливается, программа переходит в состояние прерывания. Состояние программы анализируется и затем можно произвести запуск вычислений до следующей точки прерывания. Таким образом, точки прерывания размечают программу, являясь промежуточными финишами процесса выполнения.

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

    Панель отладки и команды меню Важная группа кнопок, включающая средства управления процессом вычислений. С их помощью на важных участках можно проследить выполнение программы с точностью до оператора. Первая из кнопок (Step Into - ей соответствует нажатие клавиши F8) задает пошаговый, пооператорный режим выполнения. При вызове процедур и функций в этом режиме начинается их пошаговое выполнение. После выполнения очередного оператора происходит прерывание и программа доступна для корректировки. Вторая кнопка (Step Over - Shift + F8) вызов процедур и функций выполняет за один шаг, что позволяет не задерживаться на выполнении уже отлаженных модулей. Третья кнопка (Step Out - Ctrl + Shift +F8) позволяет прервать пошаговое выполнение процедуры и вернуться к этому режиму уже в вызывающей процедуре. Четвертая из этих кнопок (Run to Cursor - Ctrl + F8) позволяет установить курсор в нужную позицию и запустить программу на выполнение.


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

    Панель отладки и команды меню Очень полезные кнопки, по крайней мере, первая из них. С ее помощью можно изменять порядок вычислений, предписанный программой. Мы уже говорили, что в процессе отладки желтая стрелка в левом поле задает строку с текущим выполняемым оператором. VBA позволяет самому программисту устанавливать, какой оператор будет выполняться следующим, при этом, что очень важно, можно производить откат назад и возвращаться к повторному исполнению ранее выполненного оператора. Чаще всего это полезно, когда в выполняемую процедуру внесены изменения, тут же можно проанализировать эффект исправлений без того, чтобы все вычисления производить заново. Итак, если в режиме прерывания поставить курсор на любой из операторов выполняемой процедуры и щелкнуть первую из кнопок данной группы, то желтая стрелка будет перенесена к этому оператору. Этот оператор станет текущим и будет следующим выполняемым оператором. Конечно, все это можно делать в пределах выполняемой процедуры. Это замечательное для отладки свойство возможно благодаря тому, что VBA является интерпретируемым языком. Заметим, что перенос стрелки, отмечающей текущий оператор, чаще всего делается с помощью мышки простым перетаскиванием желтой стрелки в левом поле вверх или вниз к нужному оператору (строке).

    Панель отладки и команды меню Следующая группа кнопок задает инструментальные средства, позволяющие наблюдать за состоянием программы, значениями ее переменных. Этим кнопкам соответствуют команды меню View. Первые три кнопки вызывают соответственно три окна наблюдения за состоянием вычислений - окно локальных переменных (Locals), окно проверки с отладочными результатами вычислений (Immediate), окно контрольных выражений (Watch). Следующая пара кнопок также предназначена для работы с контрольными выражениями, открывая специальные окна для показа и добавления контрольных выражений. Наконец, последняя кнопка в этой группе очень полезна при работе в ситуациях, когда отлаживаются рекурсивные процедуры или процедуры, где встречается глубокая вложенность вызовов.


    Она позволяет показать стек вызовов процедур в текущий момент. Чуть позже мы подробнее рассмотрим работу с этими окнами.

    При отладке весьма полезны и другие инструментальные средства, не связанные непосредственно с панелью Debug. Они остались вне нашего рассмотрения, но о некоторых из них все-таки скажем пару слов.

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

    Панель отладки и команды меню Еще одна полезная кнопка этой панели Quick Info. Она позволяет получить подсказку о типе переменной, обо всех параметрах вызываемой функции и их типах. Для получения подсказки достаточно подвести курсор к соответствующей переменной или функции и щелкнуть кнопку. Тут же появится окно с подсказкой. Отметим также еще одно весьма полезное свойство, если просто подвести курсор к переменной, то тут же появляется окно подсказки, в котором показано значение этой переменной. Это эффективное средство наблюдения за состоянием переменных, не требующее специальных окон, наблюдение делается на лету.

    Приемы оптимизации кода

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

    Dim MyDict As New BinTree

    Public Sub WorkwithBinTree() Dim MyDict As New BinTree Dim englword As String, rusword As String GlobeVar = "Привет!" 'Создание словаря
    MyDict.SearchAndInsert key:="dictionary", info:="словарь" MyDict.SearchAndInsert key:="hardware", info:="аппаратура, аппаратные средства" MyDict.SearchAndInsert key:="processor", info:="процессор" MyDict.SearchAndInsert key:="backup", info:="резервная копия" MyDict.SearchAndInsert key:="token", info:="лексема" MyDict.SearchAndInsert key:="file", info:="файл" MyDict.SearchAndInsert key:="compiler", info:="компилятор" MyDict.SearchAndInsert key:="account", info:="учетная запись"
    'Обход словаря MyDict.PrefixOrder
    'Поиск в словаре englword = "account": rusword = "" MyDict.SearchAndInsert key:=englword, info:=rusword Debug.Print englword, rusword
    'Удаление из словаря MyDict.DelInTree englword englword = "hardware" MyDict.DelInTree englword 'Обход словаря MyDict.PrefixOrder
    'Debug.Print MyDict
    End Sub
    Пример 10.1.
    Закрыть окно




    Sub ProcWithErrors() ' Первый охраняемый блок On Error GoTo ErrHadler1 ' подключение 1-го обработчика ошибок ' Первая часть процедуры, которая может вызвать ошибку. ... On Error GoTo 0 отключение 1-го обработчика ошибок
    'Второй охраняемый блок On Error GoTo ErrHadler2 ' подключение 2-го обработчика ошибок ' Вторая часть процедуры, которая может вызвать ошибку. ... On Error GoTo 0 отключение 2-го обработчика ошибок
    'Третий охраняемый блок On Error GoTo ErrHadler3 ' подключение 3-го обработчика ошибок ' Третья часть процедуры, которая может вызвать ошибку. ... On Error GoTo 0 отключение 3-го обработчика ошибок
    RepeatPoint: ' точка, с которой возобновляется выполнение 'после обработки ошибки в 3-ей части ...
    Exit Sub 'выход из процедуры при отсутствии ошибок 'ОбработкаОшибок: ErrHandler1: ' 1-ый обработчик ошибок ... Resume 'возврат к оператору, вызвавшему ошибку в 1-ой части
    ErrHandler2: ' 2-ой обработчик ошибок ... Resume Next 'переход к оператору, следующему за оператором 'вызвавшим ошибку во 2-ой части
    ErrHandler3: ' 3-ий обработчик ошибок ... Resume RepeatPoint 'переход к строке, с которой возобновляется 'выполнение после обработки ошибки в 3-ей части End Sub
    Пример 10.2.
    Закрыть окно




    Public Sub TestFact2() Dim Msg As String Dim VictoryCount As Integer, Prize As Long On Error GoTo ErrHandler1 VictoryCount = 5 Prize = Fact2(VictoryCount) * 5 Debug.Print VictoryCount, Prize
    VictoryCount = 10 Prize = Fact2(VictoryCount) * 5 Debug.Print VictoryCount, Prize
    Exit Sub ErrHandler1: Msg = "Ошибка # " & Err.Number & " возникла в " & Err.Source _ & vbCrLf & " Описание: " & Err.Description _ & vbCrLf & " HelpFile: " & Err.HelpFile _ & vbCrLf & " HelpContext: " & Err.HelpContext MsgBox Msg, vbMsgBoxHelpButton, "Error", Err.HelpFile, Err.HelpContext 'Грубое устранение причин ошибки Err.Clear If VictoryCount < 0 Then VictoryCount = 0 If VictoryCount > 7 Then VictoryCount = 7 Resume
    End Sub
    Пример 10.3.
    Закрыть окно




    Option Explicit
    'Класс Day ' Свойства класса Private today As Date Private temperature As Integer
    Public Property Get Сегодня() As Date Сегодня = today End Property
    Public Property Let Сегодня(ByVal NewValue As Date) today = NewValue End Property
    Public Property Get Температура() As Integer Температура = temperature End Property
    Public Property Let Температура(ByVal NewValue As Integer) temperature = NewValue End Property
    Public Sub CheckDay() Dim Desc As String Dim Numb As Long Dim Source As String 'Проверка свойств объекта Select Case Month(Сегодня) Case 6 To 8 If Температура < 0 Then 'Исключительная ситуация Desc = "Ошибка: Работа с объектом предполагает положительную летнюю температуру!" Numb = vbObjectError + 513 Source = " Метод CheckDay класса Day " Err.Raise Numb, Source, Desc End If Case 1 To 2, 12 If Температура > 0 Then 'Исключительная ситуация Desc = "Ошибка: Работа с объектом предполагает отрицательную зимнюю температуру!" Numb = vbObjectError + 514 Source = " Метод CheckDay класса Day "
    Err.Raise Numb, Source, Desc End If End Select End Sub
    Пример 10.4.
    Закрыть окно




    Public Sub WorkWithDay() 'Работа с объектами класса Day Dim myday As New Day Dim Msg As String 'Охраняемый блок On Error GoTo ErrorHandler
    myday.Сегодня = "9.8.99" myday.Температура = -15 myday.CheckDay Debug.Print myday.Сегодня, myday.Температура Exit Sub ErrorHandler: If Err.Number = vbObjectError + 513 Then Msg = vbCrLf & "Введите температуру сегодняшнего дня " _ & myday.Сегодня & vbCrLf & " Учтите, она должна быть положительной" myday.Температура = InputBox(Err.Source & vbCrLf & Err.Description & Msg, "CheckDay", 15) ElseIf Err.Number = vbObjectError + 514 Then Msg = vbCrLf & "Введите температуру сегодняшнего дня " _ & myday.Сегодня & vbCrLf & " Учтите, она должна быть отрицательной" myday.Температура = InputBox(Err.Source & vbCrLf & Err.Description & Msg, "CheckDay", -15) End If Resume End Sub
    Пример 10.5.
    Закрыть окно




    Sub Testfunc1() Dim res As Variant, arg As Variant arg = 12 res = Func1(arg) If IsError(res) Then 'проверка ошибочности результата Debug.Print "Ошибка #: ", res, "аргумент : ", arg
    Else Debug.Print "Результат : ", res End If arg = "двенадцать" res = Func1(arg) If IsError(res) Then 'проверка ошибочности результата Debug.Print "Ошибка #: ", res, "аргумент : ", arg
    Else Debug.Print "Результат : ", res End If End Sub
    Пример 10.6.
    Закрыть окно



    Средства отладки

    Часть ошибок программы ловится автоматически еще на этапе компиляции. Сюда относятся все синтаксические ошибки, ошибки несоответствия типов и некоторые другие. Однако синтаксически корректная программа нуждается в отладке, поскольку, хотя результаты вычислений и получены, но они не соответствуют требуемым спецификациям. Чаще всего, еще не отлаженная программа на одних исходных данных работает правильно, на других - дает ошибочный результат. Искусство отладки состоит в том, чтобы обнаружить все ситуации, в которых работа программы приводит к ошибочным вычислениям. VBA обладает весьма изощренными средствами, предназначенными для отладки программ, т.е. для обнаружения ошибок в программах (тестирования) и их исправления. Есть две группы средств VBA, помогающие программисту выявить и исправить ошибки:
  • Первая группа позволяет контролировать ход вычислительного процесса, т.е. порядок следования операторов в процедурах, порядок вызова самих процедур. При необходимости в процессе отладки разрешается изменять этот порядок, можно, например, пропускать исполнение некоторых операторов, или повторно возвращаться к их исполнению
  • Вторая группа средств позволяет контролировать изменение состояния вычислительного процесса (значений переменных и свойств объектов) в процессе выполнения. И здесь можно вмешаться и изменить состояние, задав по ходу дела новые значения для тех или иных переменных.

  • Прежде, чем приступить к подробному рассмотрению этих средств, напомним, что в ходе отладки программа может находиться в одном из трех состояний: проектирования, вычисления и прерывания. Закончив проектирование, можно запустить программу на исполнение. Прервав исполнение программы в заданной точке, перейдя в состояние прерывания, можно проконтролировать значения переменных и свойств объектов в данной точке и, если требуется, изменить эти значения "вручную". При этом можно изменить порядок выполняемых операторов, задавать следующий исполняемый оператор, можно редактировать программный текст перед продолжением вычисления.
    Переход из состояния вычисления в состояние прерывания может происходить по самым разным причинам, например, по достижении точки прерывания, при выполнении одного из многочисленных условий прерывания, из-за пошагового выполнения программы. Все эти возможности мы еще обсудим, а сейчас рассмотрим один особый случай. Иногда программа "зацикливается" и необходимо принудительно перевести ее в с остояние прерывания. Как остановить работающую программу? Просто нажмите знакомую еще по работе в DOS пару клавиш Ctrl+Break. На экране появится следующее диалоговое окно с сообщением об остановке.

    Средства отладки
    Рис. 10.2.  Сообщение о прерывании работы программы

    В этом окне нажатие кнопки "End" остановит исполнение программы, нажатие кнопки "Continue" возобновит ее выполнение, программа снова перейдет в состояние вычисления. Если нажать кнопку "Debug", то выполнение программы прервется, и мы перейдем в режим прерывания - режим отладки. Активным станет окно с текстом программы, а в нем будет выделен оператор, на котором прервалось исполнение.

    Строковые операции

  • При работе со строками используйте введенные в Office 2000 функции Replace, функции разбора строки и другие. Ранее мы подробно рассказали об их достоинствах.
  • Избегайте, по возможности, использования конкатенации. Используйте Replace в большинстве случаев. В тех случаях, когда заменяется одна подстрока на другую такой же размерности, можно использовать функцию Mid, как в следующем примере:
    Public Sub TestCode() Dim Text As String Text = "Компилятор кода" Mid(Text, 1, 5) = "Транс" Debug.Print Text End Sub
  • Строковые константы VBA могут сократить время вычислений, позволяя избежать вызовов функций. Так, например, эффективнее использовать константу vbCrLf, чем комбинацию символов Chr(13), Chr(10), задающих возврат каретки и перевод строки.
  • Иногда медленные операции над строками можно изменить на операции работы с их кодами. Код
    If Asc(Text) = 202
    работает быстрее, чем код
    If Left(Text,1) = "K"
    Злоупотреблять такими оптимизациями не стоит, поскольку первый фрагмент менее понятен, чем второй. Он нуждается, по крайней мере, в комментариях.


  • Структура обработчика ошибок

    Обработчик, способный обработать ошибку, должен распознать ее, понять к какому классу относится ошибка - классу устранимых ошибок или классу критических ошибок, последствия которых устранить невозможно. В первом случае, обработчик устраняет причину или следствия ошибки и возвращает управление в охраняемый блок для продолжения вычислений. Во втором случае, обработчик завершает исполнение, предварительно сообщив пользователю всю возможную информацию о причинах, приведших к такому результату.
    Как правило, каждый обработчик предназначен для обнаружения и исправления ошибок некоторого класса. Поэтому, когда он встречается с ошибкой другого класса, то он не способен ее обработать. В этом случае он передает эту функцию выше стоящему обработчику. Реализуется это тем, что обработчик вызывает метод Raise с тем же номером ошибки. При возбуждении ошибки в обработчике ошибки возобновится процесс подъема по стеку вызовов и управление сможет получить следующий активный обработчик ошибки, который в свою очередь, либо обработает ошибку, либо передаст ее вверх. Заметим, что возбуждение ошибки в обработчике ошибки может быть сознательным, но может быть и из-за того, что некорректно работает сам обработчик. В любом случае возбуждение ошибки приведет к подъему по стеку вызовов.
    Типичный обработчик ошибок представляет собой оператор выбора Select, в котором каждый случай соответствует одной обрабатываемой ошибке, а для непредусмотренных ошибок происходит повторное их возбуждение и, тем самым, передача их вверх по стеку вызовов. Допустим, в охраняемом блоке процедуры ожидаются ошибки с кодами K1, K2, …, Kn - обработчик ошибок этой процедуры может быть таким:
    'ErrorHandler: Select Case Err.Number ' анализ кода ошибки. Case K1 … 'обработка ошибки с кодом K1 Case K2 … 'обработка ошибки с кодом K2 . . . Case Kn … 'обработка ошибки с кодом Kn Case Else 'Передача управления обработчику,выше стоящему в стеке вызовов
    Dim intErrNum As Integer intErrNum = Err.Number 'номер ошибки Err.Clear ' чистка объекта Err.
    Err.Raise Number:= intErrNum ' повторное возбуждение ошибки End Select

    Метод Raise здесь используется для повторения исходной ошибки. Если произойдет ошибка, отличная от ошибок с кодами K1, K2, …, Kn, управление будет передано вверх по стеку вызовов другому активному обработчику, если таковой есть. Заметьте, перед вызовом метода Raise происходит чистка объекта Err.

    Сделаем еще несколько замечаний об обработке ошибок в Office 2000:

  • Коды всех внутренних, перехватываемых ошибок можно найти в разделе справочной системы "Trappable Errors" (Перехватываемые ошибки).
  • Если ошибка выполнения возникла в некоем объекте вне VBA (например, в рабочей странице Excel) и не обработана этим объектом, а возвращена в VBA-программу, она будет автоматически преобразована VBA в ошибку с кодом 440, которая определена как "Automation Error" (Ошибка программирования объектов). Такую ошибку желательно сразу же обработать. Если же Вы хотите передать ее на обработку вверх в вызывающую процедуру, желательно возбудить ошибку со своим специальным номером, чтобы вызывающая процедура могла различать ошибки, возникающие в разных объектах.
  • Объекты Office 2000, кроме рассмотренных выше средств работы с ошибками, могут иметь дополнительные средства для их распознавания и обработки. Например, для диалоговых окон и элементов управления определено событие Error, позволяющее обрабатывать их специфические ошибки, которые не могут быть переданы в VBA. Информация об ошибках операций доступа к базам данных может быть получена с помощью объекта Error и семейства Errors из библиотеки объектов доступа к данным (DAO). Описание ошибки Microsoft Access или объекта доступа к данным можно получить по номеру ошибки методом AccessError.


  • Условная компиляция и отладка

    Заметьте, что вызов методов объекта Debug может встречаться только в период отладки программы. В тот момент, когда программа передается конечному пользователю, вызов методов этого объекта не должен встречаться в работающей программе. Это понятно, поскольку в этом режиме никакие отладочные окна не появляются, нет никаких окон проверки и методу Print некуда направлять свой вывод, носящий отладочный характер. И уж тем более не должно прерываться выполнение программы, если вдруг Assert - утверждение станет ложным. Конечный пользователь просто не будет знать, что нужно делать в этом случае.
    Когда отладка завершена, вызовы методов Print и Assert не должны встречаться в выполняемой программе. Конечно, можно просто удалить эти вызовы из текста программы или закомментировать их. Однако жизненный цикл многих программ таков, что снова и снова приходится возвращаться к отладке ранее разработанной программы, внесению заплаток, созданию новой версии, введению новых возможностей. Поэтому удаление или комментирование вызовов объекта Debug не является лучшим способом. Гораздо удобнее иметь возможность включать или выключать отладочный режим при необходимости. Для этой цели и используются средства условной компиляции. Рассмотрим их применение на примере вызовов методов объекта Debug. Идея состоит в том, что все вызовы методов этого объекта заключаются в обертку, заданную специальным оператором #If условной компиляции. Этот оператор проверяет истинность выражения, заданного, как правило, константой периода компиляции. Эта константа играет р оль флажка, в зависимости от ее значения и будут выполняться отладочные операторы. По-видимому, достаточно было бы одного примера, но скажем об этом чуть подробнее.

    Основы офисного программирования и язык VBA

    Динамическое изменение видимости команд меню

    Если некоторые команды меню выполняют действия, связанные с определенными объектами (документами), имеет смысл делать их видимыми при активизации этих объектов и скрывать, когда соответствующие объекты недоступны. Для этого свойству Visible этих команд нужно задать значение True при активизации объекта и False - при его деактивизации (закрытии, удалении и т. п.). Точно так же можно делать видимой и скрывать и панель меню, предназначенную для работы с определенным объектом.
    Проще всего выполнять переустановку свойства Visible в процедурах, обрабатывающих события активизации (открытия, загрузки, появления на экране и т. п.) и деактивизации (закрытия, выгрузки, удаления с экрана и т. п.). Если для данного объекта нет подходящих событий, можно попытаться включить изменение свойства Visible для интересующих нас команд в процедуры, обрабатывающие событие OnAction других команд меню или управляющих кнопок.
    Word сохраняет параметры настройки (в том числе и пользовательские меню) в документах и шаблонах. Поэтому эти компоненты меню видны, если соответствующий документ или шаблон доступен в данном контексте, и они исчезают с экрана, когда документ или шаблон недоступен. Excel же сохраняет пользовательские изменения интерфейса в рабочей памяти, поэтому для управления видимостью компонентов меню применяется изменение свойства Visible в процедурах обработки событий.

    Добавление команд с помощью VBA

    Добавить новую команду в меню можно, применив метод Add коллекции CommandBarControls к объекту, представляющему изменяемое меню. Чтобы добавить собственную команду, вставьте ее имя в меню, а затем в качестве значения свойства OnAction задайте имя VBA-процедуры, которая должна вызываться при выборе данной команды. В качестве значения аргумента Type (Тип) метода Add укажите msoControlButton, означающее, что вставляемый в меню элемент будет командой. Добавим команду "Накладная" в выпадающее меню "Ввод документов" из панели "Головное меню". Выбор этой команды запускает процедуру Invoice.
    Set CstmCtrl = CstmPopUp1.Controls.Add(Type:=msoControlButton) CstmCtrl.Caption = "Накладная" CstmCtrl.OnAction = "Module1.Invoice"
    Метод Add позволяет вставлять в меню и встроенные команды. Для этого при вызове задайте значение параметра Id, равное числовому идентификатору данной команды в Office 2000. Поскольку в Office 2000 количество команд доходит до 4000, мы приведем здесь лишь два фрагмента из начала таблицы идентификаторов, включающих, в частности, некоторые популярные команды из меню File и Edit.

    Таблица 11.1. Фрагмент таблицы идентификаторов встроенных команд менюИдентификаторИмя командыЛокальное имяИдентификаторИмя командыЛокальное имя
    2 &Spelling Орфография19 &Copy Копировать
    3 &Save Сохранить21 &Cut Вырезать
    4 &Print Печать22 &Paste Вставить
    18 &New Создать23 &Open Открыть

    Взгляните, как в меню "Проверки" из панели "Головное меню" можно вставить команду проверки правописания Spelling.
    Set mySpell = CommandBars("Головное меню").Controls("Проверки") _ .Controls.Add(Id:=2)

    Добавление подменю

    Подменю (или дочернее меню) примыкает к боковой стороне другого меню - родительского - на уровне той команды родительского меню, которая является заголовком подменю. Подменю можно добавлять как к выпадающим меню, так и к другим подменю и к всплывающим меню. Сначала добавляется пустое подменю, затем в него вставляются команды. Добавить подменю можно двумя способами: с помощью диалогового окна Настройка и через вызов метода Add в VBA.
    Рассмотрим вначале визуальный способ. Для добавления подменю нужно, как и раньше, активизировать диалоговое окно Настройка и сделать модифицируемое меню видимым. После этого нужно действовать так.
  • На вкладке Команды в списке Категории выберите команду Новое меню.
  • Выберите команду Новое меню справа в списке Команды и перетащите ее в то место меню, куда нужно добавить подменю. Чтобы выпадающее меню открылось, показав уже имеющиеся в нем элементы, протащите Новое меню через имя выпадающего меню. При этом горизонтальная черта будет отмечать то место в списке рубрик выпадающего меню, куда попадет заголовок нового подменю.
  • Щелкните правой кнопкой мыши Новое меню и введите заголовок нового подменю в поле Имя появившегося окна. Одновременно можно задать ключ быстрого доступа к нему, поставив символ "&" перед той буквой, которая будет этим ключом.

  • Теперь при щелчке заголовка подменю справа или слева от него появится пустое подменю.
    Чтобы добавить новое подменю программно, к объекту, представляющему родительское выпадающее меню, нужно применить метод Add коллекции CommandBarControls. В качестве параметра Type (Тип) нужно использовать значение msoControlPopup.
    В следующем примере в конец выпадающего меню "Ввод документов", расположенного на панели меню "Головное меню", добавляется подменю "о движении товаров".
    Dim CstmPopUp1 As CommandBarPopup Set CstmPopUp1 = CstmCtrl.Controls.Add(Type:=msoControlPopup) CstmPopUp1.Caption = " о движении товаров"

    Добавление собственной команды с помощью окна Настройка

    Добавление команды с помощью окна Настройка в собственное или встроенное меню зависит от приложения Office 2000, в котором оно выполняется.
  • . Microsoft Access. Чтобы добавить команду, запускающую на исполнение макрос, нужно выполнить те же действия, что и при добавлении встроенной команды. При этом в списке Категории выберите Все макросы (All Macros) и перетащите имя нужного макроса из списка Команды в меню. Вызов процедуры-функции вставляется, как и встроенная команда. Выберите в списке Категории нужную категорию и перетащите затем нужный элемент из списка Команды в свое меню. Щелкнув этот элемент правой кнопкой мыши, в появившемся меню выберите команду Свойства (Control Properties). На экране появится окно с тем же именем. В поле Подпись (Caption) этого окна замените прежнее имя функции на новое (это будет имя команды в меню). В поле Действие (On Action) введите выражение =имя-функции(), запускающее VBA-функцию на исполнение.
  • Microsoft Excel. Действуя, как и при вставке встроенной команды, выберите в списке Категории команду Макрос (Macros ) и перетащите элемент Настраиваемая команда меню (Custom Menu Item) из списка Команды в меню. Щелкните эту команду правой кнопкой мыши и выберите в появившемся меню команду Назначить макрос (Assign Macro). Затем введите в поле Имя (Macro Name) имя макроса, который должен запускаться командой меню.
  • Microsoft Word и Microsoft PowerPoint. Как при вставке встроенной команды, выберите в списке Категории команду Макрос и перетащите в меню имя нужного макроса из списка Команды.


  • Добавление встроенной команды с помощью окна Настройка

    В диалоговом окне Настройка сделайте модифицируемое меню видимым, а затем:
  • На вкладке Команды выберите в списке Категории категорию добавляемой команды - все команды данной категории будут показаны в списке Команды.
  • Выберите в списке Команды команду и перетащите в то меню или подменю, куда она добавляется. Чтобы выпадающее меню или подменю открылось, показав имеющиеся в нем элементы, протащите команду через заголовок соответствующего выпадающего меню или подменю. Горизонтальная черта отмечает место в списке команд выпадающего меню, куда попадет заголовок нового подменю, когда Вы отпустите кнопку мыши.

  • Скопировать команду можно и иначе: выведите на экран меню с нужной командой и перетащите ее в свое меню при нажатой клавише Ctrl.

    Добавление выпадающего меню с помощью VBA

    Чтобы добавить новое меню программно, нужно использовать метод Add коллекции CommandBarControls, применив его к объекту типа CommandBar, который представляет панель нашего меню. Этот метод позволяет помещать на панель кнопки (CommandBarButton), комбинированные списки (Command-BarComboBox) и выпадающие меню (ComandBarPopup). Его вызов имеет вид:
    выражение.Add(Type, Id, Parameter, Before, Temporary)
    где выражение должно возвратить объект типа CommandBarsControl, параметры в скобках необязательны. Параметр Type (Тип) задает тип добавляемого объекта. Его значение msoControlPopup указывает, что добавляемый управляющий элемент - выпадающее меню. Для пользовательских меню параметры Id и Parameter можно опустить. Значение аргумента Before - число, указывающее положение нового элемента в последовательности элементов панели (если его нет, элемент помещается в конец). Определить имя и ключ быстрого доступа к созданному меню можно, задав значение свойства Caption.
    Взгляните, как к панели "Головное меню" добавить выпадающее меню "Ввод документов" с ключом быстрого доступа "B":
    Dim CstmCtrl As CommandBarControl Set CstmCtrl = CstmBar.Controls _ .Add(Type:=msoControlPopup, Before:=1) CstmCtrl.Caption = "&Ввод документов"

    Добавление выпадающих меню

    Добавлять новые меню (подменю) и команды к существующим меню, встроенным в Office 2000 или созданным пользователем, также можно двумя способами: используя диалоговое окно Настройка или VBA. Рассмотрим, как добавить выпадающее меню к существующей панели меню (головному меню).

    Группировка команд меню

    Разделение групп логически связанных команд меню горизонтальными линиями позволяет пользователям более эффективно работать с большими меню, содержащими разнотипные команды. Сами разделяющие линии командами не являются. Установить или убрать их можно в диалоговом окне Настройка. Для этого в этом окне сделайте видимым модифицируемое меню. Затем щелкните правой кнопкой мыши команду, над которой хотите провести линию. В появившемся меню выберите команду Начало группы (Begin Group). Убирается линия аналогично.
    Ту же задачу разбиения команд на группы можно решить из VBA, присвоив значение True свойству BeginGroup (Начало группы) объекта, представляющего команду, которая должна открывать очередную группу. Чтобы убрать разделяющую линию, присвойте этому свойству False. Вот как выделить группу команд меню "Ввод документов", начинающуюся со вставленной команды "ввод накладной":
    Set InvCommand = CommandBars("Головное меню").Controls("Ввод документов") _ .Controls("ввод накладной") InvCommand.BeginGroup = True

    Использование диалогового окна Настройка

    Во всех приложениях Office 2000 можно спроектировать собственное головное меню визуально, используя для этого диалоговое окно Настройка. Для его вызова нужно в меню Вид выбрать команду Панели инструментов (Toolbars), а затем в появившемся подменю - команду Настройка.
    Новое меню создается так:
  • Щелкните кнопку Создать (New) на вкладке "Панели инструментов".
  • В появившемся окне "Создание панели инструментов" (New Toolbar) введите имя создаваемого меню и щелкните кнопку OK. На экране появится плавающее меню с заданным именем. При работе в Access появляются дополнительные возможности, позволяющие установить свойства панели, что нельзя сделать в других приложениях - Word, Excel, Power Point. При работе в Access:
  • Щелчком кнопки Свойства (Properties) выведите на экран окно "Свойства панели инструментов" (Toolbar Properties).
  • Установите в списке Тип (Type) команду "Строка меню" (Menu Bar) в качестве типа создаваемой панели и закройте окно.

  • Вот, как выглядит окно свойств:
    Использование диалогового окна Настройка
    Рис. 11.3.  Окно свойств командной панели в приложении Access
    Имя нового меню появится в списке Панели инструментов на вкладке Панели инструментов.
    В окне "Свойства панели инструментов" можно задать и другие свойства создаваемого меню. В частности, свойство закрепление (Docking) и опция перемещение (Allow Moving) позволяют разрешить или полностью или частично запретить перемещение меню по экрану. Для головного меню приложения естественно установить постоянное место на экране. Опция настройка (Allow Customizing) позволяет после завершения создания меню отключить возможность его модификации, а если отключить опцию отображение/скрытие (Allow Showing/Hiding), то меню будет на экране постоянно.

    Во всех приложениях Office 2000 можно спроектировать собственное головное меню визуально, используя для этого диалоговое окно Настройка. Для его вызова нужно в меню Вид выбрать команду Панели инструментов (Toolbars), а затем в появившемся подменю - команду Настройка.
    Новое меню создается так:
  • Щелкните кнопку Создать (New) на вкладке "Панели инструментов".
  • В появившемся окне "Создание панели инструментов" (New Toolbar) введите имя создаваемого меню и щелкните кнопку OK. На экране появится плавающее меню с заданным именем. При работе в Access появляются дополнительные возможности, позволяющие установить свойства панели, что нельзя сделать в других приложениях - Word, Excel, Power Point. При работе в Access:
  • Щелчком кнопки Свойства (Properties) выведите на экран окно "Свойства панели инструментов" (Toolbar Properties).
  • Установите в списке Тип (Type) команду "Строка меню" (Menu Bar) в качестве типа создаваемой панели и закройте окно.

  • Вот, как выглядит окно свойств:
    Использование диалогового окна Настройка
    Рис. 11.3.  Окно свойств командной панели в приложении Access
    Имя нового меню появится в списке Панели инструментов на вкладке Панели инструментов.
    В окне "Свойства панели инструментов" можно задать и другие свойства создаваемого меню. В частности, свойство закрепление (Docking) и опция перемещение (Allow Moving) позволяют разрешить или полностью или частично запретить перемещение меню по экрану. Для головного меню приложения естественно установить постоянное место на экране. Опция настройка (Allow Customizing) позволяет после завершения создания меню отключить возможность его модификации, а если отключить опцию отображение/скрытие (Allow Showing/Hiding), то меню будет на экране постоянно.

    Изменение меню во время работы программы

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

    Как добавить встроенное меню

    Диалоговое окно Настройка позволяет просто добавить (скопировать) любое из встроенных меню к меню, которое Вы создаете. При этом исходное встроенное меню не изменяется. Для этого в диалоговом окне Настройка сделайте модифицируемое меню видимым, а затем:
  • Выберите в списке Категории вкладки Команды команду Встроенные меню (Builtin Menus).
  • Выберите в списке Команды имя нужного встроенного меню и перетащите его на нужное место в панель меню, к которой Вы хотите добавить выбранное встроенное выпадающее меню. Знак I отмечает место на панели, куда попадет встроенное меню.

  • Есть и другой способ копирования встроенного меню: выберите его на его собственной панели и перетащите в свою панель меню при нажатой клавише Ctrl.

    Как восстановить удаленные встроенные компоненты меню

    Восстановить можно только встроенный удаленный элемент меню. Как и другие действия над меню, это делается в окне Настройка или из VBA. Если при этом восстановится заголовок выпадающего меню или подменю, вместе с ним восстановится само меню и все его потомки.
    Как всегда, в диалоговом окне Настройка нужно сделать модифицируемое меню видимым. Затем, щелкнув правой кнопкой мыши заголовок выпадающего меню или подменю, которое нужно восстановить, в появившемся меню выберите команду Сброс (Reset).
    Чтобы восстановить встроенную панель меню, перейдите на вкладку Панели инструментов окна Настройка, выберите в списке Панели инструментов имя восстанавливаемой панели меню и щелкните кнопку Сброс(Reset).
    Восстановить встроенный компонент меню программно можно методом Reset. Например, следующий оператор восстанавливает выпадающее подменю Favorites из панели меню Web:
    CommandBars("Web").Controls("Favorites").Reset
    А вот как восстановить всю панель меню Menu Bar из Word:
    CommandBars("Menu Bar").Reset

    Коллекция CommandBarControls и ее элементы

    Эта коллекция обладает только традиционными свойствами и методами. Единственное, что следует рассмотреть - это метод Add и его параметры:
    Function Add([Type], [Id], [Parameter], [Before], [Temporary]) As CommandBarControl
    Метод позволяет добавлять новые элементы в коллекцию. Параметр Type устанавливает тип элемента и тем самым, как мы говорили ранее, определяет и класс этого элемента, возвращаемый функцией Add. Если значением Type является константа msoControlButton, то возвращается объект класса CommandBarButton, для значений: msoControlEdit, msoControlDropdown, msoControlComboBox - возвращается объект CommandBarComboBox и, наконец, если тип имеет значение msoControlPopup, то возвращаемый класс объекта - CommandBarPopup. Параметр Id задает идентификатор встроенного элемента, добавляемого в коллекцию. Если он опущен, то на панели появится пользовательский, пустой элемент, свойства которого позднее следует определить. Параметр Parameter для встроенных элементов используется приложением при запуске команды, для пользовательских элементов может хранить некотор ую информацию об элементе. Параметр Before задает местоположение элемента на панели, если он опущен, элемент добавляется в конец панели. Значение True булева параметра Temporary говорит о том, что элемент является временным, он будет удален с панели при закрытии приложения. По умолчанию элементы являются постоянными.
    Мы уже говорили, что создаваемые элементы панели могут быть одного из трех классов. Кроме того, все они принадлежат к объединяющему классу CommandBarControl. Мы не будем подробно останавливаться на всех свойствах и методах, как отдельных классов, так и объединяющего элементы класса. Рассмотрим только, как решить главную для команд задачу - выполнить некоторую процедуру в ответ на выбор команды меню или щелчок кнопки. Действия, выполняемые командой, должны быть запрограммированы в виде макроса на языке VBA. Напомним, что макрос - это процедура без параметров. Однако есть некоторый способ передачи информации в исполняемый макрос. Для этого можно использовать параметр Parameter, задаваемый при создании элемента или, что удобнее, свойство Parameter. Конечно, для всех встроенных элементов макрос, задающий команду, уже написан. Но для собственных элементов его нужно написать самому, а затем макрос связать с элементом. Свойство OnAction, которым обладают все элементы панели, позволяет связать элемент с исполняемым макросом. Формально свойство является строкой, задающей имя макроса, который и будет вызываться в ответ на выбор пользователя.

    Коллекция CommandBars

    Пришла пора рассказать о наиболее "почтенных" представителях пользовательского интерфейса - меню и инструментальных кнопках. Интерфейс начинался с меню и до сих пор практически ни одно приложение без него не обходится. Все приложения Office 2000 обладают мощной встроенной системой меню и набором инструментальных панелей. И все эти средства доступны и при работе конечного пользователя с документами Office 2000. Но надо иметь в виду, что для "серьезных" приложений, требующих программирования и являющихся предметом нашей книги, только встроенными средствами не обойтись. Все основные приложения Office 2000 - Excel, Word, PowerPoint и Access разделяют на этапе проектирования единые средства настройки, позволяющие как модификацию встроенных, так и создание пользовательских панелей меню и инструментов. На этапе программирования во всех приложениях также используется единый набор объектов, корневым из которых является коллекция CommandBars, входящая в состав библиотеки Office. Вот как выглядит ее стр уктура:
    Коллекция CommandBars
    Рис. 11.1.  Структура коллекции CommandBars
    Элементами коллекции являются объекты класса CommandBar, каждый из которых представляет панель команд. Но то, что расположено на панели: меню, подменю, команды меню, кнопки,- все это задается другим объектом - коллекцией CommandBarControls. Объект CommandBar имеет свойство Controls, возвращающее эту коллекцию. Итак, CommandBar (панель команд) задает любую панель, на которой расположено как меню, так и инструментальные кнопки. На каждой панели, как известно, располагается некоторое число элементов. Они и составляют коллекцию CommandBarControls с элементами класса CommandBarControl. Конечно, эти элементы разнотипны и, по сути, являются объектами трех разных классов: CommandBarButton, CommandBarPopup и CommandBarComboBox. Класс, которому будет принадлежать элемент, определяется в момент его создания. В этот же момент определяется и тип элемента. По типу однозначно определяется клас с, но обратное утверждение неверно, например, элемент класса CommandBarComboBox имеет несколько возможных типов.
    Поясним, в чем сущность этих трех классов, в каком случае элемент следует относить к тому или иному классу. Прежде всего, заметим, что "панельные" элементы могут иметь свою сложную внутреннюю структуру. К примеру, у каждого пункта главного меню обычно древовидная структура, при его раскрытии появляются терминальные вершины (команды меню) и вершины - подменю, которые, в свою очередь, содержат терминальные вершины и подменю. Вы ошибаетесь, если полагаете, что инструментальные кнопки можно только щелкать, и что они просто устроены. В Office 2000 в роли кнопок могут выступать такие элементы как ComboBox, TextBox и ListBox. Класс CommandBarButton определяет терминальную вершину (кнопку или команду меню), CommandBarPopup - подменю, CommandBarComboBox - сложно организованные кнопки, заданные элементами управления. Заметьте, что объект PopUp (подменю) имеет свойство Controls, возвращающее коллекцию CommandBarControls , что и обеспечивает требуемую иерархию вложенности меню.

    Немного терминологии

    Система меню в Office 2000 включает головную строку меню (menu bar), обычно расположенную в верхней части активного окна. Оно постоянно находится на экране. Изменяться это меню может при переходе к другому активному окну или по командам из программ на VBA. Каждая команда головного меню является именем вертикально расположенного меню, выпадающего при выборе этой команды. Команда такого меню может быть именем подменю, при этом она отмечается направленной вправо стрелкой. Подменю появляется на экране справа или слева от родительского меню, когда пользователь выбирает команду с его именем.
    Немного терминологии
    увеличить изображение
    Рис. 11.2.  Строка меню Word. Меню "Вид". Подменю "Панели инструментов"
    Здесь в головном меню выбрана команда Вид (View), а в меню Вид - команда Панели инструментов (Toolbars), которая является именем соответствующего подменю. В подменю Панели инструментов выделена команда Настройка (Customize), которая будет неоднократно упоминаться далее в этой лекции, поскольку с ее помощью можно производить многие действия, связанные с изменениями меню. Меню Вид называется родительским по отношению к подменю Панели инструментов, а меню Панели инструментов - дочерним по отношению к меню Вид. Вообще команды подменю тоже могут быть именами отдельных меню и т. д. Таким образом, меню приложений Office 2000 образуют иерархическую (древовидную) структуру. Корень этой структуры - головное горизонтально расположенное меню, его непосредственными потомками являются выпадающие меню, заголовки которых и образуют головное меню. Концевые вершины (листья) этой иерархии - непосредственно исполняемые команды (часто - это вызовы диалоговых окон), а пути от корня к вершинам проходят по заголовкам соответст вующих меню и подменю.

    О роли интерфейса

    Трудно переоценить роль интерфейса пользователя в современных программах. Удачный или неудачный интерфейс во многом предопределяет успех или неудачу всей системы. В еще большей степени это относится к офисным системам, пользователи которых не искушены в программировании и зачастую не имеют большого опыта работы на компьютере. Разработчик таких систем обычно сосредоточивает усилия на создании дружелюбного и удобного интерфейса пользователя. Удобство интерфейса - вещь субъективная: то, что нравится одному пользователю, может оказаться неудобным и даже совершенно неприемлемым для другого. Поэтому при проектировании системы желательно заранее включать в нее возможность настройки интерфейса на различные категории пользователей. Приложения Office 2000 позволяют без труда перестраивать меню и панели инструментов, управлять параметрами окон, выбирать нужные форматы представления данных на экране (цвет, шрифт, размер и т. д.), создавать собственные диалоговые окна и размещать на них разнообразные управляющие элементы. Возможность присоединения ActiveX -объектов к формам и документам сделало перечень элементов управления практически неограниченным, поскольку число ActiveX -объектов в доступных разработчикам библиотеках постоянно растет.
    В этой и двух следующих лекциях мы рассмотрим ряд компонент VBA, связанных с проектированием интерфейса, которые и позволяют называть этот язык визуальным. В этой лекции мы изучим способы создания собственных меню. Эти возможности лучше всего поддержаны в Access, но и в других приложениях можно создавать собственные системы меню, как руками, так и с использованием VBA.

    Среди библиотек Office 2000, хранящих

    Среди библиотек Office 2000, хранящих объекты, есть библиотеки, связанные с конкретным приложением - Word, Excel, Access, PowerPoint, но не только они. Всякий раз, когда открывается приложение, наряду с библиотекой, связанной с этим приложением, становятся доступными библиотеки общих объектов, где в первую очередь следует отметить библиотеки MSForms и Office. Первая из этих библиотек обеспечивает единство интерфейса при проектировании диалоговых окон (форм). Она содержит объекты, определяющие как само диалоговое окно (форму), так и многочисленные элементы управления, которые могут быть помещены в это окно. Библиотека Office также содержит объекты, используемые при организации интерфейса. В Office 2000 в этой библиотеке появилось много новых объектов: Answer Wizard, Com AddIn, HTML Project, Script, WebPageFont, Language Settings. Но разговор о них пойдет не в этой книге. Заметим, что в чисто интерфейсных объектах существенных изменений не произошло. Говоря о таких объектах, в первую очередь можно выделить объект Assistant, объект CommandBar и коллекцию этих объектов. Объект Assistant и входящий в него объект Baloon предназначены для организации контекстной помощи в более живой визуальной форме, отличной от традиционных окон. Объект CommandBar составляет основу интерфейса, - он представляет панель, на которой расположены команды меню и инструментальные кнопки.

    Переименование команды меню

    VBA позволяет динамически переименовывать команды меню, используя свойство Caption (Заголовок) изменяемой команды. Это может пригодиться, например, при настройке меню на язык пользователя. В следующем примере в англоязычной версии Office 2000 имя команды "Open Database" переводится на русский язык.
    CommandBars("MyMenubar").Controls("File").Controls("Open Database") _ .Caption = "Открыть базу данных"
    Другой способ - завести переменную, ссылающуюся на данную команду:
    Set openData = CommandBars("My Menubar").Controls _ ("File").Controls("Open Database")
    и затем использовать ее в программе для доступа к команде. При этом изменить заголовок можно так:
    openData.Caption = "Открыть базу данных"

    Dim CstmBar As CommandBar Dim

    Option Explicit
    Public Sub CreateCustomMenu() Dim CstmBar As CommandBar Dim CstmPopUp1 As CommandBarPopup, CstmPopUp2 As CommandBarPopup Dim CstmCtrl As CommandBarControl Dim Exist As Boolean 'Выключаем все панели For Each CstmBar In CommandBars CstmBar.Enabled = False Next CstmBar
    'Создаем, включаем и делаем видимой собственную панель Exist = False For Each CstmBar In CommandBars If CstmBar.Name = "Головное меню" Then Exist = True Exit For End If Next CstmBar If Not Exist Then Set CstmBar = CommandBars.Add(Name:="Головное меню", _ Position:=msoBarTop, MenuBar:=True, Temporary:=False) End If CstmBar.Enabled = True CstmBar.Visible = True
    'Добавляем меню на панель Exist = False For Each CstmCtrl In CstmBar.Controls If CstmCtrl.Caption = "&Ввод документов" Then Exist = True Exit For End If Next CstmCtrl If Not Exist Then Set CstmCtrl = CstmBar.Controls _ .Add(Type:=msoControlPopup, Before:=1) CstmCtrl.Caption = "&Ввод документов" 'Добавляем две команды подменю Set CstmPopUp1 = CstmCtrl.Controls.Add(Type:=msoControlPopup) CstmPopUp1.Caption = " о движении товаров"
    Set CstmPopUp2 = CstmCtrl.Controls.Add(Type:=msoControlPopup) CstmPopUp2.Caption = " финансовых"
    'Добавляем команду в каждое подменю Set CstmCtrl = CstmPopUp1.Controls.Add(Type:=msoControlButton) CstmCtrl.Caption = "Накладная" CstmCtrl.OnAction = "Module1.Invoice"
    Set CstmCtrl = CstmPopUp2.Controls.Add(Type:=msoControlButton) CstmCtrl.Caption = "Счет" CstmCtrl.OnAction = "Module1.Account" End If
    End Sub
    Пример 11.1.
    Закрыть окно




    Option Explicit
    Public Sub CreateCustomMenu()
    Dim CstmBar As CommandBar
    Dim CstmPopUp1 As CommandBarPopup, CstmPopUp2 As CommandBarPopup
    Dim CstmCtrl As CommandBarControl
    Dim Exist As Boolean
    'Выключаем все панели
    For Each CstmBar In CommandBars
    CstmBar.Enabled = False
    Next CstmBar

    'Создаем, включаем и делаем видимой собственную панель
    Exist = False
    For Each CstmBar In CommandBars
    If CstmBar.Name = "Головное меню" Then
    Exist = True
    Exit For
    End If
    Next CstmBar
    If Not Exist Then
    Set CstmBar = CommandBars.Add(Name:="Головное меню", _
    Position:=msoBarTop, MenuBar:=True, Temporary:=False)
    End If
    CstmBar.Enabled = True
    CstmBar.Visible = True

    'Добавляем меню на панель
    Exist = False
    For Each CstmCtrl In CstmBar.Controls
    If CstmCtrl.Caption = "&Ввод документов" Then
    Exist = True
    Exit For
    End If
    Next CstmCtrl
    If Not Exist Then
    Set CstmCtrl = CstmBar.Controls _
    .Add(Type:=msoControlPopup, Before:=1)
    CstmCtrl.Caption = "&Ввод документов"
    'Добавляем две команды подменю
    Set CstmPopUp1 = CstmCtrl.Controls.Add(Type:=msoControlPopup)
    CstmPopUp1.Caption = " о движении товаров"

    Set CstmPopUp2 = CstmCtrl.Controls.Add(Type:=msoControlPopup)
    CstmPopUp2.Caption = " финансовых"

    'Добавляем команду в каждое подменю
    Set CstmCtrl = CstmPopUp1.Controls.Add(Type:=msoControlButton)
    CstmCtrl.Caption = "Накладная"
    CstmCtrl.OnAction = "Module1.Invoice"

    Set CstmCtrl = CstmPopUp2.Controls.Add(Type:=msoControlButton)
    CstmCtrl.Caption = "Счет"
    CstmCtrl.OnAction = "Module1.Account"
    End If

    End Sub

    Пример построения документа с собственным меню

    Обобщим приведенные сведения по построению меню и создадим документ, в котором все встроенные меню будут отключены и создано собственное иерархическое меню. На верхнем уровне меню будет состоять из одного пункта, оно будет включать два подменю, каждое из которых содержит по одной команде. Приведем процедуру, реализующую создание такого меню:
    Пример 11.1.
    (html, txt)
    Заметьте, вначале, используя свойство Enabled, были отключены все панели. Затем было сформировано головное меню, содержащее всего один пункт, с двумя подменю и командами. Обратите внимание, перед добавлением нового пункта, обычно делается проверка, а не был ли он уже добавлен. Подобная проверка позволяет избежать ошибок, возникающих при попытках добавить уже существующий пункт или удалить несуществующий. Чтобы наш пример был законченным, приведем процедуры, вызываемые в ответ на выбор команд меню Накладная и Счет:
    Public Sub Invoice() MsgBox ("Накладная!") End Sub
    Public Sub Account() MsgBox ("Счет!") End Sub
    Вот как выглядит документ по завершении работы процедуры CreateCustomMenu:
    Пример построения документа с собственным меню
    Рис. 11.4.  Документ, имеющий собственное меню
    В заключение, приведем процедуру, восстанавливающую стандартное окружение:
    Public Sub ResetMainMenu() Dim CstmBar As CommandBar 'Включаем все панели For Each CstmBar In CommandBars CstmBar.Enabled = True Next CstmBar
    Set CstmBar = CommandBars.Item("Menu Bar") CstmBar.Visible = True End Sub

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

    Public Sub Invoice() MsgBox ("Накладная!") End Sub

    Public Sub Account() MsgBox ("Счет!") End Sub

    Вот как выглядит документ по завершении работы процедуры CreateCustomMenu:

    Пример построения документа с собственным меню
    Рис. 11.4.  Документ, имеющий собственное меню

    В заключение, приведем процедуру, восстанавливающую стандартное окружение:

    Public Sub ResetMainMenu() Dim CstmBar As CommandBar 'Включаем все панели For Each CstmBar In CommandBars CstmBar.Enabled = True Next CstmBar

    Set CstmBar = CommandBars.Item("Menu Bar") CstmBar.Visible = True End Sub

    Проектирование интерфейса. Меню

    В предыдущих лекциях в примерах и описании VBA использовалась заключительная англоязычная бета - версия Office 2000. При описании интерфейса я перешел на русифицированную бета - версию Office 2000. Я полагаю, что программисту должно быть все равно, с какой версией он работает, как называется соответствующий пункт меню "Сервис" или "Tools". Тем не менее, разумнее описывать интерфейс применительно к русифицированному варианту. Кроме того, это входило в мои планы тестирования этой версии Office 2000.

    Создание меню с помощью VBA

    Новое меню в Excel, Word или PowerPoint обычно создается средствами VBA, поскольку в этих приложениях диалоговое окно Настройка не содержит кнопки Свойства. В Access тоже можно создать меню, используя VBA, но, все же, удобнее действовать описанным выше способом.
    Новое меню создается методом Add коллекции CommandBars (Панели команд):
    выражение.Add(Name, Position, MenuBar, Temporary)
    где выражение - обязательное выражение, возвращающее объект CommandBars, а все параметры в скобках необязательны. Name задает имя нового меню; Position определяет его положение (значения-константы msoBarLeft, msoBarTop, msoBarRight, msoBarButtom определяют положение меню слева, вверху, справа или внизу окна, msoBarFloating задает "плавающее" меню, msoBarPopup указывает, что новое меню будет всплывающим). Значение True параметра MenuBar указывает, что новое меню заменит текущую активную строку меню (по умолчанию - False). Значение True параметраTemporary означает, что новое меню будет временным и исчезнет, когда закроется содержащее его приложение (по умолчанию - False).
    В следующем примере создается новое меню "Головное меню"
    Dim CstmBar As CommandBar Set CstmBar = CommandBars.Add(Name:="Головное меню", _ Position:=msoBarTop, MenuBar:=True, Temporary:=False)

    Создание собственного головного меню

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

    Создание собственных и модификация встроенных меню

    В меню объединяют последовательности, группы, наборы команд (рубрик), одну из которых может выбрать пользователь для совершения очередного действия. Как правило, названия команд в меню достаточно информативны, так что пользователь может легко найти нужную ему команду. Команды для решения близких задач можно объединить в группу, поместив их рядом в одном меню и отделив чертой от команд, решающих другие задачи. В одном меню команды объединяют на основе одного из двух принципов: либо это различные действия над одним объектом, либо однотипные действия над различными объектами. Первый принцип реализован в меню Файл (File) всех приложений Office 2000, объединяющем различные действия над основным объектом приложения: создание, открытие и закрытие, сохранение, пересылка и печать объекта. Меню Вставка (Insert) построено по другому принципу - действие "вставить" выполняется для различных объектов. Если вариантов исполняемых действий много, их структурируют, используя подменю - списки команд, появляющиеся правее или левее выбранной команды родительского меню. Еще один вид меню - контекстные (shortcut) меню, всплывающие при нажатии правой кнопки мыши в определенном контексте. В них можно объединять действия, допустимые (факультативно) в данном контексте. Доступ пользователя к команде меню можно ускорить, определив для нее "горячие" клавиши, нажатие которых эквивалентно выбору этой команды.
    Мы уже рассмотрели основные свойства и методы коллекции CommandBars и ее объектов CommandBar, представляющих панели команд, на которых располагаются меню, подменю, кнопки и другие элементы, образующие коллекцию CommandBarControls. Рассмотрим работу с объектами, относящимися к меню. Эту работу, как правило, можно проводить двумя способами: "визуально", используя встроенные команды и диалоговые окна приложений Office 2000, или "программно", создавая в процедурах VBA объекты указанных классов, задавая и изменяя их свойства соответствующими методами.

    Свойства и методы коллекции CommandBars

    Коллекция CommandBars обладает не только традиционными свойствами и методами, но и некоторыми специфическими свойствами. Давайте кратко познакомимся с большинством из них:
  • Property ActionControl As CommandBarControl вызывается обычно в одной из OnAction процедур и возвращает объект CommandBarControl, чье свойство Action связано с этой выполняемой процедурой. Если же такого объекта нет, то возвращается Nothing, что бывает при вызове этого свойства из обычной процедуры. Позволяет отключить доступ к объекту на время выполнения процедуры.
  • Property ActiveMenuBar As CommandBar возвращает объект CommandBar, представляющий активную панель меню.
  • Property DisplayKeysInTooltips As Boolean, DisplayTooltips As Boolean - если свойства имеют значения True, то на панели отображаются назначения "горячих" клавиш.
  • Property LargeButtons As Boolean - значение True позволяет включить клавиши "большого" размера.
  • Function Add([Name], [Position], [MenuBar], [Temporary]) As CommandBar. Метод Add позволяет программным путем добавить в коллекцию новую панель, дав ей имя Name. Параметр Position указывает расположение панели на экране. Его возможные значения: msoBarLeft, msoBarTop, msoBarRight, msoBarBottom указывают куда стыковочная панель будет причалена- вверху, справа, слева или снизу экрана. Значение msoBarFloating этого параметра указывает, что панель не является стыковочной и находится в плавающем состоянии. Значение msoBarPopup указывает, что новая панель создается для контекстного меню. Булев параметр MenuBar имеет значение True, когда новая панель заменяет существующую панель меню. Параметр Temporary позволяет это назначение сделать временным.
  • Function FindControl([Type], [Id], [Tag], [Visible]) As CommandBarControl - Позволяет найти на панелях, входящих в коллекцию, элемент, удовлетворяющий критериям поиска. Параметры, являющиеся ключами поиска, могут быть опущены, достаточно задания одного из них.
  • Sub ReleaseFocus() - Все панели теряют фокус.

  • В Office 2000 у этой коллекции появились новое свойство и новое событие:
  • Property AdaptiveMenus As Boolean. Позволяет включить или выключить адаптивные меню, - собственные меню, заменяющие стандартные.
  • Event OnUpdate() - Событие возникает при любых изменениях командной панели - объекта CommandBar.


  • Свойства и методы объекта CommandBar

    Вот основные свойства и методы самого объекта CommandBar, представляющего отдельную панель:
  • Property BuiltIn As Boolean Это булево свойство определено как для панели, так и для ее элементов, значение True указывает, что панель или элемент являются встроенными, False - определены пользователем.
  • Property Context As String - позволяет установить или проверить контекст, определяющий местоположение хранения панели, то ли непосредственно с самим документом, то ли в другом возможном месте, зависящем от приложения. Это важная информация для корректной работы.
  • Property Controls As CommandBarControls - можно сказать центральное свойство, возвращающее, как мы уже говорили, коллекцию элементов, располагаемых на панели.
  • Property Enabled As Boolean, Visible As Boolean - тоже два центральных и широко используемых свойства. Первое позволяет выключить доступ к панели, обычно временно, второе делает панель вообще невидимой и тем более недоступной.
  • Property Height As Long, Left As Long, Top As Long, Width As Long - типичные свойства графических элементов, в том числе для панели и ее элементов. Задают размеры элемента.
  • Property Index As Long - возвращает порядковый номер элемента в коллекции.
  • Property Name As String, NameLocal As String - имя панели или ее элемента, для встроенных панелей (элементов) имеют смысл два имени "родное" английское имя и локализованное.
  • Property Position As MsoBarPosition - такое же свойство, как у коллекции панелей, но применяемое к отдельно взятой панели.
  • Property Protection As MsoBarProtection - защищает панель от тех или иных действий пользователя. Значением свойства являются константы, определяющие, что конкретно нельзя делать с панелью, например, менять ее размеры или передвигать.
  • Property Type As MsoBarType - возвращает тип панели.
  • Sub Delete() - удаляет панель
  • Function FindControl([Type], [Id], [Tag], [Visible], [Recursive]) As CommandBarControl - такой же метод поиска элемента, как и описанный выше для коллекции, но поиск ограничен пределами одной панели.
  • Sub Reset() - Восстанавливает установки, принятые по умолчанию, в частности конфигурацию встроенных панелей.
  • Sub ShowPopup([x], [y]) - отображает контекстную панель в позиции, заданной курсором или параметрами X и Y, если они указаны.
  • Также как и коллекция, этот объект приобрел новое булево свойство AdaptiveMenu


  • Удаление команд меню

    Удаление команд, подменю и меню позволяет упростить работу с приложением, приспособить его к нуждам отдельного пользователя. В Office 2000 можно удалять встроенные и пользовательские выпадающие меню из панелей меню, дочерние подменю из родительских меню, команды из всех видов меню. Единственное, чего сделать нельзя - удалить встроенную панель меню или встроенное всплывающее меню, даже если из них удалены все команды. Главное, что этого делать не нужно ни в коем случае. Панель можно сделать только недоступной или невидимой, что мы и демонстрировали в наших примерах.
    Удаленные из меню встроенные подменю и команды можно восстановить в нужный момент. Собственные же удаленные подменю и команды придется при необходимости воссоздавать заново.

    Удаление команды с помощью окна Настройка

    Для удаления компонента меню нужно в диалоговом окне Настройка модифицируемое меню сделать видимым, щелкнуть правой кнопкой мыши удаляемый элемент и в появившемся меню выбрать команду Удалить (Delete).
    Чтобы удалить целиком пользовательскую панель меню, нужно перейти на вкладку Панели инструментов диалогового окна Настройка и, выбрав в списке Панели инструментов имя удаляемой панели меню, щелкнуть кнопку Удалить.

    Удаление команды с помощью VBA

    Для удаления компонента меню используется метод Delete (Удалить). Этот оператор, например, удаляет выпадающее подменю Favorites из панели меню Web:
    CommandBars("Web").Controls("Favorites").Delete
    Собственную (пользовательскую) панель меню "Головное меню" можно целиком удалить оператором:
    CommandBars("Головное меню").Delete

    Управление доступом к командам меню

    Иногда удобней управлять доступом пользователя к командам меню, не скрывая и восстанавливая их на экране, а отключая реакции на их выбор и нажатие. Такие команды видны на экране, но отличаются от активных команд своим тусклым, "серым" видом. Для управления доступом к компонентам меню служит булево свойство Enabled (Включен). Если оно равно True, соответствующий компонент (команда, выпадающее меню или подменю) доступен - False делает компонент недоступным ("серым"). Установив для свойства Enabled подменю значение False, можно сделать недоступными все его команды. Так можно управлять доступом к собственным компонентам меню. Для встроенных компонентов меню переустановить свойство Enabled нельзя. Сделаем недоступной пользовательскую команду "Сохранить базу" выпадающего меню "Файлы" на панели "Worksheet Menu Bar" рабочего листа Excel.
    CommandBars("Worksheet Menu Bar").Controls("Файлы") _ Controls.Add("Сохранить базу").Enabled = False
    Все команды выпадающего меню "Файлы" можно сделать недоступными оператором:
    CommandBars("Worksheet Menu Bar").Controls("Файлы").Enabled = False

    Возможности настройки и изменения системы меню

    Office 2000 предоставляет широкие возможности для настройки и модификации меню приложений. Пользователи могут создавать новые головные меню, добавлять команды в уже существующие меню и связывать с ними новые подменю. Можно также создавать новые и изменять существующие контекстные меню. Есть средства, позволяющие в любой момент восстановить первоначальную систему меню.
    Возникают вопросы: когда создавать свою собственную систему меню, а когда лишь модернизировать систему приложения? Общий совет Microsoft состоит в том, что если добавления или изменения невелики, то следует модернизировать систему меню, а при существенных изменениях (например, добавлении нескольких новых меню и подменю) целесообразно создавать собственную систему меню. Разумеется, в этом случае следует принимать во внимание будущих пользователей создаваемой прикладной офисной системы, их образование, квалификацию, опыт работы с компьютером и т.д. Для "среднего" пользователя - типичного сотрудника типичной организации - прикладная система над Excel или Access, видимо, должна иметь собственный интерфейс, лишь в небольшой степени использующий встроенные меню, панели инструментов и диалоговые окна Office 2000. Поэтому в большинстве случаев разработчику следует создавать свое головное меню и, возможно, панель инструментов, перекрывающие частично или полностью на экране головное меню и панель инструментов соот ветствующего приложения Office 2000. Другими вариантами построения интерфейса могут быть собственные диалоговые окна или расположение управляющих элементов непосредственно на поверхности основных документов (например, на рабочих листах Excel).

    Вставка и группировка команд

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

    Вывод собственной панели меню

    Чтобы на экране показалась новая панель меню вместо текущей, задайте значение True свойству Visible объекта CommandBar, представляющего панель меню, которая должна появиться. Новая панель меню заменит прежнюю активную панель на экране. Чтобы восстановить прежнюю панель, при завершении работы программы нужно задать свойству Visible значение False.
    В Excel и PowerPoint для замены основной панели приложения применяется процедура, обрабатывающая событие, после которого должна появиться новая панель или макрос. В Word можно обеспечить вывод собственной панели меню при запуске приложения, если заранее активизировать ее и сохранить в проекте Normal. Тогда при следующем запуске эта панель появится на экране как основная. Другой вариант - установить свойство Visible в процедуре обрабатывающей событие Open.

    Основы офисного программирования и язык VBA

    Диалоговые окна и элементы управления

    Диалоговые окна (формы) и элементы управления составляют основу современного визуального интерфейса. Все эти элементы и технология работы с ними в основном стандартизованы и являются похожими для разных платформ и программных сред. Поэтому не удивительно, что приложения Excel, Word и PowerPoint разделяют единые объекты, задающие диалоговые окна и элементы управления. Эти объекты помещены в специальную библиотеку MSForms. Несколько особняком стоит Access. И здесь все доступно, но по-своему.
    В этом параграфе мы дадим краткий обзор основных объектов библиотеки MSForms и остановимся лишь на некоторых моментах работы с ними. Изложение, принятое здесь, предполагает, что читатель, так или иначе, уже работал с диалоговыми окнами и элементами управления. С другой стороны, этот материал можно рассматривать, как вводный, дающий общее представление о проблеме. Далее в этой лекции он рассмотрен более подробно со многими деталями и примерами.
    Выделим некоторые основные моменты, которые следует иметь в виду при создании визуального интерфейса:
    Все загруженные диалоговые окна представляют коллекцию UserForms. Формально, это глобальная внутренняя переменная, - Вы не найдете UserForms в окне просмотра объектов. Тем не менее, это нормальная коллекция со стандартными методами и свойствами, - метод Item позволяет по имени или номеру добраться до любого ее элемента, а метод Add добавляет новое диалоговое окно (форму) в коллекцию. Элемент коллекции - объект класса UserForm задает отдельное окно.
    Для каждого типа элементов управления в библиотеке MSForms имеется класс объектов, имя которого совпадает с именем элемента управления (его типа). Например, есть классы SpinButton и TextBox.
    Конечно, диалоговые окна создаются, как правило, не программно, а визуально. Вначале создается само окно, а затем оно заселяется элементами управления. Этот этап называется этапом проектирования и его следует отличать от этапа выполнения, когда приложение выполняется и конечный пользователь взаимодействует с приложением, в частности, через диалоговые окна и их элементы управления.
    Важно понимать, что как только Вы визуально создали диалоговое окно, как только Вы визуально поместили в него тот или иной элемент управления, - в этот же самый момент автоматически в программе появляется объект соответствующего класса и с этим объектом можно работать, вызывая его методы и изменяя его свойства.

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

    Никаких проблем с программированием не возникает, - есть обычные объекты, у которых можно менять или проверять некоторые свойства и вызывать некоторые из их методов. Единственно следует помнить, что объекты могут создаваться автоматически (но могут и программно) и имеют видимое отображение на экране дисплея.

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

    Поговорим об отладке.


    Написание обработчиков событий следует относить, по-видимому, к этапу проектирования. Когда этот этап закончен, - окно создано и заселено элементами, их свойства установлены, обработчики событий написаны, - возникает естественный вопрос " А будет ли все это правильно работать?" Чтобы вести отладку, можно, конечно, перейти в режим выполнения приложения. Однако это не эффективно, лучше вести локальную отладку - для этого достаточно выбрать команду Run Sub/UserForm из меню Run в VBA редакторе. Заметьте, что для отображения формы в режиме выполнения должен быть вызван ее метод Show.

    Несколько слов следует сказать о методах Show, Hide и Load объекта UserForm. Метод Load загружает диалоговое окно и делает его доступным для программирования (объект определен). Но окно не отображается на экране дисплея и конечный пользователь не может работать с ним и его элементами. Чтобы окно сделать видимым, нужно вызвать его метод Show, чтобы видимое окно спрятать - метод Hide.

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

    В предыдущем пункте мы говорили о том, как сделать некоторые элементы управления невидимыми, а, следовательно, и недоступными для изменений. Чаще, однако, применяется другая стратегия - видимые элементы можно сделать недоступными для изменений. Практически все элементы имеют булево свойство Enabled. Если дать ему значение True, то элемент считается включенным и доступен для изменения. Чтобы "выключить" его и сделать недоступным, достаточно изменить значение свойства Enabled на False. Обычно, программным путем проверяются контекстные условия, при выполнении которых элемент включается и является доступным для изменений.


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

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

    Load <объект класса UserForm>.

    За компанию и UnLoad является оператором с аналогичным синтаксисом.

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

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

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


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

    В Office 2000 определен весьма полезный элемент управления MultiPage. С его помощью достаточно просто организовать многостраничное диалоговое окно (окно с несколькими вкладками). Вначале в окно помещается этот элемент, устанавливается нужное число страниц, а затем, выбирая вкладку, соответствующую нужной странице, эта страница проектируется обычным способом.

    Близким по духу к MultiPage является элемент управления TabStrip. Если первый задает несколько страниц с разнородным составом элементов управления, то второй определяет несколько страниц с одинаковым набором элементов. Типичная ситуация применения TabStrip - диалоговое окно отвечает за обработку данных нескольких отделов. Все формы представления данных по каждому отделу одинаковы и отличаются только содержащейся в этих формах информацией. TabStrip для каждого из отделов создает свою вкладку.

    До сих пор мы говорили, что ActiveX и элементы управления можно располагать в диалоговых окнах. Эти окна представляют некоторый способ группирования элементов и придают структуру пользовательскому интерфейсу. Можно, однако, размещать все эти элементы и вне диалогового окна непосредственно на документе Word, рабочем листе или листе диаграмм Excel или на слайде презентации PowerPoint. Иногда, особенно когда число элементов невелико, так поступать удобнее - есть данные, а рядом - кнопка, щелчок которой обрабатывает эти данные. Злоупотреблять такой возможностью мы, правда, не рекомендуем.

    Добавление дополнительных элементов управления

    На панели элементов находятся пиктограммы не всех элементов управления, доступных в Office 2000. Доступ к дополнительным элементам управления Вы получите, щелкнув правой кнопкой панель элементов и выбрав в появившемся контекстном меню команду Дополнительные элементы (Additional Controls). На экране появится одноименное окно со списком доступных элементов управления (Available Controls).
    Добавление дополнительных элементов управления
    Рис. 12.10.  Окно Дополнительные элементы
    В этом списке отмечены элементы, находящиеся в данный момент на панели инструментов. Отметив в списке нужные дополнительные элементы, и выйдя из этого окна, Вы увидите их на панели инструментов и сможете использовать при создании своих диалоговых окон.

    Использование Me в качестве имени текущего диалогового окна

    Для упрощения написания кода процедур VBA позволяет в качестве имени текущего диалогового окна применять ключевое слово Me (это похоже на использование указателя текущего объекта This в С++). В частности, в предыдущем примере вместо оператора:
    With frmInit
    можно было использовать:
    With Me

    Изменение размеров диалогового окна

    Из процедур обработки событий можно менять свойства не только отдельных элементов управления, но и всего окна. В качестве примера рассмотрим динамическое изменение его размеров. Ранее мы уже говорили о том, что, изменяя размеры окна, можно скрывать те или иные элементы управления. Сейчас продемонстрируем это на примере. На стадии проектирования разместим в окне элементы управления, которые при первоначальном открытии окна не должны быть видны, но должны появиться в процессе работы пользователя с этим окном. Пусть в форме Cutting есть командная кнопка "Получить данные", и поля ввода "Доходы" и "Расходы" При открытии окна поля ввода недоступны, что достигается "отсечением" нижней части формы в процедуре инициализации. Вот как выглядит обработчик события Initialize нашей формы:
    Private Sub UserForm_Initialize() 'Отсечение нижней части формы Me.Height = 90 End Sub
    Так выглядит форма при ее открытии:
    Изменение размеров диалогового окна
    Рис. 12.19.  Усеченная форма в момент ее открытия
    После щелчка командной кнопки обработчик события Click восстанавливает размер, заданный при проектировании формы. Он задает также и значения скрытых ранее полей:
    Private Sub CommandButton1_Click() With Me 'Установить нормальную высоту формы .Height = 200 'Задать значения ранее не видимых полей .Доходы = 1570 .Расходы = 1500 End With
    End Sub
    А так выглядит эта же форма после щелчка командной кнопки:
    Изменение размеров диалогового окна
    Рис. 12.20.  Форма Cutting со скрытыми ранее полями

    Коллекция Dialogs и объект Dialog

    Рассматривая общие объекты, используемые при организации пользовательского интерфейса, нельзя не упомянуть коллекцию Dialogs, которую можно использовать в приложениях Word и Excel. Элементы этой коллекции - объекты класса Dialog представляют встроенные в приложение диалоговые окна, появляющиеся по ходу работы с документами. Из этой коллекции ничего нельзя удалить и ничего в нее нельзя добавить. В любой момент можно только открыть любое из существующих встроенных диалоговых окон и предоставить пользователю возможность работы с ним, например, открыть файл или организовать поиск нужной информации. Чтобы добраться до нужного диалогового окна, используется обычная конструкция: Dialogs (<имя окна>), где имя окна задается константой. Все имена констант строятся по одному принципу, - они начинаются с префикса wdDialog или xlDialog (для Word и Excel соответственно), а затем идет имя окна. В приложении Word это полное имя, включающее все меню, которые надо открыть при вызове диалогового окна "вручную". Так, например, для открытия диалогового окна "Open" из меню File в приложении Word следует вызвать метод Show следующим образом:
    Dialogs(wdDialogFileOpen).Show
    Сразу же появится окно:
    Коллекция Dialogs и объект Dialog
    увеличить изображение
    Рис. 12.1.  Диалоговое окно открытия файла
    В Excel этот же вызов будет иметь вид:
    Application.Dialogs(xlDialogOpen).Show
    Заметьте, что в первом случае - это глобальный объект и поэтому его можно вызывать непосредственно, в Excel же необходимо указание корневого объекта Application, чье свойство Dialogs и возвращает коллекцию Dialogs. Но зато в Excel достаточно короткого имени для вызова диалогового окна. Метод Show открывает диалоговое окно и позволяет пользователю работать в нем. В зависимости от того, какую кнопку нажал пользователь при выходе из окна, возвращается результат: в Excel это True или False (нажаты кнопки OK или Cancel), в Word можно точнее проанализировать, какая кнопка была нажата. Метод Show - основа работы с диалоговыми окнами, по сути больше ничего и не нужно: в нужный момент открывается нужное диалоговое окно, пользователь работает в нем и по закрытии можно понять, успешно ли закончилась его работа.

    Тем не менее, Word предоставляет некоторые дополнительные возможности. В частности, наряду с методом Show, можно применять метод Display, отличающийся тем, что хотя окно и открывается, но ввод от пользователя оно принять не может и используется только в информационных целях. Word позволяет также создавать объекты класса Dialog и программно устанавливать значения полей диалогового окна, еще до того, как оно будет показано пользователю, беря тем самым на себя часть его работы. Метод Execute позволяет эти установки зафиксировать. Вот пример, в котором делаются некоторые установки в диалоговом окне Replace:

    Public Sub Dialogs2() Dim MyDialog As Dialog, Answer As Variant Set MyDialog = Dialogs(wdDialogEditReplace) MyDialog.Find = "коллекция" MyDialog.Replace = "семейство" MyDialog.FuzzyFind = True Answer = MyDialog.Show Debug.Print Answer

    End Sub

    В процессе работы этой процедуры появится окно:

    Коллекция Dialogs и объект Dialog
    Рис. 12.2.  Диалоговое окно замены

    Обратите внимание, мы используем у объекта MyDialog такие свойства, как Find, Replace и даже FuzzyFind, соответствующие полям диалогового окна (В браузере объектов Вы не найдете поля FuzzyFind). В документации по Word можно найти список допустимых параметров для каждого типа диалогового окна.

    Поскольку действительно важно уметь установить программно некоторые характеристики в диалоговом окне, то и в Excel предусмотрена такая возможность. Здесь всю работу выполняет метод Show,- он имеет до 30 возможных параметров и, используя их, можно передать при открытии окна всю необходимую информацию.

    Модификация управляющих элементов во время работы

    Устанавливать программно новые значения свойств элементов управления можно не только при инициализации диалогового окна, но и во время работы пользователя (и системы) в нем. В любой процедуре обработки события, связанного со всем диалоговым окном или его отдельным элементом, можно присвоить значение свойству или вызвать метод любого управляющего элемента. Например, эти команды изменяют текст в окне редактора и снимают флажок в окне предыдущего примера:
    Public Sub ChangeForm() With Me .MyText.Text = "Нелюбимый цвет:" 'новый текст в редакторе .lstColors.ListIndex = 1 'черный .chkGood.Value = False 'отключение флажка End With
    End Sub
    Благодаря возможности подобных изменений система может гибко реагировать на действия пользователя и использовать одно диалоговое окно для решения нескольких связанных или однотипных задач.

    Обмен данными с диалоговым окном

    Диалоговые окна служат для обмена информацией между пользователем и документом. При открытии формы с заранее спроектированными свойствами элементов управления и после ее инициализации пользователь попадает в мир ее объектов. В этом мире он может выполнять разные действия, вводя требуемые данные, выбирая нужные ему опции и щелкая те или иные командные кнопки. В ответ на изменения, совершаемые пользователем, могут вызываться обработчики событий, которые в свою очередь производят изменения свойств объектов, как самой формы, так и объектов документа. Во многих случаях, изменения, сделанные в результате взаимодействия должны быть сохранены перед тем, как форма будет закрыта, с тем, чтобы их можно было восстановить при последующих открытиях формы. Для этого можно использовать рабочую память самого приложения (ячейки рабочих листов в Excel, записи БД в Access, текст документа в Word), внешние дисковые файлы или переменные уровня модуля.
    Приведем сейчас пример уже знакомой нам формы, дополненной командными кнопками Save и Reset, обработчики события Click которых позволяют сохранить текущее состояние элементов управления и восстановить его в нужный момент. Для хранения информации используется память документа Excel. Вот как выглядит лист Excel, в ячейках которого хранятся данные о состоянии формы:
    Обмен данными с диалоговым окном
    Рис. 12.22.  Лист Excel, хранящий информацию о состоянии элементов формы
    Пять ячеек листа SavedData рабочей книги BookOne12 хранят нужную информацию. Заметьте, ячейка A1 именована. Ее имя "Данные" будет использовано в обработчиках событий. На этом же листе расположена командная кнопка "Вызов Формы". Ее обработчик события очень прост, ѕ он вызывает форму:
    Private Sub CommandButton1_Click() frmInit.Show End Sub
    А вот как выглядит сама форма в момент ее открытия:
    Обмен данными с диалоговым окном
    Рис. 12.23.  Форма при ее открытии
    Эта форма уже использовалась в наших примерах. Ранее она была связана с документом Word, а теперь мы перетащили ее в рабочую книгу Excel, используя операции Export - Import.
    После чего форма была слегка модифицирована добавлением в нее новых командных кнопок ѕ Save, Reset и Cancel. Прежде, чем рассказать о них подробнее, приведем уже встречавшиеся обработчики событий. Наша форма одинаково реагирует на два события Initialize и Activate. Первое из них возникает, когда форма открывается после того, как она была выгружена. Если же форма удаляется с экрана методом Hide, то при ее повторном открытии событие Initialize не возникает. Поскольку мы хотим, чтобы и в этом случае происходила инициализация свойств формы, то определен и обработчик события Activate, выполняющий те же действия. Поскольку оба обработчика эквивалентны, то приведем текст только одного из них:

    Private Sub UserForm_Activate() With frmInit .Caption = "Окно после инициализации" ' заголовок диалогового окна .CurrDate.Caption = "Сегодня " & Format(Date, "dd/mm/yy") 'метка - текущая дата .MyText.Text = "Любимый цвет:" 'начальный текст в редакторе .chkGood.Value = True 'включение флажка .chkGood.Caption = " Хороший день" ' заголовок флажка With.lstColors 'задание элементов списка: .AddItem "белый" .AddItem "черный" .AddItem "синий" .AddItem "красный" .AddItem "зеленый" .AddItem "желтый" .AddItem "голубой" .ListIndex = 4 'выбор 5-го элемента в списке ("зеленый") End With End With

    End Sub

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

    Private Sub CommandButton1_Click() ChangeForm End Sub Public Sub ChangeForm() With Me .MyText.Text = "Нелюбимый цвет:" 'новый текст .lstColors.ListIndex = 1 'черный .chkGood.Value = False 'отключение флажка End With


    End Sub

    Нам осталось рассмотреть действие новых командных кнопок. Обработчик события Click кнопки Save позволяет сохранить текущее состояние элементов управления формы. Затем это состояние может быть проанализировано, например, для того, чтобы выдать психологический портрет пользователя. Цель сохранения может быть и другая, ѕ иметь возможность вернуться к текущему состоянию при повторном открытии формы. С чисто программистской точки зрения задача сохранения состояния решается просто, главный вопрос это, где и в какой форме хранить состояние элементов управления. В данном случае мы выбрали ячейки листа Excel. Вот текст этого обработчика:

    Private Sub cmdSave_Click() 'Сохранение данных формы в ячейках листа SavedData книги BookOne12 Dim Beg As Range Set Beg = Range("[BookOne12.xls]SavedData!Данные") With frmInit Beg.Offset(1, 0) =.CurrDate.Caption 'дата Beg.Offset(1, 1) =.lstColors.ListIndex 'индекс в списке Beg.Offset(1, 2) =.lstColors.Value 'цвет Beg.Offset(1, 3) =.chkGood.Value 'состояние флажка Beg.Offset(1, 4) =.MyText.Text 'текст в окне ввода 'Форма прячется .Hide End With

    End Sub

    Обратите внимание, при работе с ячейками Excel используется техника, основанная на смещении Offset. Вначале фиксируется начальная ячейка, имеющая имя "Данные", и уже относительно ее выполняются действия над остальными ячейками.

    Командная кнопка Reset позволяет восстановить сохраненное состояние элементов управления формы. Обработчик события Click для этой кнопки выполняет действия, обратные тем, что делаются при сохранении состояния. Вот текст этого обработчика:

    Private Sub cmdReset_Click() 'Восстановление данных формы из ячеек листа SavedData книги BookOne12 Dim Beg As Range Set Beg = Range("[BookOne12.xls]SavedData!Данные") With frmInit .CurrDate.Caption = Beg.Offset(1, 0) .lstColors.ListIndex = Beg.Offset(1, 1) .lstColors.Value = Beg.Offset(1, 2) .chkGood.Value = Beg.Offset(1, 3) .MyText.Text = Beg.Offset(1, 4) End With End Sub

    Обработчик события Click для кнопки Cancel просто закрывает форму без сохранения состояния элементов:

    Private Sub cmdCancel_Click() frmInit.Hide End Sub

    Общие сведения и применение

    Диалоговое окно представляет собой прямоугольную область экрана, где размещены элементы управления, с которыми работает пользователь. В Office 2000 масса встроенных элементов управления, позволяющих спроектировать и реализовать интерфейс типичной офисной системы. Если же встроенных элементов управления не хватает, Вы можете использовать в своем диалоговом окне произвольные ActiveX -объекты.
    Три приложения Office 2000 - Excel, Word и PowerPoint - содержат в редакторе VBA одинаковые средства создания диалоговых окон и программирования процедур обработки событий для них и для их управляющих элементов. В Access наряду с аналогичными средствами создания собственного диалогового окна, включены также мастера, помогающие проектировать специальные диалоговые окна, связанные с вводом и выводом информации, хранящейся в базе данных.
    Диалоговое окно и каждый элемент управления, размещенный в нем, - это объект определенного класса с предопределенным набором свойств, методов и событий. Начальные значения свойств диалогового окна и его отдельных элементов можно задать при проектировании окна. Затем при работе системы некоторые свойства могут меняться пользователем (например, текст в окне редактирования, состояния кнопок выбора и т. п.), а некоторые - из программы. Программные изменения происходят обычно в процедурах обработки событий, автоматически запускаемых при наступлении событий, связанных с диалоговым окном и его элементами.
    Чтобы диалоговое окно стало доступно пользователю, его нужно вывести на экран в ответ на некоторое действие пользователя или автоматически при некотором изменении состояния системы. Обычно диалоговые окна выводятся командами выпадающих или вспомогательных меню. Диалоговые окна могут также вызываться из других окон. Автоматически выводятся окна, в которых система после завершения задания сообщает о полученных результатах, окна с информацией об обнаруженных ошибках и т. п.

    Окно ввода данных. Функция InputBox

    Окно, создаваемое функцией InputBox, предназначено для ввода строки и содержит однострочное окно редактирования, сообщение и кнопки OK и Cancel. При выборе кнопки OK (или нажатии клавиши Enter) строка, введенная пользователем в окне редактирования, передается в программу; щелчок кнопки Cancel (нажатие клавиши Esc) означает отказ от ввода - InputBox возвращает пустую строку.
    Синтаксис вызова функции InputBox таков:
    InputBox(prompt [,title ] [,default] [, xpos] [, ypos] [,helpfile, context])
    Здесь параметры prompt (сообщение), title (заголовок), helpfile и context имеют тот же смысл, что и для функции MsgBox. Параметр default является строкой, задающей подсказку, появляющуюся в текстовом окне редактирования. Если пользователь не введет новый текст, то эта подсказка и будет служить его ответом. Когда этот параметр не задан, то окно редактирования при открытии пусто. Числовое выражение xpos задает расстояние по горизонтали левой границы окна от левой границы экрана. Если его нет, окно центрируется по горизонтали. ypos - расстояние верхней границы окна от верхней границы экрана. Если его нет, окно центрируется по вертикали.
    В следующем примере в окно функции InputBox пользователь должен ввести дату рождения. Далее она проверяется. Если она в интервале от 01.01.1900 до 01.01.1985, запрашивается имя пользователя еще одним вызовом функции InputBox, иначе выдаются информационные сообщения с помощью функции MsgBox. Так как возвращаемое при первом вызове функции InputBox значение - строка, для его преобразования в дату применяется функция DateValue (для преобразования вводимых строк в числовые значения используется функция Val).
    Sub ExInput() Dim Msg As String, Title As String, Имя As String Dim Birthday As Date
    Msg = "Введите, пожалуйста, " & vbCrLf & "дату своего рождения." 'сообщение. Title = "Окно для ввода даты" 'заголовок. ' Вывод окна для ввода даты в позицию (1500,1000): Birthday = DateValue(InputBox(Msg, Title,, 1500, 1000))
    ' Проверка даты: If Birthday < "01/01/1900" Then MsgBox "Вы выглядите гораздо моложе!", vbExclamation ElseIf Birthday > "01/01/1983" Then MsgBox "Эта программа не для детей до 16!", vbExclamation Else ' Дата корректна.
    Запрос имени пользователя: Имя = InputBox("Здравствуйте! Как Вас зовут?", "Знакомство", _ "Владимир Биллиг",,, "My.hlp", 12) MsgBox Имя & ", рады Вам помочь.", vbExclamation, " Проверка ввода" End If End Sub

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

    Окно ввода данных. Функция InputBox
    Рис. 12.5.  Окно для ввода даты

    Окно ввода данных. Функция InputBox
    Рис. 12.6.  Окно для ввода имени

    Окно ввода данных. Функция InputBox
    Рис. 12.7.  Окно приветствия

    Перемещение фокуса на элемент управления

    Только один из элементов управления формы находится в фокусе. Это означает, что этот элемент может принимать данные ввода, например, при нажатии клавиш клавиатуры или щелчки мыши. Пользователь, работающий с формой, может перемещать фокус по элементам управления в прямом направлении нажатием клавиши Tab, а в обратном - комбинацией Shift+Tab. Порядок перемещения задается на стадии проектирования окна. Программно можно установить фокус, используя методSetFocus (Поместить в фокус) элемента, который должен оказаться в фокусе.
    Рассмотрим форму, в которой есть поле ввода и командная кнопка. Нормальные действия пользователя предполагают, что он вводит информацию в поле ввода, а затем щелкает по командной кнопке. В этом случае обработчик события Click передает информацию из поля ввода переменной программе. Если же пользователь щелкнул по кнопке, забыв ввести информацию, то следует выдать предупреждающее сообщение, например, звуковой сигнал и вернуть фокус полю ввода. Вот такой сценарий и был реализован. Не будем приводить детали проектирования формы и все обработчики событий. Ограничимся только одним из них -обработчиком события щелчка командной кнопки:
    Private Sub CommandButton1_Click() With Me If.TextBox1.Text = "" Then Beep: Beep: Beep .TextBox1.SetFocus Else GlobeVar =.TextBox1.Text MsgBox GlobeVar .Hide End If End With End Sub
    Вот как выглядит эта простая форма ввода в процессе работы с ней:
    Перемещение фокуса на элемент управления
    Рис. 12.18.  Простая форма ввода

    Dim Msg As String, MyString

    Public Sub Mes2() Dim Msg As String, MyString As String Dim Btns As Integer Dim Title As String, Help As String Dim NmbCont As Integer, Result As Integer
    Title = " Вы ввели неверные данные !" ' заголовок окна. Msg = "Будем продолжать работу ?" ' сообщение. Btns = vbYesNo + vbCritical + vbDefaultButton2 ' кнопки и свойства Help = "ERRORS.HLP" ' имя файла со справкой NmbCont = 200 ' номер темы ' Вывод сообщения. Result = MsgBox(Msg, Btns, Title, Help, NmbCont) If Result = vbYes Then ' пользователь нажал кнопку Yes. MyString = "Да" ' действия по продолжению работы '... Else ' пользователь нажал кнопку No. MyString = "Нет" ' действия по прекращению работы '... End If
    End Sub
    Пример 12.1.
    Закрыть окно




    Private Sub TextBox1_Exit(ByVal Cancel As MSForms.ReturnBoolean) Dim Msg As String Const MinTemp = 34 Const MaxTemp = 42 Msg = "Ошибка ввода:" & vbCrLf 'Проверка, являются ли данные числовыми If Not IsNumeric(TextBox1.Text) Then Msg = Msg & "Введите числовые данные в формате: ##.#" MsgBox Msg Cancel = True 'Оставляет фокус в поле ввода
    ElseIf TextBox1.Text < MinTemp Then Msg = Msg & "Температура слишком низкая" & vbCrLf _ & "Проверьте, что с Вашим больным!" MsgBox Msg Cancel = True 'Оставляет фокус в поле ввода ElseIf TextBox1.Text > MaxTemp Then Msg = Msg & "Температура слишком высокая" & vbCrLf _ & "Проверьте, что с Вашим больным!" MsgBox Msg Cancel = True 'Оставляет фокус в поле ввода Else GlobeVar = TextBox1.Text Debug.Print GlobeVar End If
    End Sub
    Пример 12.2.
    Закрыть окно



    Пример создания диалогового окна

    Давайте создадим диалоговое окно "Цветная форма", в котором будут расположены изображение (Image), поле ввода (TextBox) и командная кнопка (CommandButton).
  • В меню Вставка выберите команду UserForm для создания нового диалогового окна.
  • Щелкнув правой кнопкой появившееся окно UserForm1, выберите в контекстном меню команду Свойства. Введите новое имя ColorForm1 в правом столбце строки со свойством (Name), в качестве значения свойства Caption введите заголовок окна: "Цветная форма". Щелкните правый столбец в строке со свойством BackColor (Цвет фона), затем - появившийся справа значок списка. В таблице появившегося окна щелчком выберите понравившийся цвет. Окно "перекрасится". Закройте окно свойств.
  • Перетащите с панели инструментов на окно элемент Image (изображение). Щелкните его правой кнопкой и снова выберите команду Свойства. В появившемся окне со списком свойств отыщите строку со свойством Picture (Рисунок). Чтобы перейти к поиску нужного файла с картинкой, щелкните кнопку с многоточием (…) справа в этой строке. В диалоговом окне Загрузка рисунка (Load Picture) выберите файл с симпатичной картинкой и щелкните кнопку OK.
  • Перетащите на диалоговое окно элемент управления поле ввода (TextBox). Как и в п. 3, щелкнув его правой кнопкой, вызовите окно Свойства. Введите в качестве значения свойства Value название выбранной вами картинки - оно появится в окне редактора. Затем выберите свойство Font Справа появится кнопка (…). Щелкните ее и в появившемся списке шрифтов выберите шрифт для редактора.
  • Перетащите на диалоговое окно командную кнопку (CommandButton). Вызовите окно Свойства для этой кнопки, и измените в нем значение свойства Caption (Заголовок) на "Нажми меня" - текст станет именем кнопки. Измените имя кнопки (Name) на cmdClickMe. По этому имени к кнопке будут обращаться процедуры и методы в программе. Введите справку "Командная кнопка" как значение свойства ControlTipText - надпись будет появляться на экране под кнопкой всякий раз, когда на ней окажется указатель. Установите для кнопки ключ быстрого выбора: найдите в списке свойств строку со свойством Accelerator (Ускоритель) и введите в поле справа букву "Н" - в имени кнопки первый символ "Н" будет подчеркнут и нажатие клавиш Alt+Н будет эквивалентно выбору кнопки. Установите также подходящий шрифт с помощью свойства Font, как и для поля ввода.
  • В меню Запуск выберите команду "Запуск подпрограммы/UserForm". На экране приложения появится спроектированное диалоговое окно.
    Пример создания диалогового окна
    Рис. 12.11.  Цветная форма
  • Щелкните кнопку Close на заголовке диалогового окна, чтобы выйти из него.


  • Проверка корректности данных

    Одна из важных функций диалоговых окон, - прием и передача данных, введенных пользователем, для хранения и дальнейшей обработки в систему. Основной вид элементов управления, предназначенных для ввода, - это окна редактирования (TextBox). Для предотвращения ошибок, связанных с неверным типом введенных данных или нарушением некоторых условий, которым эти данные должны удовлетворять следует проверять их корректность до выхода из диалогового окна. Чуть выше мы приводили пример работы с простой формой ввода, где проверялась ситуация, когда пользователь "забыл" ввести данные в окно ввода. Сейчас мы рассмотрим ситуацию, в которой пользователь пытается ввести некорректные данные в поле ввода. Возникает вопрос, где следует проводить проверку на корректность данных? По-видимому, одним из лучших мест является обработчик события Exit, которым обладает элемент управления TextBox. Обработчик этого события вызывается при попытке выхода из окна редактирования. Здесь и следует проводить проверку введенных данных, чтобы не допустить выхода, если данные заданы некорректно.
    Продолжим работу с простой формой ввода и предположим, что в поле ввода следует ввести температуру больного человека, значение, которой, естественно, ограничено сверху и снизу. Вот как выглядит обработчик события Exit:
    Пример 12.2.
    (html, txt)
    Событие Exit имеет параметр Cancel, значение которого следует установить в обработчике события. Если параметру присвоить значение True, то фокус остается на текущем элементе, в противном случае он переходит к элементу, следующему согласно Tab - порядка. Вот как выглядит форма при попытке задания некорректного значения температуры:
    Проверка корректности данных
    Рис. 12.21.  Проверка корректности ввода данных

    Разработка процедур, обрабатывающих события диалогового окна и его устройств

    Процедуры, обрабатывающие события, связанные с диалоговыми окнами и элементами управления, - это частный случай процедур, обрабатывающих события объектов Office 2000. С диалоговым окном (формой) и с элементом управления каждого типа связан заранее определенный набор событий, возникающих для этого объекта по инициативе пользователя или системы. Например, одно из связанных с формой событий Initialize (Инициализация) возникает при загрузке формы, а связанное с командной кнопкой (CommandButton) событие Click возникает, когда пользователь щелкает эту кнопку (или нажимает клавишу Enter, если эта кнопка определена как нажимаемая по умолчанию). Процедуры обработки событий позволяют описать поведение диалогового окна или отдельного элемента управления при наступлении того или иного события.
    Чтобы перейти в окно редактора для ввода текста процедуры обработки события, щелкните дважды объект, к которому оно относится (элемент управления или свободную от элементов часть формы). Появится окно Code (Код). В раскрывающемся списке слева вверху перечислены все объекты формы; при выборе объекта в этом списке справа вверху появляются события, связанные с данным объектом. Ниже на рис. 12.10 показан список событий для командной кнопки.
    События, для обработки которых процедуры уже написаны, выделены полужирным начертанием. Чтобы создать новую процедуру или отредактировать имеющуюся, нужно щелкнуть в списке соответствующее событие. На экране появится шаблон новой процедуры или текст написанной. Имя такой процедуры состоит из имени устройства или формы, за которым после подчеркивания идет имя события. Так, процедура, обрабатывающая событие Click для командной кнопки CommandButton1, называется CommandButton1_Click. Из-за такого способа именования процедур будьте осторожны при переименовании элементов управления. Если Вы сначала определили для элемента процедуры обработки событий, а затем переименовали его (изменив свойство Name), никаких процедур обработки событий у элемента с новым именем не будет (их придется перенести с помощью редактора).

    Разработка процедур, обрабатывающих события диалогового окна и его устройств
    Рис. 12.12.  События элемента CommandButton

    Взгляните на процедуру обработки события Click для командной кнопки cmdClickMe диалогового окна "Цветная форма":

    Private Sub cmdClickMe_Click() MsgBox "Поздравляю !" ColorForm1.TextBox1.Value = "Симпатичный, правда?"

    End Sub

    Эта процедура автоматически вызывается при выборе кнопки "Нажми меня" и выводит на экран поздравление, а затем заменяет название картинки в окне редактирования.

    Количество свойств, методов и событий, связанных с диалоговыми окнами и элементами управления чрезвычайно велико. Более подробные сведения о них приведены при описании соответствующих объектов в следующей лекции. В этом разделе мы рассмотрим несколько типичных проблем, возникающих при проектировании процедур обработки событий.

    Создание пользовательских диалоговых окон

    Новые пользовательские диалоговые окна проектируются в редакторе VBA. В процессе создания диалогового окна вначале определяется его видимое изображение на экране (форма с управляющими элементами), затем задаются их свойства и разрабатываются процедуры, обрабатывающие события, связанные с формой и ее элементами управления.
    Для создания новой формы нужно в меню редактора VBA выбрать выпадающее меню Вставка (Insert), а в нем - команду UserForm. На экране появится новое окно с пустой формой и расположенной рядом панелью элементов (Toolbox).
    Щелкните это окно правой кнопкой мыши и выберите в появившемся контекстном меню команду Свойства (Properties). На экране появится окно Свойства-UserForm со списком свойств нового окна. Если выбрать вкладку По категориям, будет показан список свойств, сгруппированных по категориям. В частности, в категории Вид можно задать цвета окна и его границ и установить нужный заголовок окна в свойстве Caption. Свойства из других групп позволяют установить шрифт, используемый в окне, положение и размер окна и др. Чтобы подробнее узнать о свойстве, щелкните его и нажмите клавишу F1.
    Создание пользовательских диалоговых окон
    Рис. 12.8.  Заготовка диалогового окна
    Затем нужно расположить в окне управляющие элементы (controls). Они выбираются на панели элементов Toolbox Если ее нет на экране, выберите команду Панель элементов (Toolbox) в меню Вид или щелкните ее пиктограмму на панели инструментов VBA. На панели элементов управляющие элементы представлены пиктограммами. Вы увидите имя элемента, установив указатель мыши на его пиктограмме.
    Перетащите нужный элемент с панели элементов на предназначенное место в окно формы. Когда вы отпустите кнопку мыши, вокруг элемента появится прямоугольная рамка с выделенными точками, за которые ее можно перемещать по экрану, чтобы точнее расположить элемент управления в диалоговом окне и установить его размеры. Уточнить положения элемента в окне можно командами меню Формат (Format). Порядок обхода управляющих элементов формы устанавливает команда Последовательность перехода (TabOrder)из меню Вид или контекстного меню формы.
    Чтобы данный управляющий элемент никогда не попадал в фокус, щелкните его правой кнопкой мыши, выберите в появившемся контекстном меню команду Свойства, а затем в появившемся списке установите для свойства TabStop значение False. Отметим, что для текстовых надписей (элементов вида Label) это значение устанавливается по умолчанию. В окне "Свойства-элемента" можно установить при проектировании и другие свойства элемента управления. Списки свойств появляются в этом окне на вкладках По алфавиту (Alphabetic) и По категориям (Categorized) на первой - в алфавитном порядке, на второй - сгруппированы по категориям. Устанавливать значение свойства можно на любой из них.

    Система автоматически именует новые элементы управления, образуя имя из типа элемента и его порядкового номера среди элементов данного типа. Например, у второй командной кнопки по умолчанию будет имя CommandButton2. Элементу можно присвоить новое имя, установив его в качестве значения свойства (Name). Помните, что имя идентифицирует объект (элемент управления или форму) в программе, поэтому изменение имени должно повлечь изменения в текстах процедур, его использующих. Не надо путать имя объекта (Name) со свойством Caption (заголовок, надпись), имеющимся у многих элементов управления. По умолчанию при создании элемента система присваивает этому свойству то же значение, что и имени Name. Изменение свойства Caption отражается на "внешности" элемента (надписи на экране), а на доступ к объекту из программы не влияет.

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

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

    Создание страниц и вкладок в диалоговых окнах

    В одном диалоговом окне можно разместить несколько страниц и вкладок со своими управляющими элементами. Страницы и вкладки стали непременными элементами интерфейса систем, работающих с Windows. Отметим, что названия этих элементов в Office 2000 отличаются от принятых, например, в Windows 95: элемент MultiPage (набор страниц) позволяет создавать страницы с разными элементами управления (традиционно они назывались вкладками), а новый элемент TabStrip (полоса вкладок) создает "вкладки" с одинаковыми наборами элементов управления. Чтобы создать новый набор страниц, нужно перетащить на форму элемент MultiPage. Вначале будут созданы две страницы, которым можно дать нужные заголовки, сделав их значениями свойства Caption в списке свойств. На следующем рисунке эти страницы получили имена "Ссуды" и "Ренты".
    Создание страниц и вкладок в диалоговых окнах
    Рис. 12.9.  Создание страниц
    Для того, чтобы добавить следующую страницу, щелкните правой кнопкой мыши правее имени последней страницы и в появившемся контекстном меню выберите команду Создать страницу (New Page). Это меню позволяет также переименовывать, перемещать и удалять страницы. Повторив операцию создания страницы несколько раз, можно разместить в окне необходимое число страниц. Затем на каждую из них нужно перетащить элементы управления.
    Аналогично создаются вкладки элементом TabStrip. Отличие в том, что каждый помещаемый на одну вкладку элемент управления одновременно попадает на все вкладки набора.

    Управление доступом к элементу

    Все элементы управления обладают свойством Enabled (Доступен), позволяющим управлять их доступностью. В результате действий пользователя или системы доступный элемент может стать активным - получить фокус клавиатуры. Если свойству Enabled присвоить False, элемент управления становится недоступен ("сереет") и не может попасть в фокус. Управление доступностью производится динамически и зависит от задач, решаемых в диалоговом окне. Типичный случай - запрет на доступ к окну поля ввода (TextBox) после того, как система поместила в него данные, которые не должны далее изменяться пользователем (например, текст справки, которую можно только прочесть и распечатать). Другой случай - отключение/включение доступа к группам переключателей или кнопок зависимого выбора (OptionButton) или отдельным таким кнопкам в зависимости от состояния флажка (CheckBox). При этом действия по изменению доступа надо проводить в процедуре, обрабатывающей событие Change (Изменение), автоматически вызываемой в тот момент, когда флажок меняет свое состояние, т. е. становится неотмеченным или отмеченным.
    Выполните следующие действия:
  • Создайте новое диалоговое окно (UserForm) и включите в него флажок (CheckBox), управляющий элемент Frame (Рамка), и командную кнопку (ComandButton). Поместите в рамку еще 4 флажка (CheckBox). Заголовки управляющих элементов (значения свойства Caption ) задайте как на рисунке:
    Управление доступом к элементу
    Рис. 12.15.  Окно "Пример управления доступом"
  • Дважды щелкните верхний флажок, чтобы вывести окно с кодом процедур, выберите в списке объектов слева вверху CheckBox1 (это имя присвоила флажку система), затем в списке процедур обработки событий справа вверху щелкните событие Change. Дополните появившуюся заготовку процедуры CheckBox1_Change:
    Private Sub CheckBox1_Change() With Me If.CheckBox1.Value Then 'отключение опций .CheckBox4.Enabled = False .CheckBox5.Enabled = False Else 'включение опций .CheckBox4.Enabled = True .CheckBox5.Enabled = True End If End With
    End Sub

  • Создайте стандартный модуль и напишите в нем две процедуры:

    Public Sub CheckOptions() ' Проверка на дорогах Dim Msg As String Msg = "Выполняю проверку на " With frmChecking

    If.CheckBox2.Value Then Msg = Msg & vbCrLf & "корректность дат" If.CheckBox3.Value Then Msg = Msg & vbCrLf & "согласованность данных" If.CheckBox4.Value Then Msg = Msg & vbCrLf & "правильность расходов" If.CheckBox5.Value Then Msg = Msg & vbCrLf & "правильность доходов" MsgBox (Msg) .Hide End With End Sub

    Public Sub SetOptions() frmChecking.Show End Sub

    Процедура SetOptions открывает нашу форму, предоставляя пользователю возможность включать или выключать те или иные флажки. Установив нужные значения, он может затем щелкнуть кнопку "Проверка". Обработчик этого события будет вызывать функцию CheckOptions, которая и производит проверку состояния каждого из четырех флажков группы. Заметьте, что проверка свойства Value не зависит от состояния свойства Enabled. Если даже флажок недоступен, но включен (Value = True), то появится сообщение о выполнении проверки. Недоступный флажок не позволяет пользователю изменить его состояние, но программно ѕ все в Ваших руках. Сделаем еще одно замечание. Реально, верхний флажок, управляющий проверкой, должен находиться в другой форме, более высокого уровня, но мы уже не стали усложнять задачу.
  • Напишите обработчик события командной кнопки:

    Private Sub CommandButton1_Click() CheckOptions End Sub

  • Теперь, если выполнить процедуру SetOptions, (по правилам, ее следовало бы вызывать в одном из обработчиков событий), то на экране появится форма. В ней можно установить флажки, например, следующим образом:

    Управление доступом к элементу
    Рис. 12.16.  Окно проверки в процессе работы

  • Если в этом состоянии окна щелкнуть командную кнопку "Проверка", то будет выведено сообщение о двух выполняемых проверках:

    Управление доступом к элементу
    Рис. 12.17.  Окно сообщений о выполняемых проверках



  • Установка начальных значений свойств элементов управления

    Начальное значение (или значение по умолчанию) свойства элемента управления можно установить в процедуре, обрабатывающей событие Initialize (Инициализация) диалогового окна, содержащего данный элемент. Это событие возникает всякий раз при выводе диалогового окна на экран и запускает на выполнение процедуру без аргументов с именем ИмяОкна_Initialize. Поэтому установленные в ней значения свойств всегда будут начальными. Это не значит, что они должны быть постоянными. Значения могут зависеть от текущей даты, данных в ячейках рабочего листа Excel, некоторых записей в базе данных Access и т. п.
    Давайте создадим диалоговое окно с процедурой инициализации.
  • Создайте новое диалоговое окно и разместите в нем статический текст Label (Метка), поле ввода TextBox, окно списка ListBox и кнопку независимого выбора (флажок) CheckBox.
  • В окне Свойства, измените их имена (свойство Name): Label - на CurrDate, TextBox - на MyText, ListBox - на lstColors, CheckBox - на chkGood, UserForm - на frmInit. Эти изменения внутренних имен элементов и окна не скажутся на его изображении в редакторе.
    Установка начальных значений свойств элементов управления
    Рис. 12.13.  Окно до инициализации
  • Дважды щелкните диалоговое окно, чтобы вызвать окно с кодом (Code). Установите в списке объектов слева вверху UserForm (по умолчанию этот объект и будет выбран при вызове окна кода) и выберите команду Initialize в списке процедур справа вверху. На экране появится шаблон процедуры инициализации диалогового окна UserForm_Initialize. Введите текст следующей процедуры.
    Private Sub UserForm_Initialize() With frmInit .Caption = "Окно после инициализации" ' заголовок диалогового окна .CurrDate.Caption = "Сегодня " & Format(Date, "dd/mm/yy") 'метка - текущая дата .MyText.Text = "Любимый цвет:" 'начальный текст в редакторе .chkGood.Value = True 'включение флажка .chkGood.Caption = " Хороший день" ' заголовок флажка With.lstColors 'задание элементов списка: .AddItem "белый" .AddItem "черный" .AddItem "синий" .AddItem "красный" .AddItem "зеленый" .AddItem "желтый" .AddItem "голубой" .ListIndex = 4 'выбор 5-го элемента в списке ("зеленый") End With End With
    End Sub
    Нумерация элементов массивов и коллекций, связанных с формами, начинается с 0. Поэтому, чтобы выделить при выводе окна пятый элемент в списке цветов, свойству ListIndex присвоено значение 4.
  • Запустите диалоговое окно в отладчике. На экране оно будет выглядеть так:
    Установка начальных значений свойств элементов управления
    Рис. 12.14.  Окно после инициализации
    Щелкнув кнопку Close (Закрыть) на строке заголовка окна, возвратитесь в режим проектирования.


  • Вывод сообщений. Функция MsgBox

    Рассмотрим сейчас встроенные диалоговые окна, которые появляются при вызове некоторых функций VBA. И начнем наше рассмотрение с функции MsgBox, предназначенной для вывода сообщений. Диалоговое окно, создаваемое функцией MsgBox, служит для вывода на экран сообщения программы и получения от пользователя простой реакции на это сообщение в виде щелчка одной из кнопок окна. В простейшем случае эта функция вызывается как процедура. Например, вызов:
    MsgBox "Сегодня на календаре" & Date
    выведет окно:
    Вывод сообщений. Функция MsgBox
    Рис. 12.3.  Пример окна сообщений
    После щелчка кнопки OK вызов функции завершится, и выполнение программы возобновится с оператора, стоящего непосредственно за этим вызовом.
    Диалоговое окно, выводимое функцией MsgBox, можно обогатить несколькими кнопками, чтобы пользователь мог указать с их помощью направление дальнейшего вычисления, а также присоединить к нему контекстную справку. В общем случае вызов MsgBox имеет вид:
    MsgBox(prompt[, buttons] [, title] [, helpfile, context])
    Здесь параметр prompt (сообщение) - строковое выражение, значение которого выводится в окне. Его максимальная длина не должна превышать 1024 символов. Разбиение на строки в этом выражении можно производить, используя символ возврата каретки (Chr(13)), символ перевода строки (Chr(10)), их комбинацию (Chr(13) & Chr(10)) или константу vbCrLf. Числовой параметр buttons (кнопки) задает виды командных кнопок, помещаемых в окне, кнопку, выбираемую по умолчанию, и модальность диалогового окна. Он получается как сумма кодов соответствующих кнопок и свойств. Явно не заданный, этот параметр считается равным 0. Строковое выражение title (заголовок) задает заголовок окна. Если его нет, заголовком становится имя приложения (см. пример, приведенный выше, где мы использовали Word). Параметры helpfile (файл справки) и context (контекст) должны присутствовать (или отсутствовать) вместе. Первый из них - имя файла, содержащего справку, которая будет выведена при нажатии клавиши F1, а второй - числовое выражение, задающее номер темы со справкой в этом файле.

    Вот коды кнопок и других свойств окна функции MsgBox:
    Имя константыКодОписание
    Коды наборов командных кнопок
    vbOKOnly 0только кнопка OK (Готово)
    VbOKCancel 1кнопки OK и Cancel (Отказ)
    VbAbortRetryIgnore 2кнопки Abort (Прервать), Retry (Продолжить) и Ignore (Игнорировать)
    VbYesNoCancel 3кнопки Yes (Да), No (Нет) и Cancel
    VbYesNo 4кнопки Yes и No
    VbRetryCancel 5кнопки Retry и Cancel
    Коды пиктограмм
    VbCritical 16важное сообщение (крестик в круге)
    VbQuestion 32запрос (вопросительный знак)
    VbExclamation 48предупреждение (восклицательный знак)
    VbInformation 64информационное сообщение (i в круге)
    Кнопка, выбранная по умолчанию
    VbDefaultButton1 0первая кнопка
    VbDefaultButton2 256вторая кнопка
    VbDefaultButton3 512третья кнопка
    VbDefaultButton4 768четвертая кнопка
    Модальность диалога
    VbApplicationModal 0модален относительно приложения
    VbSystemModal 4096модален относительно системы

    Диалоговое окно модально относительно приложения, если для продолжения работы в текущем приложении пользователь должен закончить работу в этом окне (ответить на сообщение). Модальность относительно системы означает, что все приложения будут приостановлены до завершения работы в данном окне.
    Может показаться странным, что константа VbDefaultButton4 указывает на четвертую кнопку, хотя максимальное количество кнопок, определяемых кодами от 0 до 5, - три. Дело в том, что некоторые приложения (например, Excel) при наличии параметров справка и контекст могут автоматически добавить в окно кнопку справки Help, вызывающую окно справки.
    Результирующее значение параметра buttons получается как сумма кодов по одному из каждой группы. Например, значение 36 = 4 + 32 = VbYesNo + VbQuestion означает, что в окне будет пиктограмма с вопросительным знаком и две кнопки Yes и No, причем первая из них будет считаться выбранной, если пользователь ее щелкнет или нажмет клавишу Enter.
    Как узнать, какую кнопку выбрал пользователь? Присвоить значение функции MsgBox некоторой целочисленной переменной и проверить его.


    Возвращаемые MsgBox значения приведены в таблице:
    Имя константыЗначениеНажатая кнопка
    vbOK 1OK
    vbCancel 2Cancel
    vbAbort 3Abort
    vbRetry 4Retry
    vbIgnore 5Ignore
    vbYes 6Yes
    vbNo 7No

    Если в окне есть кнопка Cancel, нажатие клавиши Esc завершает работу в окне и возвращается тот же код vbCancel, что и при выборе кнопки Cancel.
    В следующем примере создается окно с сообщением об ошибке и вопросом о продолжении вычисления. В нем пиктограмма важного сообщения и кнопки Yes и No, причем вторая - кнопка, выбираемая по умолчанию. Файл со справкой называется ERRORS.HLP, а номер контекстной справки в этом файле равен 200.
    Пример 12.1.
    (html, txt)
    При этом вызове на экране появится окно:
    Вывод сообщений. Функция MsgBox
    Рис. 12.4.  Сообщение о неверных данных

    Вызов собственного диалогового окна

    Диалоговое окно нуждается в отладке. В редакторе VBA Вы видите его изображение, но управляющие элементы находятся в "замороженном" состоянии, и их реакция на действия пользователя - это реакция редактора, а не самих элементов. Диалоговое окно запускается на исполнение командой запуска процедуры или формы Запуск подпрограммы/UserForm меню Запуск редактора или нажатием клавиши F5. - Ваше диалоговое окно появится в рабочем состоянии на основной панели того приложения Office 2000, в котором оно создано.
    А вызывается диалоговое окно в прикладной системе методом Show (Показать). Он позволяет выводить окно на экран в качестве реакции на выбор соответствующей команды меню или командной кнопки в другом окне и т. п. Например, диалоговое окно MyForm выводится так:
    MyForm.Show

    Закрытие диалогового окна

    Диалоговые окна в Office 2000 всегда работают в режиме модального диалога. Это значит, что пользователь должен полностью завершить работу в окне и закрыть его, прежде чем сможет перейти к действиям, не связанным с работой в этом диалоговом окне. Заметим, что немодальный диалог в Office 2000 также можно организовать, но для этого потребуется использовать объект Assistant с его баллончиками ѕ объектами Balloon. Для завершения работы с диалоговым окном применяется оператор UnLoad или метод Hide, вызываемые в обработчиках события, завершающих работу в диалоговом окне. Обычно в диалоговое окно включается одна или несколько кнопок, выбор которых приводит к завершению работы в нем ("Отказ", "Выход", "Сохранить", "Готово", "Конец работы" и т. п.).
    Ранее мы уже говорили о разнице между UnLoad и Hide, первый ѕ выгружает форму, второй только прячет ее. Есть и синтаксическая разница, Hide - это метод формы, в то время как UnLoad и Load - это встроенные функции, которым в качестве параметра передается имя формы. Мы обращали также внимание и на то, что при открытии спрятанной формы не возникает событие Initialize. В заключение, скажем, что в Office 97 были проблемы с методом Hide, поэтому я всегда пользовался только методом UnLoad, в Office 2000 метод Hide работает должным образом.

    Основы офисного программирования и язык VBA

    CheckBox - флажок (кнопка выбора)

    Флажок, или кнопка независимого выбора, позволяет пользователю указать, выбирает или не выбирает он опцию (обычно ее название является также названием флажка, т. е. значением свойства Caption). В зависимости от значения свойства TripleState у флажка может быть два или три состояния. По умолчанию значение этого свойства False, что соответствует двум значениям состояния флажка: True - флажок выбран, включен и False - флажок не выбран, выключен. Если для свойства TripleState установить True, у флажка появится также нейтральное состояние Null. Состояния флажка передаются в программу через свойство Value (Значение). Вот, например, возможные состояния флажка "С оптимизацией":
    CheckBox - флажок (кнопка выбора)
    Рис. 13. 2.  Возможные состояния флажка
    Верхний флажок находится в невыбранном состоянии (False), средний - в выбранном состоянии (значение True) и нижний - в нейтральном состоянии (значение Null). Кроме того, прямоугольная рамка вокруг названия нижнего флажка показывает, что он в фокусе (помечен).

    ComboBox - комбинированный список

    Комбинированный список - это элемент управления, соединяющий поле ввода с кнопкой и раскрывающимся списком. Работая с ним, пользователь может ввести значение непосредственно в поле ввода либо выбрать одно из значений в списке. Список состоит из строк данных. Данные в строке могут располагаться в одном или нескольких столбцах. Рассмотрим основные свойства объекта ComboBox.
  • ListCount определяет, сколько элементов находится в списке. Свойство доступно только для чтения и изменяется автоматически вместе с добавлением (удалением) элементов в список.
  • ListRows задает число одновременно видимых строк списка.
  • ListIndex задает номер выбранной строки; возможные значения от -1 до ListCount - 1, т. е. значение ListCount всегда на 1 больше максимального значения свойства ListIndex, так как нумерация строк начинается с 0. Если ни один элемент в списке не выбран, ListCount равно 0, а значением ListIndex будет -1.
  • ColumnCount определяет число столбцов в выводимом на экран списке; если оно равно 0, столбцы не выводятся, при значении -1 выводятся все столбцы.
  • ColumnWidths задает ширину каждого столбца для списков с несколькими столбцами. Значение этого свойства - строка, в которой размеры столбцов перечислены через точку с запятой. Пустое значение или -1 означают, что ширина столбца вычисляется автоматически, 0 - отсутствие столбца, значения > 0 задают ширину столбца в точках. Можно также рядом с числовым значением указывать другую единицу измерения. По умолчанию ширина столбца не меньше 1 дюйма (72 точек). Например, если значение ColumnCount (число столбцов) равно 4, а ширина всего окна списка 12 см, значение ColumnWidth вида "56; 112; 56; 112" устанавливает ширину первого и третьего столбцов - 56 точек (2 см), а второго и четвертого - 112 точек (4 см), то же разбиение можно получить, задав "2 cm; 4 cm; 2 cm; 4 cm". Если же задать "4 cm; 0;;8 cm", второго столбца на экране не будет, третий будет иметь минимальную вычисляемую ширину - 1 дюйм, а в четвертом будет видна лишь левая часть шириной около 5. 5 см (при этом появится горизонтальная полоса прокрутки).
    Отсутствие значения ( пустая строка) означает, что все столбцы имеют одинаковую ширину.
  • TextColumn задает номер столбца, видного пользователю. Нумерация столбцов начинается с 1. 0 означает, что будет виден только выбранный элемент из строки ListIndex. При значении 1 выводится первый столбец, чья ширина, установленная свойством ColumnWidths, больше 0.
  • BoundColumn указывает столбец со значением данных (свойством Value) в списке со многими столбцами. Если свойство равно 0, значением свойства Value будет номер ListIndex выбранной пользователем на экране строки. Если BoundColumn > 0, значение свойства Value берется из указанного столбца. Таким образом, пользователь может выбрать на экране один элемент, а в качестве значения этого выбора в программе можно задать другой. Например, список может содержать названия месяцев в первом столбце и их номера - во втором. Установив TextColumn = 1, а BoundColumn = 2, мы получим элемент, в котором пользователь выбирает месяц по названию, а программа получает в качестве значения объекта номер выбранного месяца.
  • Задать элементы списка можно программно, используя метод AddItem (например, в процедуре инициализации диалогового окна), либо установив при проектировании свойство RowSource. Его значение - строка, задающая диапазон ячеек Excel, из которых будут браться элементы списка. Например, следующие операторы задают список с 4 столбцами для элемента ComboBox1. Элементы этого списка берутся из ячеек b1:e6 текущего рабочего листа Excel:

    ComboBox1. ColumnCount = 4 ComboBox1. RowSource = "b1:e6"

  • List - двухмерный массив с элементами списка. Обращение к нему:

    объект. List (строка, столбец)

    а элементы этого массива имеют тип Variant. Нумерация строк и столбцов начинается с 0. Это свойство можно использовать для инициализации списка с несколькими столбцами в элементах управления ComboBox и ListBox. Допустим, на уровне модуля определен двухмерный массив строк ListItems (4, 3) , содержащий 3 столбца значений, которые мы хотим предложить пользователю для выбора.


    Тогда этот массив можно связать с комбинированным списком CBox в процедуре инициализации диалогового окна.

    Private Sub UserForm_Initialize () СBox. ColumnCount = 3 ' задание числа столбцов списка CBox. List () = ListItems ' присвоение значения списку End Sub

  • ListStyle определяет внешний вид списка. Если оно равно fmListStylePlain = 0, элементы списка выводятся в обычном виде без кнопок слева. Если же его значение - fmListStyleOption = 1, слева от каждого элемента списка выводится кнопка. Для списков с единственным выбором это кнопка-переключатель (OptionButton), а для списков с множественным выбором - кнопка-флажок (CheckBox). При выборе элемента в соответствующей кнопке появляется метка.
  • MatchRequired и MatchEntry определяют поведение комбинированного списка при вводе пользователем данных в поле ввода. Если MatchRequired равно True, вводимый пользователем текст может стать значением элемента, лишь когда он совпадает с одним из элементов списка. По умолчанию ему присвоено False, что не требует от вводимых пользователем данных совпадения с элементами списка. Свойству MatchEntry по умолчанию устанавливается 1, означающее, что при вводе пользователем очередного символа в списке ищется первый элемент, для которого введенное слово является префиксом и выводится в качестве значения. Если MatchEntry равно 0, поиск происходит по первой букве слова, т. е. при повторном выборе одной и той же буквы в качестве значений перебираются все элементы списка, начинающиеся на нее. При MatchEntry равном 2 список не реагирует на набираемый пользователем текст. Этот текст доступен через свойство Text.


  • События: AfterUpdate, BeforeDragOver, BeforeDropOrPaste, BeforeUpdate, Change, Click, DblClick, DropButtonClick, Enter, Error, Exit, KeyDown, KeyUp, KeyPress, MouseDown, MouseUp, MouseMove.

    Методы: AddItem, Clear, Copy, Cut, DropDown, Move, Paste, RemoveItem, SetFocus, ZOrder.

    CommandButton - командная кнопка

    Командная кнопка запускает на выполнение действия системы, которые обычно задаются в процедуре обработки события Click (щелчок кнопки). Вот ее некоторые свойства.
  • Название кнопки - значение свойства Caption - можно установить при проектировании диалогового окна, а затем менять из программы.
  • Булево свойство Cancel определяет, является ли данная кнопка кнопкой отказа. Если свойство равно True, то нажать эту кнопку можно, щелкнув по ней мышью, либо нажав клавишу , либо нажав клавишу в тот момент, когда она находится в фокусе. По умолчанию задается False. Это свойство может быть установлено только для одной кнопки в окне (это отслеживается системой автоматически). Типичные действия, которые должны выполняться при выборе кнопки отказа, состоят в восстановлении состояний и значений элементов управления и связанных с ними данных, измененных пользователем во время работы в диалоговом окне. Их следует описать в процедуре обработки события Click для данной кнопки.
  • Булево свойство Default определяет, выбирается ли кнопка по умолчанию, если пользователь щелкает окно или нажимает клавишу Enter, когда фокус не находится на другой командной кнопке. По умолчанию свойству задается значение False.
  • Булево свойство TakeFocusOnClick определяет, переместится ли фокус на данную кнопку после выбора ее щелчком. Иногда для этого свойства удобно установить False, чтобы, щелкнув кнопку, выполнить действие над содержимым элемента, находящегося в фокусе.

  • События: BeforeDragOver, BeforeDropOrPaste, BeforeUpdate, Change, Click, DblClick, Enter, Error, Exit, KeyDown, KeyUp, KeyPress, MouseDown, MouseUp, MouseMove.
    Методы: Move, SetFocus, Zorder.

    Другие свойства

    Свойство TabIndex определяет порядковый номер элемента управления в обходе всех элементов с помощью клавиши Tab. Нумерация элементов начинается с 0.
    Свойство ControlSource связывает данные основного документа приложения с состоянием элемента управления (значением свойства Value). Значение этого свойства - строка, задающая ячейку рабочего листа, связанную с данным элементом. Например, чтобы задать в качестве значения этого свойства для флажка CheckBox1 ячейку С5, используется присвоение вида:
    CheckBox1. ControlSource = "C5"
    При этом изменения состояния элемента управления будут автоматически заноситься в эту ячейку, и наоборот, изменения в данных будут отражаться на состоянии элемента управления. Если текущее значение свойства Value для элемента равно Null, ячейка, связанная с ним не содержит никакого значения (пустая). По умолчанию значение ControlSource - пустая строка.
    Свойство Tag задает текстовый комментарий к объекту. Оно никак не связано с его другими свойствами и может быть использовано для хранения вспомогательной информации об элементе управления. Иногда свойство Tag служит альтернативой свойству Name для идентификации элемента в программе.
    Свойство Font определяет шрифт, используемый в изображении данного объекта. Его значение - объект класса Font, идентифицируемый по имени (свойству Name). По умолчанию шрифт элемента управления тот, что установлен для его контейнера.

    Frame - рамка (группы)

    Элемент управления Frame служит для явно видимого объединения в группу нескольких других элементов управления. Кнопки-переключатели (OptionButton), помещенные в рамку, автоматически являются взаимоисключающими, т. е. при выборе одной из них остальные сразу же отключаются (получают значение False). Поведение других элементов, помещенных в рамку, не меняется.
    Заголовок на верхней границе рамки задается свойством Caption. Как и для диалогового окна, для рамки можно задать рисунок, являющийся фоном (свойство Picture), определить область прокрутки и вид полосы прокрутки (свойства ScrollLeft, ScrollTop, ScrollHeight, ScrollWidth, ScrollBars). Свойство Zoom определяет коэффициент уменьшения или увеличения изображений всех элементов внутри рамки (измеряется в процентах и принимает значения от 10% до 400%).
    События: AddControl, BeforeDragOver, BeforeDropOrPaste, Click, DblClick, Enter, Error, Exit, KeyDown, KeyUp, KeyPress, Layout, MouseDown, MouseUp, MouseMove, RemoveControl, Scroll, Zoom.
    Методы: Copy, Cut, Move, Paste, RedoAction, Repaint, Scroll, SetDefaultTabOrder, UndoAction, ZOrder.

    Image - изображение

    Элемент Image - это прямоугольное графическое изображение в диалоговом окне. Его используют как для улучшения внешнего вида окна, так и для показа пользователю информации, представляемой в виде фотографий, рисунков, графиков, диаграмм. Рисунок в элементе Image можно масштабировать, изменять выводимую часть (если рисунок больше изображения на экране), можно динамически изменять размеры изображения, но рисунок нельзя редактировать в диалоговом окне.
    Какой именно рисунок или графический образ показывается на экране, определяет свойство Picture. Его значением может быть изображение в одном из графических форматов:. bmp,. cur,.gif,. ico,. jpg и. wmf. Задать значение этого свойства можно при проектировании окна из списка свойств элемента Image. Для этого, щелкнув кнопку (…) в строке со свойством Picture, вызовите программу поиска и загрузки графических файлов, найдите файл с нужным изображением и выберите кнопку Open. Чтобы вывести или изменить изображение программно, используется функция LoadPicture. Например, следующий оператор выводит изображение автомобиля в элемент Image1:
    Image1. Picture = LoadPicture ("c:\windows\car. bmp")
    Свойство PictureSizeMode определяет масштабирование рисунка. Если его значение fmPictureSizeModeClip = 0, части рисунка, выходящие за рамку изображения, обрезаются, а при fmPictureSizeModeStretch = 1 рисунок масштабируется так, чтобы занимать всю площадь изображения. При этом может произойти искажение (относительное растяжение или сжатие) по горизонтали или вертикали. При значении fmPictureSizeModeZoom = 3 рисунок масштабируется с сохранением своих относительных размеров так, что, либо его высота, либо его ширина совпадают с высотой или шириной изображения на экране. Ниже показано влияние свойства PictureSizeMode на изображение.
    Image - изображение
    увеличить изображение
    Рис. 13.3.  Варианты масштабирования изображений
    Расположение рисунка внутри рамки изображения задается свойством PictureAlignment. Возможные значения этого свойства перечислены в таблице.

    Имя константыЗначениеРисунок расположен
    fmPictureAligmentTopLeft 0в верхнем левом углу
    fmPictureAlignmentTopRight 1в верхнем правом углу
    fmPictureAlignmentCenter 2по центру
    fmPictureAlignmentBottomLeft 3в нижнем левом углу
    fmPictureAlignmentBottomRight 4в нижнем правом углу
    Если значение свойства PictureSizeMode равно fmSizeModeStretch, рисунок занимает всю площадь изображения независимо от значения PictureAlignment. В частности, на рисунке 13. 3 свойство PictureAlignment для всех трех изображений имеет значение 0 (fmPictureAligmentTopLeft).

    Если размер вставляемого рисунка меньше размера элемента, в который его помещают, всю поверхность изображения можно покрыть копиями рисунка. Для этого булеву свойству PictureTiling надо присвоить True (по умолчанию у него значение False).

    Каким образом располагаются копии рисунка на изображении, зависит от значений свойств PictureAlignment и PictureSizeMode. Например, если первое равно fmPictureAligmentTopLeft, первая копия рисунка разместится в левом верхнем углу, при fmPictureAlignmentCenter первая копия рисунка располагается в центре изображения, а от нее во все стороны распространяются остальные. Если свойство PictureSizeMode равно fmSizeModeStretch, все изображение занято одной копией рисунка, при fmPictureSizeModeZoom может появиться несколько копий, обрезанных по краям изображения. Ниже показано влияние этих параметров на покрытие изображения копиями рисунка автомобиля:

    Image - изображение
    увеличить изображение
    Рис. 13. 4.  Влияние параметров PictureAlignment и PictureSizeMode

    События: BeforeDragOver, BeforeDropOrPaste, Click, DblClick, Error, MouseDown, MouseUp, MouseMove.

    Методы: Move, Zorder.

    Имя объекта

    Свойство Name задает имя объекта (диалогового окна, элемента управления или шрифта). Это внутреннее имя, идентифицирующее объект в программе. Поэтому в одном проекте не должно быть двух диалоговых окон с одним именем, а в одном окне - двух одноименных разных элементов управления.
    Имя, получаемое объектом, созданным на этапе проектирования, нельзя изменить во время исполнения программы. Для динамически создаваемых объектов можно назначать имена, используя присвоение:
    класс_объекта. Name = Имя
    где класс_объекта - название класса объектов, а Имя - строка, задающая имя конкретного объекта из указанного класса.
    При создании объектов на этапе проектирования система назначает им по умолчанию имена вида:
    класс_объектаN
    где N - порядковый номер очередного объекта в классе. Для диалоговых окон это имена UserForm1, UserForm2…, для командных кнопок - CommandButton1, CommandButton2 и т. д. В списке свойств Свойства (Properties) назначенное системой имя можно изменить на произвольную строку, удовлетворяющую условиям на имена в данном приложении. При этом надо одновременно изменить имя данного объекта во всех тех ранее созданных процедурах, где оно было использовано (в частности, в процедурах обработки событий данного элемента управления или диалогового окна).

    Коллекция Controls

    Для доступа к набору элементов управления диалогового окна можно использовать коллекцию Controls, включающую все элементы управления окна. Каждый элемент управления имеет в этой коллекции индекс, значение которого может быть числом или строкой. Для первого элемента управления индекс равен 0. Числовые индексы определяются порядком размещения элементов в коллекции. Строковое значение индекса соответствует имени (Name) элемента.
    Например, следующий цикл позволяет скрыть элементы управления диалогового окна MyForm, которые в данный момент не нужны пользователю.
    Public Sub WorkWithForm2 () Dim myForm As New frmMy2 Dim Ctrl As Control Dim msgCode As Integer, Answer As Integer For Each Ctrl In myForm. Controls 'цикл по всем элементам управления msgCode = vbYesNo + vbQuestion 'Вопрос об очередном элементе управления: Answer = MsgBox (prompt:="Скрыть элемент " & Ctrl. Name, _ Buttons:=msgCode, Title:="Вопрос") If Answer = vbYes Then 'ответ "Да" Ctrl. Visible = False 'скрыть очередной элемент End If Next Ctrl myForm. Show End Sub

    Label - метка (надпись, статический текст)

    Один из самых простых элементов управления Label (метка) служит для вывода разного рода надписей в диалоговом окне. Часто эти надписи именуют или объясняют другие элементы окна. При работе программы текст метки может меняться. Основное свойство метки - Caption - содержит в качестве значения текст метки.
    События: BeforeDragOver, BeforeDropOrPaste, Click, DblClick, Error, MouseDown, MouseUp, MouseMove.
    Методы: Move, Zorder.

    ListBox - список

    Элемент управления ListBox выводит на экран окно со списком значений, позволяя пользователю выбрать из них одно или более. В варианте с выбором одного элемента ListBox ведет себя, как и описанный выше список в элементе ComboBox. Значение передается в свойстве Value, такую же роль играют свойства ColumnCount, ColumnWidth, ControlSource, RowSource, BoundColumn, List-Index, TextColumn и др. Главное отличие - способность обеспечить множественный выбор. Режим выбора определяется свойством MultiSelect, по умолчанию его значение fmMultiSelectSingle = 0, что соответствует выбору одного значения в списке. Значение fmMultiSelectMulti = 1 задает режим множественного выбора, при котором выбор и отмена выбора элемента осуществляются щелчком мышью или нажатием клавиши "пробел". Значение fmMulti-SelectExtended = 2 задает режим, при котором выбор осуществляется щелчком или нажатием клавиши Shift, а расширить область выделенных элементов можно, используя клавиши-стрелки при нажатой клавише Shift.
    В случае множественного выбора значение свойства Value всегда - Null, а получение информации о выбранных пользователем элементах обеспечивает булево свойство Selected (выбран), которое по номеру элемента списка определяет, выбран он (True) или нет (False). Обращение к нему имеет вид:
    объект. Selected (индекс),
    где индекс - целое в интервале от 0 до ListCount - 1. Например, если в списке ListBox1 пять элементов, то следующий цикл перенесет выбранные пользователем элементы в столбец "A" рабочего листа "Лист1" в Excel.
    For i = 0 To 4 If ListBox1. Selected (i) = True Then Worksheets ("Лист1"). Cells (i+1, 1) = ListBox1. List (i) End If Next I
    Свойство Selected используется также для выбора или отмены выбора элементов из программы.
    События: AfterUpdate, BeforeDragOver, BeforeDropOrPaste, BeforeUpdate, Change, Click, DblClick, DropButtonClick, Enter, Error, Exit, KeyDown, KeyUp, KeyPress, MouseDown, MouseUp, MouseMove.
    Методы: AddItem, Clear, Move, RemoveItem, SetFocus, ZOrder.

    Метод Add (Добавить)

    Позволяет добавить элемент управления во время исполнения программы. С его помощью можно добавить страницу в набор страниц, вкладку в полосу вкладок или любой элемент управления на страницу или диалоговое окно.
    Вызов метода Add для вставки страницы или вкладки имеет вид:
    Set Object = объект. Add ([Name [, Caption [, index]]])
    Для добавления элемента управления к диалоговому окну или странице используется присвоение:
    Set Control = объект. Add (ProgID [, Name [, Visible]])
    Здесь объект - имя объекта (коллекции), к которому добавляется элемент, Name - задает имя (свойство Name) добавляемого объекта (если его нет, система присвоит добавленному элементу стандартное имя), Caption - задает заголовок страницы или вкладки, появляющийся на закладке, index - указывает положение страницы или вкладки в коллекциях Pages или Tabs, соответственно. ProgID - программный идентификатор добавляемого элемента, который представляет собой строку символов (без пробелов), идентифицирующую класс объектов. У этой строки структура обычно такова: <Поставщик>. <Компонента>. <Версия>. Программный идентификатор ProgID отображается на идентификатор класса (CLSID), т. е. на уникальный идентификатор (UUID), регистрируемый в системном реестре Windows. Необязательный булев параметр Visible указывает, виден ли объект на экране. По умолчанию он равен True (объект виден).
    Вот значения программного идентификатора ProgID для стандартных элементов управления:
    флажок Forms. CheckBox. 1
    комбинированный список Forms. ComboBox. 1
    командная кнопка Forms. CommandButton. 1
    рамка Forms. Frame. 1
    изображение Forms. Image. 1
    метка Forms. Label. 1
    список Forms. ListBox. 1
    набор страниц Forms. MultiPage. 1
    переключатель Forms. OptionButton. 1
    полоса прокрутки Forms. ScrollBar. 1
    счетчик Forms. SpinButton. 1
    полоса вкладок Forms. TabStrip. 1
    поле ввода Forms. TextBox. 1
    выключатель Forms. ToggleButton. 1

    Метод Add возвращает объект Page при вставке страницы в набор страниц и объект Tab при вставке вкладки в полосу вкладок.
    В остальных случаях он возвращает объект из класса, определяемого значением ProgID. После добавления элемента управления возбуждается событие AddControl.

    Давайте добавим элемент управления к окну во время работы программы. Допустим, диалоговое окно InsertElem содержит командную кнопку cmbNewCtrl "Добавить элемент" и группу из двух переключателей: Opb1 "поле ввода" и Opb2 "список":

    Метод Add (Добавить)
    Рис. 13. 8.  Окно перед добавлением элемента

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

    Пример 13.1.

    (html, txt)

    Вот результаты выбора командной кнопки при разных значениях переключателей:

    Метод Add (Добавить)
    увеличить изображение
    Рис. 13. 9.  Результаты вставки элементов

    Метод AddItem (Добавить элемент)

    Этот метод позволяет добавлять элементы в списки и комбинированные списки. В списки с несколькими столбцами метод AddItem добавляет строку элементов. Его вызов имеет вид:
    Перем = объект. AddItem ([Item [, varindex]])
    или
    объект. AddItem [Item [, varindex]]
    Здесь Перем - переменная типа Variant, объект - объект-список, в который добавляется элемент, Item - добавляемый элемент, varindex - номер добавляемого элемента или строки. Первый элемент (строка) в списке имеет номер 0. Значение varindex не должно превышать числа элементов списка ListCount. Если этот параметр не задан, элемент вставляется в конец списка. Поскольку в первом случае Перем в результате вызова получает пустое значение Empty, чаще используется второй вариант вызова.
    Если в списке несколько столбцов, вставляемая непустая строка содержит непустое значение элемента для первого столбца и пустые значения для остальных. Чтобы установить непустые элементы во втором и остальных столбцах, можно использовать свойство List. Например, если в примере с добавлением списка NewList требуется, чтобы этот список содержал два столбца: "имя" и "фамилия", то блок операторов для его создания может выглядеть так:
    Else ' добавляем список AddElem. Width = 350 'Расширяем форму Set NewCtrl = Controls. Add ("Forms. ListBox. 1", "NewList") NewCtrl. ColumnCount = 2 'число столбцов списка NewCtrl. Left = 96 NewCtrl. Top = 12 NewCtrl. Width = 200 NewCtrl. Height = 70 NewCtrl. AddItem ("Анна") '1-ая строка, 1-ый столбец NewCtrl. List (0, 1) = "Павлова" '1-ая строка, 2-ой столбец NewCtrl. AddItem ("Елена") '2-ая строка, 1-ый столбец NewCtrl. List (1, 1) = "Образцова" '2-ая строка, 2-ой столбец NewCtrl. AddItem ("Ирина") '3-я строка, 1-ый столбец NewCtrl. List (2, 1) = "Архипова" '3-я строка, 2-ой столбец NewCtrl. AddItem ("Мария") '4-ая строка, 1-ый столбец NewCtrl. List (3, 1) = "Каллас" '4-ая строка, 2-ой столбец
    Теперь диалоговое окно после добавления списка будет выглядеть так:
    Метод AddItem (Добавить элемент)
    Рис. 13. 10.  Список с двумя столбцами
    Для элементов, у которых установлен источник списка (свойство RowSource), метод AddItem неприменим.

    Метод Clear (Очистить)

    Этот метод удаляет все объекты, входящие в некоторый объект или коллекцию объектов. Из списка и комбинированного списка Clear удаляет все элементы списков, т. е. делает их пустыми. Из элементов набор страниц и полоса вкладок им удаляются все страницы и вкладки. Из коллекции элементов управления Controls метод Clear удаляет элементы, созданные при исполнении программы методом Add. Попытка удалить элементы, созданные на этапе проектирования приводит к ошибке. Нельзя также "чистить" элементы, данные которых связаны с основным объектом приложения, например, списки с установленным свойством RowSource. Вызов метода имеет вид:
    объект. Clear
    где объект - очищаемый объект или коллекция объектов.

    Метод Copy (Копировать)

    Этот метод выполняет копирование содержимого объекта в буфер обмена. Что именно копируется, зависит от объекта. Для поля ввода и комбинированного списка копируется выделенный в текущий момент текст. Для диалогового окна, страницы и рамки копируются активные (выбранные) в данный момент элементы. Вызов метода имеет вид:
    объект. Copy
    где объект - объект, для которого вызывается метод.
    Копирование объекта не изменяет его.

    Метод Cut (Вырезать)

    В результате вызова этого метода информация, выделенная в объекте, удаляется из него и помещается в буфер обмена. Для поля ввода и комбинированного списка удаляется и копируется в буфер текст, выделенный в текущий момент. Для диалогового окна, страницы и рамки удаляются активные (выбранные) в данный момент элементы. Удаляться могут лишь те элементы, что созданы в процессе работы программы. Вызов метода имеет вид:
    объект. Cut
    где объект - объект, для которого вызывается метод.
    Пример применения метода Cut приведен ниже при описании метода Paste.

    Метод DropDown (Вывести список)

    Раскрывает список элементов, входящих в комбинированный список. Вызов этого метода имеет вид:
    объект. DropDown
    где объект - тот комбинированный список, для которого вызывается метод.
    Метод DropDown можно использовать, чтобы показать список элементов из комбинированного списка в ответ на событие, возникшее в другом управляющем элементе. Мы включили в наше диалоговое окно еще одну возможность, ѕ добавлять комбинированный список. Вот новый фрагмент кода:
    Else 'добавляем комбинированный список AddElem. Width = 350 'Расширяем форму Set NewCtrl = Controls. Add ("Forms. ComboBox. 1", "List2") Controls ("List2"). ColumnCount = 2 'число столбцов списка Controls ("List2"). Left = 96 Controls ("List2"). Top = 12 Controls ("List2"). Width = 200
    Controls ("List2"). AddItem ("Федор") Controls ("List2"). List (0, 1) = "Шаляпин" Controls ("List2"). AddItem ("Сергей") Controls ("List2"). List (1, 1) = "Лемешев" Controls ("List2"). AddItem ("Лучано") Controls ("List2"). List (2, 1) = "Паваротти" Controls ("List2"). AddItem ("Пласидо") Controls ("List2"). List (3, 1) = "Доминго"
    Controls ("List2"). DropDown
    Теперь диалоговое окно при добавлении комбинированного списка выглядит так:
    Метод DropDown (Вывести список)
    Рис. 13. 11.  Показ элементов комбинированного списка

    Метод Move (Сдвинуть)

    Результат вызова этого метода - сдвиг диалогового окна, одного элемента управления или одновременный сдвиг всех элементов управления коллекции Controls диалогового окна.
    Для диалогового окна или отдельного элемента вызов метода имеет вид:
    объект. Move ([Left [, Top [, Width [, Height [, Layout]]]]])
    а для коллекции Controls:
    объект. Move (X, Y)
    Здесь объект - имя сдвигаемого объекта или коллекции, объекты которой сдвигаются. Все параметры в скобках в первом варианте вызова необязательны. Параметры Left и Top задают координаты верхнего левого угла объекта в точках, а Width и Height - ширину и высоту объекта в точках (все четыре имеют тип Single). Булев параметр Layout определяет, будет ли для объекта-родителя инициировано событие Layout. Во втором варианте параметры X и Y обязательны, они задают смещение всех объектов коллекции по горизонтали и вертикали (отрицательные значения - сдвиг влево и вверх, положительные - вправо и вниз).
    Метод Move можно использовать как для сдвига объекта, так и для изменения его размеров. Например, чтобы при двойном щелчке изображения Image1, имеющего координату верхней границы 18 и высоту 70, поднять его верхнюю границу и увеличить высоту на 10 точек, можно использовать процедуру.
    Private Sub Image1_DblClick (ByVal Cancel As MSForms. ReturnBoolean) Me. Image1. Move Top:=8, Height:=80 End Sub

    Метод Paste (Вставить)

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

    Метод RedoAction (Повторить действие)

    Применяется к диалоговому окну, рамке и странице для того, чтобы восстановить результат последнего действия, отмененного посредством Undo. Вызов метода имеет вид:
    BooleanVar = объект. RedoAction
    где объект - это объект, для которого требуется повторить последнее отмененное действие. При успешном вызове метод присвоит булевой переменной BooleanVar значение True, а при невозможности выполниться - False.
    Возможность применения метода RedoAction зависит от значения свойства CanRedo объекта, вызывающего метод. Если CanRedo равно False, метод неприменим. Пример использования метода RedoAction приведен ниже в описании метода Undo.

    Метод Remove (Удалить)

    Позволяет удалить элемент коллекций Controls, Pages и Tabs или элемент управления из диалогового окна, страницы или формы. Удаляться могут лишь объекты, добавленные динамически в процессе работы программы. Попытка удалить объект, созданный на этапе проектирования диалогового окна, приведет к ошибке. Вызов метода имеет вид:
    объект. Remove (collectionindex)
    где объект - идентифицирует объект (коллекцию), к которому применяется метод, а обязательный параметр collectionindexзадает номер или индекс удаляемого элемента коллекции (он может быть числом или строкой-именем элемента).

    Метод RemoveItem (Удалить элемент)

    С помощью этого метода можно удалять элементы из списков и комбинированных списков. В списках с несколькими столбцами метод RemoveItem удаляет всю строку элементов. Его вызов имеет вид:
    BooleanVar = объект. RemoveItem (index)
    где объект - идентифицирует объект (список) из которого удаляется элемент, а index - указывает номер удаляемой строки (первая строка в списке имеет номер 0). В переменной BooleanVar после выполнения удаления возвращается False. Поэтому чаще при вызове используется только правая часть присвоения. Например, для удаления из списка ListBox1 выбранного в данный момент элемента можно использовать вызов:
    ListBox1. RemoveItem (ListBox1. ListIndex)
    Метод RemoveItem не удаляет строки из списков, данные в которых связанны с объектом приложения (т. е. из списков с установленным свойством RowSource).

    Метод Repaint (Перерисовать)

    Этот метод предназначен для восстановления модифицированных изображений диалоговых окон, рамок и страниц. Поскольку система сама перерисовывает изменные элементы управления, использовать метод Repaint следует лишь тогда, когда пользователь немедленно должен увидеть результаты модификации, не дожидаясь, пока это сделает система. Вызов метода имеет вид:
    Boolean = объект. Repaint
    где объект - идентифицирует восстанавливаемый объект.

    Метод Scroll (Прокрутить)

    Сдвигает полосу прокрутки в диалоговом окне, рамке или на странице (но не применяется к отдельному элементу вида полоса прокрутки и к полосе прокрутки в поле ввода). Вызов метода имеет вид:
    объект. Scroll ([ActionX [, ActionY]])
    где объект - объект, в котором происходит прокрутка, параметр ActionX задает действие в горизонтальном направлении, а ActionY - в вертикальном. Ниже перечислены возможные значения этих параметров и разъяснен их смысл.
  • fmScrollActionNoChange = 0 не сдвигать в данном направлении;
  • fmScrollActionLineUp = 1 выполнить небольшой сдвиг вверх на вертикальной полосе или небольшой сдвиг влево на горизонтальной полосе; движение эквивалентно нажатию клавиш-стрелок "вверх" и "влево";
  • fmScrollActionLineDown = 2 выполнить небольшой сдвиг вниз на вертикальной полосе или небольшой сдвиг вправо на горизонтальной полосе; движение эквивалентно нажатию клавиш-стрелок "вниз" и "вправо"
  • fmScrollActionPageUp = 3 сдвинуться на одну страницу вверх на вертикальной полосе прокрутки или на одну страницу влево на горизонтальной полосе прокрутки; движение эквивалентно нажатию клавиши PgUp для передвижения полосы прокрутки;
  • fmScrollActionPageDown = 4 сдвинуться на одну страницу вниз на вертикальной полосе прокрутки или на одну страницу вправо на горизонтальной полосе прокрутки; движение эквивалентно нажатию клавиши PgDn для передвижения полосы прокрутки;
  • fmScrollActionBegin = 5 сдвинуться на верхний край вертикальной полосы прокрутки или на левый край горизонтальной полосы прокрутки;
  • fmScrollActionEnd = 6 сдвинуться на нижний край вертикальной полосы прокрутки или на правый край горизонтальной полосы прокрутки.


  • Метод SetDefaultTabOrder (Установить стандартный порядок обхода)

    Устанавливает для диалогового окна, рамки или страницы стандартный порядок обхода их элементов управления при нажатии клавиши Tab. Этот порядок соответствует обходу элементов слева направо и сверху вниз и совпадает с лексикографическим порядком на координатах левых верхних углов элементов . Вызов метода имеет вид:
    объект. SetDefaultTabOrder
    где объект - объект, для которого устанавливается стандартный порядок.

    Метод SetFocus (Установить фокус)

    Устанавливает фокус на вызвавший этот метод элемент управления. При невозможности это сделать фокус остается на прежнем месте, и возбуждается ошибка. Вызов метода имеет вид:
    объект. SetFocus
    где объект - объект, который стремится попасть в фокус.
    По умолчанию элемент управления, попавший в фокус в результате вызова метода SetFocus, автоматически не активизируется и не появляется поверх других закрывающих его объектов. Причина использования метода SetFocus в том, что доступ к некоторым свойствам элементов управления возможен лишь для объектов, находящихся в фокусе (например, для поля ввода это свойство LineCount, для списка - ListCount).

    Метод UndoAction (Отменить действие)

    Отменяет последнее действие, выполненное в диалоговом окне, рамке или на странице. Подтверждающий возможность отмены действия признак - значение True свойства CanUndo. Если оно равно False, последнее действие, совершенное в диалоговом окне, отменить нельзя.
    Вызов метода имеет вид:
    Boolean = объект. UndoAction
    где объект - объект, для которго отменяется последнее действие.
    Типичные действия, которые можно отменять методом UndoAction и восстанавливать методом RedoAction, - ввод и выделение текста в поле ввода, выделение элемента в списке, установка значений флажков, переключателей и выключателей и т. п. А вот добавление элемента в список отмене не подлежит.

    Метод ZOrder (Z-упорядочить)

    Помещает объект впереди или сзади всех других перекрывающихся с ним объектов на экране.
    Вызов метода имеет вид:
    объект. ZOrder ([zPosition])
    где объект - объект, меняющий свое положение, zPosition - необязательный параметр, указывающий, впереди или сзади поместить объект. При значении zPosition, равном fmTop = 0, объект помещается впереди (это значение по умолчанию), а при значении fmBottom = 1 - сзади других перекрывающих его объектов.
    Вообще при проектировании интерфейса следует избегать перекрытия одних элементов управления другими на поверхности окна, так как это затрудняет понимание и использование интерфейса. Кроме того, среди элементов управления есть набор страниц, позволяющий помещать на одном и том же месте окна различные элементы управления, но благодаря заголовкам страниц, делающий переход от набора элементов одной страницы к набору элементов другой простым и удобным.

    MultiPage - набор страниц

    Элемент управления MultiPage объединяет несколько независимых диалоговых окон - страниц (вкладок). Заголовки страниц обычно видны на одной из сторон элемента на их закладках, а переход на страницу происходит после щелчка по ее закладке. Этот простой переход с одной страницы на другую и делает MultiPage удобным средством для представления разнородных данных, относящихся к одному объекту. Такие данные в "бумажных" офисах хранятся обычно в отдельных папках и образуют дела, досье и т. п.
    Каждая страница из MultiPage - это объект типа Page, а все они включены в коллекцию Pages (страницы). При создании элемента MultiPage в него автоматически помещаются две страницы с именами Page1 и Page2. Имена можно изменять, можно добавлять и новые страницы. Чтобы на стадии проектирования добавить новую страницу, переименовать, передвинуть или удалить имеющуюся, щелкните правой кнопкой мыши полосу закладок и выберите нужное действие в появившемся контекстном меню. Рассмотрим основные свойства набора страниц.
  • Количество страниц возвращается свойством Count.
  • Свойство Value для элемента MultiPage определяет номер текущей активной страницы в коллекции Pages.
  • Свойство SelectedItem (его можно только читать) возвращает текущую активную страницу (как объект). Его можно использовать для считывания и установки свойств этой страницы и входящих в нее элементов управления.
  • Свойство Style определяет, в каком виде представляются заголовки страниц. По умолчанию оно равно fmTabStyleTabs=0 и задает представление заголовков в виде закладок в полосе заголовков. Каждая закладка с заголовком находится внутри границ своей страницы. Если значение fmTabStyleButtons = 1, заголовок каждой страницы находится на отдельной кнопке, расположенной в полосе заголовков. Переход на страницу происходит после выбора кнопки с ее заголовком. Если же значение fmTabStyleNone = 2, полоса с заголовками страниц на экран не выводится.
  • Свойство TabOrientation задает расположение полосы с заголовками страниц. Его значения описаны в следующей таблице.

    КонстантаЗначениеЗаголовки расположены
    fmTabOrientationTop 0наверху
    fmTabOrientationBottom 1внизу
    fmTabOrientationLeft 2на левой стороне
    fmTabOrientationRight 3на правой стороне
  • Булево свойство MultiRow позволяет включить возможность создания нескольких полос с закладками (по умолчанию его значение равно False, что соответствует одной полосе закладок).
  • Свойства TabFixedHeight и TabFixedWidth устанавливают или возвращают высоту и ширину закладки (в точках). При значении 0 ширина закладок устанавливается автоматически так, чтобы в каждой закладке помещалось ее название и оно занимало всю ширину элемента. При значениях > 0 у всех закладок одинаковые размеры, заданные свойством TabFixedWidth. Минимально возможный размер закладки - 4 точки.


  • Рассмотрим пример, демонстрирующий разные способы доступа к страницам набора. Допустим, в наборе страниц MyPages есть страницы: Page1, Page2, Page3 и Page4, созданные и размещенные в указанном порядке.

    Private Sub UserForm_Initialize () 'Использование индекса (числового или строкового) и коллекции Pages MyPages. Pages (0). Caption = " Страница 1 " MyPages. Pages. Item (1). Caption = " Страница 2 " 'Использование объекта Page без ссылки на коллекцию Pages: MyPages. Page3. Caption= " Страница 3 " 'Использование свойства SelectedItem: MyPages. Value = 3 MyPages. SelectedItem. Caption = " Страница 4 " End Sub

    События: AddControl, BeforeDragOver, BeforeDropOrPaste, Change, Click, DblClick, Enter, Error, Exit, KeyDown, KeyUp, KeyPress, Layout, MouseDown, MouseUp, MouseMove, RemoveControl, Scroll, Zoom.

    Методы: Move, SetFocus, ZOrder.

    Объект DataObject

    Этот объект не элемент управления, но участвует в операциях перетаскивания выделенных текстов из одного элемента управления в другой. В нем одновременно могут храниться несколько текстовых данных в разных форматах. Когда в DataObject помещается новый текст с имеющимся там форматом, то прежний текст с этим форматом заменяется на новый.
    Поведение DataObject похоже на поведение буфера обмена. Однако DataObject существует только в момент работы приложения и исчезает после ее завершения, а данные в буфере обмена при этом не теряются, DataObject может хранить только текстовые данные, а буфер обмена - и графические. С другой стороны, DataObject - настоящий OLE-объект и поддерживает в отличие от буфера обмена операции перетаскивания текста.
    Текст заносится в DataObject методом SetText, а извлекается оттуда методом GetText:
    объект. SetText (StoreData [, format])
    и
    Строка = объект. GetText ([format]),
    где объект - объект-владелец метода, StoreData - текст, который надо запомнить в объекте, format - это необязательный параметр, задающий "формат" данных (1 соответствует стандартному текстовому формату, а другие числа и строки соответствуют пользовательским форматам). Если параметр format в вызове SetText явно не указан, то запоминаемому тексту присваивается формат стандартного текста 1. Так как для каждого формата DataObject содержит лишь один текст с этим форматом, то фактически формат играет роль ключа, по которому текст заносится и извлекается из DataObject. Метод GetFormat позволяет узнать, имеется ли в объекте DataObject текст определенного формата:
    BooleanVar = объект. GetFormat (format)
    Переменная BooleanVar получит значение True, если данные с указанным форматом входят в объект. Объект DataObject может обмениваться данными с буфером обмена посредством методов GetFromClipboard и PutInClipboard. Оператор:
    String = объект. GetFromClipboard ()
    помещает содержимое буфера обмена в DataObject, а вызов:
    объект. PutInClipboard
    переносит данные из DataObject, имеющие текстовый формат 1, в буфер обмена.

    Допустим, в диалоговом окне имеются два поля ввода TextIn и TextOut, командная кнопка CmbCopy и метка Lable1. Занесем при инициализации окна в поле TextIn текстовую информацию и инициализируем глобальные переменные:

    Public NewData As DataObject Public NumClick As Integer

    Private Sub UserForm_Initialize () Set NewData = New DataObject 'инициализация объекта NumClick = 0 'число щелчков TextIn. Text = "Пример переноса данных с помощью DataObject "

    End Sub

    При последовательных щелчках командной кнопки занесем содержимое поля TextIn в объект DataObject методом SetText, из него перенесем в буфер обмена с помощью PutInClipboard, а уже из него поместим в поле TextOut методом Paste. О выполнении очередной операции будет сообщать метка Label1.

    Private Sub CmdCopy_Click () Select Case NumClick Case 0 NewData. SetText TextIn. Text Label1. Caption = "Из TextIn в DataObject" Case 1 NewData. PutInClipboard Label1. Caption = "Из DataObject в буфер обмена" Case 2 TextOut. Paste Label1. Caption = "Из буфера обмена в TextOut" End Select

    NumClick = NumClick + 1 If NumClick = 3 Then NumClick = 0

    End Sub

    Разумеется, перенос информации из одного поля в другое можно было осуществить и через буфер обмена, минуя DataObject. Для копирования данных из поля ввода в буфер обмена можно вызвать метод Copy. Более важный метод этого объекта, инициирующий операцию перетаскивания, - StartDrag рассмотрим подробнее.

    Объект-родитель

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

    Объект UserForm (диалоговое окно), коллекция UserForms (диалоговые окна)

    В этой лекции мы рассмотрим основные объекты, связанные с организацией работы диалоговых окон. К ним относятся отдельные диалоговые окна, их коллекции и стандартные элементы управления, которые можно размещать в диалоговых окнах.
    Объект UserForm (диалоговое окно или форма) - прямоугольное окно на экране с размещенными на нем элементами управления - важная часть пользовательского интерфейса. Создание и использование диалоговых окон было рассмотрено в предыдущей лекции. Здесь мы кратко опишем свойства класса UserForm и элементов управления.
    Все загруженные в приложение диалоговые окна образуют коллекцию UserForms (диалоговые окна). Она обладает стандартными свойствами Count (Количество), Item (в других коллекциях Item часто выступает как метод, хотя суть от этого не меняется) и методом Add (Добавить). Значение свойства Count равно количеству элементов в коллекции; Item позволяет получить доступ к отдельному элементу коллекции (здесь Item - свойство "по умолчанию", т. е. может опускаться), метод Add (имя_формы) позволяет поместить в коллекцию новое диалоговое окно. Обращение к элементу коллекции имеет вид:
    UserForms. Item. (index)
    Или
    UserForms (index)
    Параметр index - число от 0 до UserForms. Count - 1, задающее номер элемента в коллекции. Во втором варианте определенное по умолчанию свойство Item опущено. Заметьте, коллекцию UserForms составляют не формы, спроектированные для работы с документом, а только загруженные формы, те, для которых был выполнен метод Load.
    Как и другие объекты-коллекции, UserForms можно использовать для организации цикла по всем загруженным диалоговым окнам приложения. Выражение UserForms (index) можно подставлять как аргумент функции, имеющий тип диалогового окна UserForm
    Эта коллекция корректно работает с действительно загруженными окнами, но дает странные результаты, когда загруженных форм нет. Вот небольшой пример работы с документом, имеющим две формы с именами frmMy1 и frmMy2:
    Public Sub PropOfForm () Dim MyForm As UserForm

    Debug. Print UserForms. Count Debug. Print frmMy1. Name, frmMy1. Caption

    Debug. Print UserForms. Count UserForms. Add ("frmMy1")

    Debug. Print UserForms. Count For Each MyForm In UserForms Debug. Print MyForm. Caption Next MyForm

    UserForms. Add ("frmMy2") Debug. Print UserForms. Count

    End Sub

    Приведем результаты отладочной печати:

    0 frmMy1 Первая 1 2

    3

    Заметьте, вначале счетчик числа элементов коллекции показывает, что элементов там нет. После работы с формой в операторе Debug (но не ее загрузки) счетчик увеличивается на 1, также как и при выполнении метода Add. Однако в цикле по элементам коллекции заголовки форм не распечатываются - диалоговые формы не доступны.

    С коллекциями диалоговых окон приходится работать редко. Чаще всего, работа идет с элементами этих коллекций. Для объектов класса UserForm (Диалоговое окно) определено около 40 свойств. Основные группы свойств и некоторые входящие в них свойства таковы:

  • имя диалогового окна: (Name) - имя, используемое при обращении к окну в программе;
  • положение окна на экране: Height (Высота), Width (Ширина), Left и Top - координаты верхнего левого угла;
  • изображение окна: BackColor (Цвет фона), Forecolor (Цвет переднего плана), BorderColor (Цвет рамки), Caption (Заголовок) ;
  • картинка в окне: Picture - файл с изображением, PictureSizeMode - вариант размера картинки;
  • полоса прокрутки: ScrollBar - расположение полосы прокрутки в окне (0 - отсутствует, 1 - горизонтальная полоса, 2 - вертикальная, 3 - обе), ScrollHeight и ScrollWeidth - высота и ширина области прокрутки в диалоговом окне, ScrollLeft и ScrollTop - координаты верхнего левого угла области прокрутки;
  • шрифт: Font [значение этого свойства - объект типа шрифт (Font) ].


  • События: Activate, Deactivate, AddControl, BeforeDragOver, BeforeDropOrPaste, Click, DblClick, Error, KeyDown, KeyUp, KeyPress, Layout, MouseDown, MouseUp, MouseMove, RemoveControl, Scroll, Terminate, Zoom.

    Методы: Copy, Cut, Hide, Load, Move, Paste, PrintForm, RedoAction, Repaint, Scroll, SetDefaultTabOrder, Show, UndoAction, Unload.

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

    Dim Myf1 As New frmMy1, Myf2 As New frmMy1, Myf3 As New frmMy2 Debug. Print Myf1. Name, Myf2. Caption, Myf3. Name Myf1. BackColor = vbGreen Myf2. Show Myf1. Show

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

    OptionButton - кнопка-переключатель

    Кнопка-переключатель (радио - кнопка или просто переключатель), как и флажок, показывает, выбран ли элемент. Отличие в том, что из нескольких переключателей, объединенных в группу, выбран может быть только один. Объединять переключатели в группу можно двумя способами. Первый: сначала создается элемент Frame - рамка группы, затем в этой рамке размещают переключатели, образующие группу. Они автоматически становятся взаимоисключающими. Другой способ - присвоив группе имя, установить его как значение свойства GroupName для всех переключателей этой группы. У каждого способа свои плюсы и минусы. В первом случае рамка вокруг переключателей помогает пользователю зрительно идентифицировать группу, но она занимает дополнительную площадь в окне (а ведь может потребоваться разместить несколько групп переключателей!), да и рамка перекрывает фон окна. Второй вариант гибче: кнопки одной группы могут размещаться в окне произвольным способом, но сложнее обеспечить ясное выделение каждой группы кнопок.
    Перемещение по кнопкам одной группы осуществляется с помощью клавиш-стрелок, при этом фокус перемещается только по элементам данной группы, даже если при обходе с помощью клавиши Tab между кнопками группы включены другие элементы управления. Выбор переключателя, находящегося в фокусе, происходит при нажатии клавиши "пробел" или при щелчке любого переключателя группы - его свойство Value получает значение True, а для всех остальных переключателей группы устанавливается значение False.
    События: AfterUpdate, BeforeDragOver, BeforeDropOrPaste, Change, Click, DblClick, Enter, Error, Exit, KeyDown, KeyUp, KeyPress, MouseDown, MouseUp, MouseMove.
    Методы: Move, SetFocus, ZOrder.

    Параметры внешнего вида объекта

    Свойство BackColor определяет цвет фона объекта. При задании из списка свойств объекта на этапе проектирования цвет можно выбрать непосредственно из таблицы цветов. При установке из программы в качестве значений используются целые числа. Цвет можно задавать, используя функцию RGB. Она определяет цвет как смешение трех компонентов, ѕ красной, зеленой и синей составляющим, каждая из которых принимает значения от 0 до 255. Например, RGB (255, 255, 0) задает желтый цвет. Чаще всего, цвет задается константами VBA, например, константа vbGreen определяет зеленый цвет.
    Свойство BackStyle определяет прозрачность фона. По умолчанию его значение fmBackStyleOpaque = 1. При этом фон непрозрачен, виден его цвет, заданный свойством BackColor. Если значение BackStyle равно fmBackStyleTransparent = 0, фон прозрачен и виден объект, лежащий под данным объектом. На прозрачность рисунков свойство BackStyle не влияет.
    Свойство ForeColor задает цвет переднего плана объекта (текста) ; возможные значения этого свойства такие же, как и у BackColor.
    Свойство BorderColor задает цвет рамки (границы) объекта; возможные значения такие же, как и у BackColor.
    Свойство BorderStyle определяет наличие или отсутствие рамки. Если его значение - fmBorderStyleNone = 0, видимая рамка у элемента управления отсутствует. При значении fmBorderStyleSingle = 1, у объекта есть рамка (в одну линию). Это значение устанавливается по умолчанию для всех объектов с рамками, кроме элементов управления ComboBox, Frame, Label, ListBox и TextBox, у которых по умолчанию рамки нет.
    Альтернативный способ задания внешнего вида границы объекта предоставляет свойство SpecialEffect; вот его значения:
  • fmSpecialEffectFlat = 0 - внешний вид объекта плоский, он отделяется от окружения границей и цветом; установлено по умолчанию для элементов Image и Label, но может быть установлено для любого элемента;
  • fmSpecialEffectRaised = 1 - верхняя и левая границы объекта подсвечены, а правая и нижняя затенены;
  • fmSpecialEffectSunken = 2 - верхняя и левая границы объекта затенены, а правая и нижняя подсвечены; элемент углублен в окружающее окно; установлено по умолчанию для элементов типа Frame, CheckBox и OptionButton;
  • fmSpecialEffectEtched = 3 - углубленная рамка по краям элемента;
  • fmSpecialEffectBump = 6 - объект выделен выступом снизу и справа, а сверху и слева плоский.

  • Отметим, что значения 1, 3 и 6 не используются для элементов типаCheckBox и OptionButton.
    Для изображения границ объекта используются свойства SpecialEffect либо BorderStyle, но не оба вместе. При установке одного из них система автоматически обнуляет другое.
    Свойство ControlTipText определяет краткий текст, который появится на экране рядом с объектом, когда указатель мыши будет находиться на данном объекте.
    Булево свойство Visible определяет, виден ли объект на экране; по умолчанию True - объект виден.

    Перечень основных элементов управления

    Рассмотрим набор элементов управления. Ранее говорилось, что помимо элементов управления, появляющихся на стандартной панели, щелчок по ней правой кнопки открывает большой список дополнительных элементов управления, более того, пользователь имеет возможность расширить этот список. Мы ограничимся сейчас рассмотрением некоторого стандартного набора, который включает следующие объекты:
  • CheckBox - флажок;
  • ComboBox - комбинированный список (поле со списком) ;
  • CommandButton - командная кнопка;
  • Image - изображение (окно изображения) ;
  • Label - метка (надпись, статический текст) ;
  • ListBox - список (окно списка) ;
  • MultiPage - набор страниц;
  • OptionButton - переключатель (кнопка зависимого выбора) ;
  • ScrollBar - полоса прокрутки;
  • SpinButton - счетчик (ворот) ;
  • TabStrip - полоса вкладок;
  • TextBox - поле ввода (окно редактирования, текстовое поле) ;
  • ToggleButton - выключатель.

  • В этом списке справа указаны русские эквиваленты английских названий элементов управления. Для некоторых элементов названия и переводы еще не устоялись. Даже в разных продуктах Microsoft некоторые элементы называются по-разному, например элемент TextBox в Visual C++ называется EditСontrol.
    Значки различных элементов управления на панели элементов показаны на рис. 13.1. Заметьте, показанный здесь элемент управления ActiveX PlugIn относится к дополнительным элементам.
    Перечень основных элементов управления
    Рис. 13.1.  Панель элементов
    Классы элементов управления обладают широким набором свойств, событий и методов, полное описание которых потребовало бы отдельной книги. Поэтому мы остановимся на типичных ситуациях использования элементов управления, в частности, создании интерфейсов прикладных офисных систем. Вначале мы рассмотрим их общие свойства, затем укажем характерные свойства и перечислим события и методы каждого элемента управления и в отдельных разделах подробней рассмотрим каждое событие и метод.

    Перемещение объектов. Как реализовать технику DragAndDrop

    При организации интерфейса в диалоговых формах зачастую полезно предоставить пользователю возможность использовать технику DragAndDrop (Переместить и Опустить), когда некоторый объект захватывается мышью, перетаскивается к другому целевому объекту и при опускании изменяет свойства целевого объекта. Типичным примером является возможность перетаскивания элементов из одного списка в другой. Другим примером является перетаскивание писем и опускание их в почтовый ящик. Важным элементом этой техники является изменение внешнего вида курсора. Захват объекта происходит при подведении курсора к объекту и нажатии левой кнопки мыши, В этот момент курсор меняет свою внешнюю форму. Затем, когда происходит перемещение мыши, то в тех областях, где расположен целевой объект, курсор снова меняет форму, показывая, что цель достигнута. Если в этот момент, отпустить левую кнопку, то операция перемещения заканчивается успешно, отпускание кнопки мыши в других областях приводит к неуспеху. Объект DataObject и его метод StartDrag является частью этой технологии работы. Достаточно сложно понять суть этого метода, не разобравшись и в остальных частях этой технологии, в частности, в целом ряде событий, возникающих в процессе перетаскивания.
    О событиях пойдет речь чуть далее, а начнем рассмотрение все-таки с метода StartDrag, который инициирует операцию перетаскивания объекта DataObject, хранящего перетаскиваемую информацию. Но прежде стоит рассмотреть пример, на котором весь этот процесс будет объясняться. Будем рассматривать диалоговое окно, имеющее два элемента ѕ список и окно ввода. Список будет содержать цвета, а текстовое окно ввода ѕ цвет, любимый пользователем. Пользователь имеет возможность выбрать любой из элементов списка и перетащить его в окно ввода. Когда пользователь захватывает выбранный им элемент списка, то возникает некоторое событие. В нашем примере это событие MouseMove, обработчик которого и будет вызывать метод StartDrag объекта DataObject, хранящего значение перетаскиваемого элемента.
    Теперь, когда, хотя бы частично, прояснена ситуация, предшествующая вызову метода, рассмотрим его синтаксис:

    Function StartDrag ([Effect As fmDropEffect]) As fmDropEffect

    Эта функция обычно вызывается в операторе присваивания вида:

    ResultEffect=объект. StartDrag ([Effect as fmDropEffect])

    Необязательный параметр Effect и результат выполнения функции принадлежат перечислению fmDropEffect. Константы, входящие в это перечисление имеют следующие значения:

  • fmDropEffectNone = 0 - не копировать и не передвигать опущенный исходный элемент на место назначения,
  • fmDropEffectCopy = 1 - копировать опущенный исходный элемент на место назначения,
  • fmDropEffectMove = 2 - передвинуть опущенный исходный элемент на место назначения,
  • fmDropEffectCopyOrMove = 3 - скопировать или передвинуть опущенный исходный элемент на место назначения.


  • Параметр Effect задает цель операции и имеет по умолчанию значение 1 (fmDropEffectCopy). Обычно он опускается, поскольку значение по умолчанию задает наиболее вероятную цель операции. Возвращаемое методом StartDrag значение определяет результат выполнения операции. Его можно использовать для анализа того, что же произошло в результате перетаскивания на самом деле. Важно только понимать, что между запуском метода StartDrag в правой части оператора присваивания и присваиванием результата левой части переменной ResultEffect происходит много событий в процессе перемещения объекта, работают обработчики этих событий и результат говорит о том, как закончился этот процесс.

    Вот как выглядит спроектированное нами диалоговое окно "Поле и Список" в процессе работы с ним:

    Перемещение объектов. Как реализовать технику DragAndDrop
    Рис. 13. 6.  Окно "Поле и Список" в процессе работы

    В этом диалоговом окне пользователь имеет возможность выбрать произвольный элемент списка "Цвета" и перетащить его мышью в поле "Любимый цвет"

    Рассмотрим обработчики событий, поддерживающие процесс перетаскивания. Начнем с обработчика события Initialize для диалогового окна, обеспечивающего инициализацию начального состояния:


    Private Sub UserForm_Initialize () With Me. DraggedList . AddItem "Красный" . AddItem "Оранжевый" . AddItem "Желтый" . AddItem "Зеленый" . AddItem "Голубой" . AddItem "Синий" . AddItem "Фиолетовый" . AddItem "Черный" . AddItem "Белый"

    End With End Sub

    Здесь инициализируется список "Цвета", имеющий имя DraggedList. Когда пользователь, выбирая элемент этого списка, нажимает левую клавишу мыши, готовясь перетащить этот элемент в другое место, у списка возникает событие MouseMove, обработчик которого имеет много параметров. Описание этого события, всех его параметров, описание других используемых нами событий, будут даны в последующих параграфах, а сейчас приведем текст этого обработчика:

    Private Sub DraggedList_MouseMove (ByVal Button As Integer, ByVal Shift As Integer, _ ByVal X As Single, ByVal Y As Single) Dim MyDataObject As DataObject Dim Msg As String Msg = "Видимо, Вы уронили цвет при перетаскивании. Повторите операцию!" If Button = 1 Then Debug. Print "MouseMove" Set MyDataObject = New DataObject Dim Effect As Integer MyDataObject. SetText DraggedList. Value Effect = MyDataObject. StartDrag (fmDropEffectCopy) If Effect = 0 Then MsgBox (Msg) Debug. Print "Effect = ", Effect End If

    End Sub

    О параметрах, как уже говорилось речь пойдет впереди, а сейчас прокомментируем его работу. Если в момент возникновения события нажата левая кнопка мыши, то создается новый объект DataObject, его метод SetText копирует значение выбранного элемента списка, а метод StartDrug запускает процесс перетаскивания. Заметьте, что метод StartDrug можно было бы вызывать без параметра, ѕ на результат это не повлияло бы. Результат перетаскивания анализируется и, если он закончился неуспехом (Effect = 0), то выдается уведомляющее сообщение. Исполняемые операторы окаймлены отладочной печатью, что позволит нам продемонстрировать, что между первой печатью, уведомляющей о начале работы обработчика MouseMove, и последней печатью, уведомляющей о результате работы, будут напечатаны сообщения других обработчиков, к рассмотрению которых мы и переходим.


    Событие BeforeDragOver возникает для объекта TextIn (поле ввода "Любимый цвет"), когда курсор в процессе перетаскивания попадает в область, занятую объектом. Заметьте, событие возникает многократно, поскольку курсор находится в этой области некоторое время. Вот текст обработчика этого события:

    Private Sub TextIn_BeforeDragOver (ByVal Cancel As MSForms. ReturnBoolean, _ ByVal Data As MSForms. DataObject, ByVal X As Single, _ ByVal Y As Single, ByVal DragState As MSForms. fmDragState, _ ByVal Effect As MSForms. ReturnEffect, ByVal Shift As Integer)

    Cancel = True Effect = fmDropEffectCopy Debug. Print "DragOver" End Sub

    Заметьте, основное, что делает этот обработчик, он устанавливает значение параметра Effect. Поскольку эффект совпадает с целевым эффектом, то курсор в области, занятой полем TextIn, будет менять свою форму, принимая вид прямоугольника со знаком "+", указывающим, что цель достигнута и можно отпустить левую клавишу мыши. Завершающий оператор отладочной печати позволит проследить за числом вызовов этого обработчика.

    Следующее событие для объекта TextIn возникает, когда отпущена левая клавиша мыши в области этого объекта. Вот его обработчик:

    Private Sub TextIn_BeforeDropOrPaste (ByVal Cancel As MSForms. ReturnBoolean, _ ByVal Action As MSForms. fmAction, ByVal Data As MSForms. DataObject, _ ByVal X As Single, ByVal Y As Single, ByVal Effect As MSForms. ReturnEffect, _ ByVal Shift As Integer)

    Cancel = True Effect = fmDropEffectCopy TextIn. Text = Data. GetText Debug. Print "DragPaste" End Sub

    Здесь также возвращается значение эффекта. Главное, завершается операция перетаскивания копированием текста в поле ввода из переданного в качестве параметра объекта Data класса DataObject, хранящего значение цвета, выбранного пользователем.

    Совместная работа этих трех обработчиков событий и вызываемые в них методы объекта DataObject - SetText, GetText, StartDrag - обеспечивают в совокупности технику DragAndDrop. В заключение о двух экспериментах при работе с диалоговым окном.


    Вначале, мы не донесли выбранный нами цвет до окна ввода, отпустив левую клавишу мыши на дороге к нему. Вот как выглядело сообщение, полученное нами:

    Перемещение объектов. Как реализовать технику DragAndDrop
    Рис. 13.7.  Сообщение о потере объекта при перетаскивании

    Затем повторно выбранный нами зеленый цвет был успешно перетащен в окно "Любимый цвет". Приведем отладочную печать этих двух экспериментов:

    MouseMove Effect = 0 MouseMove DragOver DragOver DragOver DragPaste Effect = 1

    Первые две строчки связаны с неудачной попыткой переноса, когда эффект был нулевым. Следующие строки описывают успешный эксперимент. Заметьте, что между печатью, информирующей о начале работы обработчика MouseMove и его заключительной печатью об успешном эффекте, вклинились сообщения, поступающие от обработчиков событий DragOver и DropOrPaste. Заметьте, я оставил только три сообщения от обработчика DragOver, реально их было 25. На этом описание примера заканчивается, но разговор о событиях еще предстоит.

    Dim NewCtrl As Control Dim

    Dim NewCtrl As Control Dim Inserted As Boolean
    Private Sub UserForm_Initialize () Inserted = False Opb1. Value = True End Sub
    Private Sub CommandButton1_Click () If Inserted = False Then If Opb1. Value = True Then ' выбрано поле ввода ' добавляем 2 поля ввода Set NewCtrl = Controls. Add ("Forms. TextBox. 1", "Text1") NewCtrl. Left = 96 NewCtrl. Top = 12 NewCtrl. Width = 80 NewCtrl. Height = 20 NewCtrl. Text = "Введите имя" Set NewCtrl = Controls. Add ("Forms. TextBox. 1", "Text2") Controls ("Text2"). Left = 96 Controls ("Text2"). Top = 50 Controls ("Text2"). Width = 80 Controls ("Text2"). Height = 20 Else ' добавляем список Set NewCtrl = Controls. Add ("Forms. ListBox. 1", "NewList") NewCtrl. Left = 96 NewCtrl. Top = 12 NewCtrl. Width = 80 NewCtrl. Height = 70 NewCtrl. AddItem ("Анна") NewCtrl. AddItem ("Елена") NewCtrl. AddItem ("Ирина") NewCtrl. AddItem ("Мария") End If Inserted = True Else: MsgBox ("Элемент уже добавлен!" & vbCrLf & "Второй добавить не могу!") End If
    End Sub
    Пример 13.1.
    Закрыть окно




    Option Explicit Private GlobalName As String
    Private Sub CommandButton1_Click () Controls (GlobalName). Cut
    End Sub
    Private Sub CommandButton2_Click () Controls (GlobalName). Copy End Sub
    Private Sub CommandButton3_Click () Controls (GlobalName). Paste End Sub
    Private Sub TextBox1_Exit (ByVal Cancel As MSForms. ReturnBoolean) GlobalName = "TextBox1" Cancel = False End Sub
    Private Sub TextBox2_Exit (ByVal Cancel As MSForms. ReturnBoolean) GlobalName = "TextBox2" Cancel = False End Sub
    Private Sub TextBox3_Exit (ByVal Cancel As MSForms. ReturnBoolean) GlobalName = "TextBox3" Cancel = False End Sub
    Private Sub UserForm_Initialize () TextBox1. Text = "Это мой текст" TextBox2. Text = "Это ее текст" TextBox3. Text = "Это его текст"
    End Sub
    Пример 13.2.
    Закрыть окно




    Private Sub CommandButton1_Click () If UndoAndRedo. CanUndo = True Then ' отмена возможна UndoAndRedo. UndoAction Else ' отмена невозможна Beep End If
    End Sub
    Private Sub CommandButton2_Click () If NewText. Text <> "" Then ' вставка новой рубрики ListBox1. AddItem (NewText. Text) Else ' текст не введен Beep End If End Sub
    Private Sub CommandButton3_Click () If UndoAndRedo. CanRedo = True Then ' восстановление возможно UndoAndRedo. RedoAction Else ' восстановление невозможно Beep End If End Sub
    Private Sub UserForm_Initialize () ListBox1. AddItem "черный" ListBox1. AddItem "белый"
    End Sub
    Пример 13.3.
    Закрыть окно



    Расположение объекта

    Положение левого верхнего угла элемента управления задается свойствами Left (слева) и Top (сверху), определяющими координаты этого угла относительно левого верхнего угла контейнера - элемента, в который вложен данный. Контейнером может оказаться само диалоговое окно или вкладка элемента MultiPage или TabStrip.
    Если оба параметра равны 0, левый верхний угол элемента совпадает с верхним левым углом контейнера. Для большинства систем координаты Left и Top должны находиться в диапазоне от -32767 до +32767. При отрицательных значениях на экране будет видна только часть элемента управления. Значения свойств Top и Left автоматически изменяются, когда пользователь перемещает элемент управления по экрану.
    Размеры объекта на экране задаются свойствами Height (высота) и Width (ширина). Единица измерения - точка. Рекомендуемый диапазон значений этих параметров от 0 до 32767. Реальный диапазон зависит от видеосистемы компьютера. Значения свойств Height и Width автоматически изменяются, когда пользователь изменяет размер элемента управления на экране. При этом предыдущие значения размера запоминаются в свойствах OldHeight и OldWidth.

    Реализация операций Cut, Copy, Paste в диалоговых окнах

    При работе с текстами трудно обойтись без этих, ставших привычными операций. Покажем на примере, как можно реализовать их в диалоговых окнах. Рассмотрим диалоговое окно, содержащее три поля ввода TextBox1, TextBox2, TextBox3 и три командные кнопки CommandButton1, CommandButton2 и CommandButton3 со стандартными изображениями Cut, Copy, Paste: Вот как выглядит это диалоговое окно в момент открытия:
    Реализация операций Cut, Copy, Paste в диалоговых окнах
    Рис. 13. 12.  Окно "Удалить, Копировать, Вставить" в момент открытия
    Наша цель состоит в том, чтобы кнопки работали "стандартным" образом, позволяя, копировать выделенные тексты в буфер из любого текстового окна, удалять выделенные тексты, помещая их в буфер, и, наконец, приклеивать содержимое буфера в точку вставки текстового окна. Методами Cut, Copy, Paste текстовые окна обладают, и единственная проблема состоит в том, что, когда работатет обработчик события Click соответствующей командной кнопки, ему каким-то образом необходимо знать, к какому же полю ввода применить соответствующую операцию. Для этой цели мы приспособили события Exit текстовых окон. При выходе из текстового окна обработчик события Exit запоминает его имя в глобальной переменной. Поэтому, если после выхода будет нажата одна из командных кнопок - Cut, Copy или Paste, ее обработчик по имени элемента управления сумеет понять, к какому полю ввода применить операцию. Этих объяснений, видимо, достаточно, чтобы разобраться в программном коде, решающем поставленную задачу:
    Пример 13.2.
    (html, txt)
    В заключение приведем диалоговое окно, после того, как нажимались кнопки, текст копировался, удалялся, вставлялся.
    Реализация операций Cut, Copy, Paste в диалоговых окнах
    Рис. 13.13. 

    Реализация операций Undo и Redo в диалоговых окнах

    Чуть ранее мы рассмотрели пример диалогового окна, в котором созданы аналоги стандартных кнопок Copy, Cut и Paste. Конечно же, во многих ситуациях полезно уметь создавать аналоги и других кнопок, к которым привыкли пользователи. Сейчас мы рассмотрим пример диалогового окна, в котором появятся кнопки Undo и Redo, позволяющие отменять или восстанавливать те или иные действия. Как мы уже говорили, не все действия пользователя при его работе в диалоговом окне могут быть отменены. Тем не менее, наличие таких кнопок упрощает его работу.
    Рассмотрим в качестве примера диалоговое окно UndoAndRedo, где помещены поле ввода NewText, список ListBox1 и три командные кнопки CommandButton1, CommandButton2, CommandButton3. Обработчики события Click командных кнопок задают операции Undo, Add (Добавить элемент) и Redo. Текст, введенный в поле NewText, будет добавляться в качестве нового элемента в список ListBox1 при выборе кнопки Add, а выбор Undo и Redo должен приводить к отмене последнего действия или к восстановлению его результата. Если же отмена или восстановление не представляется возможной, то подается звуковой сигнал. Вот как выглядит диалоговое окно в процессе работы:
    Реализация операций Undo и Redo в диалоговых окнах
    Рис. 13. 14.  Диалоговое окно с кнопками Undo и Redo
    Приведем теперь обработчики событий в совокупности, решающие поставленную задачу:
    Пример 13.3.
    (html, txt)
    Если в этом диалоговом окне ввести текст в поле ввода и выбрать кнопку Add, в списке появится элемент с текстом из поля ввода. При попытке отменить это действие раздастся звуковой сигнал. Зато если щелкнуть кнопку "Отменить" сразу после ввода текста, он исчезнет, но будет восстановлен после нажатия кнопки "Восстановить". Если производится подряд несколько действий, каждое из которых можно отменить и восстановить, система их запоминает и позволяет проводить несколько операций отмены и/или восстановления подряд. В нашем диалоговом окне можно отметить элемент в списке, затем ввести новый текст в поле ввода, а затем отметить другой элемент в списке. Выбор кнопок "Отменить" и "Восстановить" позволяет проследить, как отменяются и восстанавливаются эти действия.

    ScrollBar - полоса прокрутки

    Элемент управления ScrollBar представляет вертикальную или горизонтальную полосу, на краях которой расположены кнопки прокрутки, а внутри перемещается бегунок. Значение Value, устанавливаемое в полосе прокрутки или возвращаемое ей, - число, определяемое положением бегунка и границами, определенными в свойствах Min и Max. Рекомендуемые значения этих границ от -32767 до +32767 (по умолчанию установлен диапазон [0, 32767]). Если отношение длин левого и правого отрезков, на которые полоса прокрутки делится бегунком, - L:R, то
    Value = (Min*R+Max*L) / (L+R).
    Обычно полоса прокрутки используется в паре с другим элементом управления, в котором может отображать или с которого может получать свое значение. Ниже представлены две полосы прокрутки - горизонтальная (ScrollBar1) и вертикальная (ScrollBar2), - значения которых отображаются в двух полях ввода TextBox1 и TextBox2.
    ScrollBar - полоса прокрутки
    увеличить изображение
    Рис. 13. 5.  Полосы прокрутки
    Достаточно определить для этих полос прокрутки процедуры обработки события Change, возникающего всякий раз при изменении положения бегунка и, следовательно, значения Value. Вот эта процедура для полосы ScrollBar1:
    Private Sub ScrollBar1_Change () TextBox1. Text = ScrollBar1. Value ' запись нового значения в поле End Sub
    И еще несколько важных свойств полосы прокрутки.
  • Горизонтальная или вертикальная ориентация полосы прокрутки определяется свойством Orientation. При его значении по умолчанию fmOrientationAuto = - 1, ориентация полосы определяется автоматически в зависимости от ее размера по горизонтали и вертикали (больший размер задает ориентацию) ; FmOrientationVertical = 0 задает вертикальную ориентацию полосы, FmOrientationHorizontal = 1 - горизонтальную.
  • Свойства LargeChange и SmallChange определяют, на сколько изменится значение Value при одном щелчке поверхности полосы между кнопкой прокрутки и бегунком в первом случае, и при щелчке кнопки прокрутки - во втором. Эти же свойства указывают, насколько при этом смещается бегунок. По умолчанию оба свойства равны 1. Рекомендуемая область значений обоих свойств от -32, 767 до 32, 767.
  • Свойство Delay (задержка) определяет время в миллисекундах, через которое последовательно возникают события Change, если пользователь непрерывно щелкает кнопку прокрутки или левую кнопку мыши, указывающей на полосу прокрутки. По умолчанию устанавливается значение в 50 миллисекунд.
  • Свойство ProportionalThumb определяет размер бегунка: True - размер бегунка пропорционален размеру области прокрутки (это значение по умолчанию) ; False - истема определяет фиксированный размер бегунка.

  • События: AfterUpdate, BeforeDragOver, BeforeUpdate, Change, Enter, Error, Exit, KeyDown, KeyUp, KeyPress, Scroll.
    Методы: Move, SetFocus, ZOrder.

    Шрифт

    Свойство Font (шрифт) определяет характеристики символов, используемых в тексте диалогового окна или элемента управления. Значение этого свойства имеет тип Object. Основное свойство шрифта - имя Name. Если оно не указано, используется системный шрифт.
    Визуальные характеристики шрифта устанавливают булевы свойства: Bold - полужирное начертание, Italic - наклонное, Underline - подчеркивание. Размер шрифта в точках задается свойством Size. Свойство Weight задает затемненность символов при печати.

    Событие AddControl (добавился элемент)

    Событие AddControl возникает при добавлении во время работы программы нового элемента управления в диалоговое окно (UserForm), рамку (Frame), набор вкладок (MultiPage) или на одну вкладку (Page). При добавлении элементов на стадии проектирования окна и при выводе окна на экран в процессе работы это событие не возникает. Инициируется оно методом Add (Добавить).
    Процедура обработки этого события не имеет аргументов при добавлении элемента в рамку или вкладку:
    Private Sub объект_AddControl ()
    Для диалогового окна в качестве параметра передается добавляемый элемент:
    Private Sub UserForm_AddControl (ByVal ctrl As MSForms. Control)
    При добавлении элемента в набор страниц вызов этой процедуры имеет вид:
    Private Sub объект_AddControl (index As Long, ctrl As Control)
    Здесь объект - это тот объект, в который добавляется элемент управления, index - индекс вкладки, в которую добавляется элемент, а ctrl - сам добавляемый элемент.
    Процедура следующего примера выводит на экран сообщение всякий раз при добавлении нового элемента управления к диалоговому окну
    Private Sub UserForm_AddControl (ByVal Ctrl As MSForms. Control) MsgBox "Появился новый элемент" End Sub

    Событие AfterUpdate (После модификации)

    Возникает после изменения пользователем данных в элементе управления. Это событие не может быть отменено. Оно происходит после события BeforeUpdate и перед событием Exit для текущего элемента и перед событием Enter для элемента, на который переместится фокус после нажатия клавиши Tab. Вызов процедуры обработки события AfterUpdate имеет вид:
    Private Sub объект_AfterUpdate ()

    Событие BeforeDragOver (Перед завершением перетаскивания)

    Это событие, возникающее при перетаскивании элемента, можно использовать для управления курсором, когда он попадает, покидает или находится в площади объекта. При перетаскивании объекта система периодически возбуждает событие BeforeDragOver, когда пользователь перемещает мышь, нажимает или отпускает ее кнопки. Положение курсора определяет целевой объект, получающий это событие.
    Набор параметров, передаваемых процедуре обработки события BeforeDragOver, зависит от вида элемента управления. Они имеют следующий вид.
  • для рамки (Frame) :
    Private Sub объект_BeforeDragOver (ByVal Cancel As MSForms. ReturnBoolean, ctrl As Control, ByVal Data As DataObject, ByVal X As Single, ByVal Y As Single, ByVal DragState As fmDragState, ByVal Effect As MSForms. ReturnEffect, ByVal Shift As fmShiftState) ;
  • для набора страниц (MultiPage) :
    Private Sub объект_BeforeDragOver (index As Long, ByVal Cancel As MSForms. ReturnBoolean, ctrl As Control, ByVal Data As DataObject, ByVal X As Single, ByVal Y As Single, ByVal DragState As fmDragState, ByVal Effect As MSForms. ReturnEffect, ByVal Shift As fmShiftState) ;
  • для полосы вкладок (TabStrip) :
    Private Sub объект_BeforeDragOver (index As Long, ByVal Cancel As MSForms. ReturnBoolean, ByVal Data As DataObject, ByVal X As Single, ByVal Y As Single, ByVal DragState As fmDragState, ByVal Effect As MSForms. ReturnEffect, ByVal Shift As fmShiftState) ;
  • для других элементов управления:
    Private Sub объект_BeforeDragOver (ByVal Cancel As MSForms. ReturnBoolean, ByVal Data As DataObject, ByVal X As Single, ByVal Y As Single, ByVal DragState As fmDragState, ByVal Effect As MSForms. ReturnEffect, ByVal Shift As fmShiftState).

  • Здесь объект - имя объекта, которому принадлежит процедура, index - индекс вкладки из набора вкладок, затрагиваемой операцией перетаскивания, Cancel - обязательный статус обработки события, False означает, что оно будет обрабатываться элементом (по умолчанию), True показывает, что событие будет обрабатываться приложением. Параметр ctrl - тот элемент, перетаскивание которого завершается.
    Параметр Data содержит перетаскиваемые данные в формате объекта типа DataObject. Параметры X и Y задают горизонтальную и вертикальную координаты положения указателя мыши в элементе, измеряемые в точках. X - расстояние от левого края элемента, Y - от верхнего края. Параметр DragState указывает состояние указателя мыши относительно целевого элемента. Его возможные значения: fmDragStateEnter = 0 - указатель внутри целевого элемента, fmDragStateLeave = 1 - указатель вне области целевого элемента, fmDragStateOver = 2 - указатель находится в новом положении, но внутри того же элемента. Параметр Effect определяет возможные операции, выполняемые над опущенным исходным элементом. Его возможные значения те же, что и у описанного выше метода StartDrag. Параметр Shift задает состояние клавиш Shift, Ctrl и Alt. Его возможные значения: fmShiftMask = 1 - нажата клавиша Shift, fmCtrlMask = 2 - клавиша Ctrl, fmAltMask = 4 - Alt.
    Большинство элементов управления не поддерживает перетаскивание при значении аргумента Cancel = False (это значение устанавливается по умолчанию). Эти элементы не позволяют перетащить что-либо на себя и не возбуждают событие BeforeDropOrPaste. Исключения - поле ввода (TextBox) и комбинированный список (ComboBox), разрешающие перетаскивание даже при значении параметра Cancel = False.

    Событие BeforeDropOrPaste (Перед опусканием или вставкой)

    Возникает, когда пользователь собирается поместить или вставить данные в объект. Для набора страниц и полосы вкладок VBA инициирует это событие, когда перемещает данные на элемент, а для остальных элементов - перед операциями опускания или вставки.
    Набор параметров, передаваемых процедуре обработки события BeforeDropOrPaste, зависит от типа элемента управления. Они имеют такой вид:
  • для рамки (Frame) :
    Private Sub объект_BeforeDropOrPaste (ByVal Cancel As MSForms. ReturnBoolean, ctrl As Control, ByVal Action As fmAction, ByVal Data As DataObject, ByVal X As Single, ByVal Y As Single, ByVal Effect As MSForms. ReturnEffect, ByVal Shift As fmShiftState),
  • для набора страниц (MultiPage) :
    Private Sub объект_BeforeDropOrPaste (index As Long, ByVal Cancel As MSForms. ReturnBoolean, ctrl As Control, ByVal Action As fmAction, ByVal Data As DataObject, ByVal X As Single, ByVal Y As Single, ByVal Effect As MSForms. ReturnEffect, ByVal Shift As fmShiftState),
  • для полосы вкладок (TabStrip) :
    Private Sub объект_BeforeDropOrPaste (index As Long, ByVal Cancel As MSForms. ReturnBoolean, ByVal Action As fmAction, ByVal Data As DataObject, ByVal X As Single, ByVal Y As Single, ByVal Effect As MSForms. ReturnEffect, ByVal Shift As fmShiftState),
  • для других элементов:
    Private Sub объект_BeforeDropOrPaste (ByVal Cancel As MSForms. Return-Boolean, ByVal Action As fmAction, ByVal Data As DataObject, ByVal X As Single, ByVal Y As Single, ByVal Effect As MSForms. ReturnEffect, ByVal Shift As fmShiftState)

  • Здесь имя элемента объект и параметры index, Cancel, Data, Effect, X, Y и Shift имеют тот же смысл и значение, что и в процедуре обработки события BeforeDragOver, описанной выше. Параметр Action определяет результат выполняемой операции перетаскивания, основываясь на текущем состоянии клавиатуры. Возможные значения: fmActionPaste = 2 - вставить выбранный объект на предназначенное место, fmActionDragDrop = 3 - указывает, что пользователь перетащил объект с исходного места и опустил его на место назначения.
    При обработке этого события можно переустанавливать параметр Action (Action), чтобы задать то действие, которое следует выполнить.
    Если Effect равно fmDropEffectCopyOrMove, в качестве значения для Action можно установить fmDropEffectNone, fmDropEffectCopy или fmDropEffectMove. Если Effect равно fmDropEffectCopy или fmDropEffectMove, можно задать значение fmDropEffectNone. Если Effect установлено как fmDropEffectNone, Action изменять нельзя.

    Событие BeforeUpdate (Перед модификацией)

    Возникает перед изменением данных в элементе управления. Его появление не зависит от того, связаны ли данные элемента с таблицей Excel, т. е. определено ли свойство RowSource. Событие BeforeUpdate предшествует событиям AfterUpdate и Exit для того же элемента управления и событию Enter для элемента, который следующим попадет в фокус. Вот заголовок процедуры обработки этого события:
    Private Sub объект_BeforeUpdate (ByVal Cancel As MSForms. ReturnBoolean)
    Здесь объект - имя объекта, которому принадлежит процедура, а Cancel - обязательный статус обработки события, его значение False означает, что оно будет обрабатываться элементом (устанавливается по умолчанию), True - что событие будет обрабатываться приложением. При этом элемент остается в фокусе, но ни AfterUpdate, ни Exit для него не инициируются.

    Событие Change (Изменение)

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

  • Заголовок процедуры обработки события Change:
    Private Sub объект_Change ()
    где объект - имя объекта, которому принадлежит процедура. Эта процедура - то место в программе, где удобно синхронизировать данные в различных устройствах. Например, она позволяет при выборе нового элемента в списке показать относящийся к нему текст в поле ввода, или изменить показываемое пользователю числовое значение линейки прокрутки при перемещении ее бегунка и т. п.

    Событие Click (Щелчок)

    Это событие возникает по двум причинам:
  • пользователь щелкнул элемент управления;
  • пользователь явно выбрал некоторое значение при работе с элементом управления с несколькими возможными значениями.

  • По первой из этих причин событие Click возбуждается для таких элементов, как командная кнопка (CommandButton), рамка (Frame), изображение (Image), метка (Label), полоса прокрутки (ScrollBar) и счетчик (SpinButton). По второй - для элементов флажок (CheckBox), комбинированный список (ComboBox), список (ListBox), набор страниц (MultiPage), полоса вкладок (TabStrip) и выключатель (ToggleButton). Переключатель (OptionButton) также возбуждает это событие, когда меняет свое значение на True. В частности, событие Click наступает при:
  • щелчке элемента управления (щелчок правой кнопкой не вызывает событие Click) ;
  • щелчке свободного места диалогового окна и по отключенному серому элементу;
  • нажатии клавиш Enter или "пробел", когда в фокусе находится командная кнопка [если никакая командная кнопка не находится в фокусе, то при нажатии Enter событие Click наступает для предопределенной командной кнопки (со значением True свойства Default) ];
  • нажатии клавиши Esc в диалоговом окне с командной кнопкой, для которой свойство Cancel имеет значение True, при условии, что никакая другая командная кнопка не находится в фокусе;
  • нажатии клавиш-ускорителей.

  • При щелчке последовательно возбуждаются три события: MouseDown, MouseUp и Click.
    Для некоторых элементов управления событие Click связано с изменением значения элемента (свойства Value). Например, для набора страниц и полосы вкладок это происходит при щелчке новой закладки; для флажка и выключателя - при щелчке или нажатии клавиши "пробел", когда они находятся в фокусе, или при изменении их значений из программы; для списка или комбинированного списка - при выборе значения точно соответствующего некоторому значению в раскрывающемся списке (в частности, Click не возникает, если для строки в поле ввода комбинированного списка есть несколько возможных продолжений в раскрывающемся списке) и др. В этих случаях для отслеживания изменений значения элемента лучше использовать событие Change.
    Вот заголовок процедуры обработки события Click для набора страниц и полосы вкладок:
    Private Sub объект_Click (index As Long)
    а для остальных элементов управления:
    Private Sub объект_Click ()
    Здесь объект - имя объекта, которому принадлежит процедура, а index - индекс страницы, на которой произошел щелчок.

    Событие DblClick (Двойной щелчок)

    Возникает, когда пользователь, установив указатель на объект, производит двойной щелчок. Для элементов, возбуждающих и событие Click, к событию DblClick приводит следующая цепь событий:
  • MouseDown;
  • MouseUp;
  • Click;
  • DblClick.

  • А для элементов, не поддерживающих Click (например, поля ввода), из этой последовательности нужно удалить Click.
    Если возвращенное значение свойства Cancel равно True, то при двойном щелчке пользователя второй щелчок игнорируется. Это позволяет, например, сделать так, что одинарный и двойной щелчки имеют одинаковое действие.
    Заголовок процедуры обработки события Click для набора страниц и полосы вкладок имеет вид:
    Private Sub объект_DblClick (index As Long, ByVal Cancel As MSForms. ReturnBoolean)
    а для остальных элементов:
    Private Sub объект_DblClick (ByVal Cancel As MSForms. ReturnBoolean)
    Здесь объект - имя объекта, которому принадлежит процедура, index - индекс страницы, на которой произошел щелчок, а Cancel - обязательный статус обработки события, его значение. False (по умолчанию) означает, что оно будет обрабатываться элементом, True - что событие будет обрабатываться приложением.

    Событие DropButtonClick (Щелчок кнопки списка)

    Возникает всякий раз при появлении и исчезновении выпадающего списка. Причиной появления этого события может быть исполняемая программа (вызов метода DropDown) или нажатие пользователем клавиши F4. Когда список исчезает с экрана, событие DropButtonClick инициируется системой.
    Заголовок процедуры обработки события имеет вид:
    Private Sub объект_DropButtonClick ()

    Событие Error (Ошибка)

    Возникает, когда элемент управления, обнаружив ошибку (не обрабатываемую данным приложением Office 2000), не может возвратить информацию о ней в вызывающую программу.
    Процедура обработки события Error определяет действия системы при возникновении ошибки. Заголовок этой процедуры для набора страниц и полосы вкладок имеет вид:
    Private Sub объект_Error (index As Long, ByVal Number As Integer, ByVal Description As MSForms. ReturnString, ByVal SCode As SCode, ByVal Source As String, ByVal HelpFile As String, ByVal HelpContext As Long, ByVal CancelDisplay As MSForms. ReturnBoolean) ;
    для других элементов:
    Private Sub объект_Error (ByVal Number As Integer, ByVal Description As MSForms. ReturnString, ByVal SCode As SCode, ByVal Source As String, ByVal HelpFile As String, ByVal HelpContext As Long, ByVal CancelDisplay As MSForms. ReturnBoolean).
    Здесь параметры имеют следующий смысл:
  • объект - имя объекта, которому принадлежит процедура,
  • index - индекс страницы, на которой произошла ошибка,
  • Number - уникальный номер, используемый элементом для идентификации ошибки,
  • Description - текст с описанием ошибки,
  • SCode - задает код OLE статуса для данной ошибки (нижние 16 битов задают то же число, что и Number),
  • Source - строка, идентифицирующая элемент управления, который инициирует ошибку,
  • HelpFile - полное имя пути к справочному файлу, содержащему описание ошибки,
  • HelpContext - идентификатор контекста файла справки с темой, описывающей ошибку,
  • CancelDisplay - булев параметр, определяющий выводить ли описание ошибки в окне сообщения.


  • Событие KeyPress (Клавиша нажата)

    Возникает, когда пользователь нажимает клавишу с печатаемым символом (ANSI-клавишу). Это событие обычно происходит между нажатием и отпуском нажатой клавиши. Оно также появляется при посылке ANSI-кода в диалоговое окно из макроса с помощью операции SendKeys или из программы VBA оператором SendKeys. Точнее, событие KeyPress возникает при нажатии клавиш:
  • любого печатаемого символа;
  • Ctrl+символ стандартного алфавита;
  • Ctrl+ любой специальный символ;
  • Backspace;
  • Esc.

  • Событие KeyPress не возникает:
  • при нажатии клавиш Tab, Del или Enter;
  • при нажатии клавиш-стрелок;
  • когда при нажатии клавиши перемещается фокус.

  • Пока нажата ANSI-клавиша, чередуются события KeyDown и KeyPress. Когда пользователь отпускает эту клавишу, инициируется событие KeyUp. Сообщения о нажатии клавиш получает диалоговое окно или элемент управления, находящиеся в фокусе. При этом диалоговое окно может находиться в фокусе, только если в нем нет элементов управления либо все они отключены.
    Заголовки процедур обработки этих событий выглядят так:
    Private Sub объект_KeyPress (ByVal KeyANSI As MSForms. ReturnInteger)
    Здесь объект - имя объекта, которому принадлежит процедура, KeyANSI - число, являющееся стандартным ANSI-кодом нажатой клавиши.
    В процедуре обработки события KeyPress можно задать реакцию системы на нажатие пользователем стандартных клавиш или отмеченных выше комбинаций, имеющих код ANSI. Нажатия других клавиш или комбинаций следует обрабатывать процедурами для событий KeyDown и KeyUp.

    Событие Layout (Расположение)

    Возникает, когда диалоговое окно, рамка или набор страниц изменяют свои размеры. Для элементов со свойством AutoSize, равным True, это событие возникает, когда пользователь изменяет значения свойств, влияющих на размер элементов. Например, увеличение размера шрифта может привести к увеличению размеров поля ввода и рамки, в которой это поле находится, что вызовет событие Layout.
    Для набора страниц заголовок процедуры обработки события Layout имеет вид:
    Private Sub объект_Layout (index As Long),
    а для других объектов:
    Private Sub объект_Layout ()
    Здесь объект - имя объекта, которому принадлежит процедура, index - индекс той страницы из набора страниц, которая меняет размер.
    В процедуре обработки события Layout, можно вычислить новое положение элементов управления и перерисовать экран.

    Событие MouseMove (Мышь движется)

    Происходит, когда пользователь двигает мышь. Это событие применимо к диалоговым окнам, элементам управления и меткам. Оно непрерывно генерируется при прохождении указателя мыши по объекту. При сдвиге диалогового окна событие MouseMove может возникнуть и при постоянном положении мыши. Если диалоговое окно сдвигается в процедуре обработки этого события, может возникнуть непрерывная цепь событий MouseMove.
    Если два элемента управления расположены очень близко друг к другу, то при быстром движении мыши событие MouseMove может не возникнуть для пространства между этими элементами. В таком случае нужно предусмотреть обработку этого события для обоих элементов.
    Вот заголовок процедуры обработки события MouseMove для набора страниц и полосы вкладок:
    Private Sub объект_MouseMove (index As Long, ByVal Button As fmButton, ByVal Shift As fmShiftState, ByVal X As Single, ByVal Y As Single),
    а для других элементов:
    Private Sub объект_MouseMove (ByVal Button As fmButton, ByVal Shift As fmShiftState, ByVal X As Single, ByVal Y As Single),
    Здесь параметры объект, index, Button, Shift, X и Y имеют тот же смысл, что и для событий MouseDown и MouseUp рассмотренных выше. Но множество значений для параметра Button у MouseMove больше.
    ЗначениеОписание
    0Никакая кнопка не нажата.
    1Нажата левая кнопка.
    2Нажата правая кнопка.
    3Нажаты левая и правая кнопки.
    4Нажата средняя кнопка.
    5Нажаты левая и средняя кнопки.
    6Нажаты средняя и правая кнопки.
    7Нажаты все три кнопки.

    Значение, передаваемое в параметре Button, позволяет определить, какие кнопки мыши нажаты при ее движении, а параметр Shift - выяснить, какие из управляющих клавиш Shift, Ctrl и Alt при этом нажаты.

    Событие RemoveControl (Удаление элемента)

    Возникает при удалении элемента управления из диалогового окна, рамки или страницы.
    Заголовок процедуры обработки события RemoveControl для набора страниц имеет вид:
    Private Subобъект_RemoveControl (index As Long, ctrl As Control),
    а для других объектов:
    Private Sub объект_RemoveControl (ctrl As Control)
    Здесь объект - имя объекта, которому принадлежит процедура, index - индекс страницы, с которой удаляется элемент, ctrl - удаляемый элемент.

    Событие Scroll (Прокрутка)

    Возникает, когда пользователь изменяет положение бегунка в полосе прокрутки и может быть возбуждено для полосы прокрутки, диалогового окна и рамки. Источником события Scroll может быть также вызов метода Scroll для диалогового окна. Оно не наступает при изменении значения полосы прокрутки из программы или если пользователь щелкнул не бегунок.
    Заголовок процедуры обработки события Scroll для полосы прокрутки имеет вид:
    Private Sub объект_Scroll (),
  • для набора страниц:
    Private Sub объект_Scroll (index As Long, ActionX As fmScrollAction, ActionY As fmScrollAction, ByVal RequestDx As Single, ByVal RequestDy As Single, ByVal ActualDx As MSForms. ReturnSingle, ByVal ActualDy As MSForms. ReturnSingle),
  • для рамки и диалогового окна:
    Private Sub объект_Scroll (ActionX As fmScrollAction, ActionY As fmScrollAction, ByVal RequestDx As Single, ByVal RequestDy As Single, ByVal ActualDx As MSForms. ReturnSingle, ByVal ActualDy As MSForms. ReturnSingle).

  • Здесь параметры имеют следующий смысл:
  • объект - имя объекта, которому принадлежит процедура,
  • index - индекс страницы, на которой происходит прокрутка,
  • ActionX - действие в горизонтальном направлении,
  • ActionY - действие в вертикальном направлении,
  • RequestDx - расстояние в точках, на которое требуется выполнить прокрутку по горизонтали,
  • RequestDy - расстояние в точках, на которое требуется выполнить прокрутку по вертикали,
  • ActualDx - расстояние в точках, на которое произошла прокрутка по горизонтали,
  • ActualDy - расстояние в точках, на которое произошла прокрутка по вертикали.

  • Вот возможные значения параметров ActionX и ActionY:
  • fmScrollActionNoChange = 0 нет изменений;
  • fmScrollActionLineUp = 1 небольшой сдвиг вверх на вертикальной полосе; небольшой сдвиг влево на горизонтальной полосе; движение эквивалентно нажатию стрелок вверх и влево на клавиатуре;
  • fmScrollActionLineDown = 2 небольшой сдвиг вниз на вертикальной полосе; небольшой сдвиг вправо на горизонтальной полосе; движение эквивалентно нажатию стрелок вниз и вправо на клавиатуре;
  • fmScrollActionPageUp = 3 на одну страницу вверх на вертикальной полосе прокрутки, на одну страницу влево на горизонтальной полосе прокрутки; движение эквивалентно нажатию клавиши PgUp на клавиатуре для передвижения полосы прокрутки;
  • fmScrollActionPageDown = 4 на одну страницу вниз на вертикальной полосе прокрутки, на одну страницу вправо на горизонтальной полосе прокрутки; движение эквивалентно нажатию клавиши PgDn на клавиатуре для передвижения полосы прокрутки;
  • fmScrollActionBegin = 5 на верхний край вертикальной полосы прокрутки, на левый край горизонтальной полосы прокрутки;
  • fmScrollActionEnd = 6 на нижний край вертикальной полосы прокрутки, на правый край горизонтальной полосы прокрутки;
  • fmScrollActionPropertyChange = 8 означает, что изменилось значение свойства ScrollTop или свойства ScrollLeft; направления и размер сдвига зависят от того, какое из этих свойств изменилось и каково его новое значение;
  • fmScrollActionControlRequest = 9 элемент управления запрашивает свой контейнер выполнить сдвиг; размер сдвига зависит от конкретных взаимодействующих элементов;
  • fmScrollActionFocusRequest = 10 пользователь переходит к другому элементу управления; размер сдвига зависит от расположения выбранного элемента, этот элемент должен переместиться так, чтобы стать полностью видимым.

  • Событие Scroll с помощью параметров ActionX и ActionY сообщает о происшедшем действии, а с помощью параметров ActualDx и ActualDy передает расстояние, на которое перемещается полоса прокрутки. Используя эти данные, процедура обработки может вычислить новое положение бегунка в полосе и переместить его.

    Событие Zoom (Расширение)

    Возникает при изменении свойства Zoom у диалогового окна или элементов рамка и набор страниц.
    Заголовок процедуры обработки события Zoom для набора страниц имеет вид:
    Private Sub объект_Zoom (индекс As Long, Процент As Integer)
    а для рамки:
    Private Sub объект_Zoom (Percent As Integer)
    Здесь объект - имя объекта, которому принадлежит процедура, index - индекс страницы, которая изменяет размер, а Percent (Процент) - задает размер растяжения или сжатия в процентах. Его возможные значения лежат в диапазоне от 10% до 400%. Если значения меньше 100, размер диалогового окна уменьшается, больше 100 - увеличивается. В процедуре можно задать величину этого параметра.
    В следующем примере при расширении окна в него добавляется полоса прокрутки, а при уменьшении - она убирается.
    Private Sub UserForm_Zoom (Proc As Integer) Dim NewSize As Double If Proc >= 100 Then ' увеличение размера ScrollBars = fmScrollBarsBoth ' создать полосы прокрутки ScrollLeft = 0 ScrollTop = 0 NewSize = Width * Proc / 100 ScrollWidth = NewSize ' ширина полосы прокрутки NewSize = Height * Proc / 100 ScrollHeight = NewSize ' высота полосы прокрутки Else ' уменьшение размера ScrollBars = fmScrollBarsNone ' скрыть полосу прокрутки ScrollLeft = 0 ScrollTop = 0 End If End Sub

    События Enter, Exit (Вход, Выход)

    Событие Enter возникает перед тем, как фокус переместится на данный элемент из другого элемента диалогового окна. Exit возникает перед тем, как фокус переместится с данного элемента на другой.
    Заголовки процедур обработки этих событий имеют вид:
    Private Sub объект_Enter () Private Sub объект_Exit (ByVal Cancel As MSForms. ReturnBoolean)
    Здесь объект - имя объекта, которому принадлежит процедура, а Cancel - обязательный статус обработки события, его значение False показывает, что оно будет обрабатываться элементом (устанавливается по умолчанию), True - что событие будет обрабатываться приложением, при этом фокус остается на текущем элементе. Таким образом, чтобы оставить в фокусе текущий элемент в процедуре объект_Exit, задайте True параметру Cancel.
    Событие Enter естественно использовать для модификации данных в элементе в тот момент, когда он помещается в фокус, в зависимости от состояний и значений других элементов диалогового окна или от данных основного документа приложения. Exit позволяет проверять при выходе из поля ввода оставленные пользователем данные и сообщать об обнаруженных в них ошибках, а при выходе из списка изменять значения других элементов в зависимости от сделанного пользователем выбора.

    События KeyDown, KeyUp (Клавиша нажата, Клавиша отпущена)

    Событие KeyDown возникает, когда пользователь нажимает клавишу на клавиатуре, а событие KeyUp, - когда отпускает. Пока клавиша нажата, чередуются события KeyDown и KeyPress. Сообщения о нажатии клавиш получает диалоговое окно или элемент управления, находящиеся в фокусе. При этом диалоговое окно может находиться в фокусе, только если в нем нет элементов управления либо все они отключены ("серые"). Эти события появляются и при посылке в диалоговое окно кодов символов из макроса с помощью операции SendKeys или из программы VBA оператором SendKeys. Если посланный символ приводит к перемещению фокуса от одного элемента к другому, событие KeyDown происходит для первого из них, а события KeyPress и KeyUp - для второго.
    События KeyDown и KeyUp не возникают при нажатии клавиш, если нажата клавиша:
  • - Enter, а в диалоговом окне есть командная кнопка со свойством Default, равным True;
  • - Esc, а в диалоговом окне есть кнопка, для которой свойство Cancel равно True.

  • Заголовки процедур обработки этих событий выглядят так:
    Private Sub объект_KeyDown (ByVal KeyCode As MSForms. ReturnInteger, ByVal Shift As fmShiftState)
    Private Sub объект_KeyUp (ByVal KeyCode As MSForms. ReturnInteger, ByVal Shift As fmShiftState)
    Здесь объект - имя объекта, которому принадлежит процедура, KeyCode - число, являющееся кодом нажатой или отпущенной клавиши. Параметр Shift задает состояние клавиш Shift, Ctrl и Alt. Его возможные значения: fmShiftMask = 1 - нажата клавиша Shift, fmCtrlMask = 2 - нажата клавиша Ctrl, fmAltMask = 4 - нажата клавиша Alt.
    Типичные примеры использования событий KeyDown и KeyUp связаны с обработкой нажатий дополнительных (не буквенных) клавиш: функциональных, клавиш-стрелок, Home, End, Pageup, Pagedown и Tab, цифровых. Они позволяют отреагировать также на нажатие комбинаций, состоящих из клавиши стандартной клавиатуры и одной из дополнительных клавиш-модификаторов (Shift, Ctrl или Alt). Для работы же со стандартными символами ANSI удобней использовать событие KeyPress.

    События MouseDown, MouseUp (Мышь нажата, Мышь отпущена)

    Возникают, когда пользователь нажимает (MouseDown) и отпускает (MouseUp) кнопку мыши.
    Заголовки процедур обработки этих событий для набора страниц и полосы вкладок имеют вид:
    Private Sub объект_MouseDown (index As Long, ByVal Button As fmButton, ByVal Shift As fmShiftState, ByVal X As Single, ByVal Y As Single)
    и
    Private Sub объект_MouseUp (index As Long, ByVal Button As fmButton, ByVal Shift As fmShiftState, ByVal X As Single, ByVal Y As Single),
    а для других элементов:
    Private Sub объект_MouseDown (ByVal Button As fmButton, ByVal Shift As fmShiftState, ByVal X As Single, ByVal Y As Single)
    и
    Private Sub объект_MouseUp (ByVal Button As fmButton, ByVal Shift As fmShiftState, ByVal X As Single, ByVal Y As Single)
    Здесь параметры имеют следующий смысл:
  • объект - имя объекта, которому принадлежит процедура,
  • index - индекс страницы или вкладки, для которой инициируется событие,
  • Button - число, определяющее, какая кнопка нажата (1 - левая, 2 - правая, 4 - средняя),
  • Shift - состояние клавиш Shift, Ctrl и Alt,
  • X и Y - горизонтальная и вертикальная координаты указателя мыши (измеряются в точках от левого края и от верхнего края окна, рамки или страницы).

  • Возможные значения параметра Shift:
    ЗначениеОписание
    1нажата клавиша Shift.
    2нажата клавиша Ctrl.
    3нажаты клавиши Shift и Ctrl.
    4нажата клавиша Alt.
    5нажаты клавиши Alt и Shift.
    6нажаты клавиши Alt и Ctrl.
    7нажаты клавиши Alt, Shift и Ctrl.

    Для полосы вкладок параметр index определяет вкладку, которую щелкает пользователь. index - 1 означает, что пользователь щелкнул вне вкладки.
    В диалоговом окне события MouseDown и MouseUp генерируются, если пользователь нажимает и отпускает кнопку мыши, когда указатель находится на свободном месте окна или на полосе прокрутки.
    Типичная последовательность событий, связанных со щелчками выглядит так:
  • MouseDown;
  • MouseUp;
  • Click;
  • DblClick;
  • MouseUp.

  • Если кнопка мыши нажимается в тот момент, когда указатель находится над некоторым объектом, то этот объект "захватывает" мышь и получает в дальнейшем все связанные с ней события, включая последнее MouseUp. Поэтому координаты X и Y не всегда задают точку внутри границ объекта, получающего событие, связанное с мышью.
    С помощью процедур обработки событий MouseDown и MouseUp можно выяснить, какая кнопка мыши нажата, и определить реакцию системы на эти щелчки. Параметр Shift позволяет также выяснить, какие из управляющих клавиш Shift, Ctrl и Alt при этом нажаты.

    События SpinDown (Уменьшить счетчик), SpinUp (Увеличить счетчик)

    Событие SpinDown возникает, когда пользователь щелкает кнопку счетчика "стрелка-вниз" для вертикального счетчика или "стрелка-влево" для горизонтального счетчика. Оно уменьшает значение счетчика. Событие SpinUp возникает, когда выбираются кнопки-стрелки "вверх" или "вправо". Это событие связано с увеличением значения счетчика.
    Процедуры обработки этих событий не имеют аргументов:
    Private Sub объект_SpinDown (), Private Sub объект_SpinUp ().

    SpinButton - счетчик

    SpinButton (счетчик, ворот) позволяет пользователю увеличивать и уменьшать числовую характеристику до тех пор, пока он не установит требуемое значение. Один щелчок кнопки прокрутки увеличивает или уменьшает значение свойства Value на величину, заданную свойством SmallChange. Как и для ScrollBar, интервал изменения числовой характеристики определяется значениями свойств Min и Max, вертикальная или горизонтальная ориентация счетчика - свойством Orientation, а задержка между повторными событиями Change - свойством Delay.
    Чтобы изменения Value были видны пользователю, счетчик надо связать с полем ввода или с меткой в процедуре обработки события Change так же, как для полосы прокрутки.
    События: AfterUpdate, BeforeDragOver, BeforeUpdate, Change, Enter, Error, Exit, KeyDown, KeyUp, KeyPress, SpinDown, SpinUp.
    Методы: Move, SetFocus, ZOrder.

    Свойства поведения объекта

    Булево свойство AutoSize обеспечивает автоматическое приспособление размеров элемента управления к размерам его внутреннего содержимого (названия, текста, элемента списка). Значение по умолчанию False - размеры элемента считаются постоянными; если размеры содержимого выходят за рамки объекта, оно обрезается.
    Булево свойство Enabled определяет, может ли элемент управления оказаться в фокусе в результате действий пользователя. True (по умолчанию) делает элемент доступным. False отключает элемент, т. е. не позволяет пользователю получить доступ к нему с помощью мыши и клавиатуры (но элемент управления и в этом случае доступен для процедур программы). Отключенный элемент "сереет", т. е. выглядит на экране более тускло, чем включенный.
    Если отключена рамка Frame, одновременно недоступны все находящиеся в ней элементы управления.
    Булево свойство TabStop определяет, может ли пользователь установить фокус на данный элемент управления, используя стандартные клавиши Tab или Shift+Tab; по умолчанию равно True, т. е. элемент доступен.
    Когда в результате нажатия клавиши Tab или комбинации Shift+Tab фокус должен переместиться на элемент MultiPage или TabStrip, он устанавливается на первую не отключенную закладку соответствующего элемента, т. е. на страницу со значением True свойства Enabled. Если таковой нет, соответствующий элемент отключен и не может попасть в фокус.
    Если для командной кнопки значение Enabled - True, а TabStop - False, она не попадает в фокус при обходе элементов с помощью клавиши Tab, но окажется в фокусе, если ее щелкнуть (при условии, что ее свойство TakeFocusOnClick установлено в True).
    Булево свойство Locked определяет, может ли пользователь редактировать объект (изменять его значение). По умолчанию оно равно False, т. е. содержимое объекта доступно для редактирования.
    Комбинации свойств Enabled и Locked задают различные варианты поведения элемента управления:
  • - оба свойства истинны: элемент может попасть в фокус и пользователь может копировать, но не редактировать данные элемента; изображение элемента в диалоговом окне нормальное (не серое) ;
  • - Enabled истинно, Locked ложно: элемент управления может попасть в фокус, а его содержимое можно копировать и изменять;
  • - Enabled ложно, Locked истинно: элемент не может попасть в фокус и выглядит тусклым; его данные нельзя ни изменить, ни скопировать; то же происходит, когда оба эти свойства ложны.


  • TabStrip - полоса вкладок

    Элемент TabStrip внешне похож на элемент MultiPage. Он также представляет набор отдельных диалоговых окон - вкладок. Но эти вкладки не являются независимыми диалоговыми окнами - они не содержат элементов управления. Элементы в области, ограниченной рамкой вкладки, так же принадлежат диалоговому окну, как и сама полоса вкладок. Поэтому набор элементов управления, видимых на вкладке, достаточно определить один раз. Он автоматически будет одинаково выглядеть на всех вкладках.
    Полосы вкладок в основном используются для вывода/ввода однотипной информации, относящейся к разным объектам (это, например, анкеты, сведения об организациях-поставщиках и покупателях и т. п. ). Имена объектов служат заголовками вкладок. При этом объектов не должно быть слишком много, так как хотя бы начальная часть имени каждого должна быть видна на закладке. Для увеличения числа представленных объектов можно установить свойство MultiRow в True, что позволит создать несколько полос с закладками объектов.
    Каждая вкладка из TabStrip - объект типа Tab, а все они включены в коллекцию Tabs. Объекты класса Tab обладают собственными свойствами (например, Name, Caption, index), но не имеют собственных методов и событий. Поэтому для доступа и работы с ними используются события включающей их полосы вкладок TabStrip.
    При создании в элемент TabStrip автоматически помещаются две вкладки Tab1 и Tab2. Эти имена можно изменять и можно также добавлять новые вкладки. Чтобы на стадии проектирования добавить новую вкладку, переименовать, передвинуть или удалить имеющуюся, щелкните правой кнопкой мыши полосу закладок и выберите действие в появившемся контекстном меню.
    Как и для элемента MultiPage, свойство Value определяет номер текущей активной вкладки в коллекции Tabs, а свойство SelectedItem (его можно только читать) возвращает текущую активную вкладку (как объект). Его используют для установки значений свойств ее элементов управления, соответствующих объекту, информация о котором должна быть представлена на вкладке.
    Свойство Count возвращает число вкладок на полосе.

    Взгляните на различные способы доступа к отдельным вкладкам TabStrip: в процедуре инициализации диалогового окна Myform использовано 6 разных способов присвоения названий шести вкладкам Tab1 - Tab6.

    Private Sub UserForm_Initialize () Dim name1 As String ' Использование коллекции Tabs с числовым параметром TabStrip1. Tabs (0). Caption = "Вкладка 1" 'Использование коллекции Tabs со строковым параметром: name1 = TabStrip1. Tabs (1). Name TabStrip1. Tabs (name1). Caption = "Вкладка 2" 'Использование метода Item коллекции Tabs: TabStrip1. Tabs. Item (2). Caption = "Вкладка 3" ' с номером name1 = TabStrip1. Tabs (3). Name TabStrip1. Tabs. Item (name1). Caption = "Вкладка 4" ' с именем 'Использование имени (Name) объекта Tab: TabStrip1. Tab5. Caption = "Вкладка 5" 'Использование свойства SelectedItem: TabStrip1. Value = 5 ' установка активной вкладки TabStrip1. SelectedItem. Caption = "Вкладка 6" End Sub

    Номер текущей активной вкладки передается системой в качестве параметра процедуре, обрабатывающей событие Click. Это позволяет устанавливать нужные значения свойств. Допустим, полоса вкладок MyTabStrip содержит 3 вкладки с информацией о сотрудниках, и на них расположены элементы управления: метка Label1, заголовок которой определяет должность сотрудника, и поле ввода Address с его адресом. Тогда процедура установки этих свойств может выглядеть так:

    Private Sub MyTabStrip_Click (ByVal Index As Long) Select Case Index Case 0 Me. Label1. Caption = "зав. сектором" Me. Address. Text = "ул. Молодежная, 42, кв. 124" Case 1 Me. Label1. Caption = "администратор БД" Me. Address. Text = "ул. Московская, 12, кв. 34" Case 2 Me. Label1. Caption = "программист" Me. Address. Text = "пр. Спортивный, 143, кв. 56" End Select End Sub

    Большинство свойств элементов MultiPage и TabStrip совпадает как по имени, так и по значению.К ним относятся свойства Style, TabOrientation, MultiRow, TabFixedHeight и TabFixedWidth. Специфические свойства TabStrip - параметры ClientHeight, ClientLeft, ClientTop, ClientWidth - задают расположение и размеры (в точках) внутренней области вкладки, содержащей все элементы управления.

    События: BeforeDragOver, BeforeDropOrPaste, Change, Click, DblClick, Enter, Error, Exit, KeyDown, KeyUp, KeyPress, Layout, MouseDown, MouseUp, MouseMove.

    Методы: Move, SetFocus, ZOrder.

    Temp

    новое слово  теперь Привет, вот вставка рисунка  А текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается продолжается текст текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается
    Temp
    продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается текст теперь продолжается

    TextBox - поле ввода (окно редактирования)

    Элемент TextBox (поле ввода) - основное средство для ввода пользователем числовых и текстовых данных в систему. Его можно использовать и для вывода информации для пользователя. Поле ввода представляет собой простой одно - или многострочный текстовый редактор, имеющий, кроме указанных выше общих свойств объектов-элементов управления, целый ряд специфических свойств.
  • Свойство Text содержит текст, находящийся в поле ввода (то же значение имеет и свойство Value).
  • Булево свойство MultiLine определяет, сколько редактор содержит строк: одну или несколько. False (по умолчанию) - одна строка; True - допускается много строк.
  • Переход на новую строку при этом регулируется булевым свойством WordWrap. Если оно равно True, текст автоматически переносится на новую строку при достижении правой границы поля. При значении False новая строка в редакторе появляется лишь при обнаружении или при вставке в текст символа перехода на новую строку [клавиши Ctrl+Enter или Shift+Enter - при вводе и Chr (13) или Chr (10) - при выводе]. Иначе при достижении правой границы поля текст сдвигается влево, и ввод продолжается в той же строке. Параметр WordWrap влияет на работу только многострочного редактора (т. е. при MultiLine = True).
  • Булево свойство AutoSize заставляет поле автоматически менять размер, подстраивая его под текст. Его имеет смысл устанавливать в True для полей, используемых для вывода информации (например, для полей с многострочными заголовками или сообщениями).
  • Свойство MaxLength задает максимальную допустимую длину текста в поле ввода. По умолчанию равно 0, что означает отсутствие ограничений (точнее, ограничения накладываются лишь доступной памятью).
  • Свойство TextLength возвращает текущий размер текста в поле (для многострочного текста учитываются также символы перевода строки), а LineCount - текущее количество строк в тексте.
  • Свойство CurLine устанавливает или возвращает номер строки редактора, в которой находится курсор. Строки нумеруются с 0. Свойство CurX задает текущую горизонтальную позицию курсора, а свойство CurTargetX совпадает с CurX при работе в текущей строке, но сохраняет старое значение при переходе на новую строку в результате нажатия клавиш-стрелок, пролистывания или при вставке новой строки.
    Эти свойства позволяют управлять положением курсора при перемещении по строкам редактора. Единицей измерения для обоих свойств служит 0. 01 см. Все три свойства работают, только когда поле ввода находится в фокусе.
  • Свойство SelText возвращает текст, выделенный в поле ввода, свойство SelLength возвращает его длину, а SelStart определяет начальную позицию выделенного текста в поле, а если такого текста нет, то позицию курсора. Значения двух последних свойств измеряются в символах и могут лежать в интервале от 0 до общего числа символов в поле. Доступ ко всем трем свойствам возможен всегда независимо от того, находится ли поле ввода в фокусе. Изменение значения свойства SelStart приводит к снятию выделения с текста (SelText становится пустой строкой), установке курсора в новую позицию и установке SelLenght в 0.
  • Поле ввода можно использовать для ввода паролей и других данных, которые не должны появляться на экране, когда пользователь набирает их на клавиатуре. Эта функция поддерживается свойством PasswordChar, задающим символ-заместитель, который появляется на экране при нажатии пользователем любой клавиши. Обычно в качестве такого символа выбирают звездочку "*", тире "-", подчеркивание "_" и т. п. Свойство Text при этом сохраняет настоящий введенный пользователем текст.


  • События: AfterUpdate, BeforeDragOver, BeforeDropOrPaste, BeforeUpdate, Change, DblClick, DropButtonClick, Enter, Error, Exit, KeyDown, KeyUp, KeyPress, MouseDown, MouseUp, MouseMove.

    Методы: Copy, Cut, Move, Paste, SetFocus, ZOrder.

    ToggleButton - выключатель

    Элемент выключатель (ToggleButton) показывает, выбран/включен элемент или нет. По своим свойствам и поведению выключатель практически не отличается от элемента CheckBox. Внешнее их отличие в том, что флажок показывает свое состояние с помощью метки, стоящей рядом с его названием, а выключатель - через положение кнопки: "нажата"/"отжата", при этом название пишется внутри прямоугольных границ кнопки.
    События: AfterUpdate, BeforeDragOver, BeforeDropOrPaste, BeforeUpdate, Change, Click, DblClick, Enter, Error, Exit, KeyDown, KeyUp, KeyPress, MouseDown, MouseUp, MouseMove.
    Методы: Move, SetFocus, Zorder.

    Значение объекта

    Свойство Value определяет состояние элемента управления или его содержимое.
    Для элементов-кнопок CheckBox, OptionButton и ToggleButton свойство Value может принимать значения: Null - нейтральное состояние, True - элемент выбран (включен, отмечен), False - элемент не выбран (отключен, не отмечен).
    Для элементов ScrollBar и SpinButton значение Value - целое число в промежутке, заданном свойствами Min и Max.
    Для элементов-списков ComboBox и ListBox это значение в выбранной строке (если в списке несколько столбцов, берется значение из столбца, заданного свойством BoundColumn).
    Для управляющей кнопки CommandButton значение всегда False. Если из программы установить его в True, это вызовет событие Click (щелчок кнопки).
    Для элементов MultiPage и TabStrip значение задает номер текущей активной страницы (вкладки). Нумерация страниц начинается с 0.
    Значение элемента TextBox - текущий текст в поле редактирования.
    В списках, допускающих множественный выбор, свойство Value не применяется.
    Значение элемента управления, оказавшегося в фокусе, хранится также в свойстве BoundValue, которое сохраняет значение в процессе редактирования устройства, в то время как Value отражает текущие изменения значения. По завершении редактирования оба свойства совпадают.

    Основы офисного программирования и язык VBA

    Чтение файлов последовательного доступа

    Для чтения данных из файлов последовательного доступа используются операторы:
  • Input# ѕ данные в файле записаны оператором Write#
  • LineInput# ѕ данные в файле записаны оператором Print#.

  • Оператор Input# вызывается так:
    Input #номер-файла, список-переменных
  • Параметр номер-файла - номер открытого файла,
  • список-переменных - одно или несколько разделенных запятыми имен переменных, в которые будут считываться данные из файла. Переменные могут быть разных типов, естественное требование их тип должен быть согласован с типом записи в момент ее создания. Переменные не должны быть массивами или объектами, но могут быть элементами массивов или полями пользовательских записей.

  • При чтении автоматически выполняются преобразования данных, обратные тем, что использовались при записи оператором Write#. В частности, кавычки вокруг строк, разделяющие запятые и пустые строки, игнорируются. Слово #NULL# дает значение Null, #TRUE# и #FALSE# переводятся в булевы значения True и False, универсальные даты вида #yyyy-mm-dd hh:mm:ss# переводятся в формат соответствующих переменных типа дата/время, #ERROR номер-ошибки# при чтении передает в переменную типа Variant номер ошибки. Если для числовой переменной соответствующие по порядку в файле данные будут не числовыми, ей будет присвоено значение 0.
    По достижении конца файла, следующая попытка чтения из него приведет к ошибке. Во избежание этого для проверки на конец файла используется функцию:
    EOF(номер-файла)
    Она возвращает булево значение True, когда при чтении достигается конец файла, открытого в режиме последовательного доступа Input или произвольного доступа Random.
    Приведем процедуру чтения записей ранее созданного файла "readme.txt". Заметьте, мы поступаем корректно, читаемые переменные имеют тип, согласованный с типом читаемой записи.
    Пример 14.4.
    (html, txt)
    В результате будет напечатано:
    Первая строка файла Зона 1 Зона 2 Привет, старик! Мама мыла раму мылом. False 14.06.99 Null 3,1416 3,14 03,142 6 Error 2000
    Обратите внимание, процедура завершается циклом, типичным при чтении последовательных файлов.
    В данном случае он не работал ни разу, поскольку все записи файла уже были прочитаны к началу выполнения цикла.

    Для построчного ввода данных из файлов с последовательным доступом, созданных оператором Print, вызывается оператор Line Input#:

    Line Input #номер-файла, переменная

    Здесь номер-файла имеет тот же смысл, что и для оператора Input, а переменная - имя строковой переменной или переменной типа Variant, в которую будет прочитана очередная строка файла. Оператор Line Input# считывает данные посимвольно, пока не обнаружит признак конца строки - символ возврата каретки (Chr(13)) или пару "возврат каретки - перевод строки" (Chr(13) + Chr(10)). Эти признаки в переменную не записываются. Никаких преобразований данных при построчном вводе не производится. Мы уже приводили пример чтения файла "read.me", созданного оператором Print#.

    Следует заметить, что любой файл может быть открыт для чтения и прочитан оператором Line Input#. Другое дело, что, чаще всего, это не даст желаемого результата. Если, например, прочитать файл "readme.txt" с использованием оператора Line Input#, то будут получены следующие результаты:

    "Первая строка файла" "Зона 1","Зона 2" "Привет,","старик!" "Мама ","мыла ","раму мылом. " #FALSE# #1999-06-14# #NULL# 3.1416,"3,14","03,142" 6 #ERROR 2000#

    Несмотря на успех чтения, результаты не приемлемы, поскольку не выполняется нужного редактирования. Сохраняются кавычки, запятые, ограничители, в одной строке содержатся данные разных типов. Поэтому оператор Line Input # следует применять, как правило, для чтения строк из файлов, записанных с помощью оператора Print#.

    Оператор Line Input # позволяет читать файл строка за строкой. Наряду с этим в языке VBA есть возможность чтения из файла произвольного числа символов или произвольного числа байтов. Для чтения символов Input или Binary файлов используется функция Input, имеющая следующий синтаксис:

    Input(размер, [#]номер-файла)

    где размер - количество считываемых символов, номер-файла - номер открытого файла (последовательного или бинарного). Данные, считываемые этой функцией, обычно должны быть записаны в файл операторами Print# или Put.

    Еще один вариант этой функции:

    InputB(размер, [#]номер-файла)

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


    Несмотря на успех чтения, результаты не приемлемы, поскольку не выполняется нужного редактирования. Сохраняются кавычки, запятые, ограничители, в одной строке содержатся данные разных типов. Поэтому оператор Line Input # следует применять, как правило, для чтения строк из файлов, записанных с помощью оператора Print#.

    Оператор Line Input # позволяет читать файл строка за строкой. Наряду с этим в языке VBA есть возможность чтения из файла произвольного числа символов или произвольного числа байтов. Для чтения символов Input или Binary файлов используется функция Input, имеющая следующий синтаксис:

    Input(размер, [#]номер-файла)

    где размер - количество считываемых символов, номер-файла - номер открытого файла (последовательного или бинарного). Данные, считываемые этой функцией, обычно должны быть записаны в файл операторами Print# или Put.

    Еще один вариант этой функции:

    InputB(размер, [#]номер-файла)

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

    Один пример работы с Binary файлом

    Бинарные файлы пишутся и читаются порциями, состоящими из произвольного числа байтов. Доступ возможен к любому байту этого файла в произвольном порядке. Никакого редактирования при операциях с этим файлом не производится и вся ответственность за сохранением структуры информации ложится на программиста. Операции над этим файлом выполняются операторами Get и Put, но можно читать этот файл побайтно, используя и уже упоминавшуюся функцию Input. Мы не будем строить специальный пример для работы с этим файлом, а ограничимся примером чтения ранее созданного текстового файла "readme.txt", который откроем, как Binary файл. В этом примере мы прочитаем последовательно этот файл порциями по 20 байтов. Заодно покажем, как организуется чтение "хвоста" файла ѕ его последней порции. Возможно, с содержательной точки зрения пример не очень хорош, поскольку будет нарушена реальная структура читаемых данных, нов этом то и вся суть дела при работе с такими файлами. Программист сам должен восстановить структуру данных, чем мы, в данном случае, заниматься не будем. Но вот сам пример:
    Public Sub Test3() 'Чтение Binary файла Dim MyLocation As Long, MyLoc As Long, MyLine As String
    Open Path & "readme.txt" For Binary As #1
    MyLocation = 0 Do While MyLocation + 20 < LOF(1) ' Читаем допустимую порцию MyLine = Input(20, #1) MyLocation = Loc(1): MyLoc = Seek(1) ' 2 способа определения текущей позиции Debug.Print MyLine; Tab; MyLocation; Tab; MyLoc Loop 'Читаем последнюю порцию MyLoc = LOF(1) - MyLocation If MyLoc > 0 Then MyLine = Input(MyLoc, #1) MyLocation = Loc(1): MyLoc = Seek(1) Debug.Print MyLine, MyLocation, MyLoc, LOF(1) End If Close #1 End Sub
    Перед комментариями приведем результаты работы:
    "Первая строка файла 20 21 " "Зона 1","Зона 2" 40 41
    "Привет,","старик! 60 61 " "Мама ","мыла "," 80 81 раму мылом. " #FALS 100 101 E# #1999-06-14# #N 120 121 ULL# 3.1416,"3,14", 140 141 "03,142" 6 #ERROR 160 161 2000# 167 168 167

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

    И последний совет в этой лекции, средств VBA для работы с файлами произвольного доступа достаточно для выполнения основных операций: создания, записи, чтения и модификации данных. Но средства поиска данных в файле (оператор Seek) весьма примитивны. С их помощью трудно решать задачи упорядочения файлов, поиска записей по значениям отдельных полей и другие задачи, характерные для работы с базами данных. Для небольших файлов можно создавать индексные массивы, в которых номера записей упорядочены в соответствии с требуемым критерием, и поддерживать этот порядок при всех изменениях файла. Для больших - заводить свои индексные файлы и т. п. Но владельцам Office 2000 не следует заниматься программированием собственных СУБД. В их распоряжении - возможность переписать содержимое любого файла произвольного доступа в ячейки рабочего листа Excel или в базу данных (таблицу) Access и использовать для работы с его данными всю мощь этих инструментов.

    Как правило, приложения Office имеют

    Как правило, приложения Office имеют дело с файлами, хранящими документы этих приложений. Это файлы с документами Word (с расширениями.doc), рабочими книгами Excel (с расширениями.xls), базами данных Access (с расширениями.mdb). Операции по созданию, открытию и записи этих файлов выполняются с помощью соответствующих методов приложений, которые запускаются командами меню File. Но офисные системы должны уметь работать и с файлами других форматов. Например, часто требуется считывать данные из обычных текстовых файлов и заносить информацию, в них хранящуюся, в таблицы Excel или базы данных Access. Возможна и обратная ситуация, когда, используя данные из приложений Office, требуется сохранять полученную информацию в файлах. В общем, есть некоторые ситуации, когда предпочтительнее работать с собственными файлами, а не со стандартными базами данных. По этой причине в VBA включены средства для работы с внешними файлами, позволяющие их создавать, открывать для работы и осуществлять ввод-вывод данных. Файлы VBA делятся на три группы.

  • Файлы последовательного доступа могут открываться в режиме чтения (Input), записи (Output), или присоединения (Append). Специальных средств поиска для них нет, и чтобы получить данные из такого файла, его нужно открыть и прочесть последовательно все данные до нужного места. Изменения (запись данных) в таком файле всегда происходят в его конце. Существенной особенностью этого файла является редактирование данных в процессе записи и чтения. В файл можно записывать данные разных типов, При вводе они будут отредактированы и преобразованы в строку. Таким образом, последовательный файл можно рассматривать, как поток данных, представляющий последовательность строк переменой длины. Признаки конца строк и являются разделителями элементов файла. Доступ к элементам файла только последовательный и этот поток можно только читать или только писать, но нельзя делать то и другое одновременно.
  • Файлы произвольного доступа состоят из записей постоянной длины, что позволяет организовать быстрый поиск данных и их локальное изменение внутри файла без его переписывания.
    В таких файлах чаще всего хранят записи пользовательского типа, все поля которых имеют фиксированную длину, удобно также хранить числовые данные в двоичном формате. В этой модели файла поток данных представляет последовательность пронумерованных элементов одинаковой длины. Возможен прямой доступ к элементу по его номеру. Разрешается одновременно выполнять операции чтения и записи в файл, можно изменять содержимое отдельных элементов. Эта простая и эффективная в реализации модель файла очень удобна в тех случаях, когда она применима, например, как мы уже говорили для хранения числовых данных. Серьезным ограничением на ее применение является требование постоянства длины элементов файла.
  • Бинарные (или бинарного доступа) файлы позволяют считывать и записывать информацию побайтно; обычно используются для хранения графических данных или любой другой информации в неструктурированном виде. Здесь также как и в предыдущей модели, элементы файла имеют постоянную длину в 1 байт. Существенным отличием является то, что никакого редактирования в момент чтения или записи не производится. Одной операцией может быть записано или прочитано произвольное число байтов.


  • Можно заметить, что среди допустимых моделей файлов VBA нет наиболее общей, когда разрешен прямой доступ к элементам файла, когда элементы могут иметь произвольную длину, когда ключ может быть произвольным и не обязательно должен задавать номер записи. Отсутствие такого типа файлов действительно представляет серьезное ограничение, но этому есть и не менее серьезное оправдание. В таких сложных ситуациях рекомендуется переходить на работу с базой данных. Это тем более оправдано тем, что база данных Access доступна из любого приложения Office 2000.

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

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

    Тип доступаЗапись данныхЧтение данных
    ПоследовательныйPrint#, Write#Input#, Line Input#
    ПроизвольныйPut Get
    БинарныйPutGet

    Открытие и создание файлов

    Файлы перечисленных видов открываются и могут создаваться оператором Open, который вызывается так:
    Open имя-файла For режим [Access доступ] [блокировка] As [#]номерФайла [Len=длина-записи]
  • Параметр имя-файла - строковое выражение, задающее имя открываемого (создаваемого) файла, в него может входить также путь ѕ имя диска и имена каталогов (папок) на пути к файлу.
  • Параметр режим - ключевое слово, которое для файлов последовательного доступа может принимать одно из значений: Input (ввод), Output (вывод), Append (присоединение). Для файлов с произвольным доступом в качестве режима нужно указать Random, а для бинарных файлов - Binary.
  • Параметр доступ необязателен, он ограничивает набор операций, которые разрешено выполнять над открываемым файлом; возможные значения: Read(только чтение), Write (только запись), Read Write (и то и другое).
  • Параметр блокировка позволяет ограничить набор операций над открываемым файлом, выполняемых другими процессами; возможные значения: Shared (разделяемый, ѕ другим процессам разрешается работать с файлом без всяких ограничений), Lock Read(запретить чтение), Lock Write (запретить запись), Lock Read Write (запретить чтение и запись). Здесь требуется некоторые пояснения. С одним и тем же физическим файлом одновременно могут работать разные приложения. Возможна и ситуация, когда одно приложение одновременно работает с несколькими экземплярами файла, присваивая каждому экземпляру свой уникальный номер. Параметр блокировка позволяет регулировать отношения между клиентами, получающими доступ к файлу.
  • Параметр номерФайла - целое число в интервале от 1 до 511, идентифицирующее файл для других операций. Для каждой операции открытия файла номер-файла должен отличаться от номеров всех открытых в текущий момент файлов. Узнать очередной свободный номер позволяет функция FreeFile.
  • Параметр длина-записи - число не более 32767 - обязателен для файлов произвольного доступа и задает для них длину одной записи файла в байтах. Для последовательных файлов он необязателен (для них он задает размер буфера, создаваемого при открытии), для бинарных файлов он игнорируется.


  • Если последовательный файл, открываемый в режиме вывода (Output), существует, его содержимое будет утеряно, и данные будут записываться в его начало. Если его нет, создается новый файл. Точно так же будет создан новый файл, если файл, определяемый параметром имя-файла, не существует, а режим открытия задан как Append, Random или Binary. При попытке открыть несуществующий файл в режиме Input будет получено сообщение об ошибке.

    Последовательные файлы в режиме чтения, файлы произвольного доступа и бинарные можно открывать по несколько раз с различными номерами. С каждым из номеров будет связан свой буфер, что может повысить эффективность работы с файлом. Файл, открытый для чтения, должен быть закрыт перед его открытием в режимах записи Output или Append.

    Рассмотрим детали открытия и создания файлов. В первом примере открывается файл последовательного доступа "read.me" для чтения:

    Open "read.me" For Input As #1

    Имя можно передавать и через переменную или более сложное строковое выражение:

    Const Path = "e:\O2000\CD2000\Ch14\"

    Public Sub Openfile() Dim NewFile As String, NewNum As Integer NewFile = "read.me" NewNum = FreeFile ' очередной незанятый номер файла Open Path & NewFile For Output As NewNum

    End Sub

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

    Файл с последовательным доступом особенно удобен, когда при работе с ним требуется последовательно прочесть и обработать все записи файла. Если же данные из файла обрабатываются в произвольном порядке, или файл открывается для того, чтобы получить доступ к небольшому числу его записей, крайне желательно иметь возможность произвольного (прямого) доступа к нужному элементу файла. Такую возможность и обеспечивают файлы VBA с произвольным доступом, хотя у них есть серьезное ограничение, ѕ все записи должны иметь одинаковый размер.


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

    Type Person ' пользовательский тип записи. Возраст As Integer Фамилия As String * 20 Имя As String * 20 End Type

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

    Пример 14.1.

    (html, txt)

    В этом примере для определения свободного номера файла использовалась функция FreeFile, а для определения длины записи ѕ функция Len. Файл произвольного доступа открыт как для чтения, так и для записи, а бинарный файл только для чтения. Заметьте, что бинарный файл закрыт для чтения другими процессами. Поэтому если повторно запустить процедуру открытия, то файл произвольного доступа будет повторно открыт с другим номером, но возникнет ошибка при повторном открытии бинарного файла. Возможность одновременного открытия под разными номерами нескольких логических экземпляров одного и того же физического файла бывает крайне полезной в ряде ситуаций.

    Функция

    FileAttr(номер-файла, 1)

    по номеру открытого файла возвращает число, указывающее режим открытия: 1 - Input, 2 - Output, 4 - Random, 8 - Append, 32 - Binary. Второй аргумент достался функции FileAttr в наследство от 16-разрядного режима, где при его значении 2, она возвращала системный указатель на файл.

    Мы написали простую процедуру PrintAttr, которая по номеру файла определяет его атрибуты и выводит сообщение в окно отладки.Приведем результаты работы процедуры OpenTwoFiles:

    файл # 1 Открыт для Random Длина Записи: 42 файл # 2 Открыт для Binary


    В этом примере для определения свободного номера файла использовалась функция FreeFile, а для определения длины записи ѕ функция Len. Файл произвольного доступа открыт как для чтения, так и для записи, а бинарный файл только для чтения. Заметьте, что бинарный файл закрыт для чтения другими процессами. Поэтому если повторно запустить процедуру открытия, то файл произвольного доступа будет повторно открыт с другим номером, но возникнет ошибка при повторном открытии бинарного файла. Возможность одновременного открытия под разными номерами нескольких логических экземпляров одного и того же физического файла бывает крайне полезной в ряде ситуаций.

    Функция

    FileAttr(номер-файла, 1)

    по номеру открытого файла возвращает число, указывающее режим открытия: 1 - Input, 2 - Output, 4 - Random, 8 - Append, 32 - Binary. Второй аргумент достался функции FileAttr в наследство от 16-разрядного режима, где при его значении 2, она возвращала системный указатель на файл.

    Мы написали простую процедуру PrintAttr, которая по номеру файла определяет его атрибуты и выводит сообщение в окно отладки. Приведем результаты работы процедуры OpenTwoFiles:

    файл # 1 Открыт для Random Длина Записи: 42 файл # 2 Открыт для Binary

    Dim attr As Integer, Num

    Public Sub OpenTwoFiles() Dim attr As Integer, Num As Integer Dim MyFriend As Person ' объявление переменной ' Открываем файл с произвольным доступом для записи: Num = FreeFile Open "Friends" For Random Access Read Write As Num Len = Len(MyFriend) PrintAttr (Num) Debug.Print "Длина Записи:", Len(MyFriend) ' Открываем двоичный файл Num = FreeFile Open "pict.bmp" For Binary Access Read Lock Read As Num PrintAttr (Num)
    End Sub
    Public Sub PrintAttr(ByVal Num As Integer) 'Эта процедура по номеру файла определяет его атрибуты 'и выводит соответствующее сообщение в окно отладки Dim Msg As String, attr As Integer
    attr = FileAttr(Num, 1) Debug.Print "файл # ", Num Select Case attr Case 1: Msg = "Input" Case 2: Msg = "Output" Case 4: Msg = "Random" Case 8: Msg = "Append" Case 32: Msg = "Binary" Case Else: Msg = "Таких файлов не бывает" End Select Debug.Print "Открыт для ", Msg
    End Sub
    Пример 14.1.
    Закрыть окно




    Public Sub WritingWithPrint() Dim MyBool As Boolean, MyDate As Date Dim MyNull As Variant, MyFloat As Double, MyErr As Variant
    Print #1, "Первая строка файла" ' запись текстовой строки. 'Первая строка файла Print #1, "Зона 1"; Tab; "Зона 2" ' запись в двух позициях табуляции. 'Зона 1 Зона 2 Print #1, "Зона 1", "Зона 2" ' запятая разделяет как табуляции. 'Зона 1 Зона 2 Print #1, "Привет,"; "старик!" '; склеивает выражения. 'Привет,старик! Print #1, "Привет,"; "старик!" ' пробел действует аналогично. 'Привет,старик! Print #1, Spc(5); "5 пробелов " ' вставка 5 пробелов слева. ' 5 пробелов Print #1, Tab(20); "Старик,"; Tab(10); "привет!" ' печать в соответствующих столбцах. ' Старик ' привет! Print #1, "one"; " "; "two" 'разделение пробелом 'one two MyBool = False ' булева переменная Print #1, MyBool 'False MyDate = #6/14/1999# ' значение даты Print #1, MyDate ' печать в кратком формате '14.06.99 MyNull = Null ' нулевое значение Print #1, MyNull 'Null MyFloat = 3.1416 'вещественное значение ' использование функции Format: Print #1, MyFloat, Format(MyFloat, "#.00"), Format(MyFloat, "00.000") '3,1416 3,14 03,142 On Error Resume Next Err.Raise 6 MyErr = Err Print #1, MyErr MyErr = CVErr(2000) Print #1, MyErr End Sub
    Пример 14.2.
    Закрыть окно




    Public Sub WritingWithWrite() Dim MyStr As String, MyBool As Boolean, MyDate As Date Dim MyNull As Variant, MyFloat As Double, MyErr As Variant
    'Открытие файла readme.txt Open Path & "readme.txt" For Output As #7
    'Создание файла Write #7, "Первая строка файла" ' запись текстовой строки. Write #7, "Зона 1", "Зона 2" ' запись двух строк. Write #7, "Привет,", "старик!" 'еще две строки MyStr = "раму мылом. " Write #7, "Мама ", "мыла ", MyStr MyBool = False ' булева переменная Write #7, MyBool MyDate = #6/14/1999# ' значение даты Write #7, MyDate ' запись даты MyNull = Null ' нулевое значение Write #7, MyNull MyFloat = 3.1416 'вещественное значение ' использование функции Format: Write #7, MyFloat, Format(MyFloat, "0.00"), Format(MyFloat, "00.000") On Error Resume Next Err.Raise 6 MyErr = Err Write #7, MyErr MyErr = CVErr(2000) Write #7, MyErr
    End Sub
    Пример 14.3.
    Закрыть окно




    Public Sub ReadingWithInput() Dim MyStr As String, MyBool As Boolean, MyDate As Date Dim MyNull As Variant, MyFloat As Double, MyErr As Variant Dim i As Integer 'Открытие файла readme.txt Open Path & "readme.txt" For Input As #7
    'Чтение файла 'Первые 8 строк For i = 1 To 8 Input #7, MyStr Debug.Print MyStr Next i
    'Данные разных типов Input #7, MyBool, MyDate, MyNull, MyFloat Debug.Print MyBool, MyDate, MyNull, MyFloat
    'читаем отформатированные числа Input #7, MyStr Debug.Print MyStr Input #7, MyStr Debug.Print MyStr
    'дважды читаем данные типа Error Input #7, MyErr Debug.Print MyErr Input #7, MyErr Debug.Print MyErr
    'чтение до конца файла Do While Not EOF(7) Input #7, MyStr Debug.Print MyStr Loop End Sub
    Пример 14.4.
    Закрыть окно




    Public Sub CreateRec(Rec As Товар) ' Создает запись типа Товар Randomize Rec.КодТовара = Int(Rnd * 9 + 1) 'Код от 1 до 9 Rec.Наименование = "Товар" & Rec.КодТовара Rec.Цена = Int(Rnd * 99 + 1) * Rec.КодТовара
    End Sub
    Public Sub PrintRec(Rec As Товар) 'Печать записи о товаре Debug.Print "Код Товара:", Rec.КодТовара, "Цена:", Rec.Цена End Sub
    Public Sub CreateRandomFile() Dim i As Integer, NewRec As Товар Open Path & "Товары.9" For Random Access Write As #1 Len = Len(NewRec) 'Создаем 10 записей For i = 1 To 10 Call CreateRec(NewRec) Call PrintRec(NewRec) Put #1, NewRec.КодТовара, NewRec
    Next i Debug.Print "Файл Товары.9 успешно создан" Debug.Print "Размер файла", LOF(1) Close #1 End Sub
    Пример 14.5.
    Закрыть окно




    Public Sub Test2() 'Эта процедура работает с файлом, записи которого ' содержат строки переменной длины, не превосхоящей максимума, 'а также данные типа Variant
    Dim Fam As String Dim Other As Variant 'Открытие файла произвольного доступа для чтения и записи Open Path & "Strings.var" For Random Access Read Write As #5 Len = 20
    Fam = "Степанов" Put 5, 1, Fam Get 5, 1, Fam Debug.Print Fam, LOF(5) Fam = "Архангельский" Put 5, 2, Fam Get 5, 2, Fam Debug.Print Fam, LOF(5) Fam = "Куц" Put 5, 1, Fam Get 5, 1, Fam Debug.Print Fam, LOF(5)
    'Запись типа Variant Other = "Петров" Put 5, 3, Other Get 5, 3, Other Debug.Print Other, LOF(5) Other = 125.25 Put 5, 4, Other Get 5, 4, Other Debug.Print Other, LOF(5) Other = 125 Put 5, 5, Other Get 5, 5, Other Debug.Print Other, LOF(5)
    Close #5 End Sub
    Пример 14.6.
    Закрыть окно



    Работа с данными переменной длины

    До сих пор мы усиленно подчеркивали, что при работе с файлом произвольного доступа размер записи фиксирован и это предполагает хранение в таких файлов данных, размер которых фиксирован в момент их объявления. На самом деле, это не так и в таких файлах можно хранить и строки переменной длины и данные типа Variant. В этих случаях просто следует быть более осторожным, поскольку из-за сложности правил хранения таких данных повышается возможность ошибки. Вот некоторые правила, согласно которым оператор Put размещает данные в файле, а оператор Get их читает:
  • Данные всегда помещаются в файл, начиная с границ записей, установленных параметром Len при открытии файла. Если при этом длина данных, записываемых в файл, меньше размера записи, заданного параметром Len, оператор Put заполнит остающееся до длины записи место "случайными" данными из буфера файла, а новые данные поместит, начиная со следующей границы. При попытке записать данные, длина которых больше Len, появится сообщение об ошибке. Таким образом, параметр Len задает максимальный размер записи, внутри которого может храниться значение переменного размера.
  • При записи строк переменной длины, переменных типа Variant, динамических массивов VBA помещает туда же дополнительную информацию для их расшифровки.
  • При записи строки переменной длины Put помещает непосредственно перед ней 2 байта с ее длиной.
  • Два дополнительных байта также помещаются в файл при записи переменной типа Variant. Их содержимое идентифицирует тип значения (как в функции VarType). Например, если оно равно 3, это соответствует типу Long, и запись такой переменной займет 6 байтов (2 - для типа и 4 - для самого числа). Значение VarType = 8 соответствует строковому типу (String). В этом случае Put запишет 2 байта с VarType, затем 2 байта с длиной строки и затем саму строку.
  • В случае динамического массива Put записывает перед его элементами описатель с длиной 2 + 8* (число измерений массива).
  • Массивы фиксированного размера записываются без описателей.
    Без всякой дополнительной информации записываются и значения остальных типов переменных с фиксированными размерами.
  • Для бинарных файлов параметр Len при записи роли не играет. Put записывает значения переменных подряд без пропусков и не помещает длины динамических строк и дескрипторы массивов.
  • Для переменных с нефиксированными размерами Get выполняет преобразования, обратные тем, что при записи выполнял оператор Put. Например, при чтении в строку переменной длины Get определяет по 2-байтовому описателю длину строки, а затем читает ее содержимое в переменную. Аналогичная расшифровка происходит и при чтении в переменные типа Variant и в динамические массивы.
  • Для бинарных файлов Get читает данные подряд. В переменные фиксированного размера считывается соответствующее их размеру число байтов. При чтении в строку переменной длины в нее считывается число символов, равное длине ее текущего значения.


  • Приведем пример работы с файлом произвольного доступа, записи которого содержат данные не фиксированного размера:

    Пример 14.6.

    (html, txt)

    Вот результаты ее работы:

    Степанов 10 Архангельский 35 Куц 35 Петров 50 125,25 70 125 84

    Приведем комментарии:

  • Операторы Put и Get позволяют записать и корректно прочитать данные переменного размера, не превосходящего максимальный размер, заданный при открытии файла.
  • При записи первой строки переменной длины размер файла стал равным 10 байтам, из которых 8 байтов занимает сама строка, а два байта, предшествующий ей описатель. Описатель позволяет оператору Get корректно прочитать эту строку.
  • Запись второй строки начинается с 21-го байта, границы первой записи, общий размер файла становится равным 35 с учетом размера второй строки и ее описателя.
  • Третья запись не изменяет размера файла, поскольку происходит изменение значения первой записи, так что файл по-прежнему содержит только две записи.
  • Далее начинается запись данных типа Variant. Теперь при записи строки используются два описателя, занимающие 4 байта.
  • Две последние записи содержат числовые данные Double (8 байтов) и Integer (2 байта) с их описателями.


  • Заканчивая тему работы с файлами произвольного доступа, хотим еще раз обратить внимание на некоторые опасные моменты, возникающие в процессе работы с ними и не контролируемые VBA:

  • Явно задавайте при открытии файла, как при чтении, так и при записи параметр Len ѕ максимальный размер записи. Если этого не сделать, то программа будет работать, но, к сожалению, результаты могут быть не предсказуемыми.
  • Будьте особенно внимательными при работе с данными переменной длины, помните, что к данным автоматически присоединяются описатели.
  • Размер файла определяется записью с максимальным номером. Чтобы избежать большого числа "дырок" в файле стройте разумные функции ключа так, чтобы ключи записей занимали достаточно плотный начальный интервал от 1 до N.



  • Описатель позволяет оператору Get корректно прочитать эту строку.
  • Запись второй строки начинается с 21-го байта, границы первой записи, общий размер файла становится равным 35 с учетом размера второй строки и ее описателя.
  • Третья запись не изменяет размера файла, поскольку происходит изменение значения первой записи, так что файл по-прежнему содержит только две записи.
  • Далее начинается запись данных типа Variant. Теперь при записи строки используются два описателя, занимающие 4 байта.
  • Две последние записи содержат числовые данные Double (8 байтов) и Integer (2 байта) с их описателями.


  • Заканчивая тему работы с файлами произвольного доступа, хотим еще раз обратить внимание на некоторые опасные моменты, возникающие в процессе работы с ними и не контролируемые VBA:

  • Явно задавайте при открытии файла, как при чтении, так и при записи параметр Len ѕ максимальный размер записи. Если этого не сделать, то программа будет работать, но, к сожалению, результаты могут быть не предсказуемыми.
  • Будьте особенно внимательными при работе с данными переменной длины, помните, что к данным автоматически присоединяются описатели.
  • Размер файла определяется записью с максимальным номером. Чтобы избежать большого числа "дырок" в файле стройте разумные функции ключа так, чтобы ключи записей занимали достаточно плотный начальный интервал от 1 до N.


  • Ввод-вывод для файлов произвольного доступа и бинарных файлов

    Для последовательных файлов запись данных всегда происходит в конец файла, а чтение после открытия производится с самого начала. Для бинарных файлов и файлов произвольного доступа операция чтения выполняется оператором Get, операция записи ѕ оператором Put. Эти операции позволяют читать и записывать записи в произвольном порядке. Установить нужную позицию записи или чтения можно, вызвав оператор Seek:
    Seek [#]номер-файла, позиция
    Параметр номер-файла - номер открытого в режиме Random или Binary файла; позиция - число от 1 до 2 147 483 647, определяющее место в файле, куда будут помещены данные следующим оператором Put или откуда они будут считаны следующим оператором Get. Для файлов произвольного доступа позиция задает номер записи в файле, для бинарных и последовательных файлов - номер байта (символа). Иногда, в процессе поиска позиции необходимо передвинуться вперед или назад на некоторое число записей относительно текущей позиции. Функция Seek может быть использована для того, чтобы определить текущую позицию:
    Seek(номер-файла)
    Она возвращает число в диапазоне от 1 до 2 147 483 647. Для файлов произвольного доступа - это номер следующей считываемой или записываемой записи, для файлов, открытых в остальных режимах, - позиция байта, в которой будет выполняться следующая операция
    Операторы Put и Get позволяют сами установить позицию, переопределив, тем самым, позицию, установленную оператором Seek. Эти операторы имеют одинаковый, простой и ясный синтаксис:
    Put [#]номер-файла, [номер-записи], переменная Get [#]номер-файла, [номер-записи], переменная
    Параметры этих операторов имеют следующий смысл:
  • Параметр номер-файла - номер открытого в режиме Random или Binary файла;
  • номер-записи - числовое выражение, определяющее позицию в файле, куда будут помещены вводимые данные или откуда данные будут прочитаны. Для файлов произвольного доступа - это номер записи в файле, для бинарных файлов - порядковый номер байта в файле. Нумерация записей (байтов) в файле начинается с 1.
    Если параметр номер- записи опустить, данные будут записаны (прочитаны) в текущую позицию. Уже говорилось, что установить текущую позицию можно оператором Seek. Отметим также, что после выполнения операторов Get и Put текущей становится позиция очередной записи (байта), следующей за только что прочитанной записью (байтом). Заметьте, даже если этот параметр опущен, разделяющая запятая все же остается в операторе вызова.
  • Параметр переменная - имя переменной, значение которой должно быть записано в файл (в которую будет прочитана запись).


  • Вот простой пример записи и чтения в файл произвольного доступа:

    Public Sub Test1() Dim N As Integer Open Path & "test.1" For Random Access Read Write As #1 Len = 2 N = 1 Put 1, 1, N Get 1, 1, N Debug.Print N, LOF(1) N = 2 Put 1, 2, N Get 1, 2, N Debug.Print N, LOF(1) N = 3 Put 1, 1, N Get 1, 1, N Debug.Print N, LOF(1) Close #1

    End Sub

    Используемая здесь функция LOF(номер-файла) возвращает длину файла в байтах. Заметьте, будет создан файл из двух записей, поскольку третий по счету оператор Put изменяет значение первой записи, так что перед закрытием функция LOF возвратит значение 4.

    Но давайте рассмотрим чуть более серьезный пример. Рассмотрим работу с файлом, который назовем "Товары.9". Этот файл будет содержать информацию о товарах. Код товара будет служить ключом записи. Зная код товара, можно будет найти запись в файле. На практике, когда формальным ключом записи, как в нашем случае, является ее порядковый номер, всегда задается некоторая функция, которая реальный ключ, возможно заданный несколькими параметрами, преобразует в порядковый номер. В нашем примере, чтобы не вводить таких преобразований, будем полагать, что код товара изменяется в пределах от 1 до N и, следовательно, он может служить формальным ключом записи. Вот как задается пользовательский тип, описывающий товар:

    Type Товар КодТовара As Integer Наименование As String * 10 Цена As Integer End Type

    Создадим файл, хранящий записи этого типа.


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

    Пример 14.5.

    (html, txt)

    Приведем результаты отладочной печати:

    Код Товара: 7 Цена: 294 Код Товара: 3 Цена: 141 Код Товара: 3 Цена: 84 Код Товара: 1 Цена: 18 Код Товара: 6 Цена: 246 Код Товара: 8 Цена: 472 Код Товара: 8 Цена: 376 Код Товара: 7 Цена: 483 Код Товара: 2 Цена: 168 Код Товара: 8 Цена: 224 Файл Товары.9 успешно создан Размер файла 112

    Прокомментируем эти процедуры и полученные результаты. Первая процедура генерирует запись, используя датчик случайных чисел ѕ функцию Rnd. Ранее в своих примерах мы уже использовали эту функцию и подробно говорили о том, как она работает. Вторая процедура позволяет вывести запись в окно отладки. Основной является процедура CreateRandomFile, в которой открывается файл "Товары.9" как файл с произвольным доступом, открытый для записи. При открытии, что очень важно, мы не забыли указать длину записи. Поскольку файл предназначен для хранения значений переменных типа "Товар", то этот тип и определяет длину записи, ѕ она равна 14 байтов (10 байтов занимает строка постоянной длины и 4 байта требуется для двух полей типа Integer).В процедуре 10 раз генерируется новая запись и 10 раз выполняется оператор Put, производящий запись в файл. Но как показывают результаты отладочной печати, некоторые коды повторяются, поэтому в таких случаях происходит обновление содержимого записи. В данном примере только 6 записей имеют уникальный код, и 4 раза происходило обновление уже созданных записей. Важно обратить внимание на длину созданного файла. Анализируя эту характеристику, можно понять, что файл содержит 8 записей, а не 10 и не 6. Дело в том, что длина файла произвольного доступа определяется записью с максимальным номером. Пусть в файле есть запись с максимальным номером M. И пусть добавляется новая запись с номером N, большим M. В этот момент в файле будут созданы новые записи с номерами от M+1 до N включительно.


    Запись с номером N будет действительно записана, а остальные получат значения по умолчанию, в соответствии с соглашениями, принятыми в VBA. Так что, если в файл пишется одна единственная запись с ключом 10000, то это означает, что в файле автоматически появляется 10000 записей и таков будет его размер. В нашем примере максимальный код равен 8,ѕ отсюда и размер файла равен 112 = 8*14

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

    Public Sub PrintFile() Dim N As Integer, i As Integer, Code As Integer Dim MyRec As Товар 'Открыть файл Товаров для обработки Open Path & "Товары.9" For Random Access Read As #1 Len = Len(MyRec)

    N = LOF(1) \ Len(MyRec) 'Число записей Debug.Print "Число записей файла-- ", N For i = 1 To N 'Установить позицию Seek 1, i 'Получить запись с заданным кодом Get #1,, MyRec Call PrintRec(MyRec) Next i Close #1 End Sub

    Вот на какие моменты следует обратить внимание. Файл открывается теперь для чтения и при открытии корректно указывается длина записи. Для подсчета числа записей файла используется упоминавшаяся функция LOF. Заметьте, мы используем оператор Seek для установки позиции, но это скорее для проформы, поскольку в данной ситуации Seek не изменяет уже установленной позиции. Вот результаты работы этой процедуры:

    Число записей файла-- 8 Код Товара: 1 Цена: 18 Код Товара: 2 Цена: 168 Код Товара: 3 Цена: 84 Код Товара: 0 Цена: 0 Код Товара: 0 Цена: 0 Код Товара: 6 Цена: 246 Код Товара: 7 Цена: 483 Код Товара: 8 Цена: 224

    Мы уже пояснили, почему в файле оказалось 8 записей и почему часть из них имеет нулевые значения.

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

    Public Sub WorkWithFile() Dim N As Integer, i As Integer, Code As Integer Dim MyRec As Товар Randomize 'Открыть файл Товаров для обработки Open Path & "Товары.9" For Random Access Read As #1 Len = Len(MyRec)


    N = Int(Rnd * 9 + 1) 'Число обрабатываемых записей Debug.Print "Число обрабатываемых записей -- ", N For i = 1 To N Code = Int(Rnd * 9 + 1) ' Установить позицию Seek 1, Code 'Получить запись с заданным кодом Get #1,, MyRec If MyRec.КодТовара = 0 Then Debug.Print "В файле нет записи с кодом:", Code Else: Call PrintRec(MyRec) End If Next i Close #1 End Sub

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

    Число обрабатываемых записей -- 2 Код Товара: 8 Цена: 224 В файле нет записи с кодом: 5

    Закрытие файлов

    Для закрытия файла служит оператор Close, вызываемый:
    Close [список-номеров-файлов]
    Параметр список-номеров-файлов может содержать один или несколько номеров ранее открытых файлов, которые должны быть закрыты. Как обычно, номера в списке разделяются запятыми. Если параметр опущен, то закрываются все открытые в данный момент файлы. После выполнения оператора Close номера закрытых файлов снова можно использовать при открытии в операторе Open.
    Закрыть все открытые оператором Open файлы может также оператор Reset. Как и вызов Close без параметров, он освободит все номера открытых файлов и перенесет содержимое их буферов на диск.

    Запись в файлы последовательного доступа

    При записи и чтении данных в последовательный файл происходит автоматическое редактирование данных. В зависимости от того, на кого ориентировано это редактирование, человека или программу компьютера применяются две формы редактирования. Соответственно операторы записи и чтения существуют в двух вариантах:
  • Для записи "человеко-ориентированных" данных применяется оператор Print#, представляющий данные в формате, подготовленном для отображения на экране дисплея. По существу, мы уже хорошо знакомы с этим оператором, поскольку ни одна наша программа не обходилась без вывода данных в окно отладки, а применяемый для этих целей метод Print объекта Debug по синтаксису и по действию подобен оператору Print#.
  • Для записи данных, обрабатываемых после их чтения программой, используется оператор Write#. Заметим, в каком - то смысле это более универсальный оператор, чем Print, он, например, не зависит от локализации.

  • Оператор Print# записывает в файл форматированные данные. Его вызов имеет вид:
    Print #номер-файла, [список-вывода]
    Здесь номер-файла - номер файла, открытого в режиме последовательного доступа для записи (Append или Output), список-вывода - одно или несколько выражений, разделенных пробелами или точками с запятой, определяющих выводимые данные. Каждое выражение в этом списке может быть снабжено дополнительной информацией о расположении данных, делающее их восприятие более удобным для пользователя. Общий вид такого выражения:
    [{Spc(n) | Tab[(n)]}] [выражение] [симв-поз]
  • Spc(n) задает количество вставляемых пробелов.
  • Альтернативный параметр Tab(n) указывает абсолютный номер n столбца, в котором будет начинаться текст, определяемый параметром выражение. Если не указывать аргумент у Tab, то позицией вставки является следующая зона печати. (Можно полагать, что при выводе экран разделен на зоны, и табуляция позволяет переходить от одной зоны к другой).
  • Параметр выражение задает выражение, значение которого после редактирования и преобразования его в текстовую строку будет записано в файл.
    На выражения не накладывается особых ограничений, Это могут быть как строковые, так и числовые выражения, они могут содержать данные разных типов. Даты записываются в файл в кратком формате, значения булевых переменных - как ключевые слова Ложь (False) или Истина (True), пустое значение Empty - как пустое слово, нулевое значение Null выводится как "Null". Данные об ошибке выводятся в виде кода ошибки. Числовые данные выводятся в формате, зависящем от локализации. При выводе можно вызвать для форматирования данных функцию Format.
  • Параметр симв-поз задает положение следующего выражения. Если он равен ";" или пробелу, вставка следующего выражения будет происходить сразу за последним выведенным символом, Tab(n) указывает номер n столбца, начиная с которого будет выводиться выражение. Tab без аргумента или "," - переход в следующую позицию табуляции (зону печати).


  • Создадим теперь открытый ранее файл read.me, записывая в него выражения, позволяющие продемонстрировать все возможности оператора Print:

    Пример 14.2.

    (html, txt)

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

    Public Sub ReadingWithLine() Dim MyStr As String

    Close #1 Open Path & "read.me" For Input As #1 Do While Not EOF(1) Line Input #1, MyStr Debug.Print MyStr Loop End Sub

    Текст в окне отладки выглядит в полном соответствии с нашими ожиданиями. Вот он:

    Первая строка файла Зона 1 Зона 2 Зона 1 Зона 2 Привет,старик! Привет,старик! 5 пробелов Старик, привет! one two False 14.06.99 Null 3,1416 3,14 03,142 6 Error 2000

    Обратите внимание, ошибки, возбуждаемые методом Raise и функцией CVErr, редактируются по-разному, в первом случае записывается только код ошибки, во втором ѕ код сопровождается ключевым словом Error, не переводимым в локализованных версиях.

    Заметьте, точно такой же текст можно получить, если в процедуре WritingWithPrint заменить оператор Print на Debug.Print, поскольку этот метод и этот оператор порождают одинаковый результат, разница лишь в том, что один из них помещает результаты в файл, а другой ѕ в окно отладки.


    При выводе данных можно управлять шириной выводимых строк. Ширину выводимых строк для открытого файла устанавливает оператор:

    Width #номер-файла, ширина

    Параметр ширина принимает значения от 0 до 255. Если при печати в файл оператором Print добавление значения очередного выражения в строку приведет к превышению установленной границы, это значение будет печататься с новой строки. При этом значение одного выражения не переносится, даже если его длина превышает установленную границу. Например, если для файла с номером 1 ограничить ширину строки 5 символами:

    Width #1,5

    а затем напечатать строку:

    Str = "раму мылом" Print #1, "мама", "мыла", Str

    то в файле окажутся строки:

    мама мыла раму мылом

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

    Как было показано, оператор Print вводит в файл данные разных типов, редактируя их в момент ввода и преобразуя в текстовый формат. Попутно он занимается и форматированием текста, приводя его к форме, удобной для отображения на дисплее. Во многих ситуациях форматирование, ориентированное на выдачу на экран дисплея, не является необходимым. В этих случаях для создания последовательного файла применяется Write#. Его синтаксис:

    Write #номер-файла, [список-вывода ]

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

    Также как и оператор Print, оператор Write производит редактирование данных в момент записи. При этом приняты следующие соглашения:

  • В числах в качестве разделителя целой и дробной частей всегда используется точка.
  • В качестве булевых значений используются слова #TRUE# и #FALSE#.
  • При записи дат применяется универсальный формат.
  • Ошибки выводятся в формате #ERROR код-ошибки#.
  • Для пустого значения выражения (Empty) ничего не записывается
  • Значение Null записывается как #Null#.



  • В отличие от оператора Print, оператор Write# вставляет при записи запятые между соседними выражениями и заключает строковые значения в кавычки. После записи последнего выражения вставляется перевод на новую строку (Chr(13) + Chr(10)).

    Следующая процедура создает файл с последовательным доступом "readme.txt" и записывает в него данные с помощью оператора Write#. При записи мы старались повторить содержимое предыдущего файла, чтобы можно было сравнить работу, выполняемую операторами Print и Write. Вот текст этой процедуры:

    Пример 14.3.

    (html, txt)

    Результат выполнения этой процедуры - файл "readme.txt" - в текстовом редакторе выглядит так:

    "Первая строка файла" "Зона 1","Зона 2" "Привет,","старик!" "Мама ","мыла ","раму мылом. " #FALSE# #1999-06-14# #NULL# 3.1416,"3,14","03,142" 6 #ERROR 2000#

    Сравните эти строки с теми, что выдаются оператором Print.


    'Создание файла Write #7, "Первая строка файла" ' запись текстовой строки. Write #7, "Зона 1", "Зона 2" ' запись двух строк. Write #7, "Привет,", "старик!" 'еще две строки MyStr = "раму мылом. " Write #7, "Мама ", "мыла ", MyStr MyBool = False ' булева переменная Write #7, MyBool MyDate = #6/14/1999# ' значение даты Write #7, MyDate ' запись даты MyNull = Null ' нулевое значение Write #7, MyNull MyFloat = 3.1416 'вещественное значение ' использование функции Format: Write #7, MyFloat, Format(MyFloat, "0.00"), Format(MyFloat, "00.000") On Error Resume Next Err.Raise 6 MyErr = Err Write #7, MyErr MyErr = CVErr(2000) Write #7, MyErr

    End Sub

    Пример 14.3.

    Результат выполнения этой процедуры - файл "readme.txt" - в текстовом редакторе выглядит так:

    "Первая строка файла" "Зона 1","Зона 2" "Привет,","старик!" "Мама ","мыла ","раму мылом. " #FALSE# #1999-06-14# #NULL# 3.1416,"3,14","03,142" 6 #ERROR 2000#

    Сравните эти строки с теми, что выдаются оператором Print.

    Основы офисного программирования и язык VBA

    Дополнительные условия

    Для того чтобы программа была полезной, она должна работать в условиях реального мира. В данном случае заказчик поставил следующие требования:
  • программа должна работать на любых компьютерах, на которых запускается Windows, в том числе без CD и сети;
  • программа должна работать без инсталляции какого-либо ПО, системы управления базами данных, сети, интернета и так далее. Единственная предпосылка - наличие на компьютере Windows и Word. Это довольно строгое ограничение - но, тем не менее, жизненное: инсталляция (или проверка наличия) того же BDE или MS Access на 100-200 компьютерах может значительно откорректировать бюджет любого начинания;
  • вопросы должны изменяться самым простым и непосредственным образом, для этой операции не нужно проходить никакого инструктажа или обучения;
  • подготовленные "конверты" с вопросами должны иметь минимальную степень защиты от модификации, так чтобы экзаменуемый в присутствии экзаменатора не мог нечаянно или с минимальными усилиями изменить результаты в свою пользу. Безусловно, это не касается "людей от компьютера", которые в комфортных условиях и при наличии времени могут сломать и видоизменить все что угодно - подразумевается, что экзаменуемый находится в системе тестирования и не занимается хаком нашей программы.

  • Итак, отталкиваясь от всего перечисленного, начнем строить нашу тестовую систему. Первое, что приходится сделать, это выбрать Word как в качестве источника данных, так и в качестве среды выполнения. Такое решение - самое естественное, поскольку ни на какой другой рантайм мы не можем рассчитывать. Первой идеей было использовать текстовый режим и gcc+couses. Но редактирование текстовых файлов в dos-кодировке - это задача, с которой справится не каждый современный пользователь, и, как говорят, "текстовый режим выглядит не современно".
    Первое последствие из выбора Word в качестве инструмента: наша база данных будет представлена обычным Word-документом определенного формата. В силу некоторых исторических причин формат текста был следующим:

    Подвижный VBA'стик в кислотной ActiveX-среде

    Арсений Чеботарев, "Комиздат"
    Обычно книги и статьи по VBA посвящены тому, что можно сделать на VBA, но для чего он совсем не предназначен. Так, например, у меня есть книга (на 800 страниц), посвященная, в основном, таким вещам, как системные вызовы и конструирование древовидных структур вручную. Это как раз то, чего нужно бы избегать,- если вы не пишете трояны, конечно.
    В этом смысле данная статья - полная противоположность книгам такого рода, то есть мы займемся именно тем, для чего VBA предназначен: созданием и "оживлением" COM-объектов. Полученная программка будет полезна не только в качестве примера, но и практически - то есть ее можно использовать и даже, если повезет, продать.

    Постановка задачи

    Есть сборник вопросов в некоторой предметной области (в терминах экзаменаторов - банк данных). Вопросы разделены на несколько тем или групп - уровень деления 1, то есть у тем нет подтем. Все вопросы имеют свой "вес" от 50 до 100, вес определяет важность вопроса: 100 соответствует наиболее важным, а 50 - самым "проходным" вопросам. Каждому вопросу соответствует несколько (обычно 3-5) ответов. Каждый ответ тоже имеет свой "вес" от 100 до 0: 100 соответствует абсолютно правильным и полным ответам, 80 - частично правильным, 50 - не лишенным элементов истины и 0 - полностью неверным. Предполагается, что для каждого вопроса существует один и только один абсолютно правильный ответ. Для проведения экзамена из всего банка случайным образом выбирается определенное количество вопросов, их порядок меняется произвольным образом. При этом можно выбрать одну, несколько или все темы. Ответы также меняются местами. Предварительно выбранные вопросы кладутся в "конверт", который экзаменуемый вскрывает во время экзамена.
    После ответов подсчитывается общий балл. Происходит это следующим образом: складываются все веса всех вопросов как максимальное число баллов, которое возможно набрать. Реально набранное число баллов определяется суммой Sum (Vx*Mx) - то есть веса вопросов на вес полученного ответа. Успеваемость экзамена вычисляется как процент набранных баллов от максимально возможных.

    Use Case с точки зрения пользователя и заказчика

    Типичный заказчик: вуз, школа, курсы - любое учебное заведение, которое желает экзаменовать своих учеников методом тестирования. Для использования понадобится, естественно, составить собственный банк вопросов и ответов в предметной области. Процесс это кропотливый и трудоемкий, но здесь мы совершенно его не рассматриваем.
    Прежде чем начать работу, включите выполнение макросов: в Сервис> Макросы> Безопасность поставьте переключатель в положение Низкая.
    Не страшно ли включать макросы в Office? Нет, не страшно. Проблема распадается на два случая. Если у вас стоит антивирус - то трояны в макросах "отдыхают" по-любому. Если же антивируса у вас нет - то и макровирусов вам тоже незачем бояться, у вас уже и так, наверное, полный диск другой живности.
    Ко всему прочему, макровирусы не отличаются жестокостью, так что вы можете установить антивирус, но не включать его в режиме постоянного сканирования, а всего лишь раз в день запускать на сканирование. Дополнительно можно рекомендовать антивирусную защиту почты - пользуйтесь почтовым сервером, который применяет антивирусные сканеры, поскольку этот путь инфицирования сейчас наиболее распространен. (Из личного опыта: за 15 лет работы за клавиатурой я не потерял ни единого бита из-за вирусов - а всё только по причине собственной рассеянности и пьянства. Так что для меня вирусы - скорее легенда, чем реальная угроза.)
    И последнее: никто не отменял резервного копирования и других методов backup'а, таких как хранение эталонной системы в формате Norton Ghost. Благо, CD-RW и прочие носители сейчас дешевле грибов.

    Use Case с точки зрения программиста

    Проиллюстрированная далее программа показывает, как VBA получает доступ, создает и управляет ActiveX-элементами за пределами иерархии классов Office. Создавать элементы управления не имеет особого смысла, если не обрабатывать специфические для них события. Поскольку ActiveX-элементы у нас будут создаваться динамически и их количество не будет даже предварительно известно, то и обработчики будут генерироваться динамически. Да и поведение наших "кнопок", хотя и будет выбираться из нескольких вариантов, но все-таки должно определяться в последний момент… Короче, если вы пробовали генерировать программы, начиная от LISP и prolog и заканчивая визардами в VS, то вы представляете, о чем речь.

    &Вопросы для системного администратора

    "100" Чем нужно зажимать сетевой джек:
    "100" Специальным инструментом.
    "50" Плоскогубцами.
    "0" Зубами.
    "0" А зачем его вообще зажимать?
    "80" Что вы делали на Новый Год:
    "0" Гулял с друзьями.
    "100" Переставлял Линукс.
    "100" Как у вас проложен сетевой кабель:
    "50" В коробе.
    "0" По полу.
    "100" Мы давно перешли на WaveLAN.

    То есть первая строка со значком & обозначает начало темы, потом идут группы строк, первая из которых - вопрос, а остальные - ответы. Строки предваряются весовыми коэффициентами: "важности" - для вопросов, и "степени верности" - для ответов. Согласитесь, что ввести и модифицировать такой файл может любой пользователь. Для представления банка данных создадим три тривиальных класса: ' class BLine Public Weight As Integer Public Value As String Public ff As InlineShape Public loc As Range Public Sub Parse (s As String) p1 = InStr (s, """") p2 = InStr (p1 + 1, s, """") If (p1 > 0) And (p2 > p1) Then Weight = Val (Mid (s, p1 + 1, p2 - 1)) Value = Trim (Mid (s, p2 + 1)) Else Weight = 0 Value = s End If End Sub
    ' class Question Public Question As New BLine Public Answers As New Collection 'Of BLine (s)
    ' class Theme Public Title As String Public Questions As New Collection 'Of Question (s) Public Selected As Boolean
    Как видите, вопрос - это собственно вопрос и коллекция ответов, а тема - это название темы и коллекция вопросов. Это похоже на представление списков в LISP - голова и хвост. Самый главный кирпич всей иерархии, класс BLline, включает в себя строку и ее вес, а также дополнительные поля, смысл которых прояснится позже. Тривиальный метод Perse принимает строку и преобразует ее в поля объекта - немудреный суржик перегрузки конструктора в C++.
    Перед тем как собственно выбирать и тасовать вопросы и ответы, построим древовидную структуру, несколько напоминающую DOM-представление,- для этого вызывается специальная функция.
    Предполагается, что некто, ответственный за составление вопросов, активно читает и исправляет их - и, когда наступает компромисс между совестью и усталостью, формирует нужное количество именных "конвертов" с вопросами для каждого студента.

    Есть четыре возможности запустить макрос: по нажатию горячей клавиши, по нажатию кнопки на панели, по системному событию и явно через меню Макросы. Поскольку этот вопрос нас пока не занимает, то пусть наш процесс запускается по Ctrl+K - кнопки на панелях инструментов имеют свойство теряться, а сами панели - быть закрытыми шаловливыми конечностями пользователей.

    С высоты птичьего полета выполняем такие вот действия:
  • инициализируем используемые структуры и получаем ссылки на необходимые системные (в смысле, Word) объекты;
  • строим иерархическое дерево Test - Theme (s) - Question (s) - Bline + Bline (s);
  • выводим список тем и количество вопросов в каждой - для выбора вопросов только по этим темам. Опуская сам процесс выбора и задействованное при этом диалоговое окно, можно только сказать, что после его закрытия поле Selected выбранных тем принимает значение TRUE;
  • после того как темы выбраны, все вопросы по всем темам сбрасываются в одну большую коллекцию и как следует тасуются. Поскольку объекты представляются ссылками, то новая коллекция содержит только ссылки, а не сами объекты;
  • формируется новый документ с отобранными вопросами в нужном количестве. Документ имеет несколько необычных свойств: он защищен на уровне защиты страницы, в нем отключена проверка правописания (для подавления ненужных визуальных эффектов) и в него внедрены ActiveX-элементы типа checkbox. К каждому элементу динамически прикрепляется обработчик. Задача обработчика - воспринимать ввод пользователя. Как только пользователь выбирает один ответ, другие варианты блокируются и сам вопрос подсвечивается определенным цветом - в зависимости от правильности ответа.


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


    Этот документ воспринимает ввод пользователя, видоизменяя сам документ, и в конце концов формирует "протокол тестирования".

    Обратите внимание на то, как мы динамически создаем OLE controls в слое документа (есть еще слой векторной графики с абсолютными координатами). Существует также два уровня доступа к OLE-элементу - фактически для каждого элемента создается мини-контейнер со своими собственными свойствами. Для доступа к настоящему OLE приходится обращаться на уровень ниже - к полю Object (это похоже на то, как MFC или Delphi инкапсулирует объекты Windows). К каждому элементу управления "приделывается" персональный и в общем случае ни на что не похожий обработчик - с помощью техники, знакомой конструкторам Wizard'ов и прочих RAD'ов.

    &Вопросы для системного администратора

    Текст основной (а фактически - и единственной) функции приведен в листинге 1. Имя ее не имеет значения - главное, чтобы она вызывалась по Ctrl+K или другим известным способом.

    Модуль Module1, экспортируемый из первичного документа и импортируемый в "билет" через файл. Небольшая обработка на предмет "а не закончились ли у нас вопросы?"; если да - то заполнение "протокола" (листинг 2).

    Обработчики событий для формы выбора тем - приводится для полноты изложения листинг 3. Сама форма выглядит примерно так, как показано на рисунке.

    

        Программирование: Языки - Технологии - Разработка