Die WinAPI Plattform

Tutorials

Tutorial Nummer 07

(07.) Timer programmieren

In diesem Tutorial zeige ich dir, wie man den Nachrichten Timer von Windows initalisiert und benutzt. Der Timer ist zwar sehr ungenau, aber dazu später mehr. Dann zeige ich dir in diesem Tutorial etwas verschiedene Fenstertypen.

#define STRICT
#include <windows.h>

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

Nun deklarieren wir drei Konstanten. Einmal ist das die Kennziffer des Timers. Was schon andeutet, dass man mehrere Timer zur gleichen Zeit benutzen kann (da eine Kennziffer immer zum Unterscheiden benutzt wird). Die Kennziffer muss vom Typ unsigned int sein. Die Konstanten SizeX und SizeY bestimmen die Größe des Fensters. Man sollte solche Zahlen immer hinter Konstanten verstecken, damit man bei Änderung der Zahl nicht den ganzen Code durchsuchen muss (außerdem ist SizeX wesentlich Aussagekräftiger als 60).

const UINT TimerSec = 1;
const UINT SizeX    = 60;
const UINT SizeY    = 80;

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                   PSTR szCmdLine, int iCmdShow)
{
   MSG         msg;
   HWND        hWnd;
   WNDCLASS    wc;
   
   const char szAppName[] = "Stopuhr";
   
   wc.cbClsExtra          = 0;
   wc.cbWndExtra          = 0;
   wc.hbrBackground       = (HBRUSH) GetStockObject(WHITE_BRUSH);
   wc.hCursor             = LoadCursor(NULL, IDC_ARROW);
   wc.hIcon               = LoadIcon(NULL, IDI_APPLICATION);
   wc.hInstance           = hInstance;
   wc.lpfnWndProc         = WndProc;
   wc.lpszClassName       = szAppName;
   wc.lpszMenuName        = NULL;
   wc.style               = CS_HREDRAW | CS_VREDRAW;
   
   RegisterClass(&wc);
   
   hWnd = CreateWindow(  szAppName,
                         szAppName,

Der nächste Parameter beschreibt den Window Stil (zuvor: WS_OVERLAPPEDWINDOW). In diesem Tutorial benutze ich einen anderen Stil, da es diesmal nicht sinnvoll ist die Größe des Fensters zu verändern zu dürfen. Eigentlich benutzen wir eine Kombination aus verschiedenen Stilen. WS_OVERLAPPED bewirkt, dass das Fenster ein Rahmen und eine Titelleiste besitzt. WS_SYSMENU fügt das System Menü hinzu. So ist auch WS_OVERLAPPEDWINDOW eine Zusammenstellung aus mehreren Stilen. Es besteht aus den sechs Stilen WS_OVERLAPPED, WS_CAPTION, WS_SYSMENU, WS_THICKFRAME, WS_MINIMIZEBOX und WS_MAXIMIZEBOX. WS_THICKFRAME erweitert das Fenster um die Möglichkeit vom Benutzer in der Größe verändert zu werden.

                         WS_OVERLAPPED | WS_SYSMENU,

In die folgenden vier Parameter haben wir immer CW_USEDEFAULT eingesetzt. Dadurch wurde das Fenster immer in 'default' Größe und Position angezeigt. Nun benutzen wir für die Größe feste Werte.

                         CW_USEDEFAULT,
                         CW_USEDEFAULT,
                         SizeX,
                         SizeY,
                         NULL,
                         NULL,
                         hInstance,
                         NULL);
                         
   ShowWindow(hWnd, iCmdShow);
   UpdateWindow(hWnd);
   
   while (GetMessage(&msg, NULL, 0, 0))
   {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
   }
   
   return msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
   static RECT     rect;

Nun deklarieren wir drei statische Variablen. Die erste speichert, ob die Uhr läuft, oder nicht. Die nächsten beiden speichern die Zeit.

   static BOOL     isActive;
   static int      iSec;
   static int      iMin;
   
   switch (message)
   {

Da der Anwendungsbereich kleiner ist, als das angegebene Fenster (Rand, Titelleiste), müssen wir uns die Größe gesondert besorgen. Da diese Werte nicht verändert werden, wird diese Nachricht auch nur einmal bearbeitet werden.

   case WM_SIZE:
      {
         rect.right  = LOWORD(lParam);
         rect.bottom = HIWORD(lParam);
         return 0;
      }

Nun fangen wir die WM_KEYDOWN Nachricht ab, um auf die Tasteneingabe des Benutzers reagieren zu können. Unser Programm soll folgende Tastendrücke bearbeiten: mit der Leertaste startet bzw. stopt man die Stopuhr. Mit der Backspace Taste soll man die Uhr wieder auf 0:00 stellen können. Gleichzeitig soll die Uhr gestopt werden. Die Escapetaste beendet das Programm.

   case WM_KEYDOWN:
      {
         switch (wParam)
         {

VK_SPACE steht für die Leertaste. Dann verneinen wir isActive und prüfen, ob wir die Stoppuhr aktiviert haben.

         case VK_SPACE:
            {
               isActive = !isActive;
               
               if (isActive)
               {

Die nächst Anweisung ist neu, mit ihr weisen wir Windows an uns in bestimmten Abständen eine Nachricht vom Typ WM_TIMER zu schicken. Windows bekommt von einem Hardware Timer in bestimmten abständen einen Impuls. Für jeden Timer hat Windows eine Variable, die sie 'runterzählt'. Ist Windows wieder bei Null angekommen, wird eine Nachricht des Typs WM_TIMER verschickt mit der Timer ID in wParam. Da die Nachricht aber so behandelt wird wie jede andere auch, muss sie warten bis sie endlich an die Reihe kommt. Wenn das Programm also ein wenig 'hinterher hinkt' kann die Nachricht schon mal ein wenig später ankommen. Der erste Parameter der Funktion SetTimer ist der Handle zu dem Fenster, welches die WM_TIMER Nachricht bekommen soll. Der zweite ist die ID des Timers (siehe oben), der dritte Parameter gibt die Zeit zwischen zwei Nachrichten in Milisekunden (von 0 bis 2 hoch 32) an. Wenn man keine Nachrichten bekommen möchte, sonder möchte, dass immer eine Funktion aufgerufen wird, muss man im vierten Parameter die Adresse der Funktion angeben.

                  SetTimer(hWnd, TimerSec, 1000, NULL);
               }
               else
               {

Und nun gleich das Gegenstück zu SetTimer hinterher. Die KillTimer Funktion weist Windows an uns keine weiteren Nachrichten vom Typ WM_TIMER mit dieser ID zu schicken. Der erste Parameter ist wieder das Fenster, an das der Timer die Nachrichten geschickt hat. Als zweiten Parameter wird die ID des Timers verlangt.

                  KillTimer(hWnd, TimerSec);
               }
               return 0;
            }

Die Konstante VK_BACK steht für die Backspacetaste. Wenn diese Taste gedrückt wurde, dann wird dieser Zweig ausgeführt. Falls der Timer noch aktiv war, wird er jetzt ausgeschaltet (KillTimer). Dann werden die Werte für Minuten und Sekunden wieder auf Null zurückgestellt. Das InvalidateRect bewirkt, dass die Daten auf dem Bildschirm sofort aktualisiert werden.

         case VK_BACK:
            {
               if (isActive)
               {
                  isActive = FALSE;
                  KillTimer(hWnd, TimerSec);
               }
               iMin = iSec = 0;
               InvalidateRect(hWnd, NULL, TRUE);
               return 0;
            }

Wenn die Escapetaste gedrückt wurde (VK_ESCAPE), dann wird das Programm durch das Senden der WM_CLOSE Nachricht beendet.

         case VK_ESCAPE:
            {
               SendMessage(hWnd, WM_CLOSE, 0, 0);
               return 0;
            }
         }
         return 0;
      }

Die folgende Nachricht (WM_TIMER) wird immer dann dem Fenster geschickt, wenn wieder die in SetTimer angegebene Zeit (in Millisekunden) abgelaufen ist. In wParam steht die ID des Timers, da wir aber nur einen Timer benutzten, brauchen wir diesen Wert nicht abfragen. Wenn iSec kleiner ist als 59 wird es um eins erhöht. Ansonsten wird iMin inkrementiert (um eins erhöht) und iSec auf Null gesetzt.

   case WM_TIMER:
      {
         if (iSec < 59)
            ++iSec;
         else
         {
            iSec = 0;
            ++iMin;
         }
         InvalidateRect(hWnd, NULL, TRUE);
         return 0;
      }
      
   case WM_PAINT:
      {
         PAINTSTRUCT   ps;
         HDC           hDC;
         SIZE          size;
         char          sTime[6];
         int           iLength;

Die Funktion wsprintf verhält sich genauso, wie die C Funktion sprintf (also wie printf, nur das das Ergebnis in ein Array geschrieben wird). Das 02i in wsprintf gibt an, dass die Integer Zahl mit mindestens zwei Stellen ausgegeben werden soll und die 0 vor der 2 weist Windows an, die fehlenden Stellen mit Nullen zu belegen.

         iLength = wsprintf(sTime, "%i:%02i", iMin, iSec);
         
         hDC = BeginPaint(hWnd, &ps);
         {
            GetTextExtentPoint32(hDC, sTime, iLength, &size);
         
            TextOut(hDC, rect.right / 2 - size.cx / 2, rect.bottom / 2 -
                         size.cy / 2, sTime, iLength);
         }
         EndPaint(hWnd, &ps);         
         return 0;
      }
   case WM_DESTROY:
      {
         if (isActive)
         {
            KillTimer(hWnd, TimerSec);
         }
         PostQuitMessage(0);
         return 0;
      }
   }
   return DefWindowProc(hWnd, message, wParam, lParam);
}

