В начало
Статьи
Библиотека
Разное

Вот здесь - новый сайт, заходите немедленно!

kift - Коллекция Интересных Фактов и Теорий

А тут можно поболтать и побухтеть, милости просим:

kift - неизданное

Введение в программирование на примере VBA

Часть III. Создание объекта на основе класса

Аннотация
Предисловие
Часть I. Макрос Word
     1. Проектирование и запись макроса
     2. Разбор макроса
     3. Внесение исправлений
     Итоги
Часть II. Макрос-приложение
     4. Проектирование
     5. Визуальное редактирование
     6. Запуск и остановка
     7. Вывод данных
     8. Выбор ответа
     9. Перемещение по списку
     10. Обратное перемещение
     11. Новая версия
     12. Реализация новой версии
     13. Завершение работы
     Итоги
Часть III. Объект на основе класса
     14. "Основное" приложение
    15. Компонент-таймер
     Итоги
Послесловие

Занятие 15. Создание компонента-таймера

Цель занятие – создание компонента-таймера, управляющего «основным» приложением.

Цикл 2

Для упорядочения наших представлений о дальнейшей работе схематично подытожим сделанное.

Мы имеем вполне самостоятельный компонент (несмотря на то, что это – главная форма приложения, правомерно назвать ее компонентом), выполняющий некоторые действия и законченный логически. Но для реализации требуемой функциональности нужен другой компонент. Наша задача – определить «точки соприкосновения» созданного и проектируемого компонентов.

Созданный компонент-форма содержит функцию, выполняющую действия (по выводу картинки). Уже упоминалось, что нецелесообразно выполнять функцию по «указке» этого же компонента (формы), потому что тогда основное приложение будет «перегружено» несвойственной ему функциональностью. Иначе говоря, основное приложение должно выводить картинки – и, возможно, делать что-то еще, но не определять момент вывода!

Анализ

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

Подцикл 1. Создание дополнительного компонента

Создание приложения обычно начинается с определения механизма взаимодействия с пользователем – визуального интерфейса. Аналогично этому конструирование компонента следует начинать с механизма взаимодействия с другими компонентами приложения – программного интерфейса.

Проектирование программного интерфейса

Начнем с уточнения деталей – как взаимодействует «основное» приложение и вновь создаваемый компонент.

  • Взаимодействие основного и дополнительного компонентов можно изобразить так:

Не правда ли, фраза «оповещение о наступлении нужного момента времени» вполне синонимична выражению «оповещение о наступлении события»?

Именно событие мы будем применять для этого.

Кроме того, у основного приложения уже есть два контрола, предназначенных для запуска и остановки процесса – кнопки cmdStart и cmdStop. Следует предусмотреть соответствующие атрибуты у создаваемого компонента:

А теперь определим, какие разновидности атрибутов будут определять подобные отношения компонентов.

Подобное «направление» атрибута говорит о том, что <компонент> вызывает метод другого компонента. Если такой вызов делается не программистом путем явного указания в коде, а программой, то это – сообщение о событии.

Обратное направление атрибута:

говорит о том, что <компонент> будет выполнять метод или обрабатывать событие. Как вы уже видели, принципиальной разницы между методом и обработчиком события в VBA нет.

Согласно этим схемам составим списки атрибутов приложения и будущего компонента:

Приложение должно обладать такими атрибутами:

  • вызов метода «запуск» компонента,
  • вызов метода «остановка» компонента,
  • обработка события «наступление момента» компонента.

А будущий компонент будет иметь:

  • метод «запуск»,
  • метод «остановка»,
  • событие «наступление момента».

Еще раз остановимся на различии между генерацией события и его обработкой. Обработка события производится «принимающим» компонентом, тем, кто оповещается. Событие же генерируется «оповещающим» компонентом, тем, кто оповещает.

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

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

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

Подобный список атрибутов описывает программный интерфейс компонента.

Добавив новый атрибут, получаем описания интерфейса:

Основное приложение.

Вызов метода «старт».

