7. Таймер

Во многих программах требуется следить за временем или выполнять какие-либо периодические действия. Программы MS-DOS для работы с таймером перехватывали аппаратное прерывание таймера, встраивая свой собственный обработчик для прерывания INT 8h. Обычные приложения Windows не могут самостоятельно обрабатывать прерывания таймера, поэтому для работы с ним нужно использовать другие способы.

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

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

Есть и другой способ, также основанный на передаче сообщений. При использовании этого способа сообщения WM_TIMER посылаются не функции окна, а специальной функции, описанной с ключевым словом _export. Эта функция напоминает функцию окна и, так же как и функция окна, вызывается не из приложения, а из Windows. Функции, которые вызываются из Windows, имеют специальный пролог и эпилог и называются функциями обратного вызова (callback function). Функция окна и функция, специально предназначенная для обработки сообщений таймера, являются примерами функций обратного вызова.

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

DWORD WINAPI GetTimerResolution(void);

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

7.1. Создание и уничтожение таймера

Для создания виртуального таймера приложение должно использовать функцию SetTimer:

UINT WINAPI SetTimer(HWND hwnd, UINT idTimer,
   UINT uTimeout, TIMERPROC tmprc);

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

Второй параметр (idTimer) определяет идентификатор таймера (он не должен быть равен нулю). Идентификатор используется только в том случае, если первый параметр функции SetTimer содержит идентификатор окна. Так как для одного окна можно создать несколько таймеров, для того чтобы различать сообщения, приходящие от разных таймеров, приложение при создании должно снабдить каждый таймер собственным идентификатором.

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

Третий параметр (uTimeout) определяет период следования сообщений от таймера в миллисекундах. Учтите, что физический таймер тикает приблизительно 18,21 раза в секунду (точное значение составляет 1000/54,925). Поэтому, даже если вы укажете, что таймер должен тикать каждую миллисекунду, сообщения будут приходить с интервалом не менее 55 миллисекунд.

Последний параметр (tmprc) определяет адрес функции, которая будет получать сообщения WM_TIMER (мы будем называть эту функцию функцией таймера). Этот параметр необходимо обязательно указать, если первый параметр функции SetTimer равен NULL.

Тип TIMERPROC описан в файле windows.h следующим образом:

typedef void (CALLBACK* TIMERPROC)(HWND hwnd,
  UINT msg, UINT idTimer, DWORD dwTime);

Сравните это с описанием типа WNDPROC, который используется для знакомой вам функции окна:

typedef LRESULT (CALLBACK* WNDPROC)(HWND hwnd,
  UINT msg, WPARAM wParam, LPARAM lParam);

Как видно из описания, функция таймера не возвращает никакого значения, имеет другие (по сравнению с функцией окна) параметры, но описана с тем же ключевым словом CALLBACK:

#define CALLBACK _far _pascal

Возвращаемое функцией SetTimer значение является идентификатором созданного таймера (если в качестве первого параметра функции было указано значение NULL). В любом случае функция SetTimer возвращает нулевое значение, если она не смогла создать таймер. В Windows версии 3.0 максимальное количество созданных во всей системе таймеров было 16. Для Windows версии 3.1 это ограничение снято.

Тем не менее, если приложение больше не нуждается в услугах таймера, оно должно уничтожить таймер, вызвав функцию KillTimer:

BOOL WINAPI KillTimer (HWND hwnd, UINT idTimer);

Первый параметр функции (hwnd) определяет идентификатор окна, указанный при создании таймера функцией SetTimer.

Второй параметр (idTimer) - идентификатор уничтожаемого таймера. Это должен быть либо тот идентификатор, который вы указали при создании таймера (если таймер создавался для окна), либо значение, полученное при создании таймера от функции SetTimer (для таймера, имеющего собственную функцию обработки сообщений).

Функция KillTimer возвращает значение TRUE при успешном уничтожении таймера или FALSE, если она не смогла найти таймер с указанным идентификатором.

