Die WinAPI Plattform

Tutorials

Tutorial Nummer 08

(08.) Objekte zeichnen

In diesem Tutorial werde ich dir zeigen, wie man einfache Objekte auf den Anwendungsbereich zeichnet. Damit du die Timer Routinen nicht vergisst, wird ein Objekt (Ellipse) um den Mittelpunkt des Anwendungsbereiches kreisen. Und damit es nicht zu einfach wird ändert die Ellipse nach jeder Sekunde ihre Geschwindigkeit. Um das Flackern der Objekte auf dem Bildschirm ein wenig zu reduzieren, geben wir bei der InvalidateRect Funktion auch noch das Rechteck an, welches neu gezeichnet werden soll.

Zunächst brauchen wir, wie immer, die windows.h Datei. In diesem Fall brauchen wir auch noch math.h, wegen der Berechnung der Kreisbahn (cos und sin).

#include <windows.h>
#include <math.h>

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

Nun definieren wir zwei Konstanten, die die IDs der beiden Timer enthalten. Den einen Timer benutzten wir, um die ständige Bewegung des Kreises aufrecht zu halten, mit dem Anderen steuern wir die Geschwindingkeit. Danach deklarieren wir noch die Konstante für Pi.

#define ID_TIMER_INC    1
#define ID_TIMER_FAST   2
char szAppName[] = "Objekte zeichnen";
const double pi = 3.14159;

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                   PSTR szCmdLine, int iCmdShow)
{
   MSG         msg;
   HWND        hWnd;
   WNDCLASS    wc;
   
   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,
                         WS_OVERLAPPEDWINDOW,
                         CW_USEDEFAULT,
                         CW_USEDEFAULT,
                         CW_USEDEFAULT,
                         CW_USEDEFAULT,
                         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)
{

In der RECT Struktur speichern wir wieder die Maße des Anwendungsbereiches. Die Variable dGrad speichert die aktuelle Gradzahl des Kreises um den Mittelpunkt, ist also 0, wenn der Kreis ganz Rechts ist und 180, wenn er ganz links ist.

   static RECT     rect;
   static double   dGrad;
   
   switch (message)
   {

In der WM_CREATE Nachricht setzten wir die beiden Timer, der eine gibt den Takt zum Neuzeichenen des Kreises. Hier wäre es natürlich Wünschenswert diesen Wert noch zu verkleinern, um eine höhere Bildwiederholungsfrequenz zu bekommen, aber da Windows 98 es so oder so nicht schneller hinbekommt, lassen wir es so (wer Windows NT benutzt kann es ja mal ausprobieren). Der zweite Timer steuert die Geschwindigkeit der Bewegung des Kreises.

   case WM_CREATE:
      {
         SetTimer(hWnd, ID_TIMER_INC, 50, NULL);
         SetTimer(hWnd, ID_TIMER_FAST, 1000, NULL);
         return 0;
      }
   case WM_SIZE:
      {
         rect.right  = LOWORD(lParam);
         rect.bottom = HIWORD(lParam);
         return 0;
      }
   case WM_PAINT:
      {
         PAINTSTRUCT    ps;
         HDC            hDC;
         
         hDC = BeginPaint(hWnd, &ps);

In diesem Tutorial schließe ich den Bereich zwischen BeginPaint und EndPaint in geschweifte Klammern ein. Man kann sie genau so gut auch weglassen, jedoch ist es so leichter ersichtlich, dass dies ein spezieller Bereich ist. Die darauf folgende Zeile ist recht kompliziert. Als erstes deklarieren wir eine neue Struktur HBRUSH, in der wir den Handle des alten (also den 'default' (weißen)) Brushes speichern wollen. Denn nach Beendigung müssen wir alles wieder aufräumen, das heißt, wir müssen den alten Brush wieder zum Standard machen und unseren Brush müssen wir wieder löschen, da Windows für ihn speicher belegt hat (würde man den Brush nicht wieder freigeben, so wäre nach einigen WM_PAINT Nachrichten ein Speicher voll und CreateSolidBrush würde nur noch einen Fehlerwert zurückgeben). Die erste Funktion hinter dem Gleichheitszeichen (SelectObject) sagt Windows, dass wir für diesen Gerätekontext die Füllfarbe ändern wollen (SelectObject ist nicht nur zum Auswählen von Füllfarben, sondern auch für Schriften und Stifte). Dazu müssen wir erst einen Brush erzeugen, dies tun wir mit CreateSolidBrush. Diese Funktion erwartet den Farbwert, mit dem die Fläche komplett ausgefüllt (solid) werden soll, als 24 Bit RGB (rot, grün und blau) Wert in einer Integer Zahl. Die jeweiligen Farbwerte sind wie folgt in der 32 Bit Integer Zahl 'versteckt': Die ersten 8 Bits (0-7) belegt der rot Anteil (0-255), die nächsten 8 Bits (8-15) belegt der grün Anteil, das 16te bis 23te Bit belegt der blau Anteil und die letzten 8 Bits sind 0 (werden nicht gebraucht). Das RGB Makro setzt die Werte entsprchend ein.

         {
            HBRUSH hOldBrush = (HBRUSH)SelectObject(hDC, 
                                             CreateSolidBrush(RGB(10,120,220)));

Als nächstes zeichnen wir drei Geometrische Objekte, als erstes ein RoundRect (Rechteck mit abgerundeten Ecken). Die ersten vier Parameter geben die Eckpunkte des (unabgerundeten) Rechtecks an. Die ersten beiden geben den ersten Punkt (X, Y) an und die nächsten beiden Parameter den zweiten, gegenüberliegenden Eckpunkt. Die letzten beiden Parameter geben die Stärke der Abrundung an. Der eine die Abrundung in horizontaler Richtung und der andere in vertikaler Richtung. Die Werte ergeben eine Ellipse (oder Kreis), die dann zur Abrundung benutzt wird. Dies kann man sich so vorstellen, dass die Ellipse so in eine Ecke gelegt wird, dass sie gerade noch im Rechteck liegt. Dann wird der Bereich, der Außerhalb der Ellipse liegt einfach abgeschnitten (und das dann an jeder Ecke).

            RoundRect(hDC, rect.right / 2 - 30, rect.bottom / 2 - 30,
                           rect.right / 2 + 30, rect.bottom / 2 + 30, 15, 15);

Die nächste Figur ist eine Ellipse (in diesem Fall der Sonderfall: ein Kreis). Auch hier geben wir wieder die Koordinaten eines Rechtecks an. Die Ellipse wird so geformt, dass sie jede Seite im Mittelpunkt einmal berührt. Da die cos bzw. sin Funktionen den Wert im Bogenmaß verlangen, müssen wir den erst aus dem Gradmaß umrechenen. So entspricht 180° pi und 360° 2pi. Mit dem Cosinus berechnen wir die X-Position bei einer bestimmten Gradzahl im Kreis. Da dieser Wert immer zwischen -1 (180°), 0 (90° und 270°) und 1 (0°) liegt, müssen wir ihn noch mit 100 mulitplizieren. Analoges gilt für Sinus und den Y-Wert.

            Ellipse(hDC, rect.right  / 2 + (int)(cos(dGrad *pi/180) * 100) - 10,
                         rect.bottom / 2 + (int)(sin(dGrad *pi/180) * 100) - 10,
                         rect.right  / 2 + (int)(cos(dGrad *pi/180) * 100) + 10,
                         rect.bottom / 2 + (int)(sin(dGrad *pi/180) * 100) + 10);

Als nächstes zeichnen wir zwei Paralele Linien. Zuerst müssen wir aber den Starpunkt mit MoveToEx angeben. Und danach mit LineTo den Enpunkt setzten. Diese Methode ist beim Zeichnen einzelner Linien etwas umständlich, aber wenn man mehrere Linien hintereinander zeichnet, dann hat diese Methode seine Vorteile. Wie eben schon angedeutet setzt LineTo auch die Position für die nächste Zeichenoperation (außer man setzt eben wieder eine neue mit MoveToEx). Hätten wir das erste MoveToEx weggelassen, dann hätte die Linie bei (0|0) gestartet. In den vierten Parameter von MoveToEx kann man einen Zeiger auf eine POINT Struktur eintragen, in der wird dann die vorherige (alte) Zeichenposition gespeichert. Hier wären das beim ersten Aufruf die Koordinaten (0|0), da wir den alten Punkt aber nicht brauchen, können wir auch NULL eintragen.

            MoveToEx(hDC, rect.right / 2 + 150, rect.bottom / 2 - 150, NULL);
            LineTo(	 hDC, rect.right / 2 + 150, rect.bottom / 2 + 150);
            MoveToEx(hDC, rect.right / 2 - 150, rect.bottom / 2 - 150, NULL);
            LineTo(  hDC, rect.right / 2 - 150, rect.bottom / 2 + 150);

Vor Beendigung der Zeichenoperation müssen wir unseren Brush wieder deselektieren und löschen. Als erstes legen wir fest, dass der alte Brush wieder der aktuelle ist (SelectObject). Den Rückgabewert von SelectObject geben wir an DeleteObject weiter, denn das ist unser benutzer Brush, den wir löschen wollen.

            DeleteObject(SelectObject(hDC, hOldBrush));
         }
         EndPaint(hWnd, &ps);
         return 0;
      }
   case WM_TIMER:
      {

In der Variable dInc speichern wir, um wieviel die Gradzahl erhöht werden soll. Wir initalisieren die Variable mit 0.2. In wParam ist wieder die ID des Timers gespeichert.

         static double dInc = .2;
         switch (wParam)
         {
         case ID_TIMER_INC:
            {

Nun deklarieren wir eine RECT Struktur, in der wir die Rechtecks Daten zum Neuzeichnen des Anwendungsbereiches speichern werden. Dann erhöhen wir die dGrad Variable, wenn diese 360 Grad erreicht hat, wird sie wieder auf 0 gesetzt. Danach berechnen wir das Rechteck, das neu gezeichnet werden soll. Denn indem wir nicht immer den ganzen Anwendungsbereich neu zeichnen lassen, reduzieren wir die Flackereffekte.

               RECT      InvalRect;
               
               dGrad += dInc;
               if (dGrad >= 360)
                  dGrad = 0;
               
               InvalRect.left   = rect.right / 2 +
                                  (int)(cos(dGrad * pi / 180) * 100) - 16;
               InvalRect.top    = rect.bottom / 2 +
                                  (int)(sin(dGrad * pi / 180) * 100) - 16;
               InvalRect.bottom = rect.bottom / 2 +
                                  (int)(sin(dGrad * pi / 180) * 100) + 16;
               InvalRect.right  = rect.right  / 2 +
                                  (int)(cos(dGrad * pi / 180) * 100) + 16;
               InvalidateRect(hWnd, &InvalRect, TRUE);
               break;
            }
         case ID_TIMER_FAST:
            {

Hat dieser Timer die Nachricht ausgelöst, dann erhöhen, oder erniedrigen wir dInc. Hat dInc einen Wert überschritten, dann wird die Richtung umgekehrt.

               static bool   bFlag;
               
               if (bFlag)
                  dInc -= .2;
               else
                  dInc += .2;
               
               if (dInc >= 3 || dInc <= .2)
                  bFlag = !bFlag;
               break;
            }
         }
         return 0;
      }
   case WM_DESTROY:
      {
         KillTimer(hWnd, ID_TIMER_INC);
         KillTimer(hWnd, ID_TIMER_FAST);
         PostQuitMessage(0);
         return 0;
      }
   }
   return DefWindowProc(hWnd, message, wParam, lParam);
}

Ich hoffe, dass Tutorial war verständlich, wenn du Fragen, Verbesserungsvorschläge oder Kritik hast, dann maile mir einfach.

Als dieses Tutorial neu war, habe ich an dieser Stelle einen Tetris (tm) Clone Wettbewerb veranstaltet. Die Leser sollten damals mit ihrem jetzigen Wissen (also bis einschließlich diesem Tutorial) ein Tetris Clone schreiben. Das dies möglich war, zeigte ich mit einem Beispiel Tetris Clone (siehe Download).

Sieger dieses Wettbewerbes wurde Mark Fekete (Fek) mit seinem Tetris (tm) Clone 'FieberAlptraum'. Wer im noch nachträglich gratulieren will oder eine Frage zu seinen Source Code hat, der kann ihm auch eine E-Mail schreiben (fekmfg@web.de). Seinen Source findest du im Download Bereich.

webmaster@win-api.de