Приводит к запуску компонента и началу его работы.

Вызов метода «остановка».

Приводит к прекращению работы компонента.

Обработка события «наступление момента».

Приводит к выполнению какой-то деятельности основным приложением.

Управление свойством «продолжительность».

Позволяет изменять длительность отсчитываемого интервала времени.

Компонент:

Метод «старт».

Запуск компонента.

Метод «остановка».

Остановка компонента.

Событие «наступление момента».

Возникает при выполнении компонентом своей задачи.

Свойство «продолжительность».

Определяет длительность отсчитываемого интервала.

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

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

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

Вернемся к интерфейсам нашей программы.

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

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

Вполне достаточен был бы тип свойства Byte – целое число от 0 до 255. Но для простоты назначим тип Integer, размерностью от (примерно) –32000 до (примерно) 32000. Отрицательные значения использовать не будем.

Определимся с именами атрибутов.

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

Поэтому достаточно определиться с именами атрибутов компонента.

Это будет выглядеть так:

Компонент:

Имя Timer.

Метод «старт».

Запуск компонента.

Имя StartTimer.

Метод «остановка».

Остановка компонента.

Имя StopTimer.

Событие «наступление момента».

Возникает при выполнении компонентом своей задачи.

Имя Tick.

Свойство «продолжительность».

Определяет длительность отсчитываемого интервала.

Имя Interval.

Обратите внимание на имя события – Tick. Так как компонент выполняет функции таймера, да и имя компонента TimerТаймер, то вполне естественно, что он будет «тикать». Остальные имена очевидны.

  • Отобразим интерфейс компонента схематически:

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

Обратите внимание на двунаправленность стрелки, символизирующей свойство компонента. Действительно, нет причин для запрета чтения длительности интервала.

Мы определились с программным интерфейсом будущего объекта.

Класс

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

В своей работе VBA не ограничивается набором объектов, уже содержащихся в нем. Язык VBA имеет средства для создания собственных объектов (следует заметить, что возможности VB гораздо шире).

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

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

Такие сущности называются классами.

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

Ничего совсем неожиданного в понятии класса нет. Формы VBA – тоже классы, имеющие визуальное отображение. К сожалению, VBA не позволит вам создать класс с визуальным отображением, для решения такой задачи следует использовать VB.

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

Реализация

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

Для этого надо «воплотить» в коде описание программного интерфейса, созданное нами раньше. Как вы увидите, сделать это просто – но только в том случае, если была тщательно и скрупулезно выполнена подготовительная работа.

Сначала добавим к проекту новый модуль – модуль класса.

  • В окне ProjectExplorer выполните «правый» щелчок.
  • В появившемся контекстном меню выберите команду InsertClass Module (ВставитьМодуль Класса):

Не забудьте, что мы работаем с IDE VBA и сейчас открыт наш проект Multy.

Откроется окно кода, а в окне Project Explorer появится новый элемент:

Также изменится содержимое окна Properties:

Сразу изменим имя класса.

Будущий компонент будет называться tmrMain (от Timer MainГлавный Таймер). Но это – имя объекта, созданного на основе класса. Имя класса должно быть другим – вспомните, как мы работали с пользовательскими типами, имя типа не совпадало с именем переменной, созданной на его основе.

Пусть имя класса будет cTimer (от class Timer).

  • В окне Properties измените значение свойства Name:

Реализация программного интерфейса класса

Теперь займемся реализацией программного интерфейса – пока без добавления «полезного» кода.

  • Добавим в модуль кода класса две заготовки методов – StartTimer и StopTimer. Определим их как Public – нам нужно, чтобы другие компоненты проекта могли обращаться к этим методам:

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

  • Добавим заготовку свойства Interval типа Integer.

Это делается отдельно для чтения и отдельно для записи свойства, примерно так:

Разберем подробнее, что же мы сделали.

В области глобальных объявлений  внесена строка:

Dim intInterval As Integer

Тем самым создается внутренняя переменная intInterval типа Integer. В ней будет храниться значение свойства.