7.2. Сообщение WM_TIMER

Параметр wParam сообщения WM_TIMER содержит идентификатор таймера, который был указан или получен от функции SetTimer при создании таймера.

С помощью параметра lParam можно определить адрес функции, которая обрабатывает сообщения таймера.

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

Заметим, что сообщение WM_TIMER является низкоприоритетным. Это означает, что функция DispatchMessage посылает это сообщение приложению только в том случае, если в очереди приложения нет других сообщений. В этом отличие таймера Windows от аналогичных средств MS-DOS, реализованных с помощью перехвата прерывания INT 8h.

Выполнение программы MS-DOS прерывается синхронно с приходом аппаратного прерывания таймера и программа MS-DOS, перехватившая это прерывание, немедленно оповещается о нем. Выполнение приложения Windows тоже, разумеется, прерывается по аппаратному прерыванию таймера, но оповещение об этом событии приходит не всегда, и как правило, позже, вместе с сообщением WM_TIMER.

7.3. Подключение таймера к окну

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

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

#define FIRST_TIMER 1
int nTimerID;
nTimerID = SetTimer(hwnd, FIRST_TIMER, 1000, NULL);

В данном примере создается таймер с идентификатором FIRST_TIMER, который будет посылать сообщения примерно раз в секунду.

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

KillTimer(hwnd, FIRST_TIMER);

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

KillTimer(hwnd, FIRST_TIMER);
nTimerID = SetTimer(hwnd, FIRST_TIMER, 100, NULL);

7.4. Использование функции таймера

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

void CALLBACK _export
TimerProc(HWND hwnd, UINT msg, UINT idTimer, DWORD dwTime);

Как и для функции окна, для функции таймера можно выбрать любое имя. При создании таймера вам надо указать адрес функции таймера, а имя не имеет никакого значения.

Первый параметр функции таймера - идентификатор окна, с которым связан таймер. Если при создании таймера в качестве идентификатора было указано значение NULL, это же значение будет передано функции таймера.

Второй параметр представляет собой идентификатор сообщения WM_TIMER.

Третий параметр является идентификатором таймера, пославшего сообщение WM_TIMER.

И наконец, последний параметр - текущее время по системным часам компьютера. Это время выражается в количестве тиков таймера с момента запуска Windows. Вы можете узнать текущее системное время в любой момент, если воспользуетесь функцией GetCurrentTime или GetTickCount:

DWORD WINAPI GetCurrentTime(void);
DWORD WINAPI GetTickCount(void);

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

Если для создания приложения вы пользуетесь современными средствами разработки, такими, как Borland C++ версии 3.1, Microsoft С++ версии 7.0, Microsoft Visual C++, для определения функции обратного вызова достаточно использовать ключевое слово _export. В этом случае вы можете создать таймер, например, так:

nTimerID = SetTimer(hwnd, 0, 1000, (TIMERPROC)TimerProc);

Для удаления таймера в этом случае необходимо использовать идентификатор, возвращенный функцией SetTimer:

KillTimer(hwnd, nTimerID );

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

Когда вы не можете ограничиться использованием ключевого слова _export, для работы с функциями обратного вызова нужно сделать специальный переходник (thunk), вызвав функцию MakeProcInstance:

FARPROC WINAPI MakeProcInstance(FARPROC lpProc, 
    HINSTANCE hinst);

В качестве первого параметра функции (lpProc) необходимо передать адрес функции, для которой создается переходник, а в качестве второго (hinst) - идентификатор приложения hInstance, полученный функцией WinMain при запуске приложения.

Функция MakeProcInstance для указанной функции создает функцию-переходник, возвращая ее адрес, а также обеспечивает функции доступ к сегменту данных приложения, загружая соответствующим образом сегментные регистры.

Процедура создания таймера с использованием функции MakeProcInstance может выглядеть следующим образом:

TIMERPROC lpfnTimerProc;
lpfnTimerProc = (TIMERPROC)MakeProcInstance(
  (FARPROC)TimerProc, hInstance);
