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

Полагаю, что многие, кто сталкивался с проблемой точного отсчёта времени в 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 для объекта синхронизации «событие», дескриптор которого передан третьим параметром;

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

  1. Определяем действия, которые необходимо выполнить при срабатывании таймера и создаем процедуру, адрес которой будем передавать третьим параметром;
  2. Создаем таймер и регистрируем процедуру с помощью функции timeSetEvent
  3. После того как таймер отработал «убиваем» его с помощью функции 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 и заменив его на новый программный. Кроме того, при небольшой доработке программы Вы сможете узнать не только расстояние пройденное мышкой по экрану, но и (что немаловажно для неискушенных пользователей «Мышометров») скорость движения мыши по экрану.

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

5 2 голоса
Рейтинг статьи
уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.
Подписаться
Уведомить о
14 Комментарий
Межтекстовые Отзывы
Посмотреть все комментарии
Витя
Витя
03/10/2009 11:00

Очень ценная штука.

Karp13
Karp13
03/10/2009 19:27

Я тебя люблю, Чувак!! ^_^
Очень классная статья!! Кок раз то что искал!!))

WOWan
WOWan
26/03/2010 13:43

Статья — супер!!!

если еще микросекунды реализовать…

Евгений
Евгений
30/11/2010 23:13

Всё это очень замечательно, но вылетает ошибка по поводу нехватки памяти или что-то в этом духе. Пробовал на разных компах и на мощном такая же беда. Что делать как быть?

Евгений
Евгений
30/11/2010 23:48

Access violation at address 00401BFC in module ‘Project1.exe’.Write of addres 00003131
А вот ещё одна… Error creating window device context — вылетает вылетает несколько штук если программа работает более десяти минут(замечена с обычным таймером)… тоже проверял на разных компах.

Евгений
Евгений
01/12/2010 00:35

[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; //Соответственно, максимальный период… Подробнее »

Евгений
Евгений
01/12/2010 00:43

ой, я теги честно поставил

Евгений
Евгений
02/12/2010 23:57

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

DenNik
DenNik
14/02/2013 18:24

Непонятен второй параметр функции timeSetEvent (точность таймера). В справке Делфи он называется разрешение таймера. На что он влияет? И еще: при работе таймера загрузка процессора взлетает до 100%. менял это самое разрешение таймера от 1 до максимума — все равно проц грузится. Куда это годится??

DenNik
DenNik
14/02/2013 18:33
Ответить на  DenNik

прошу прощения, насчет загрузки процессора — это мой косяк. разобрался. но вопрос по поводу разрешения таймера остается открытым. ЧТО ТАКОЕ РАЗРЕШЕНИЕ ТАЙМЕРА?