Dr. Erhard Henkes, www.henkessoft.de, 18.10.2005

zurück zum Inhaltsverzeichnis

Anmerkung:
Die Inhalte dieses Kapitels sind auch für die MFC-Programmierung geeignet, denn dort wird der "handle to device context" nur gekapselt. Die Funktionen können in MFC entsprechend als Member-Funktionen eingesetzt werden. Die Systematik wurde jedoch komplett übernommen.

 

GDI-Programmierung

 

Grundlagen

 

Einer der Besonderheiten von MS Windows ist der Einsatz einer grafischen Benutzeroberfläche mit feinst abgestuften Farben. Diese grafische Schnittstelle zwischen Mensch und Maschine nennt man Graphical User Interface (GUI). Kann man sich bei der Konsolenprogrammierung noch auf die eigentliche Aufgabenstellung konzentrieren, so muß man sich bei der Windows-Programmierung zum großen Teil mit der Gestaltung und Funktion grafischer Elemente auseinandersetzen.

 

Damit der Programmierer sich zumindest über den inneren Mechanismus individueller Bildschirm- und Druckerausgaben keine tieferen Gedanken machen muß, steht hier eine Schnittstelle zur Verfügung, die unter der Bezeichnung Graphics Device Interface (GDI) bekannt ist. Das Betriebssystem nutzt die GDI auch intensiv für die Darstellung seiner eigenen grafischen Elemente.

 

Wenn Sie auf den Bildschirm eines Rechners schauen, dann blicken Sie auf eine große Menge sogenannter Pixel (Bildpunkte). Dieser Begriff leitet sich von "picture element" her. Stellen Sie sich ein Pixel nicht als physikalischen Punkt, sondern als logischen Punkt vor. Während in der Mathematik ein Punkt keinerlei Ausdehnung hat, kann man sich dies auf einem Computermonitor als kleines Rechteck vorstellen. Bei einer typischen Auflösung von z.B. 1024 * 768 Pixel sind dies schon 786432 solcher Punkte. Jedem dieser Punkte kann man bei einer 24-bit-Einstellung der Grafikkarte einen aus 16777216 (2 hoch 24) Farbwerten zuordnen. Ein beliebiges Bild auf dem Monitor entspricht unter diesen Randbedingungen 16777216 hoch 786432 Möglichkeiten.

 

Wir könnten die Grafikprogrammierung somit prinzipiell unter Zuhilfenahme einer Funktion SetPixel( x, y, color ) erledigen. Eine Win32API-Funktion, die an der Stelle P(x,y) ein farbiges Rechteck der Farbe color darstellt, gibt es tatsächlich.
Die korrekte Syntax in Win32API ist:


COLORREF SetPixel
(
    HDC hdc,           // handle to device context
    int X,             // x-coordinate of pixel
    int Y,             // y-coordinate of pixel
    COLORREF crColor   // pixel color
)

Die Parameter int X und int Y sind die logischen Bildschirmkoordinaten P ( x | y ), die in der linken oberen Ecke bei P ( 0 , 0 ) beginnen und in der unteren rechten Ecke bei P ( 1023 , 767 ) enden.

 

Der Farbwert wird häufig als RGB-Wert dargestellt. RGB bedeutet red-green-blue und stellt für jede dieser Farbkomponenten einen Bereich von 0 bis 255 zur Verfügung. Der Wert 255 entspricht hierbei jeweils 100% Farbtiefe.

 

Was bedeutet aber „device context“? Dies heißt übersetzt Gerätekontext. Es handelt sich um eine Datenstruktur, die Informationen über die Eigenschaften eines Gerätes, z. B. eines Bildschirms oder Druckers enthält. Diese Gerätekontexte ermöglichen die geräteunabhängige Ausgabe, einer der ganz großen Vorteile, die MS Windows gegenüber MS DOS brachte. 

 

Die Funktionsanweisung SetPixel(hdc,100,100,RGB(255,0,0)) kann somit verwendet werden, um sowohl auf einen Bildschirm als auch auf einen Drucker einen roten Punkt an der Stelle P ( 100 , 100 ) auszugeben. Man muß nur den richtigen Gerätekontext hdc als ersten Parameter angeben. Völlig hardwareunabhängig ist man nicht, da man z.B. auf einem Monochrom-Drucker keine verschiedenen Farben ausgeben kann. Ohne rote Farbe gibt es eben keinen roten Klecks. Farbige Punkte auf einem Bildschirm kann man natürlich auch nur auf einem Farbmonitor mit einer passenden 24-Bit-Grafikkarte und richtig eingerichteter Treibersoftware darstellen. Nehmen wir nun an, dass alle diese Gerätschaften bestens funktionieren und wir uns nur um die Grafikausgabe kümmern müssen. Beginnen wir mit der Ausgabe von Punkten auf dem Bildschirm. Zunächst setzen wir genau einen roten Punkt an die Stelle P ( 100 , 100 ):