nTimerID = SetTimer(hwnd, 0, 1000, (TIMERPROC)lpfnTimerProc);

После уничтожения таймера следует уничтожить созданный функцией MakeProcInstance переходник, для чего следует вызвать функцию FreeProcInstance:

void WINAPI FreeProcInstance(FARPROC lpProc);

Этой функции необходимо передать адрес уничтожаемой функции-переходника:

FreeProcInstance(lpfnTimerProc);

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

Старые средства разработки приложений Windows требуют, чтобы все функции обратного вызова, такие, как функции окон и функции таймеров, были описаны в файле определения модуля, имеющем расширение .def, при помощи оператора EXPORTS, например:

EXPORTS WndProc
        TimerProc

Транслятор Borland C++ версии 3.1 распознает ключевое слово _export и автоматически формирует нужный пролог и эпилог для функций обратного вызова. То же самое относится и к трансляторам Microsoft C++ версии 7.0 и Microsoft Visual C++. Поэтому в наших примерах функции MakeProcInstance и FreeProcInstance, а также оператор файла определения модуля EXPORTS не используются.

7.5. Приложение TIMBEEP

Приложение TIMBEEP демонстрирует простейший случай использования таймера.

Главный файл приложения приведен в листинге 7.1.

Листинг 7.1. Файл timbeep\timbeep.cpp

// ----------------------------------------
// Обработка сообщений от таймера
// ----------------------------------------

#define STRICT
#include <windows.h>
#include <mem.h>

BOOL InitApp(HINSTANCE);
LRESULT CALLBACK _export WndProc(HWND, UINT, WPARAM, LPARAM);

char const szClassName[]   = "TIMBEEPAppClass";
char const szWindowTitle[] = "TIMBEEP Application";

// =====================================
// Функция WinMain
// =====================================
#pragma argsused

int PASCAL
WinMain(HINSTANCE hInstance,
        HINSTANCE hPrevInstance,
        LPSTR     lpszCmdLine,
        int       nCmdShow)
{
  MSG  msg;   // структура для работы с сообщениями
  HWND hwnd;  // идентификатор главного окна приложения

  if(!InitApp(hInstance))
      return FALSE;

  hwnd = CreateWindow(
    szClassName, szWindowTitle, 
    WS_OVERLAPPEDWINDOW,
    CW_USEDEFAULT, CW_USEDEFAULT,
    CW_USEDEFAULT, CW_USEDEFAULT,
    0, 0, hInstance, NULL); 

  if(!hwnd)
    return FALSE;

  ShowWindow(hwnd, nCmdShow);
  UpdateWindow(hwnd);

  while(GetMessage(&msg, 0, 0, 0))
  {
    DispatchMessage(&msg);
  }
  return msg.wParam;
}

// =====================================
// Функция InitApp
// =====================================

BOOL
InitApp(HINSTANCE hInstance)
{
  ATOM aWndClass; // атом для кода возврата
  WNDCLASS wc;    // структура для регистрации
                  // класса окна

  memset(&wc, 0, sizeof(wc));
  wc.style         = 0;
  wc.lpfnWndProc   = (WNDPROC) WndProc;
  wc.cbClsExtra    = 0;
  wc.cbWndExtra    = 0;
  wc.hInstance     = hInstance;
  wc.hIcon   = LoadIcon(NULL, IDI_APPLICATION);
  wc.hCursor = LoadCursor(NULL, IDC_ARROW);
  wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
  wc.lpszMenuName  = (LPSTR)NULL;
  wc.lpszClassName = (LPSTR)szClassName;

  aWndClass = RegisterClass(&wc);
  return (aWndClass != 0);
}

Функция WinMain создает главное окно приложения. Функция главного окна (листинг 7.2) будет получать приблизительно раз в секунду сообщение с кодом WM_TIMER.

/Листинг 7.2. Файл timbeep\wndproc.cpp