Далее:

Public Property Get Interval() As Integer
Interval = intInterval
End Property

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

Затем находится код:

Public Property Let Interval(i As Integer)
intInterval = i
End Property

Это – свойство для записи. Когда внешняя программа устанавливает значение свойства, происходит примерно следующее (в псевдокоде):

‘эта строка – в вызывающей программе
Timer.Interval = <значение>

‘псевдокод, поясняющий происходящее:
i = <значение> ‘переменная i объявлена как параметр свойства для записи
intInterval = i ‘значение сохраняется в переменной

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

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

Public Interval As Integer

Действительно, в простых случаях можно ограничиться и этим.

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

Событие

Осталось добавить в код класса событие.

В VBA это делается способом, для нас еще неизвестным:

Событие сперва следует объявить – так же, как объявляется переменная. А уже потом вызвать это событие в коде какого-то метода, примерно так (псевдокод):

‘объявление события
Event <имя_события>

‘в коде
RaiseEvent <имя_события>

При желании можно передавать аргументы:

‘объявление события
Event <имя_события> (<параметр_1>...)

‘в коде
RaiseEvent <имя_события> <аргумент_1>...

Подробнее об этом вы можете узнать в справке VBA.

Процедура-инициатор события

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

Имя MainГлавный, – зарезервировано языком для особых целей, и назвать так метод класса нельзя. Назовем метод MainCycleГлавныйЦикл.

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

  • Измените код:

Проверим, что же получилось.

  • Откройте окно Object Browser (нажатием [F2]).
  • В верхнем списке выберите проект Multy:

В списке компонентов проекта появился элемент cTimer. Такое имя мы присвоили созданному классу.

  • Выберите этот элемент:

Справа – список атрибутов класса.

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

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

Подцикл 2. Подключение дополнительного компонента к приложению

Следующий этап работы – подключение класса к основному приложения.

Создание объекта на основе класса

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

Эта работа производится на стороне «основного» приложения.

Подробнее в псевдокоде:

‘объявление переменной типа класса:
Dim WithEvents <имя_объекта> As <имя_класса>

‘явное создание объекта
Set <имя_объекта> = New <имя_класса>

‘явное уничтожение объекта
Set <имя_объекта> = Nothing

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

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

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

В исполняемом коде должна быть строка явного создания объекта типа класса, начинающаяся словом SetУстановить. Слово NewНовый, – указывает на создание нового объекта. Вот пример:

Set <объект> = New <класс>

Это значит, что будет создан новый <объект> типа <класс>.

Возможен вариант записи без слова New:

Set <объект_1> = <объект_2>

При этом <объект_1> и <объект_2> – разные имена одного и того же объекта. Новый объект подобным образом не создается.

Подобная запись недопустима:

Set <объект> = <класс>

VBA сообщит об ошибке, увидев подобное. Действительно, создать другое имя для класса нельзя (в VBA, во всяком случае), а указания на создание объекта здесь нет.

Будьте внимательны и вносите в код слово New, если вы хотите создать новый объект.

Правилом хорошего тона считается явное уничтожение объекта по окончании работы с ним. Это делается присвоением объектной переменной значения NothingНичто.

Проектирование

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

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

Реализация

Перейдем к реализации того, что мы только что спроектировали.

  • В области глобальных объявлений кода формы frmMain поместите строку объявления:

Option Explicit

Dim index As Integer
Dim intPicturesCount As Integer
Dim WithEvents tmrMain As cTimer

  • В код события инициализации формы внесем строку создания объекта таймера:

Private Sub UserForm_Initialize()
index = 1
intPicturesCount = imglistMain.ListImages.Count + 1
    Set tmrMain = New cTimer
End Sub

  • Создайте обработчик события Terminate для формы, поместите в нем код уничтожения таймера:

Private Sub UserForm_Terminate()
    Set tmrMain = Nothing
End Sub

Можно использовать класс.

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

  • Перейдите в окно кода класса cTimer.
  • В метод StartTimer добавьте строку:

Public Sub StartTimer()
    MsgBox "Start"
End Sub

  • А в метод StopTimer

Public Sub StopTimer()
    MsgBox "Stop"
End Sub

Как видите, ничего нового, все это мы уже делали когда-то.

Вызовем эти методы из формы, привязав их к нажатиям кнопок:

  • В обработчик события нажатия кнопки cmdStart внесите вызов метода запуска таймера:

Private Sub cmdStart_Click()
    tmrMain.StartTimer
End Sub

  • И аналогично оформите обработчик события нажатия на кнопку cmdStop:

Private Sub cmdStop_Click()
    tmrMain.StopTimer
End Sub

Попробуем проверить, что же получилось.

Отладка

Наконец-то можно приступить к отладке.

  • Запустите приложение и щелкните на одной, а потом – на другой кнопке.

Должны появляться окошки с предопределенными нами надписями.

Если вы были внимательны – все должно работать.

  • Удалите тестовый код из кода класса (обе строки MsgBox).
  • Код формы можно оставить как есть – обращения к объекту готовы.

Тестирование события класса

Создадим «обратную» связь события объекта таймера и формы.

  • В окне кода формы в левом списке выберите строку tmrMain:

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

Все, как мы и хотели.

  • Поместите в обработчик этого события строку:

Private Sub tmrMain_Tick()
    MsgBox "Tick"
End Sub

  • А в методе класса StartTimer вызовите метод MainCycle, в котором инициируется событие:

Public Sub StartTimer()
    MainCycle
End Sub

  • Сделайте пробный запуск и щелкните на кнопке cmdStart.

Должно возникнуть окошко, оповещающее нас о получении формой события Tick.

  • Если все получилось – удалите из кода формы строку MsgBox. Вызов процедуры MainCycle в коде класса оставьте.

Подцикл 3. Добавление в таймер полезной функциональности

Задачей следующего этапа разработки будет наполнение класса полезным кодом.

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

Требования

Таймер через определенные интервалы времени сообщает основной программе о прохождении интервала.

Требования подразумевают непрерывную, однообразную работу компонента, начинающуюся с вызова метода StartTimer и продолжающуюся вплоть до обращения к методу StopTimer. О том, следует ли определять свойство Interval до запуска компонента, или можно менять его во время работы, пока не говорим.

Проектирование

Проанализируем требования.

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

Выбор технологии

Вы знаете о циклах с индексами For...Next и о циклах без индекса Do...Loop. Конструкция цикла с индексом ожидает конечное число повторений, по достижению индексом заранее определенной величины цикл самопроизвольно прекращается. Видимо, для наших целей это не очень подходит.

Цикл Do...Loop работает до тех пор, пока не будет получено указание о прекращении работы. Это описание почти буквально подходит под наши требования.

Итак, основой компонента будет какая-то разновидность цикла Do...Loop.

  • Схематизируем это:

Конструирование цикла

Перейдем к псевдокоду для большей понятности дальнейшего.

  • Присвоим сущности «основной цикл» имя MainCycle – действительно, именно здесь будет вызываться событие Tick.

Тогда в псевдокоде:

Private Sub MainCycle()
     Do
           RaiseEvent Tick
     Loop
End Sub

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

У цикла Do...Loop, как вы знаете, есть разновидности, совмещающие цикл с проверкой условия. Условие может проверяться в начале и в конце цикла, может проводиться проверка на соблюдение условия и на выполнение условия. Это мы уже рассматривали ранее.

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

Псевдокод:

Private Sub MainCycle()
     Do
           RaiseEvent Tick
     Loop <проверка условия>
End Sub

Проверка условия может происходить двояко – при помощи слов языка Until и While. Если цикл создан с использованием слова Until, то он будет работать, пока не будет выполнено условие. Слово While позволяет работу до тех пор, пока условие выполняется. Как видите, принципиальной разницы нет, переформулированием условия можно сделать эти конструкции работающими совершенно одинаково.