#include <windows.h>

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

         switch (message)   
         {    
           case WM_PAINT:        
              hdc = BeginPaint (hwnd, &ps); // Gerätekontext
              SetPixel(hdc, 100, 100, RGB(255,0,0)); // Punkt setzen
              EndPaint (hwnd, &ps);
           return 0;        

           case WM_DESTROY:        
             PostQuitMessage(0);        
           return 0;   
         }
         return DefWindowProc (hwnd, message, wParam, lParam);
}

int WINAPI WinMain( HINSTANCE hI, HINSTANCE hPrI, PSTR szCmdLine, int iCmdShow )
{
         static TCHAR szName[] = TEXT("Fensterklasse");    
         HWND hwnd ;

         WNDCLASS wc;
         wc.style         = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
         wc.lpfnWndProc   = WndProc;   
         wc.cbClsExtra    = 0;
         wc.cbWndExtra    = 0;   
         wc.hInstance     = hI;
         wc.hIcon         = LoadIcon (NULL, IDI_WINLOGO);
         wc.hCursor       = LoadCursor (NULL, IDC_ARROW);
         wc.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH);
         wc.lpszMenuName  = NULL;   
         wc.lpszClassName = szName;
 
         RegisterClass (&wc);
         hwnd = CreateWindow (szName, TEXT("Punkte setzen"), WS_OVERLAPPEDWINDOW,
                              0, 0, 200, 200, NULL, NULL, hI, NULL);       
      
         ShowWindow (hwnd, iCmdShow);   
         UpdateWindow (hwnd);   

         MSG msg;   

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

 

Die bezüglich GDI entscheidende Stelle im Programm findet man hier:

 

case WM_PAINT:         

    hdc = BeginPaint( hwnd, &ps );            // Gerätekontext

      SetPixel( hdc, 100, 100, RGB(255,0,0) );         // Punkt setzen

    EndPaint( hwnd, &ps );

return 0;         

 

Mit der Funktion BeginPaint(...) erzeugen wir einen Gerätekontext

 

HDC BeginPaint

(

    HWND hwnd,

    LPPAINTSTRUCT &ps             // Adresse der Struktur PAINTSTRUCT

)

 

Der zweite Parameter ist ein Zeiger auf eine Struktur, die das Zeichnen in den Anwendungsbereich („client area“) ermöglicht. Der Rückgabewert der Funktion BeginPaint(...) ist ein Gerätekontext für die Ausgabe innerhalb des durch hwnd spezifierten Fensters. BeginPaint(...) erstellt den Gerätekontext, und EndPaint(...) zerstört ihn. Im Bereich zwischen diesen beiden Anweisungen kann mit hdc gezeichnet werden.

 

Führen Sie das Programm nun aus. Sie haben sicher Mühe, den einzelnen Punkt zu erkennen. Das liegt an der Größe des Bildschirms. Aber genau dies ist beabsichtigt. Das Pixel ist ein grafischer Grundbaustein und soll daher möglichst klein sein. Wenn man einen größeren Bildschirm oder einen Beamer verwendet, stellt man daher eher auf eine höhrere Auflösung um, z.B. 1280 * 1024 oder 1600 * 1200, damit das Bild nicht allzu „pixelig“ wird. 

 

Damit Sie das "Gezeichnete" besser erkennen, zeichnen wir eine Linie. Wir verwenden hierzu unsere Grundfunktion SetPixel(...) in einer Schleife:

 

LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )
{   
         HDC hdc;   
         PAINTSTRUCT ps;   
         int i;

         switch( message )   
         {   
           case WM_PAINT:        
             hdc = BeginPaint( hwnd, &ps );
               for(i=0; i<1024; i++) SetPixel( hdc, i, 100, RGB(0,0,255) );
             EndPaint( hwnd, &ps );
           return 0;
           ...


Die Linie sehen Sie nun sicher besser als den einzelnen Punkt. Nun wollen wir den gesamten Anwendungs­be­reich mit Farbe ausfüllen. Dazu dient eine geschachtelte for-Schleife:


LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )
{   
         HDC hdc;   
         PAINTSTRUCT ps;   
         int i,j;

         switch( message )   
         {   
           case WM_PAINT:        
             hdc = BeginPaint(hwnd, &ps);
               for(i=0; i<1024; i++)
                 for(j=0; j<768; j++) SetPixel( hdc, i, j, RGB(0,0,255) );
             EndPaint (hwnd, &ps);
           return 0;
           ...



Beim Ausführen des Programmes erkennt man gut, dass die Fläche nun Pixel für Pixel "neu eingefärbt" wird. Dazu benötigt auch ein schneller Rechner seine Zeit. Wenn man einen Teil des Fensters aus dem sichtbaren Bild schiebt und wieder hereinzieht, dann wird dieser Teil anschließend erneut eingefärbt. Wenn man das Fenster in der Größe ändert, dann tritt dieser Effekt des Neuzeichnens ebenfalls ein. Dies zeigt anschaulich Funktion und Wirkung der Nachricht WM_PAINT.

 

Sie haben keine Auflösung von 1024 * 768 eingestellt? Kein Problem. Wir beschaffen uns derartige System­da­ten mittels der Funktion GetSystemMetrics(...):

 

LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )
{   
    HDC hdc;   
    PAINTSTRUCT ps;   
    int i,j;

    int cx = GetSystemMetrics( SM_CXSCREEN );
    int cy = GetSystemMetrics( SM_CYSCREEN );

    switch( message )   
    {   
      case WM_PAINT:        
        hdc = BeginPaint( hwnd, &ps );
         for( i=0; i<cx; i++ )
           for( j=0; j<cy; j++ ) SetPixel( hdc, i, j, RGB(0,0,255) );
        EndPaint( hwnd, &ps );
        return 0;
        ...

Natürlich müssen Linien und gefüllten Flächen nicht allesamt mit SetPixel(...) erstellt werden. Das wäre zwar prinzipiell möglich, aber auch sehr mühsam und nicht gerade programmiererfreundlich.

 

Die GDI umfaßt daher eine große Zahl von Zeichenfunktionen, die man durch eigenes Ausprobieren kennen­ler­nen sollte. Zum einfachen Bewegen zu P(x,y) verwendet man MoveToEx( hdc, x, y, LPPOINT ). Der Para­meter LPPOINT ist hierbei ein Zeiger auf den vorherigen Punkt. Meistens wird hier der NULL-Zeiger ge­setzt. Das aus der 16-bit-Zeit noch bekannte MoveTo(...) ist bei Win32API verschwunden. Sie werden es bei den GDI-Funktionen der MFC übrigens wieder finden. Eine gerade Linie zu P(x,y) zieht man mit LineTo( hdc, x, y ). Nachstehend findet sich ein einfaches Beispiel zum Experimentieren:

 

LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )
{   
  HDC hdc;   
  PAINTSTRUCT ps;   
  int i,j;

  int cx = GetSystemMetrics( SM_CXSCREEN );
  int cy = GetSystemMetrics( SM_CYSCREEN );

  switch (message)   
  {   
    case WM_PAINT:        
    hdc = BeginPaint( hwnd, &ps ); // Gerätekontext
      for( i=0; i<cx; i+=cx/30 )
        for( j=0; j<cy; j+=cy/30 )
        {
          MoveToEx( hdc, i, j, NULL );
          LineTo(   hdc, i+cx/100, j+cy/100 );
        }
    EndPaint( hwnd, &ps );
    return 0;
    ...




Bevor wir zu anderen Funktionen übergehen, wollen wir zunächst das Thema Farbe klären. Bei SetPixel(...) konnte man die Farbe des Punktes direkt angeben. Bei LineTo(...) ist dies nicht möglich. Hier benötigen wir neben dem Gerätekontext noch einen Zeichenstift namens
HPEN. Wir bauen diesen in unser Programm ein:

LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )
{   
  HDC hdc;    
  PAINTSTRUCT ps;   
  int i,j;

  int cx = GetSystemMetrics( SM_CXSCREEN );
  int cy = GetSystemMetrics( SM_CYSCREEN );

  static HPEN MyPen; // logischer Stift

  switch (message)   
  {   
    case WM_CREATE:
      MyPen = CreatePen( PS_SOLID, 2, RGB (255,0,0) ); // roten, dicken Stift erzeugen
    return 0;   
      
    case WM_PAINT:        
      hdc = BeginPaint( hwnd, &ps );          // Gerätekontext
        SelectObject( hdc, MyPen );           // Stift dem Gerätekontext zuordnen
        for( i=0; i<cx; i+=cx/30 )
         for( j=0; j<cy; j+=cy/30 )
          {
           MoveToEx( hdc, i, j, NULL );
           LineTo( hdc, i+cx/100, j+cy/100 );
         }
      EndPaint( hwnd, &ps );
    return 0;
 
    case WM_DESTROY:        
      DeleteObject( MyPen ); // Stift zerstören
      PostQuitMessage(0);        
    return 0;   
  }
  return DefWindowProc( hwnd, message, wParam, lParam );
}

 

 

 

Die wesentlichen Schritte im Umgang mit HPEN sind:

 

static HPEN MyPen;                               // Handle für logischen Stift

MyPen = CreatePen( PS_SOLID, 2, RGB (255,0,0) ); // Stift erzeugen

SelectObject( hdc, MyPen );                      // Stift dem Gerätekontext zuordnen

DeleteObject( MyPen );                           // Stift zerstören

 

Hier taucht auch der Begriff des Objektes auf. Unser Zeichenstift ist eines der GDI-Objekte.
Die Vielfalt steckt in der Funktion CreatePen(...):

 

HPEN CreatePen
(
  int fnPenStyle,     // PenStyle
  int nWidth,         // Breite
  COLORREF crColor    // Farbe
);


Hier kann man den PenStyle, die Breite des Stiftes und die Farbe festlegen.

 

Möglichkeiten für fnPenStyle:

 

PS_SOLID

PS_DASH      PS_DOT       PS_DASHDOT   PS_DASHDOTDOT

PS_NULL

PS_INSIDEFRAME

 

Probieren Sie die Varianten selbst aus und schlagen Sie in MSDN oder einer anderen Win32API-Hilfe nach. Nur durch Ausprobieren prägt man sich die Möglichkeiten gut ein.

 

An diesen einfachen Beispielen haben Sie bereits die grundlegende Funktionsweise des GDI verstanden. Sie können sich nun einen Gerätekontext und diesem zugeordnet einen logischen Stift beschaffen und damit einzelne Punkte und Linien zeichnen.

 

Die GDI ist variantenreich. Es gibt mehr als nur einen Gerätekontext, verschiedene GDI-Objekte und eine Vielzahl teilweise recht komplizierter Funktionen. Darüber hinaus gibt es verschiedene Möglichkeiten zur Manipulation des logischen Koordinatensystems. Verschaffen wir uns hier zunächst einen Überblick.

 


Gerätekontexte
(Auswahl):

 

BeginPaint(...)   / EndPaint(...)       client area         / WM_PAINT

GetDC(...)        / ReleaseDC(...)      client area         / andere Nachrichten als WM_PAINT

GetDCEx(...)      / ReleaseDC(...)      client area         / Kombination mit clipping region

GetWindowDC(...)  / ReleaseDC(...)      gesamtes Fenster    / wichtig: WM_NCPAINT

CreateDC(...)     / DeleteDC(...)       Fenster, Drucker ...   

 


GDI-Objekte:

 

HPEN           Stift

HBRUSH         Pinsel

HFONT          Text

HBITMAP        Bild

HPALETTE       Palette

HRGN           Region

 


GDI-Funktionen:

 

Punkte:

 

SetPixel          GetPixel

 

Linien und Kurven:

 

LineTo            MoveToEx

Arc               ArcTo  *          AngleArc *       

SetArcDirection   GetArcDirection  

Polyline          PolylineTo        PolyPolyline 

PolyBezier        PolyBezierTo

PolyDraw *                                            

LineDDA           LineDDAProc                          * ab Windows NT

 

Gefüllte Flächen:

 

Rectangle    RoundRect    Ellipse

Pie          Chord

Polygon      PolyPolygon

 

Arc(...), Chord(...) und Pie(...) benutzen allesamt die gleichen Parameter, die auf der Geometrie der Funktion Arc(...) beruhen:

 

Arc( hdc, xLeft, yTop, xRight, yBottom, xStart, yStart, xEnd, yEnd );

 

Eine Ellipse wird durch das Rechteck mit den Eckpunkten LinksOben(xLeft,yTop) und RechtsUnten(xRight,yBottom) begrenzt. Die Funktion Arc(...) zeichnet gegen den Uhrzeigersinn einen Bogen auf dieser Ellipse. Stellen Sie sich nun eine Hilfslinie vor, die den Punkt(xStart,yStart) mit dem Mittelpunkt der Ellipse verbindet. Von dem Schnittpunkt dieser Hilfslinie mit der Ellipse wird ein Bogen gezeichnet bis zum Schnittpunkt einer zweiten Hilfslinie zwischen Mittelpunkt und Punkt(xEnd,yEnd). Das ist nicht unkompliziert. Lassen Sie sich nicht verwirren, sondern probieren Sie es einfach selbst mit verschiedenen Werten aus.

 

Sie verstehen das einfach nicht richtig? Damit Sie mit dieser komplizierten Geometrie nicht alleine gelassen sind, benutzen wir einfach ein kleines Programm, das uns die Hilfslinien, das Rechteck, die Ellipse und den Bogen mit verschieden farbigen Stiften zeichnet. Wir überprüfen die Win32API einfach mit sich selbst:


#include <windows.h>

LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )
{   
       HDC hdc;   
       PAINTSTRUCT ps;   
       static HPEN MyPen0, MyPen1, MyPen2, MyPen3, MyPen4;
    
       int xStart = 200;
       int yStart = 100;
       int xEnd   = 100;
       int yEnd   = 300;

       switch( message )   
       {   
         case WM_CREATE:
           MyPen0 = CreatePen( PS_DASHDOTDOT,0,RGB(255,127,0) );   // orangebraun
           MyPen1 = CreatePen( PS_SOLID,3,RGB(255,0,0) );          // rot
           MyPen2 = CreatePen( PS_DASHDOT,0,RGB(0,255,0) );        // grün
           MyPen3 = CreatePen( PS_DOT,0,RGB(0,0,255) );            // blau
           MyPen4 = CreatePen( PS_DASHDOT,0,RGB(0,255,255) );      // blau-grün
         return 0;

         case WM_PAINT:        
           hdc = BeginPaint(hwnd, &ps);        // Gerätekontext
            
             SelectObject( hdc,MyPen0 );

             Rectangle( hdc,100,100,600,400 ); /* xLeft, yTop, xRight, yBottom */
            
             SelectObject( hdc,MyPen3 );

             Ellipse( hdc,100,100,600,400 );   /* wie Rectangle */
            
             SelectObject(hdc,MyPen2);

             MoveToEx( hdc,((600-100)/2)+100,((400-100)/2)+100,NULL ); // Mitte
             LineTo( hdc,xStart,yStart );
            
             SelectObject( hdc,MyPen4 );

             MoveToEx( hdc,((600-100)/2)+100,((400-100)/2)+100,NULL ); // Mitte
             LineTo( hdc,xEnd,yEnd );
            
             SelectObject( hdc,MyPen1 );

             Arc( hdc,100,100,600,400,xStart,yStart,xEnd,yEnd );
          
           EndPaint (hwnd, &ps);

         return 0;        

        case WM_DESTROY:        
          DeleteObject( MyPen0 );
          DeleteObject( MyPen1 );
          DeleteObject( MyPen2 );
          DeleteObject( MyPen3 );
          DeleteObject( MyPen4 );
          PostQuitMessage(0);        
        return 0;   
       }
       return DefWindowProc( hwnd, message, wParam, lParam );
}

int WINAPI WinMain( HINSTANCE hI, HINSTANCE hPrI, PSTR szCmdLine, int iCmdShow )
{
     static TCHAR szAppName[] = TEXT("Fensterklasse");   
     HWND hwnd;
  
     WNDCLASS wc;
     wc.style         = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
     wc.lpfnWndProc   = WndProc;   
     wc.cbClsExtra    = 0;
     wc.cbWndExtra    = 0;   
     wc.hInstance     = hI;
     wc.hIcon         = LoadIcon (NULL, IDI_WINLOGO);
     wc.hCursor       = LoadCursor (NULL, IDC_ARROW);
     wc.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH);
     wc.lpszMenuName  = NULL;   
     wc.lpszClassName = szAppName;
     RegisterClass (&wc);

     hwnd = CreateWindow (szAppName, TEXT("Arc verstehen"), WS_OVERLAPPEDWINDOW,
                          0, 0, 700, 500, NULL, NULL, hI, NULL);       

     ShowWindow  ( hwnd, iCmdShow );   
     UpdateWindow( hwnd);   

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

 

 

 

 

 

Offenbar alles in Ordnung mit der Funktion Arc(...). Experimentieren Sie mit den Hilfslinien, damit die Wirkungsweise verständlich wird. Wenn Sie auf solche neue Funktionen mit einer Vielzahl von Parametern stoßen, basteln Sie sich ein solches Experimentierfeld und speichern es in einem speziellen Verzeichnis "Vorlagen" ab.

 

Die Funktion Chord(...) verbindet die Endpunkte des Bogens direkt, während Pie(...) die Endpunkte des Bogens mit dem Ellipsenmittelpunkt verbindet. Wir werden die Funktionen für gefüllte Flächen in einem Programm austesten:

 

 

#include <windows.h>

LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )
{   
  HDC hdc;   
  PAINTSTRUCT ps;   
  static HPEN MyPen;
  static HBRUSH MyBrush1, MyBrush2;

  switch( message )   
  {   
    case WM_CREATE:
      MyPen = CreatePen( PS_DOT,0,RGB(0,0,255) );
      MyBrush1 = CreateSolidBrush( RGB(0,255,0) );
      MyBrush2 = CreateHatchBrush( HS_CROSS,RGB(255,255,0) );
    return 0;
 
    case WM_PAINT:        
      hdc = BeginPaint( hwnd, &ps );           //Gerätekontext
        SetBkColor( hdc, RGB(255,0,0) );       //Hintergrundfarbe
        SelectObject( hdc,MyPen );       
       
        SelectObject( hdc,MyBrush1 );

        Rectangle( hdc,100,50,300,150 );    /* xLeft, yTop, xRight, yBottom */
        Ellipse ( hdc,100,250,300,350 );    /* wie Rectangle */
        RoundRect( hdc,100,450,300,550,50,20); 
        /* xLeft, yTop, xRight, yBottom, xCornerEllipse, yCornerEllipse */
       
        SelectObject( hdc,MyBrush2 );

        Pie( hdc,400,50,600,150,600,150,400,150 );
        /* xLeft, yTop, xRight, yBottom, xStart, yStart, xEnd, yEnd */
        Chord( hdc,400,250,600,350, 600,350, 400,350 );  // wie Pie
     
      EndPaint( hwnd, &ps );

    return 0;        

    case WM_DESTROY:        
      DeleteObject( MyPen );          
      DeleteObject( MyBrush1 );
      DeleteObject( MyBrush2 );
      PostQuitMessage (0);        
    return 0;   

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


int WINAPI WinMain( HINSTANCE hI, HINSTANCE hPrI, PSTR szCmdLine, int iCmdShow )
{   
  static TCHAR szName[] = TEXT("Win_Punkte_setzen");   
  HWND hwnd;
  
  WNDCLASS wc;
  wc.style         = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
  wc.lpfnWndProc   = WndProc ;   
  wc.cbClsExtra    = 0 ;
  wc.cbWndExtra    = 0 ;   
  wc.hInstance     = hI;
  wc.hIcon         = LoadIcon( NULL, IDI_WINLOGO );
  wc.hCursor       = LoadCursor( NULL, IDC_ARROW );
  wc.hbrBackground = (HBRUSH) GetStockObject( WHITE_BRUSH );
  wc.lpszMenuName  = NULL ;   
  wc.lpszClassName = szName ;
  RegisterClass( &wc );

  hwnd = CreateWindow( szName, TEXT("Gefüllte Flächen"), WS_OVERLAPPEDWINDOW,  
                       0, 0, 700, 600, NULL, NULL, hInstance, NULL );       
    
  ShowWindow   ( hwnd, iCmdShow );   
  UpdateWindow ( hwnd );   
  
  MSG msg;   
  while( GetMessage( &msg, NULL, 0, 0 ) )   
  {
    TranslateMessage(&msg);        
    DispatchMessage (&msg);
  }
  return msg.wParam ;
}


 


Wir haben in dem Programm Gebrauch gemacht von den GDI-Objekten HPEN (Stift) und HBRUSH (Pinsel). Der Stift erstellt die Umrandung und der Pinsel füllt füllt das Innere der jeweils entstehenden Fläche aus. Der Pinsel MyBrush2 hat in unserem Fall z.B. die Farbe Gelb. Die Hintergrundfarbe, die das Muster ausfüllt ist Rot:

 

MyBrush2 = CreateHatchBrush(HS_CROSS, RGB(255,255,0));

SetBkColor (hdc, RGB(255,0,0)); //Hintergrundfarbe

 

Informieren Sie sich über die möglichen Parameter (MSDN oder andere WinAPI32-Hilfe) und experimentieren Sie, damit die Wirkung verständlich wird.

 

Bei der Funktion CreateHatchBrush( style, color ) können z.B. folgende Styles ausgewählt werden:

 

HS_BDIAGONAL HS_CROSS            HS_DIAGCROSS

HS_FDIAGONAL HS_HORIZONTAL       HS_VERTICAL

 

Kehren wir zu den Linien-Funktionen zurück. Bisher haben wir zum Erzeugen von Linien die Funktionen MoveToEx(...) und LineTo(...) kennengelernt. Prinzipiell kann man auch durch wiederholte Anwendung von SetPixel(...) eine je nach Punktdichte gepunktete oder durchgezogene Linie erzeugen.

 

Eine interessante Möglichkeit nutzt die Funktion Polyline(...). Hier benötigt man ein Array von Punkten (Struktur POINT), dessen Adresse als Parameter übergeben wird.

 

BOOL Polyline
(
  HDC hdc,            // Gerätekontext
  const POINT *lppt,  // Zeiger auf array von POINT
  int cPoints         // Zahl der Elemente im Array
)

Auf diese Weise kann man z.B. die Berechnung oder Festlegung der Punkte vom eigentlichen Zeichenvorgang im Sourcecode trennen. Nachfolgend finden Sie ein praktisches Beispiel zum Experimentieren:

#include <windows.h>
#include <cmath>

double f(double x, int cx)
{
  return x * cos(30*x/cx) * sin(30*x/cx) ;
}
 
LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )
{   
  HDC hdc;   
  PAINTSTRUCT ps;   
  int i,j;
  double x,y;
  static int cx, cy;
  static HPEN MyPen1, MyPen2;        // logische Stifte
  const int NUM = 2000;              // Größe des Arrays
       
  switch( message )   
  {   
    case WM_CREATE:
      MyPen1 = CreatePen (PS_SOLID, 2, RGB(0,255,0)); // grünen Stift erzeugen
      MyPen2 = CreatePen (PS_SOLID, 1, RGB(255,0,0)); // roten  Stift erzeugen
    return 0;   

    case WM_SIZE:
      cx = LOWORD(lParam);
      cy = HIWORD(lParam);
      POINT array[NUM];
    return 0;
       
    case WM_PAINT:        
      hdc = BeginPaint(hwnd, &ps); // Gerätekontext
               
      // x-Achse ziehen
      MoveToEx(hdc, 0, cy/2, NULL);
      LineTo(hdc, cx, cy/2);
       
      // Methode Polyline(...)
      SelectObject( hdc, MyPen1 );   // Stift dem Gerätekontext zuordnen
      for( i=0; i<cx; i++ )
      {
         x = i-cx/2;       // x = f(i)
         y = f(x,cx);      // Funktion
         j = int(cy/2-y);  // j = f(y)
         array[i].x = i;
         array[i].y = j;
      }
      Polyline( hdc, array, cx );
 

      // Methode SetPixel(...)
      SelectObject( hdc, MyPen2 );   // Stift dem Gerätekontext zuordnen
                  for( i=0; i<cx; i++ )
                  {
                    x = i-cx/2;              // x = f(i)
                    y = f(x,cx);             // Funktion
                    j = int(cy/2-y);         // j = f(y)
                    SetPixel( hdc, i, j, RGB(255,0,0) );
                  }
 
                  EndPaint( hwnd, &ps );
    return 0;
                  
    case WM_DESTROY:        
      DeleteObject( MyPen1 );
      DeleteObject( MyPen2 );
      PostQuitMessage(0);        
    return 0;   
  }
  return DefWindowProc( hwnd, message, wParam, lParam );
}

int WINAPI WinMain( ...)
{
    //... (siehe oben)     
}

 

 

 

 

 

 

 Die wesentlichen Schritte beim Einsatz von Polyline(...) waren:

 

POINT array[ ... ];                  // POINT Array

for(...) 

{

         array[i].x = ... ;         // Array-Elemente definieren

         array[i].y = ... ;

}

Polyline( hdc, array, anzahl );      // Zeichenfunktion

 

Die Array-Elemente hätte man z.B. auch aus einer Datei einlesen können.

 



Koordinatensysteme

 

Bei der Funktionsdarstellung mittels des oben aufgeführten Programms ist sicher aufgefallen, dass wir nicht ein­fach die Variable x in die Schleife nehmen konnten, um damit jeweils das zugehörige y = f(x) auszurechnen. Statt­dessen mußten wir mit Hilfsvariablen die Umrechnung des Koordinatensystems unserer Bildschirmanzeige in ein übliches mathematisches Koordinatensystem mit vier Quadranten vornehmen.

 

for( i=0; i<cx; i++ )

{

         x = i-cx/2;       // Transformation i -à x

         y = f( x,cx );    // y = f(x)

         j = int( cy/2-y );         // Transformation y -à y

         SetPixel( hdc, i, j, RGB(255,0,0) );

}

 

Dies ist eine Möglichkeit. Manchmal wünscht man aber, dass das logische Koordinatensystem unseres Geräte­kon­textes verändert wird. Wir wollen z.B. den Punkt P(0,0) wie in der Mathematik üblich in der Mitte des Bil­des. Hierfür gibt es eine einfache Funktion:

 

BOOL SetViewPortOrgEx

(

HDC hdc,            // Gerätekontext

int X,                     // neue x-Koordinate des Ursprungs

int Y,                     // neue y-Koordinate des Ursprungs

POINT *lpPoint      // Zeiger auf POINT-Struktur mit vorherigem Urspung

)

 

Der Ursprung P(0,0), der beim Standard in der linken oberen Ecke des Bildschirms liegt, kann also jetzt mit der Anweisung

 

SetViewportOrgEx(hdc, cx/2, cy/2, NULL);

 

in die Mitte des Bildschirms verlegt werden. Unter "Viewport" versteht man die "physische" Fläche, auf der gezeichnet wird. Die Ausdehnungen werden dort in Pixel gemessen. Das probieren wir sofort aus. Wir packen noch eine zweite quadratische Funktion dazu, damit wir sicher sehen, wo "oben und unten" ist:



#include <windows.h>
#include <cmath>

double f1( double x, int cx ) { return x * cos(30*x/cx) * sin(30*x/cx) ; }
double f2( double x, int cx ) { return x * x /cx ; }
 
LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )
{   
  HDC  hdc;   
  PAINTSTRUCT ps;   
  double x,y;
  static int cx, cy;
       
  switch( message )   
  {   
    case WM_SIZE:
      cx = LOWORD( lParam );
      cy = HIWORD( lParam );
    return 0;
      
    case WM_PAINT:        
      hdc = BeginPaint(hwnd, &ps);
                                           
      // Koordinatensystem einrichten
      SetViewportOrgEx(hdc, cx/2, cy/2, NULL); // (0,0) in der Mitte
                 
      // x-Achse ziehen
      MoveToEx(hdc, -cx/2, 0, NULL);
      LineTo(hdc, +cx/2, 0);
 
      // y-Achse ziehen
      MoveToEx(hdc, 0, -cy/2, NULL);
      LineTo(hdc, 0, cy/2);
       
      // Funktion darstellen
      for(x=-cx/2; x<cx/2; x++)
      {
        y = f1( x,cx );  // Funktion f1
        SetPixel(hdc, int(x), int(y), RGB(255,0,0));
        y = f2( x,cx );  // Funktion f2
        SetPixel(hdc, int(x), int(y), RGB(0,0,255));
      }                  
 
      EndPaint (hwnd, &ps);
    return 0;
                 
    case WM_DESTROY:        
      PostQuitMessage(0);        
    return 0;   
  }
  return DefWindowProc( hwnd, message, wParam, lParam );
}
 

int WINAPI WinMain (...)
{
    //... siehe oben
}

 

 

 

Die x-Achse ersteckt sich nun von -cx/2 bis cx/2 und die y-Achse von -cy/2 bis cy/2. Jetzt ist eine direkte Zuordnung zwischen x und y innerhalb der Schleife möglich:

 

for(x=-cx/2; x<cx/2; x++)

{

         y = f1(x,cx);

         SetPixel(hdc, int(x), int(y), RGB(255,0,0));

         ...

}       

 

Dies bringt für die Lesbarkeit des Programmes eine deutliche Verbesserung.  Störend ist lediglich, dass der positive Teil der y-Achse nach unten zeigt. Das rührt vom Standard-Koordinatensystem mit dem mapping mode MM_TEXT her. Hier zeigt die positive Richtung der y-Achse nach unten. Bei allen anderen mapping modes ist das umgekehrt. Verändert wird der mapping mode mit der Funktion

 

int SetMapMode

(     

HDC hdc,            // Gerätekontext

int fnMapMode       // mapping mode

)



Wir wenden dies direkt an:

// Koordinatensystem einrichten

SetMapMode(hdc, MM_LOENGLISH);   

SetViewportOrgEx(hdc, cx/2, cy/2, NULL); // (0,0) in der Mitte


 

 

Nun steht unsere Funktion nicht mehr auf dem Kopf. Man erkennt, dass die logischen Einheiten von MM_LOENGLISH  (eine logische Einheit entspricht 0,01 Zoll = 0,254 Millimeter) den logischen Pixel-Einheiten von MM_TEXT recht nahe kommen.

 

Es gibt eine ganze Reihe von Funktionen zur Beeinflussung des Koordinatensystems. Verwenden Sie diese nicht zu "bunt" gemischt, sonst ist die Konfusion total.

 

Nachfolgend finden Sie eine Übersicht über die Mapping Modes der Funktion SetMapMode(...):

 

 

MM_HIENGLISH

Logische Einheit: 0,001 Zoll = 0,0254 Millimeter.

Nach rechts: positives x,  nach oben: positives y.

MM_HIMETRIC

 

Logische Einheit: 0,01 Millimeter.

Nach rechts: positives x,  nach oben: positives y.

 

 

MM_LOENGLISH

Logische Einheit: 0,01 Zoll = 0,254 Millimeter.

Nach rechts: positives x,  nach oben: positives y.

 

MM_LOMETRIC

Logische Einheit: 0,1 Millimeter.

Nach rechts: positives x, nach oben: positives y.

 

MM_TEXT

Logische Einheit: 1 Pixel.

Nach rechts: positives x, nach unten: positives y.

MM_TWIPS

 

Logische Einheit: 1/1440 Zoll ( "twip" = 1/20 * 1/72 Zoll ).

Nach rechts: positives x, nach oben: positives y.

 

 

 

Daneben gibt es noch MM_ANISOTROPIC und MM_ISOTROPIC, die eine eigene Gestaltung des Koordinatensystems zulassen.

 

Wir probieren nun in unserem Programm den Parameter MM_HIENGLISH aus:

 

// Koordinatensystem einrichten
SetMapMode(hdc, MM_HIENGLISH);  
SetViewportOrgEx(hdc, cx/2, cy/2, NULL); // (0,0) in der Mitte
             
// x-Achse ziehen
MoveToEx(hdc, -10*cx/2, 0, NULL);
LineTo(hdc, +10*cx/2, 0);
 
// y-Achse ziehen
MoveToEx(hdc, 0, -10*cy/2, NULL);
LineTo(hdc, 0, 10*cy/2);
     
// Funktion darstellen
for(x=-10*cx/2; x<10*cx/2; x++) { ... /*Funktionen*/ }

 

 

 

 



Das Programm zeigt, dass die Darstellung nun um den Faktor 10 verkleinert ist. Hier kann daher das Zehnfache bezüglich x- und y-Achse dargestellt werden kann.