/ =====================================
// Функция WndProc
// =====================================

#define STRICT
#include <windows.h>

#define BEEP_TIMER 1

LRESULT CALLBACK _export
WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{

  switch (msg)
  {
    case WM_CREATE:
    {
       // Создаем таймер, посылающий сообщения
       // функции окна примерно раз в секунду
       SetTimer(hwnd, BEEP_TIMER, 1000, NULL);
       return 0;
    }

    case WM_TIMER:
    {
       // В ответ на сообщение таймера выдаем
       // звуковой сигнал
       MessageBeep(-1);
       return 0;
    }

    case WM_DESTROY:
    {
      // Перед уничтожением окна уничтожаем
      // созданный ранее таймер
      KillTimer(hwnd, BEEP_TIMER);

      PostQuitMessage(0);
      return 0;
    }
  }
  return DefWindowProc(hwnd, msg, wParam, lParam);
}

По сообщению WM_CREATE (оно приходит при создании окна) функция окна создает таймер:

SetTimer(hwnd, BEEP_TIMER, 1000, NULL);

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

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

Перед уничтожением окна в функцию окна передается сообщение WM_DESTROY, по которому приложение уничтожает созданный ранее таймер:

KillTimer(hwnd, BEEP_TIMER);

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

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

Файл определения модуля для приложения TIMBEEP приведен в листинге 7.3.

Листинг 7.3. Файл timbeep\timbeep.def

; =============================
; Файл определения модуля
; =============================
NAME        TIMBEEP
DESCRIPTION 'Приложение TIMBEEP, (C) 1994, Frolov A.V.'
EXETYPE     windows
STUB        'winstub.exe'
STACKSIZE   5120
HEAPSIZE    1024
CODE        preload moveable discardable
DATA        preload moveable multiple

7.6. Приложение TMCLOCK

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

Главный файл приложения, содержащий функцию WinMain, представлен в листинге 7.4.

Листинг 7.4. Файл tmclock\tmclock.cpp

// ----------------------------------------
// Простейшие часы
// ----------------------------------------

#define STRICT
#include <windows.h>
#include <mem.h>

BOOL InitApp(HINSTANCE);
LRESULT CALLBACK _export WndProc(HWND, UINT, WPARAM, LPARAM);

char const szClassName[]   = "TMCLOCKAppClass";
char const szWindowTitle[] = "TMCLOCK Application";

TEXTMETRIC tm;
int cxChar, cyChar;
RECT rc;

// =====================================
// Функция WinMain
// =====================================
#pragma argsused

int PASCAL
WinMain(HINSTANCE hInstance,
        HINSTANCE hPrevInstance,
        LPSTR     lpszCmdLine,
        int       nCmdShow)
{
  MSG  msg;   // структура для работы с сообщениями
  HWND hwnd;  // идентификатор главного окна приложения

  if(!InitApp(hInstance))
      return FALSE;

  // Получаем координаты окна Desktop.
  // Это окно занимает всю поверхность экрана,
  // и на нем расположены все остальные окна
  GetWindowRect(GetDesktopWindow(), &rc);

  // Создаем временное окно с толстой
  // рамкой для изменения размера, но без
  // заголовка и системного меню.
  // При создании окна указываем произвольные
  // размеры окна и произвольное расположение
  hwnd = CreateWindow(
    szClassName, szWindowTitle, 
    WS_POPUPWINDOW | WS_THICKFRAME,
    100, 100,
    100, 100,
    0, 0, hInstance, NULL); 

  if(!hwnd)
    return FALSE;

  // Передвигаем окно в правый нижний
  // угол экрана
  MoveWindow(hwnd,
    rc.right  - cxChar * 15,
    rc.bottom - cyChar * 3,
    cxChar * 10, cyChar * 2, TRUE);

  // Отображаем окно в новом месте
  ShowWindow(hwnd, nCmdShow);
  UpdateWindow(hwnd);

  while(GetMessage(&msg, 0, 0, 0))
  {
    DispatchMessage(&msg);
  }
  return msg.wParam;
}

