Полагаю, что многие, кто сталкивался с проблемой точного отсчёта времени в Delphi, знают, что стандартный компонент Timer со страницы палитры компонентов System не может обеспечить гарантированно точность отсчёта промежутка времени менее 50 мс. Более того, при работе с этим компонентом частенько случаются «заскоки» при работе в разных ОС. Например, такая проблема с работой компонента рассматривается здесь. Как же сделать так, чтобы программа отсчитывала время точно?
Не будем изобретать велосипед, а воспользуемся уже имеющимся, хотя и малоосвещенным способом — создадим свой мультимедиа таймер с высокой точностью отсчёта промежутков времени.
Для работы воспользуемся ранее рассмотренным проектом из статьи «Мышометры и им подобные звери«. Доработаем проект таким образом, чтобы избавиться от использования стандартного компонента и вместе с тем не нарушить порядок работы программы.
1. Техническая реализация таймера высокой точности.
Вначале уясним, что за программный таймер мы создаем и чем он отличается от компонента Timer, помещенного на форму. А отличается наш таймер, кроме высокой точности, тем, что его не нежно привязывать к окну: при срабатывании стандартного таймера окну, за которым он закреплен, посылается сообщение WM_TIMER. Создаваемый же нами таймер работает по-другому. А теперь приступим к его реализации.
Первое, что вам следует зделать — объявить в секции uses модуь MMSystem. Собственно в нем (модуле) и содержатся все необходимые типы и методы для работы с нашим таймером.
Теперь объявляем переменную:
TimerID : UINT; //идентификатор таймера
Напишем процедуру, вызываемую при срабатывании таймера. Она в принципе уже готова (событие onTimer у компонента Timer на главной форме), но для порядка приведу её здесь с небольшим изменениями — в процедуре дополнительно просчитывается текущая скорость движения мыши по экрану:
procedure TimerProc(uTimerID, uMessage: UINT; dwUser, dw1, dw2: DWORD) stdcall; var curPos: TPoint; delta:real; begin GetCursorPos(curPos); if (curPos.X<>LastPos.X)or(curPos.Y<>LastPos.Y) then begin if doUpdate then begin delta:=SQRT(sqr(curPos.X-LastPos.X)+sqr(curPos.Y-LastPos.Y)); distance:=distance+delta; LastDelta:=delta*(GetDeviceCaps(DC, HORZSIZE)/Screen.Width); LastPos:=curPos; Form1.label18.Caption:=CurrToStr(distance*GetDeviceCaps(DC, HORZSIZE)/Screen.Width*0.001); end; end; Form1.Label21.Caption:=CurrToStr(LastDelta*10)+' м/с' end;
Теперь создаем наш програмный таймер:
TimerID:=timeSetEvent(10, timeGetMinPeriod, TimerProc, 0, TIME_CALLBACK_FUNCTION or TIME_PERIODIC);
Создавать таймер можно где угодно, лишь бы он выполнял свои функции. Я разместил создание в событии onClick единственной кнопки приложения :).
В приведенном выше отрывке программы с помощью функции timeSetEvent происходит регистрация и запоминание адреса процедуры TimerProc, вызываемой периодически при срабатывании таймера. При успешном создании таймера функция timeSetEvent возвращает ненулевое значение — идентификатор созданного таймера. Оно может использоваться в дальнейшем для определения, какой именно таймер сработал. Значение, возвращаемое функцией timeSetEvent, также необходимо при удалении таймера:
timeKillEvent(TimerID)
Функция timeKillEvent возвращает целочисленное значение:
- TIMER_NOERROR — если её вызов завершился успешно;
- MMSYSERR_INVALPARAM — если таймера, заданного параметром функции, не существует.
Теперь обратимся вновь к процедуре timeSetEvent. Что же мы там задали:
- 10 — интервал между срабатываниями таймера, мс
- timeGetMinPeriod — точность таймера (см. код функции ниже)
- TimerProc — адрес процедуры, вызываемой при срабатывании таймера;
- 0 — параметр, передаваемый в процедуру обратного вызова;
- TIME_CALLBACK_FUNCTION or TIME_PERIODIC — тип таймера.
Как вы можете заметить, последний параметр функции — это битовая маска. Флаги этой маски задают два аспекта поведения таймера: количество срабатываний и тип действия, которое требуется выполнить при срабатывании таймера.
Количество срабатываний таймера определяется двумя значениями:
- TIMER_ONESHOT — таймер срабатывает один раз. Для таких таймеров вызывать timeKillEvent после срабатывания не нужно.
- TIMER_PERIODIC — таймер срабатывает периодически через заданные промежутки времени.
Тип действия, выполняемого таймером, задается при помощи следующих констант:
- TIME_CALLBACK_FUNCTION — при срабатывании таймера вызывается процудура, адрес которой был передан третьим параметром;
- TIME_CALLBACK_EVENT_SET — вызывает SetEvent для объекта синхронизации «событие», дескриптор которого передан третьим параметром;
- TIME_CALLBACK_EVENT_PULSE — вызывает PulseEvent для объекта синхронизации «событие», дескриптор которого передан третьим параметром;
Т.к. объекты синхронизации нас не интересуют, то рассматривать их в этой статье смысла нет. Вместо этого ещё раз повторим действия, которые необходимо выполнить при работе с программным таймером. Это:
- Определяем действия, которые необходимо выполнить при срабатывании таймера и создаем процедуру, адрес которой будем передавать третьим параметром;
- Создаем таймер и регистрируем процедуру с помощью функции timeSetEvent
- После того как таймер отработал «убиваем» его с помощью функции timeKillEvent
Осталось рассмотреть дополнительные функции по работе с таймером.
2. Дополнительные процедуры по работе с программными таймерами.
Во-первых рассмотрим процедуру timeGetMinPeriod, которая определяет минимальный период таймера. Выглядит она следующим образом:
function timeGetMinPeriod(): DWORD; var time: TTimeCaps; begin timeGetDevCaps(Addr(time), SizeOf(time)); timeGetMinPeriod := time.wPeriodMin; end;
Соответственно, максимальный период таймера будет определяться следующим образом:
function timeGetMaxPeriod(): Cardinal; var time: TTimeCaps; begin timeGetDevCaps(Addr(time), SizeOf(time)); timeGetMaxPeriod := time.wPeriodMax; end;
Чтобы установить новый период срабатывания таймера, перед началом измерения необходимо выполнить функцию:
function timeSetTimerPeriod(period: Cardinal): Boolean; begin if timeBeginPeriod(period) = TIMERR_NOERROR then begin //Сохраним значение для восстановления состояния таймера lastPeriod := period; timeSetTimerPeriod := True; end else//Неудача timeSetTimerPeriod := False; end;
Ну, и наконец, для восстановления периода таймера необходимо обязательно выполнить функцию:
function timeRestoreTimerPeriod(): Boolean; begin timeEndPeriod(lastPeriod) = TIMERR_NOERROR end;
Теперь Вы можете смело доработать программу, убрав с главной форму компонент Timer и заменив его на новый программный. Кроме того, при небольшой доработке программы Вы сможете узнать не только расстояние пройденное мышкой по экрану, но и (что немаловажно для неискушенных пользователей «Мышометров») скорость движения мыши по экрану.
В этой статье я рассмотрел всего лишь один из возможных способов отсчёт промежутков времени с высокой точностью. Если же Вам необходимо засечь время, необходимое для выполнения какого-либо длительного процесса в программе, то тут Вам на помощь прийдут и другие способы, например использование «тиков» и пр. Но это уже совсем другая история ;)
Очень ценная штука.
Я тебя люблю, Чувак!! ^_^
Очень классная статья!! Кок раз то что искал!!))
Статья — супер!!!
если еще микросекунды реализовать…
Всё это очень замечательно, но вылетает ошибка по поводу нехватки памяти или что-то в этом духе. Пробовал на разных компах и на мощном такая же беда. Что делать как быть?
Ошибка Access Violation? Точный код с ошибкой в студию — будем разбираться.
Access violation at address 00401BFC in module ‘Project1.exe’.Write of addres 00003131
А вот ещё одна… Error creating window device context — вылетает вылетает несколько штук если программа работает более десяти минут(замечена с обычным таймером)… тоже проверял на разных компах.
А код программы-то где? :) Про AV я и так понял, что она вылетает. Скиньте код вашего таймера, желательно весь
[code]unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, MMSystem, StdCtrls, ExtCtrls; type TForm1 = class(TForm) Label1: TLabel; Button1: TButton; Button2: TButton; Label2: TLabel; Timer1: TTimer; Button3: TButton; Button4: TButton; procedure Button1Click(Sender: TObject); procedure Button2Click(Sender: TObject); procedure Timer1Timer(Sender: TObject); procedure Button3Click(Sender: TObject); procedure Button4Click(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; TimerID : UINT; LastPos: TPoint; lastPeriod:Cardinal; LastDelta:Real; doUpdate: boolean; distance: single; DC: HDC; curPos: TPoint; delta:real; a:integer; b:TPoint; implementation {$R *.dfm} function timeGetMinPeriod(): DWORD; var time: TTimeCaps; begin timeGetDevCaps(Addr(time), SizeOf(time)); timeGetMinPeriod := time.wPeriodMin; end; //Соответственно, максимальный период… Подробнее »
ой, я теги честно поставил
Пожалуйста, давайте всё же разберёмся что тут не так… очень нужна программа такая.
Да конечно разберемся :) Просто неделя-то рабочая вот времени особенно и нету на Delphi. Сегодня-завтра гляну ваш листинг и отпишусь
Евгений, воспроизвел ваш листинг в точности как вы его здесь представили. ждал минут 10 — никаких AV не было вообще. Сбросьте (если это возможно) мне на email (vlad383@mail.ru) весь проект целиком
Непонятен второй параметр функции timeSetEvent (точность таймера). В справке Делфи он называется разрешение таймера. На что он влияет? И еще: при работе таймера загрузка процессора взлетает до 100%. менял это самое разрешение таймера от 1 до максимума — все равно проц грузится. Куда это годится??
прошу прощения, насчет загрузки процессора — это мой косяк. разобрался. но вопрос по поводу разрешения таймера остается открытым. ЧТО ТАКОЕ РАЗРЕШЕНИЕ ТАЙМЕРА?