Тем не менее, нам нужно выбрать.

Так как для остановки цикла применяется вызов метода StopTimer, логичным будет предположить, что этот метод что-то изменяет в работе компонента. Значит, для создания цикла естественным будет использование слова Until, прерывающего работу цикла при изменении условия.

Псевдокод:

Private Sub MainCycle()
     Do
           RaiseEvent Tick
     Loop Until <условие>
End Sub

Раньше при проверке условий мы применяли переменные логического типа – флаги. Нет препятствий к этому и сейчас.

Пусть <условие> будет определяться переменной логического типа, именуемой, например, fStop (от flag Stop).

Тогда установка флага будет прекращать цикл:

Private Sub MainCycle ()
     Do
           RaiseEvent Tick
     Loop Until fStop = True
End Sub

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

Псевдокод метода:

Public Sub StopTimer()
fStop = True
End Sub

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

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

Вот как это делается:

Option Explicit
Dim intInterval As Integer
Public Event Tick()
Dim fStop As Boolean
.
.
.

Теперь следует реализовать метод StartTimer.

Конструирование метода запуска таймера

В этом методе мы будем запускать процедуру MainCycle, как показано в псевдокоде:

Public Sub StartTimer()
MainCycle
End Sub

Но это не все. Не было бы смысла в процедуре, не делающей ничего, кроме вызова другой процедуры.

Не забывайте, что один раз установленная переменная fStop будет установленной до тех пор, пока вы не измените ее значение самостоятельно – или до конца «жизни» объекта. Поэтому если оставить так, как есть, то после остановки цикла методом StopTimer повторный запуск не удастся.

В процедуре StartTimer следует принудительно сбрасывать флаг до того, как будет запущен цикл. Это простое дополнение позволит останавливать и запускать цикл столько, сколько хотите:

Public Sub StartTimer()
    fStop = False
    MainCycle
End Sub

  • Как видите, созданный нами псевдокод достоин использования как код VBA. Внесите изменения в код класса cTimer.
Реализация

Не делайте запуск. Работа еще не закончена.

Реализация таймера согласно схеме:

     Do
           RaiseEvent Tick
     Loop Until fStop = True

имеет два больших недостатка.

Недостатки кода VBA

Во-первых, здесь нет никакого механизма регулирования «скорости» цикла. Действительно, свойство Interval мы еще никак не использовали. Во-вторых, подобный цикл будет «крутиться» постоянно, занимая ресурсы компьютера впустую.

Понятие о WinAPI

Для решения этой проблемы недостаточно возможностей VBA. Мы обратимся к набору функций самой Windows.

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

Совокупность этих функций называется Windows API – от Application Programming InterfaceИнтерфейс Прикладного Программирования.

Функции Windows API (WinAPI) вполне доступны для использования из VBA. При этом вам придется иметь в виду следующее.

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

Во-вторых, в состав справочной системы VBA не входит описание WinAPI – оно слишком объемно. Источники информации вам придется искать самим. Наиболее полное описание вы найдете в справочной системе MSDN, описывающей практически все программные продукты фирмы Microsoft. Но эта библиотека справки распространяется платно, и русского перевода ее нет.

Для программистов VB и VBA наилучшим источником сведений будет книга «Win32 API и Visual Basic», написанная Дэном Эпплманом и изданная в русском переводе в 2001 году издательством «Питер». Эта книга, помимо теоретических сведений, содержит описание нескольких сотен функций WinAPI и множество примеров их использования.

Большое количество примеров работы с WinAPI вы также найдете в Internet.

Для работы с функцией WinAPI ее следует объявить. Синтаксис объявления такой:

Declare Sub <имя функции> Lib <”имя библиотеки”>

Могут использоваться слова Public/ Private, вместо слова Sub вы можете использовать слово Function – но тогда придется указать тип возвращаемого функцией значения. Подробнее о синтаксисе объявления вы узнаете из справки VBA – поиск по словосочетанию Declare Statement.