// =====================================
// Функция InitApp
// =====================================

BOOL
InitApp(HINSTANCE hInstance)
{
  ATOM aWndClass; // атом для кода возврата
  WNDCLASS wc;    // структура для регистрации
                  // класса окна

  memset(&wc, 0, sizeof(wc));
  wc.style         = CS_HREDRAW | CS_VREDRAW;
  wc.lpfnWndProc   = (WNDPROC) WndProc;
  wc.cbClsExtra    = 0;
  wc.cbWndExtra    = 0;
  wc.hInstance     = hInstance;
  wc.hIcon   = LoadIcon(NULL, IDI_APPLICATION);
  wc.hCursor = LoadCursor(NULL, IDC_ARROW);
  wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
  wc.lpszMenuName  = (LPSTR)NULL;
  wc.lpszClassName = (LPSTR)szClassName;

  aWndClass = RegisterClass(&wc);
  return (aWndClass != 0);
}

По сравнению с предыдущими приложениями функция WinMain приложения TMCLOCK имеет некоторые особенности.

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

Вначале мы получаем и запоминаем размеры так называемого окна Desktop, на поверхности которого расположены все окна (и пиктограммы) Windows. Функция GetDesktopWindow возвращает идентификатор окна Desktop, который мы используем для определения размера, передавая его функции GetWindowRect.

Далее мы создаем временное окно с толстой рамкой для изменения размера. Расположения и размеры окна на данном этапе нас не интересуют, так как мы скоро их изменим:

hwnd = CreateWindow(
  szClassName, szWindowTitle, 
  WS_POPUPWINDOW | WS_THICKFRAME,
  100, 100,100, 100, 0, 0, hInstance, NULL); 

Во время своего создания главное окно приложения получает сообщение WM_CREATE, обработчик которого определяет метрики шрифта, а также записывает размеры шрифта в переменные cxChar и cyChar.

После этого главное окно приложения уменьшается в размерах и перемещается в правый нижний угол окна Desktop:

MoveWindow(hwnd,
  rc.right  - cxChar * 15,
  rc.bottom - cyChar * 3,
  cxChar * 10, cyChar * 2, TRUE);

Функция MoveWindow определяет новое расположение и размеры окна:

BOOL WINAPI MoveWindow(HWND hwnd,
   int nLeft, int nTop, int nWidth, int nHeight,
   BOOL fRepaint);

Первый параметр функции (hwnd) указывает идентификатор перемещаемого окна.

Второй параметр (nLeft) указывает координату левой границы окна, третий (nTop) - координаты нижней границы окна.

Четвертый (nWidth) и пятый (nHeight) параметры определяют соответственно ширину и высоту окна.

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

После перемещения окна оно отображается (уже на новом месте). Далее запускается обычный цикл обработки сообщений.

Исходные тексты функции главного окна и функции таймера приведены в листинге 7.5.

Листинг 7.5. Файл tmclock\wndproc.cpp

#define STRICT
#include <windows.h>
#include <time.h>

// Идентификатор таймера, который используется
// для измерения времени
#define CLOCK_TIMER 1

// Прототип функции таймера
void CALLBACK _export TimerProc(HWND, UINT, UINT, DWORD);

// Переменная для хранения идентификатора таймера,
// который используется для выдачи звукового сигнала
int nBeepTimerID;

// Внешние переменные
extern TEXTMETRIC tm;
extern int cxChar, cyChar;

// =====================================
// Функция WndProc
// =====================================

