Die WinAPI Plattform

Tutorials

Tutorial Nummer 12

(12.) Ein Child mit Gedächtnis

In dem letzten Tutorial habe ich dir erklärt, wie man mit Hilfe von Child Fenstern die Arbeit gut aufteilen kann. Doch hat man das Problem, dass eine ChildProc Funktion nicht für jedes der Child Fenster eigene static Variablen deklariert, sondern für alle die selben benutzen würde. Man muss bei Childfenstern, die mehr als einmal im Programm benutzt werden eine andere Art der Speicherung finden. Eine Variante zeige ich dir in diesem Tutoiral. Dazu schreiben wir ein Programm, in dem drei Child Fenster jeweils eine der drei Hauptfarben darstellt, also eins rot, das andere grün und das letzte blau. Wält man eines dieser Child Fenster (genauer: den dafür vorgesehenen Bereich) aus, so wird diese Farbe mit in den Hintergrund des Hauptfensters gemischt.

#include <windows.h>

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

Wie im letzten Tutoiral brauchen wir die ChildWndProc, die hier CheckProc heißt. Dazu den entsprechenden Klassennamen und noch drei eigene Nachrichten. Die erste (PM_CHECKED) wird bei jeder Statusveränderung zum Hauptfenster geschickt mit dem Status der Checkbox in wParam und dem Handle des Child Fensters in lParam. Diese Werte fragen wir in diesem Programm zwar nicht ab, aber dann ist das Child Fenster auch noch für andere Programme zu benutzen, die diese Werte vielleicht brauchen. Man hätte dann natürlich diese Funktionen in eine extra Bibliothek compilieren sollen. Die zweite Nachricht wird von der WndProc zum Child Fenster geschickt. Über diese Nachricht kann man die Hintergrundfarbe des Childfensters ändern. Mit der letzten Nachricht (PM_ISCHECKED) können wir den aktuellen Status des Child Fensters abfragen.

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

const char szCheckName[]   = "OwnCheck";
const UINT PM_CHECKED      = WM_APP + 1;
const UINT PM_COLOR        = WM_APP + 2;
const UINT PM_ISCHECKED    = WM_APP + 3;

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                   PSTR szCmdLine, int iCmdShow)
{
   MSG        msg;
   HWND       hWnd;
   WNDCLASS   wc;

   const char szAppName[] = "Ein Child mit Gedächtnis";

   wc.cbClsExtra          = 0;
   wc.cbWndExtra          = 0;
   wc.hbrBackground       = NULL;
   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);

Um die Hintergrundfarbe und den Status des Child Fensters zu speichern, brauchen wir zwei Integer Variablen. Diese lassen wir für jedes Child Fenster als extra Speicher anfordern. Damit haben wir auch das Problem der nicht getrennten Speicherbereiche gelöst.

   wc.cbWndExtra          = 2 * sizeof(int);
   wc.hIcon               = NULL;
   wc.lpfnWndProc         = CheckProc;
   wc.lpszClassName       = szCheckName;
   
   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 unserem Hauptfenster brauchen wir nur einen Handle zu jedem Child Fenster und eine RECT Struktur, in der wir wieder die Maße des Fensters eintragen.

   static HWND   hR, hG, hB;
   static RECT   rect;
   
   switch (message)
   {
   case WM_CREATE:
      {

In der WM_CREATE Nachricht erzeugen wir jedes der drei Child Fenster. Das positionieren überlassen wir der WM_SIZE Nachricht, weshalb wir hier als Positionswerte einfach Null eintragen. Danach schicken wir jedem der drei Child Fenster die Nachricht WM_COLOR und setzten damit die Hintergrundfarbe des entprechenden Child Fensters.

         hR = CreateWindow(  szCheckName,
                             NULL,
                             WS_CHILD | WS_VISIBLE | WS_DLGFRAME,
                             0, 0, 0, 0,
                             hWnd,
                             NULL,
                             ((LPCREATESTRUCT) lParam) -> hInstance,
                             NULL);

         hG = CreateWindow(  szCheckName,
                             NULL,
                             WS_CHILD | WS_VISIBLE | WS_DLGFRAME,
                             0, 0, 0, 0,
                             hWnd,
                             NULL,
                             ((LPCREATESTRUCT) lParam) -> hInstance,
                             NULL);

         hB = CreateWindow(  szCheckName,
                             NULL,
                             WS_CHILD | WS_VISIBLE | WS_DLGFRAME,
                             0, 0, 0, 0,
                             hWnd,
                             NULL,
                             ((LPCREATESTRUCT) lParam) -> hInstance,
                             NULL);

         SendMessage(hR, PM_COLOR, RGB(255, 0, 0), 0);
         SendMessage(hG, PM_COLOR, RGB(0, 255, 0), 0);
         SendMessage(hB, PM_COLOR, RGB(0, 0, 255), 0);
         return 0;
      }

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

Wenn die Größe des Fensters verändert wurde, verschieben wir die Child Fenster so, dass sie wieder unten in der Mitte liegen.

         MoveWindow(hR, rect.right / 2 - 60, rect.bottom - 30, 30, 20, TRUE);
         MoveWindow(hG, rect.right / 2 - 15, rect.bottom - 30, 30, 20, TRUE);
         MoveWindow(hB, rect.right / 2 + 30, rect.bottom - 30, 30, 20, TRUE);

         return 0;
      }

   case WM_PAINT:
      {
         PAINTSTRUCT    ps;
         HDC            hDC;

         hDC = BeginPaint(hWnd, &ps);
         {

In der WM_PAINT Nachricht des Hauptfensters zeichnen wir einfach den kompletten Hintergrund mit einer Farbe aus. Diese Farbe müssen wir jedoch erst ermitteln. Jede der drei Child Fenster gibt an, ob der jeweilige Farbwert vertreten sein soll. Wir fragen diesen Status ab, indem wir dem Child Fenster die PM_ISCHECKED Nachricht schicken. Die Antwort steht im Rückgabewert der SendMessage Funktion. Da diese Farbe bei dem Wert 1 voll vertreten sein soll, müssen wir diesen Wert noch mit 255 multiplizieren.

            HBRUSH hOldBrush = (HBRUSH) SelectObject(hDC,
                                  CreateSolidBrush(RGB(
                                     SendMessage(hR, PM_ISCHECKED, 0, 0)*255,
                                     SendMessage(hG, PM_ISCHECKED, 0, 0)*255,
                                     SendMessage(hB, PM_ISCHECKED, 0, 0)*255)));
            
            Rectangle(hDC, rect.left, rect.top, rect.right, rect.bottom);
            
            DeleteObject(SelectObject(hDC, hOldBrush));
         }
         EndPaint(hWnd, &ps);
         
         return 0;
      }