Функция WinAPI Sleep

Так вот, в составе WinAPI есть очень простая по использованию функция, обеспечивающая задержку выполнения программы на указанное при вызове функции время. Эта функция называется Sleep – от Спать.

Объявление этой функции выглядит так:

Declare Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)

 Как видите, это – процедура, обозначенная словом Sub, то есть значение ей не возвращается.

Расположена эта процедура в библиотеке Windows, которая называется kernel32.dll, на что указывает слово Lib и последующее имя.

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

Далее следует описание параметра процедуры. Имя параметра – dwMilliseconds, – произвольно, вы можете использовать свое.

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

Тип параметра процедуры – Long (Длинный). Этот тип соответствует целым числам в диапазоне (примерно) от минус двух миллиардов до двух миллиардов. Тип Long наиболее часто используется в Windows API.

Потребность коррекции кода класса – изменение типа свойства интервала

Так как ранее мы определили тип свойства Interval как Integer, вам следует внести коррективы в код класса cTimer. Измените тип переменной intInterval на Long, и таким же образом поменяйте тип обоих процедур свойств Interval:

Option Explicit
Dim intInterval As Long
.
.
.

Public Property Get Interval() As Long
Interval = intInterval
End Property

Public Property Let Interval(i As Long)
intInterval = i
End Property

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

Добавление Sleep к компоненту

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

  • Поместите строку объявления процедуры Sleep в области глобальных объявления класса cTimer:

Option Explicit
Dim intInterval As Long
Public Event Tick()
Dim fStop As Boolean
Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)

Мы указали процедуру как Private. Во-первых, в модуле класса нельзя объявлять процедуру WinAPI как Public – язык VBA вам не позволит этого, а во-вторых, нам и не требуется доступность этой функции вне объекта Timer.

Теперь решим, где и как будет использоваться функция Sleep.

Ее следует поместить в тело цикла Do...Loop для обеспечения задержки при каждом проходе цикла. Нет принципиальной разницы, размещать вызов процедуры Sleep до генерации события Tick, или же после него.

Это будет выглядеть так:

Private Sub MainCycle()
Do
Sleep
RaiseEvent Tick
Loop Until fStop = True
End Sub

Но мы опять забыли о свойстве Interval.

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

Следует помнить лишь, что, как явствует из имени параметра процедуры Sleep, это значение трактуется как миллисекунды – то есть тысячные доли секунды.

Если вы хотите манипулировать секундами, следует предусмотреть преобразование (умножить на 1000). Есть смысл проводить расчеты при получении значения, то есть в процедуре свойств Property Let Interval.

Наш код принял вид:

Private Sub MainCycle()
Do
Sleep intInterval
RaiseEvent Tick
Loop Until fStop = True
End Sub

Перед тем, как произвести пробный запуск, доведем до конца код формы frmMain.

  • Во-первых, сделаем работоспособным обработчик процедуры tmrMain_Tick, а именно – будем вызывать в нем функцию отображения картинки:

Private Sub tmrMain_Tick()
    DisplayPicture
End Sub

  • Во-вторых, позаботимся о передаче таймеру значения интервала.

Для простоты мы не будет помещать на форму какие-то элементы управления, предназначенные для задания значения интервала – подобное вы сможете сделать сами при дальнейшем усовершенствовании приложения. Зададим значение интервала один раз, при создании объекта в процедуре инициализации формы:

Private Sub UserForm_Initialize()
index = 1
intPicturesCount = imglistMain.ListImages.Count + 1
Set tmrMain = New cTimer
    tmrMain.Interval = 1000
End Sub

Не забудьте, что значение – в тысячных долях секунды.

  • Обязательно сохраните проект!

Не забывайте о потенциальной опасности отладки приложений, использующих WinAPI.

Отладка

И вновь отладка.

  • Произведите пробный запуск.

И тут мы сталкиваемся с проблемой.

Мало того, что никаких картинок не видно, так еще и приложение не реагирует на щелчки мыши и нажатия клавиш! Неужели зависло?