LRESULT CALLBACK _export
WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
  HDC        hdc;
  PAINTSTRUCT ps;

  switch (msg)
  {
    case WM_CREATE:
    {
      // Создаем таймер, посылающий сообщения
      // функции окна примерно раз в секунду
      SetTimer(hwnd, CLOCK_TIMER, 1000, NULL);

      // Создаем таймер, который периодически
      // раз в секунду посылает сообщения в
      // функцию таймера TimerProc
      nBeepTimerID = SetTimer(hwnd, 0,  1000,
          (TIMERPROC)TimerProc);

      hdc = GetDC(hwnd);

      // Выбираем шрифт с фиксированной шириной букв
      SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT));

      // Заполняем структуру информацией
      // о метрике шрифта, выбранного в
      // контекст отображения
      GetTextMetrics(hdc, &tm);

      // Запоминаем значение ширины для
      // самого широкого символа
      cxChar = tm.tmMaxCharWidth;

      // Запоминаем значение высоты букв с
      // учетом межстрочного интервала
      cyChar = tm.tmHeight + tm.tmExternalLeading;

      ReleaseDC(hwnd, hdc);
      return 0;
    }

    // Для обеспечения возможности перемещения
    // окна, не имеющего заголовка, встраиваем
    // свой обработчик сообщения WM_NCHITTEST
    case WM_NCHITTEST:
    {
      long lRetVal;

      // Вызываем функцию DefWindowProc и проверяем
      // возвращаемое ей значение
      lRetVal = DefWindowProc(hwnd, msg, wParam, lParam);

      // Если курсор мыши находится на одном из
      // элементов толстой рамки, предназначенной
      // для изменения размера окна, возвращаем
      // неизмененное значение, полученное от
      // функции DefWindowProc
      if(lRetVal == HTLEFT        ||
         lRetVal == HTRIGHT       ||
         lRetVal == HTTOP         ||
         lRetVal == HTBOTTOM      ||
         lRetVal == HTBOTTOMRIGHT ||
         lRetVal == HTTOPRIGHT    ||
         lRetVal == HTTOPLEFT     ||
         lRetVal == HTBOTTOMLEFT)
      {
      return lRetVal;
      }

      // В противном случае возвращаем значение
      // HTCAPTION, которое соответствует
      // заголовку окна.
      else
      {
        return HTCAPTION;
      }
    }

    // Каждую секунду перерисовываем
    // внутреннюю область окна
    case WM_TIMER:
    {
       InvalidateRect(hwnd, NULL, FALSE);
       return 0;
    }

    case WM_DESTROY:
    {
      // Перед уничтожением окна уничтожаем
      // созданные ранее таймеры
      KillTimer(hwnd, CLOCK_TIMER);
      KillTimer(hwnd, nBeepTimerID);

      PostQuitMessage(0);
      return 0;
    }

    case WM_PAINT:
    {
      BYTE   szBuf[80];
      int    nBufSize;
      time_t t;
      struct tm *ltime;
      RECT    rc;

      hdc = BeginPaint(hwnd, &ps);

      // Определяем время и его отдельные компоненты
      time(&t);
      ltime = localtime(&t);

      // Подготавливаем буфер, заполняя его
      // строкой с текущим временем
      nBufSize = wsprintf(szBuf, "%02d:%02d:%02d",
        ltime->tm_hour,
        ltime->tm_min,
        ltime->tm_sec);

      // Выбираем шрифт с фиксированной шириной букв
      SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT));

      // Получаем координаты и размер окна
      GetClientRect(hwnd, &rc);

      // Выводим время в центре окна
      DrawText(hdc, (LPSTR)szBuf, nBufSize, &rc,
        DT_CENTER | DT_VCENTER | 
        DT_NOCLIP | DT_SINGLELINE);

      EndPaint(hwnd, &ps);
    }
  }
  return DefWindowProc(hwnd, msg, wParam, lParam);
}

// =====================================
// Функция TimerProc
// =====================================
#pragma argsused
void CALLBACK _export
TimerProc(HWND hwnd, UINT msg, UINT idTimer, DWORD dwTime)
{
  // Просто выдаем звуковой сигнал
  MessageBeep(-1);
  return;
}

Обработчик сообщения WM_CREATE функции главного окна создает два таймера. Первый таймер посылает сообщения в функцию главного окна (раз в секунду):