Die PM_CHECKED Nachricht bekommen wir von unseren Child Fenstern immer dann, wenn sich ein Status verändert hat. Wir können uns bei dieser Nachricht darauf beschränken, InvalidateRect aufzurufen, da lokal in der WM_PAINT Nachricht die Daten besorgt werden. Wenn wir das alles hier erledigen würden, dann brauchten wir das nicht mehr in WM_PAINT tuen, aber wir müssten hier statische Variablen setzten und vorher noch die Child Handles vergleichen.

   case PM_CHECKED:
      {
         InvalidateRect(hWnd, NULL, FALSE);
         return 0;
      }

   case WM_DESTROY:
      {
         PostQuitMessage(0);
         return 0;
      }
   }

   return DefWindowProc(hWnd, message, wParam, lParam);
}

LRESULT CALLBACK CheckProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
   switch (message)
   {

In der WM_CREATE Nachricht des Child Fensters wollen wir den extra angelegten Speicher mit Werten vorbelegen. Dies machen wir mit Hilfe der SetWindowLong Funktion. Wie der Name schon sagt kann man damit einen long Wert in dem Fenster Speicher setzten. Der erste Parameter muss der Handle zu dem Fenster sein, in dessen Speicher man schreiben will. Der zweite Parameter gibt die Position im Speicherbereich an. Null ist die erste Position und sizeof(long) (bzw. sizeof(int) ist das gleiche) die zweite. Der dritte Parameter ist der Wert, der gespeichert werden soll. An der ersten Position speichern wir den Status der Checkbox. Null, wenn sie nich ausgewählt ist, und eins, wenn sie es ist. An der zweiten Position speichern wir die Hintergrundfarbe des Child Fensters. Diesen Wert belegen wir mit dem Windowsgrau vor.

   case WM_CREATE:
      {
         SetWindowLong(hWnd, 0, 0);
         SetWindowLong(hWnd, sizeof(int), RGB(191, 191, 191));
         return 0;
      }

Über die PM_COLOR Nachricht kann man die Hintergrundfarbe des Child Fensters beeinflussen. Also setzen wir einfach mit SetWindowLong an der entsprechenden Position den neuen Farbwert, der uns durch wParam übergeben wurde, ein. Mit dem Aufruf der InvalidateRect Funktion gehen wir sicher, dass der Hintergrund sofort mit dieser Farbe gefüllt wird.

   case PM_COLOR:
      {
         SetWindowLong(hWnd, sizeof(int), wParam);
         InvalidateRect(hWnd, NULL, FALSE);
         
         return 0;
      }

   case WM_PAINT:
      {
         PAINTSTRUCT   ps;
         HDC           hDC;
         
         hDC = BeginPaint(hWnd, &ps);
         {

In der WM_PAINT Nachricht zeichnen wir erst einmal den Hintergrund mit der Farbe, die wir im Fensterspeicher eingespeichert haben, aus. Diese Farbe bekommen wir durch die GetWindowLong Funktion mit dem Handle des Fensters als ersten Parameter und der Speicherposition im zweiten Parameter. Dann zeichnen wir das Rechteck gößer, als der Anwendungsbereich des Child Fensters groß ist. Damit verhindern wir, dass der Rahmen mit gezeichnet wird (alles, was außerhalb des Anwendungsbereich des Child Fensters ist, wird nicht gezeichnet). Normalerweise müsste man jedoch sowieso auf die momentane Größe des Child Fensters individuell reagieren. Hier tue ich das nicht, da wir die Größe des Child Fensters nie verändern. Die nächste Rectangle Funktion zeichnet die weiße Checkbox und wenn diese gerade ausgewählt ist (dies prüfen wir durch das Abfragen des Fensterspeichers über die GetWindowLong Funktion und der Position 0), zeichnen wir noch ein X in diese Checkbox. Hier ist zu erwähnen, dass de rechten und unteren Werte um eins vermindert sind (im Gegensatz zu den in der Rectangle Funktion) und trotzdem passt das X genau in das Rechteck. Dies liegt daran, dass Windows den rechten und unteren Rand bei Rechtecken um eins kleiner zeichnet. Das hat den Vorteil, dass man die Rechtecke genau untereinander zeichnen kann und sie sich trotzdem nicht überschneiden.

            HBRUSH hOldBrush = (HBRUSH) SelectObject(hDC,
                                          CreateSolidBrush(
                                            GetWindowLong(hWnd, sizeof(int))));
            
            Rectangle(hDC, -1, -1, 25, 15);
            
            DeleteObject(SelectObject(hDC, hOldBrush));
            
            Rectangle(hDC, 6, 1, 18, 13);
            if (GetWindowLong(hWnd, 0))
            {
               MoveToEx(hDC, 6, 1, NULL);
               LineTo(hDC, 17, 12);
               MoveToEx(hDC, 6, 12, NULL);
               LineTo(hDC, 17, 1);
            }
         }
         EndPaint(hWnd, &ps);
         
         return 0;
      }

In der WM_LBUTTONDOWN Nachricht prüfen wir, ob das Rechteck 'getroffen' wurde, wenn dem so ist, setzten wir den Wert, der speichert, ob die Checkbox ausgewählt ist, auf das Gegenteil. Dann senden wir eine Nachricht an unser Hauptfenster mit den entsprechenden Informationen und lassen das Child Fenster neu zeichnen, da die Checkbox jetzt entwerder mit einem X versehen werden muss, oder dieses muss entfehrnt werden.

   case WM_LBUTTONDOWN:
      {
         if (LOWORD(lParam)  >=  6   &&
             LOWORD(lParam)  <   18  &&
             HIWORD(lParam)  >=  1   &&
             HIWORD(lParam)  <   13)
         {
            SetWindowLong(hWnd, 0, !GetWindowLong(hWnd, 0));
            SendMessage(  GetParent(hWnd), PM_CHECKED, 
                          GetWindowLong(hWnd, 0), (LPARAM)hWnd);
            InvalidateRect(hWnd, NULL, FALSE);
         }
         return 0;
      }

Wenn das Hauptfenster wissen will, wie der aktuelle Status der Checkbox ist, dann sendet es an uns die PM_ISCHECKED Nachricht. Diesen Wert ermitteln wir mit der GetWindowLong Funktion und geben ihn einfach als Rückgabewert zurück. Dieser Wert wird dann auch der Rückgabewert der SendMessage Funktion sein.

   case PM_ISCHECKED:
      {
         return GetWindowLong(hWnd, 0);
      }
   }
   
   return DefWindowProc(hWnd, message, wParam, lParam);
}

In diesem Tutorial hast du gelernt, wie man einem Child Fenster ein 'Gedächtnis' gibt, also Werte lokal speichern kann. Wenn du eine ganze Struktur speichern willst, dann hast du zwei Möglichkeiten. Einmal kannst du in der WM_CREATE Nachricht mit new dir diesen Speicherbereich so besorgen und einfach den Zeiger darauf in dem Fensterspeicher speichern. Oder du schreibst eine Funktion, die diese Struktur in long Werte aufbröselt und dann einzeln speichert. Bei bedarf muss das natürlich auch wieder Rückgängig gemacht werden können.

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