Часть
1. Основы Visual Basiс
|
||||||
12.10.2005 | ||||||
Глава
22.
|
||||||
Функция
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 на единицу
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
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
|