SetTimer(hwnd, CLOCK_TIMER, 1000, NULL);

Второй таймер посылает сообщения в функцию TimerProc (также один раз в секунду):

nBeepTimerID = SetTimer(hwnd, 0,  1000,
    (TIMERPROC)TimerProc);

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

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

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

Если же вы нажали клавишу мыши во внутренней области окна, наш обработчик возвращает значение HTCAPTION, соответствующее заголовку окна. Благодаря этому Windows сможет перемещать окно, не имеющее заголовка.

Обработчик сообщения WM_TIMER получает управление каждую секунду. Его задача сводится просто к тому, что он объявляет все окно как требующее перерисовки. В этом случае каждую секунду функция окна будет получать сообщение WM_PAINT.

Задача обработчика сообщения WM_PAINT сводится к отображению по центру окна времени в формате ЧЧ:ММ:СС, где ЧЧ - часы, ММ - минуты, СС - секунды.

Для определения текущего времени обработчик сообщения вызывает функцию time, которая записывает информацию о времени на момент своего вызова в структуру типа time_t, определенную в файле time.h следующим образом:

typedef long time_t;

Для раскодирования информации о времени и представления ее в удобном для обработки виде приложение вызывает функцию localtime. Эта функция возвращает указатель на статическую структуру типа tm, содержащую отдельные компоненты времени. Тип tm описан в файле time.h:

struct tm
{
  int   tm_sec;   // секунды
  int   tm_min;   // минуты
  int   tm_hour;  // часы (0...23)
  int   tm_mday;  // день месяца (1...31)
  int   tm_mon;   // месяц (0...11)
  int   tm_year;  // год (календарный год минус 1900)
  int   tm_wday;  // номер дня недели 
                  //   (0...6, 0 - воскресенье)
  int   tm_yday;  // день года (0...365)
  int   tm_isdst; // флаг летнего времени (0 - летнее время
                  //    не используется)
};

Перед выводом обработчик сообщения определяет координаты и размер окна:

GetClientRect(hwnd, &rc);

Полученные координаты используются для вывода текстовой строки, содержащей время:

DrawText(hdc, (LPSTR)szBuf, nBufSize, &rc,
  DT_CENTER | DT_VCENTER | 
  DT_NOCLIP | DT_SINGLELINE);

Параметры функции DrawText подобраны таким образом, чтобы текстовая строка выводилась в центре окна. Параметр DT_CENTER используется для центрирования текста по горизонтали, параметр DT_VCENTER - для центрирования текста по вертикали. Если не указать параметр DT_SINGLELINE, означающий, что в окно выводится только одна строка текста, эта строка окажется в верхней части экрана, то есть не будет отцентрована по вертикали. Параметр DT_NOCLIP обеспечивает более быстрый вывод текста, запрещая проверку области ограничения при выводе

Когда приложение завершает свою работу, обработчик сообщения WM_DESTROY уничтожает оба созданных по сообщению WM_CREATE таймера:

KillTimer(hwnd, CLOCK_TIMER);
KillTimer(hwnd, nBeepTimerID);

Функция таймера, предназначенная для выдачи звукового сигнала, очень проста. Ее роль ограничивается именно выдачей звукового сигнала:

void CALLBACK _export
TimerProc(HWND hwnd, UINT msg, UINT idTimer, DWORD dwTime)
{
  // Просто выдаем звуковой сигнал
  MessageBeep(-1);
  return;
}

Файл определения модуля приложения TMCLOCK приведен в листинге 7.6.

Листинг 7.6. Файл tmclock\tmclock.def

; =============================
; Файл определения модуля
; =============================
NAME        TMCLOCK
DESCRIPTION 'Приложение TMCLOCK, (C) 1994, Frolov A.V.'
EXETYPE     windows
STUB        'winstub.exe'
STACKSIZE   5120
HEAPSIZE    1024
CODE        preload moveable discardable
DATA        preload moveable multiple