Ich weiss nicht, ob du es bemerkt hättest, aber wenn du die Zeit mal unter Windows 98/95 gemessen hast, wirst du merken, dass die Stoppuhr auf einer Minute ungefähr drei Sekunden falsch geht (unter Windows NT ist alles in Ordnung). Dies liegt daran, dass Windows 9x noch Rücksicht auf alte DOS Programme nimmt und deshalb den Hardware Timer immer noch auf 18.2 Ticks/Sekunde (ca. alle 54 ms) programmiert hat. Von daher ist die Angabe von Millisekunden in der SetTimer Funktion nur ein theoretischer Wert. Denn ein Programm kann unter Windows 9x nur 18,2 WM_TIMER Nachrichten pro Sekunde bekommen. Wenn man nun 1000 Millisekunden angibt, dann rundet Windows 9x diesen Wert auch noch auf ein gerades vielfache des realen Timerwertes, sodass die Genauigkeit immer mehr abnimmt. Da Windows NT den Timer auf 100 Ticks/Sekunde programmiert hat, ist die Timer Nachricht dort wesentlich zuverlässiger. Die WM_TIMER Nachricht ist also als sehr exakter Tacktgeber nicht zu gebrauchen.

Übungen

  1. Schreibe ein Programm, das jede 20 Sekunden (20000 ms) über eine Messagebox fragt, ob es beendet werden soll.
  2. Schreibe ein Newsticker Programm. Also ein Programm, das jede halbe Sekunde den 'Newstext' um eine Stelle weiterschiebt.
  3. Programmiere ein Programm, dass einen Text einließt (Tutorial 6 "Texte einlesen") und dazu am Ende des Textes ein blinkendes Caret (Schreib Cursor) anzeigt.

Wenn du meinst, dass irgend eine Stelle in diesem Tutorial ungenau ist oder etwas nicht erklärt wird, dann schreibe mir einfach eine E-Mail.

webmaster@win-api.de