Часть 1. Основы Visual Basiс
На главную самоучителя
12.10.2005
Глава 22.
Функция DoEvents. Анализ ее влияния на быстродействие.
Организация прерывания цикла.
Цикл ожидания действий пользователя.
Скачать исходник примера "DoEvents"

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

Функция DoEvents.

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

Эта функция, согласно мануалу, возвращает значение типа Integer, представляющее собой количество открытых форм, если фокус находится на форме Visual Basic и ноль, если фокус находится на окнах других приложений. Ну это кому что интересно. Главное это то, что эта функция передает управление операционной системе, то бишь Windows, для обработки других событий. И после обработки Windows'ом всех событий из очереди и передачи всех нажатий клавиш из очереди функции SendKeys возвращает управление обратно в Visual Basic.

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

Private Sub Command1_Click()
Dim x As Long
For x = 1 To 300000
Text1 = x
Next x
End Sub

Этот цикл по нашему разумению должен выводить в Text1 постоянно меняющееся значение переменной X от 1 до 300000. Запустим проект, нажмем кнопку Command1 и вместо созерцания мигающих циферек в текстбоксе мы видим... ничего мы не видим - форма наша не обновляется до окончания цикла. Да мы еще и не можем корректно приостановит выполнение цикла (я имею в виду , если он откомпилирован). Более того, попробовав передвинуть мышью форму мы убедимся, что это невозможно, а Менеджер задач (Task Manager) может выдать сообщение, что программа зависла и не отвечает (not responding). Вот так, ходим по минам, а подрываемся на говне.
Однако всего этого можно избежать, если ввести в цикл функцию DoEvents. Тогда наша процедурка может выглядеть так:

Private Sub Command1_Click()
Dim x As Long
For x = 1 To 300000
Text1 = x
DoEvents
Next x
End Sub


Конечно мигание цифр не радует глаз. Кроме того, при передаче управления Windows выполнение программы Visual Basic (в нашем случае цикла) приостанавливается, но зато все наши проблемы мгновенно решились: и форму можно двигать и форма обновляется.
В связи с тем, что повторный вызов функции DoEvents в момент, когда функция еще не передала управление из операционной системы назад в Visual Basic может вызвать непредвиденные последствия, нежелательно вызывать процедуру DoEvents из других процедур программы. Видимо эти же соображения имеют место, когда предполагается, что нежелательно использовать функцию DoEvents
в процедурах обработки событий Click. Но мне не удалось добиться никаких особенных фатальных результатов, кроме перезапуска цикла, даже беспрестанно кликая по кнопке Command1.

Основным недостатком этой функции является очень значительное уменьшение быстродействия в длинных по времени циклах (в коротких ее вообще нет смысла использовать). Самое простое решение этого вопроса - запуск DoEvents не при каждой итерации, а например, при каждом сотом (или тысячном) проходе цикла, в зависимости от его параметров. Решить это можно, например, вводя дополнительную переменную-счетчик (в нашем примере y). С каждым проходом цикла счетчик увеличивается на единицу, а при достижении им значения, например 1000, вызывает функцию DoEvents и обнуляется, чтобы при дальнейшей работе цикла снова увеличится до 1000. Тогда наша процедура будет выглядеть следующим образом:

Private Sub Command1_Click()
Dim x As Long
Dim y As Long
For x = 1 To 300000
Text1 = x
y = y + 1
If y = 1000 Then 'проверяем счетчик итераций
'и если он совпадает с заданым числом, запускаем DoEvents и обнуляем счетчик

DoEvents
y = 0
End If

Next x
End Sub

Если тебя уже раздражает такое "ламерское" (но на мой взгляд удобное и наглядное решение), можно отследить тысячный проход цикла по-другому, например деля значения счетчика цикла x на эту тысячу с остатком (Mod) при каждой итерации, тогда при остатке ноль можно считать, что значение х кратно нашей тысяче. Тогда наш код будет выглядеть так:

Private Sub Command1_Click()
Dim x As Long
Dim y As Long
For x = 1 To 300000
Text1 = x
y = y + 1
If x Mod 1000 = 0 Then DoEvents
'получаем остаток от деления
'и если он равен нулю, запускаем DoEvents

Next x
End Sub



А может быть снижение быстродействия не так уж и значительно и им можно пренебречь? Чтобы нам не принимать все на веру, мы сами проведем соответствующее сравнительное исследование зависимости быстродействия от частоты срабатывания функции DoEvents. Для этого нам на форме дополнительно понадобятся Timer1, и еще два текстбокса Text2 и Text3. В уже имеющийся Text1 мы будем вводить интервал цикла (по умолчанию он у нас будет 300000), в Text2 - на какой итерации (1-ой, т.е. каждой, 100-ой, 10000-й и т.д ) будет включаться функция DoEvents. В Text3 выведем результат измерений в миллисекундах.
Объявим три переменных:

Option Explicit
Dim Iter As Long 'итерация срабатывания DoEvents
Dim RunTime As Integer 'время работы цикла
Dim RunLong As Long 'длина цикла


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

Private Sub Form_Load()
Text2 = 1
Text1 = 1000000
End Sub

Код процедуры Command1_Click Изменим следующим образом:

Private Sub Command1_Click()
Dim x As Long
Dim y As Long
Iter = Val(Text2) 'Присваиваем переменным значения для цикла
RunLong = Val(Text1)
RunTime = 0
Text3 = "Цикл в работе"
Timer1.Interval = 1 'устанавливаем интервал таймера в 1 мск
Timer1.Enabled = True 'запускаем таймер

For x = 1 To RunLong
'обратите внимание на то, что вывод значений переменной цикла X не выводится в Text1,
'так как это непомерные затраты времени и исказят наши значения.
'Text1 = x

y = y + 1
If y = Iter Then
DoEvents
y = 0
End If
Next
x
Timer1.Enabled = False 'выключаем таймер
Text3 = RunTime 'выводим значение счетчика таймера в текстбокс
End Sub

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

Private Sub Timer1_Timer()
RunTime = RunTime + 1
End Sub

Скажу сразу, что данный эксперимент не ставит собой целью получить точные данные, т. к. это невозможно хотя бы из-за того, что процессор в Windows'е постоянно выполняет определенную работу в фоновом режиме, и хотя мы этого не видим, но отнимает время у нашей программы. Для получения корректных результатов промежуток времени работы цикла должен быть большой, с тем, чтобы погрешность, скажем 5 мск была бы не критичной.
Попробуй запустить тест. При этом, конечно, не надо двигать форму по экрану, чтобы не занимать дополнительного времени. Мы увидим, что при включении функции DoEvents каждую 10-ю итерацию ускоряет цикл в 7-8 раз, каждую 100-ю - в 18-20 раз. Т.е. функция DoEvents снижает быстродействие просто в разы!!! Но тем не менее, использовать надо, но очень разумно.

Используя эту функцию мы можем позволить пользователю прервать продолжительный процесс. Для этого достаточно ввести булеву переменную для флага, например Flag
, которая и будет сигнализировать о том, что процесс необходимо прервать.
Добавим на форму кнопку Command2, которой и будем прерывать выполнение цикла. В секцию (General) добавим объявление флага


Dim Flag As Boolean

В процедуре Command1_Click, прямо в самом начале установим начальное для цикла значения флага:

Flag = False

а в самом цикле поставим условие на выход

If Flag = True Then Exit Sub

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

Private Sub Command2_Click()
Flag = True
Text3 = "Цикл прерван"
End Sub


Вот и готово. Теперь наша кнопка Command2 прерывает цикл. Скачать исходник примера можно вверху страницы.

С помощью функции DoEvents можно также организовать цикл ожидания, прерываемый пользователем. В качестве примера сделаем программку-шутку, которая иммитирует предупреждение пользователю о начале форматирования жесткого диска. Для этого в новом Exe-проекте на форме нам понадобится Label1, Command1 и Timer1.
Объявим пару переменных:

Option Explicit
Dim RunTime As Integer 'время в секундах
Dim Flag As Boolean 'флаг, сигнализирующей о прерывании цикла

Для процедуры, собственно, цикла ожидания я выбрал событие формы _Activate, так как оно возникает, когда форма уже загружена и инициализирована и элементы на ней видимы.

Private Sub Form_Activate()
RunTime = 10 'устанавливаем начальные значения секунд
Timer1.Interval = 1000 'интервал срабатывания таймера 1 сек
Timer1.Enabled = True ' включаем таймер
'организуем цикл, работающий до тех пор, пока переменная RunTime
'не стане равной нулю (изменяется таймером)

Do Until RunTime = 0
DoEvents 'постоянно передаем управление Windows
If Flag Then 'проверяется, нажата ли кнопка Command1 для
'прерывания цикла (Flag=True) или нет (Flag=False)

Label1 = "Молодец, успел, но все равно форматирование начинается!"
Exit Sub 'выход из процедуры, если кнопка Command1 нажималась
End If 'конец условия
Loop 'конец цикла
Timer1.Enabled = False 'выключение таймера
Label1 = "Не успел, не успел, надеюсь на винчестере ничего ценного нет?"
End Sub

В процедуре таймера при каждом срабатывании (ежесекундно) уменьшаем значение RunTime на единицу

Private Sub Timer1_Timer()
RunTime = RunTime - 1
'и выводим соответствующее сообщение в лейбл
Label1 = "Форматирование диска начнется через " & RunTime & " сек. " _
& " Успеешь нажать кнопку?"
End Sub


Процедура Command1_Click() управляет флагом и выключает таймер

Private Sub Command1_Click()
Timer1.Enabled = False
Flag = True
End Sub

Скачать исходник этого примера можно отсюда.


Copyright © 2005 4us

 

 

Сайт создан в системе uCoz