Остановить отлаживаемое приложение VBA можно нажатием [Ctrl]+[Break].

  • Сделайте это.

Приложение перейдет в пошаговый режим, наподобие того, который мы наблюдали раньше при запуске программы нажатием [F8].

Вы можете пройти программу в пошаговом режиме, отслеживая значения переменных, но скажем заранее – все верно.

Проблема вот в чем.

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

Здесь же цикл долгий, и, тем более, замедленный процедурой Sleep. Возникает впечатление зависания приложения. А вывод на экран картинок не происходит по той же причине – для «рисования» картинок требуются ресурсы приложения-субстрата.

Оператор DoEvents

Решение в том, чтобы во время работы цикла позволить выполнять свои задачи другим программам. Для этого в составе VBA есть оператор DoEvents – от Делай с Событиями.

Этот оператор следует поместить в тело цикла Do...Loop, место его размещения большой роли не играет:

Private Sub MainCycle()
Do
Sleep intInterval
RaiseEvent Tick
    DoEvents
    Loop Until fStop = True
End Sub

  • Повторите пробный запуск.

Теперь все работает!

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

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

  • Подробно закомментируйте код.

Повторное использование кода в VBA

И последнее.

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

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

Среда IDE VBA предоставляет возможность перемещения кода между документами.

  • Убедитесь, что вы находитесь в окне кода класса cTimer.
  • Выполните команду меню IDE VBA FileExport File…ФайлЭкспорт Файла.

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

  • Путем обычной навигации по каталогам выберите папку, в которой вы сохраняете свои проекты VBA и подтвердите выбор нажатием [Enter].

Просматривая свой рабочий каталог в проводнике Windows, вы обнаружите новый файл с именем cTimer.cls – в нем и находится код нашего класса.

Командой FileImport File… вы сможете произвести обратное действие – вставить содержимое файла в проект VBA.

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

Итог занятия

Мы прошли последнее занятие учебника, самое сложное и информативное. Для начала составьте конспект:

  • Программный интерфейс, понятие, структура, принцип неизменности.
  • Класс, понятие.
  • Событие, синтаксис, использование.
  • Квалификатор WithEvents, смысл его использования.
  • Понятие о WinAPI.
  • Объявление функций WinAPI, синтаксис.
  • Функция Sleep, применение, синтаксис.
  • Оператор DoEvents, смысл, применение.

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

Понятие интерфейсаinterface (от англ. [Взаимодействие] Между Лицами[-Участниками]), – одно из важных в программировании вообще. Под интерфейсом понимается соглашение о взаимодействии двух сторон, участвующих в этом взаимодействии. Визуальный интерфейс, выступающий частным случаем интерфейса, определяет какие-то видимые сущности и правила обращения с ними для получения определенного результата. Например, пользователь, встретив на экране кнопку- CommandButton, вправе предположить, что при щелчке на этой кнопке приложение совершит какое-то действие. А, увидев, предположим, список- ListBox, пользователь будет искать в нем какие-то сгруппированные значения для того, чтобы сделать выбор.

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

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

Чрезвычайно важная концепция, лежащая в основе объектности как таковой – класс. Синтаксис и применение классов довольно подробно рассмотрены в материале занятия. Следует уточнить еще несколько моментов. При создании класса силами VBA одному модулю класса будет всегда соответствовать один класс. То есть нельзя в одном модуле класса разместить несколько классов, и нельзя разбить класс на несколько модулей класса, хотя вполне допускается использование классом каких-то процедур (функций), находящихся в других компонентах программы. Помните лишь, что подобное использование классом «посторонних» процедур (функций) может лишить класс «объектности». Имеется в виду то, что объект, созданный на основе класса должен быть по возможности самодостаточен – «посторонние» процедуры (функции) лишают класс этого. Кроме того, использование постороннего кода мешает повторному использованию кода, так как придется распространять и подключать к проекту не только модуль класса, но и все модули, использующие друг друга.

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

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

Hosted by uCoz