Laden...

[Tutorial] Zeichnen in Windows-Forms-Programmen (Paint/OnPaint, PictureBox)

Letzter Beitrag vor 14 Jahren 7 Posts 155.426 Views
[Tutorial] Zeichnen in Windows-Forms-Programmen (Paint/OnPaint, PictureBox)

Hallo Community,

das richtige Zeichnen in Windows-Anwendungen ist vor allem eine Frage der richtigen Herangehensweise. Und diese ist anders als man vielleicht erwartet.

Die Zeichenlogik kehrt sich um

Man muss sich davon verabschieden, dass man zeichnen kann, wann und wo man will. In einer Windows-Anwendung sagt Windows, wann man welche Bereiche zeichnen muss. Das liegt daran, dass auf den Bildschirm Gezeichnetes flüchtig ist. Wenn man ein Fenster in den Vordergrund holt, müssen in diesem Moment alle bisher verdeckten Flächen des Fenster neu gezeichnet werden, weil deren Inhalte durch die Verdeckung verloren gegangen sind. Das gleiche gilt, wenn das Fenster minimiert wurde.

Nur im OnPaint oder Paint-EventHandler zeichnen

In einer Windows-Anwendung darf man nur im OnPaint (oder - was im Prinzip das gleiche ist - im Paint-EventHandler) zeichnen. OnPaint wird von Windows (und nur von Windows) aufgerufen, wenn etwas (neu) zu zeichnen ist. Da die Steuerung von Windows ausgeht, muss man also im OnPaint zu jedem beliebigen Zeitpunkt wissen, was gezeichnet werden soll.

Man muss sich den Zustand der Zeichnung merken

Man muss sich also irgendwie merken, was gezeichnet werden soll. Wie man sich das am besten merkt, hängt von der jeweiligen Anwendung ab. Ich verwende hier eine sehr allgemeine Möglichkeit, in der man sich eine Liste der zu zeichnenden grafischen Objekte (Linien, Rechtecke, Kreise, ...) merkt.

Und so sieht dann das OnPaint aus


//==========================================================================
/// <summary>
///    OnPaint zeichnet den momentanen Zustand der grafischen Objekte.
/// </summary>
protected override void OnPaint (PaintEventArgs e)
{
   //-----------------------------------------------------------------------
   // Es ist wichtig, dass wir (zuerst) die Oberklasse machen lassen.
   //-----------------------------------------------------------------------
   base.OnPaint (e);

   //-----------------------------------------------------------------------
   // Wir lassen sich alle Objekte selbst (neu-)zeichnen.
   //-----------------------------------------------------------------------
   foreach (MyGraphicObject go in _listgo) {
      go.Draw (e.Graphics);
   }
}

In den PaintEventArgs, findet man fast alles, was man zum Zeichnen braucht. Insbesondere das Graphics-Objekt, das man für Zeichenoperationen wie DrawRectangle braucht. Dieses wird hier als e.Graphics an die Draw-Methode des grafischen Objekts übergeben, die so implementiert ist:


public override void Draw (Graphics g)
{
   if (Visible) {
      g.DrawRectangle (Pen, _rect);
   }
}

Anm.: Man sollte im Paint/OnPaint immer mit e.Graphics zeichnen und nie mit Control.CreateGraphics versuchen selbst ein Graphics-Objekt zu erzeugen. Control.CreateGraphics braucht man auch an anderen Stellen kaum und sollte es am besten gleich wieder vergessen. Für e.Graphics darf man nicht Dispose aufrufen.

Und wie kann man nun beeinflussen, was gezeichnet wird?

An den Codestellen, an denen man eigentlich etwas zeichnen wollen würde, ändert man stattdessen den gemerkten Zustand. Man könnte der Liste zum Beispiel ein neues grafisches Objekt hinzufügen:


_listgo.Add (new MyRectangle (...));

oder den Zustand eines bestehenden grafischen Objekts ändern:


_listgo [0].Visible = false;

**
Wie kommt die Zeichnung auf den Schirm?**

Damit die Änderung am Zustand, die man vorgenommen hat, auch auf dem Bildschirm sichtbar wird. Muss man


Invalidate ();

aufrufen. Damit sagt man Windows Bescheid, dass aus Sicht der Anwendung neu gezeichnet werden muss, was Windows veranlasst (früher oder später) OnPaint aufzurufen, sofern der invalidierte Bereich überhaupt sichtbar ist. Das ist mit ein Grund, warum man OnPaint nie direkt aufrufen sollte.

Welches OnPaint/Paint muss ich denn nun überschreiben/verwenden?

Man muss immer das OnPaint/Paint von dem Control überschreiben/verwenden, in das man zeichnen will. Hier wurde das OnPaint des Form überschrieben.

Und wo ist der Unterschied zwischen OnPaint und Paint?

Genaugenommen ist Paint ein Ereignis und OnPaint die Methode, die das Ereignis ausgelöst. Aus Sicht einer Unterklasse, kann man aber auch OnPaint als Ereignis ansehen und direkt überschreiben. Wenn man OnPaint überschreibt, muss man base.OnPaint aufrufen. Wenn man für Paint einen EventHandler schreibt, muss und darf man das nicht.

Gibt es Situationen, in denen OnPaint/Paint doch nicht der richtige Ort ist?

Verschiedene (Listen-)Controls (z.B. ListBox, ListView, ComboBox) haben einen OwnerDraw-Modus, bei dem man die Einträge selber zeichnen kann. Dann zeichnet man nicht im OnPaint, sondern im OnDrawItem. Die Zeichenlogik ist aber die gleiche: Windows bestimmt, wann es notwendig ist, OnDrawItem aufzurufen und OnDrawItem muss somit zu jedem beliebigen Zeitpunkt in der Lage sein, den Eintrag im aktuellen Zustand zu zeichnen. Das Prinzip ist also genau das gleiche wie bei OnPaint. Bei TreeView heißt die entsprechende Methode OnDrawNode.

Außerdem gibt es weiter unten noch ein Beispiel, bei dem PictureBoxen verwendet werden, das ohne OnPaint/Paint auskommt. Das unterliegt aber einigen Einschränkungen und eignet sich eher nur für statische Inhalte. Im Allgemeinen gilt also weiterhin: Zeichnen nur im Paint/OnPaint.

Was tue ich, wenn es flackert?

Dazu gibt es einen eigenen FAQ-Beitrag und einen eigenen Artikel:

[FAQ] Flackernde Controls vermeiden / Schnelles, flackerfreies Zeichnen
[Artikel] Flackernde Controls und flackerndes Zeichnen vermeiden

Wie kann ich das Zeichnen optimieren, wenn der Bildaufbau zu langsam ist?

Eine z.T. erhebliche Beschleunigung der Zeichnung lässt sich erreichen, wenn pro Zeichenvorgang nicht das gesamte Control neu gezeichnet wird, sondern nur die Bereiche, wo sich tatsächlich Zeichnungsobjekte befinden bzw. nur die Bereiche, die sich überhaupt geändert haben. Dieses Konzept wird im Snippet "Gezieltes OwnerDrawing" - schnelles Zeichnen bewegter Objekte von ErfinderDesRades umgesetzt.

Wie kann ich gezeichnete Objekte mit der Maus verschieben?

Zu diesem Thema hat dankenswerterweise progger einen eigenen Artikel geschrieben:

[Tutorial] Gezeichnete Objekte mit der Maus verschieben

Was muss ich beachten, wenn ich in ein gescrolltes Panel zeichnen will?

Zu den Koordinaten muss die AutoScrollPosition addiert werden, siehe Panel-Autoscroll ohne Controls [==> korrekt in das gescrollte Panel zeichnen].

Das komplette erste Beispielprogramm findet ihr weiter unten.

Animationsbeispiel

Kann man sich den Zustand auch einfacher merken?

Wie einfach es ist, sich den Zustand zu merken, hängt von der jeweiligen Anwendung ab. Man muss sich eben alle für das Zeichnen relevanten, veränderbaren Informationen merken. Wenn sich nur wenig an der Zeichnung ändern kann, muss man sich auch nur wenig merken, wie mein zweites Beispielprogramm zeigt.

Obwohl die Zeichnung eher ein wenig aufwändiger aussieht, als im ersten Beispiel, besteht der Zustand hier nur aus einer einzigen int Variablen, die angibt, in welchem Winkel gezeichnet werden soll:


private int _iAngle = 0;

Wie kann man Animationen realisieren?

Dieses zweite Programm ist gleichzeitig ein Beispiel für einer Timer-gesteuerten Animation. Nachdem der Timer mit


Timer t = new Timer ();
t.Interval = 20;
t.Tick += FormTick;
t.Start ();

erzeugt, eingestellt und gestartet wurde, wird in Abständen von 20 Millisekunden der Tick-EventHandler aufgerufen, der so implementiert ist.


protected void FormTick (Object objSender, EventArgs e)
{
   //-----------------------------------------------------------------------
   // Aktualisieren des Zustands
   // Wir erhöhen einfach den Winkel und fangen bei 360° wieder bei null an.
   //-----------------------------------------------------------------------
   if (++_iAngle == 360) {
      _iAngle = 0;
   }

   //-----------------------------------------------------------------------
   // Neuzeichnen anstoßen
   //-----------------------------------------------------------------------
   Invalidate ();
}

Das komplette zweite Beispielprogramm findet ihr weiter unten.

Die Verwendung von PictureBox

Gibt es eine Alternative, bei der ich mir den Zustand gar nicht merken muss?

Die Alternative ist, in das Bild einer PictureBox zu zeichnen (PictureBox.Image). Dann übernimmt die PictureBox das Neuzeichnen, wenn verdeckte Bereiche wieder sichtbar werden.

Wenn man PictureBoxen verwendet (und nur dann 🙂, kann man im Prinzip alles vergessen, was ich bisher gesagt habe.

Wenn man PictureBoxen verwendet*zeichnet man nicht im OnPaint/Paint, sondern man kann zeichnen, wo man will, *zeichnet man nicht in die PictureBox und nicht auf den Bildschirm, sondern in das Bild der PictureBox (PictureBox.Image), *muss man sich keinen Zustand merken.

**
Wenn es so einfach geht, wo ist der Haken?**

Das Problem bei der Verwendung von PictureBoxen ist, dass man einmal Gezeichnetes nicht oder zumindest nicht so einfach wieder los wird. Durch das Zeichnen werden die Pixel von PictureBox.Image eingefärbt. Ein Kreis, den man in PictureBox.Image gezeichnet hat, ist also kein Kreis als Objekt, sondern hat nur zur Einfärbung von Pixeln geführt.

Im Beispielprogramm wird der Effekt dadurch abgemildert, dass zwei PictureBoxen so über oder besser ineinander gelegt werden, dass man durch die obere die untere sieht. Dadurch kann man in das Bild der unteren die statischen Zeile (in diesem Fall das Spielfeld) und in das Bild der oberen alle veränderlichen Teile zeichnen. Wenn sich etwas ändert, kann das Bild der oberen gelöscht werden und dadurch müssen nur die veränderlichen Teile neu gezeichnet werden.

Wo bekommt man das Graphics-Objekt her?

Wenn man PictureBoxen verwendet, zeichnet man ja nicht im OnPaint/Paint. Daher hat man nicht das Graphics-Objekt aus den EventArgs, sondern muss sich selbst eins erzeugen:


g = Graphics.FromImage (pictureBox.Image);

Die ganzen Objekte im Zusammenhang mit dem Zeichnen (Graphics, Brush, Pen, ...) sollte man sich einmal erzeugen und dann immer wieder verwenden. Erzeugt man sie doch unnötigerweise bei jeder Zeichenoperation neu, sollte man nach dem Gebrauch zumindest Dispose aufrufen, da alle diese Objekte unverwaltete, begrenzte Ressourcen enthalten.

Das komplette dritte Beispielprogramm findet ihr weiter unten.

Wann sollte man PictureBoxen (nicht) verwenden?

Man sollte PictureBoxen nur verwenden, wenn man PictureBox.Image auf ein Bild setzt. Ob das Bild fertig geladen oder zur Programmlaufzeit erstellt wird, spielt keine Rolle. Doch wenn man PictureBox.Image gar nicht verwendet, dann sollte man wie eingangs beschrieben besser auf ein einfaches Panel oder direkt in das Form zeichnen, nicht auf eine PictureBox.

Schluss

Dieser Artikel kann und will nur einen ersten Einstieg in das Zeichnen unter Windows bieten. Gerade dieses Thema sollte man durch Bücher vertiefen, weil es eine Menge zu beachten gibt, was hier nur angerissen wurde oder ganz unter den Tisch gefallen ist. Für ein erstes und schnelles Erfolgserlebnis sollte der Artikel jedoch gut sein.

herbivore

Kompletter Code des ersten Beispielprogramms

Beispiel mit der Liste der grafischen Objekte


using System;
using System.Windows.Forms;
using System.Drawing;
using System.Collections.Generic;

//*****************************************************************************
/// <summary>
///    Die abstrakte Oberklasse aller konkreten grafischen Objekte.
/// </summary>
public abstract class MyGraphicObject
{
   //--------------------------------------------------------------------------
   private Pen  _pen;
   private bool _fVisible = true;

   //==========================================================================
   public Pen Pen
   {
      get { return _pen; }
   }

   //==========================================================================
   public bool Visible
   {
      get { return _fVisible; }
      set { _fVisible = value; }
   }

   //==========================================================================
   public MyGraphicObject (Pen pen)
   {
      _pen = pen;
   }

   //==========================================================================
   public abstract void Draw (Graphics g);

   //==========================================================================
   public abstract bool Contains (Point pt);

}

//*****************************************************************************
/// <summary>
///    Unterklasse von MyGraphicObject als Beispiel für ein konkretes
///    grafisches Objekt. In der Praxis hätte man mehrere solche Klassen.
/// </summary>
public class MyRectangle : MyGraphicObject
{
   //--------------------------------------------------------------------------
   private Rectangle _rect;

   //==========================================================================
   public MyRectangle (Pen pen, Rectangle rect) : base (pen)
   {
      _rect = rect;
   }

   //==========================================================================
   public override void Draw (Graphics g)
   {
      if (Visible) {
         g.DrawRectangle (Pen, _rect);
      }
   }

   //==========================================================================
   public override bool Contains (Point pt)
   {
      return _rect.Contains (pt);
   }
}


//*****************************************************************************
/// <summary>
///    Beispielanwendung
/// </summary>
public class GraphicObjectsWindow : Form
{
   //--------------------------------------------------------------------------
   const int iWidth      = 160;
   const int iHeight     = 160;

   //--------------------------------------------------------------------------
   List <MyGraphicObject> _listgo = new List <MyGraphicObject> ();

   //==========================================================================
   public GraphicObjectsWindow ()
   {
      //-----------------------------------------------------------------------
      // Initialisierung des Fensters
      //-----------------------------------------------------------------------
      Text = "GraphicObjects";
      ClientSize = new Size (iWidth, iHeight);
      DoubleBuffered = true;
      BackColor = Color.White;
      MouseClick += FormMouseClick;

      //-----------------------------------------------------------------------
      // Wir erzeugen ein Feld aus 5x5 Rechtecken.
      // Damit bestimmen wir, was gezeichnet werden soll, ohne hier selbst
      // zu zeichnen.
      //-----------------------------------------------------------------------
      for (int x = 0; x < 5; ++x) {
         for (int y = 0; y < 5; ++y) {
            _listgo.Add (new MyRectangle (
                            Pens.Black,
                            new Rectangle (30 * x + 10, 30 * y + 10, 20, 20)
                        ));
         }
      }
   }

   //==========================================================================
   /// <summary>
   ///    Führt eine Zustandsänderung durch und stößt das Neuzeichnen an.
   /// </summary>
   protected void FormMouseClick (Object objSender, MouseEventArgs e)
   {
      //-----------------------------------------------------------------------
      // Wir suchen das Rechteck auf das geklickt wurde und ändern dessen
      // Sichtbarkeit.
      // Damit bestimmen wir, was gezeichnet werden soll, ohne hier selbst
      // zu zeichnen.
      // Anm.: Wenn sich die Objekte überlappen könnten/würden, müsste man
      // _listgo von hinten nach vorne durchsuchen.
      //-----------------------------------------------------------------------
      foreach (MyGraphicObject go in _listgo) {
         if (go.Contains (new Point (e.X, e.Y))) {
            go.Visible = !go.Visible;
            break;
         }
      }

      //-----------------------------------------------------------------------
      // Neuzeichnen anstoßen
      //-----------------------------------------------------------------------
      Invalidate ();
   }

   //==========================================================================
   /// <summary>
   ///    OnPaint zeichnet den momentanen Zustand der grafischen Objekte.
   /// </summary>
   protected override void OnPaint (PaintEventArgs e)
   {
      //-----------------------------------------------------------------------
      // Es ist wichtig, dass wir (zuerst) die Oberklasse machen lassen.
      //-----------------------------------------------------------------------
      base.OnPaint (e);

      //-----------------------------------------------------------------------
      // Wir lassen sich alle Objekte selbst (neu-)zeichnen.
      //-----------------------------------------------------------------------
      foreach (MyGraphicObject go in _listgo) {
         go.Draw (e.Graphics);
      }
   }
}

//*****************************************************************************
internal static class App
{
   //==========================================================================
   private static void Main ()
   {
      Application.Run (new GraphicObjectsWindow ());
   }
}

Kompletter Code des zweiten Beispielprogramms

Animationsbeispiel:


using System;
using System.IO;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Drawing2D;

//*****************************************************************************
public class AnimationWindow : Form
{
   //--------------------------------------------------------------------------
   const    int    iWidth        = 400;
   const    int    iHeight       = 400;
   const    int    iRadius       = 120;
   const    int    iSmallRadius  = iRadius/4;
   const    int    iLineWidth    = 6;
   const    int    iNumShovels   = 15;
   readonly PointF ptfCenter     = new PointF (iWidth/2, iHeight/2);
   readonly Font   font          = new Font ("Arial", 12);

   //--------------------------------------------------------------------------
   private Pen _pen;
   private int _iAngle = 0;
   private int _iTextHeight = 0;

   //==========================================================================
   public AnimationWindow ()
   {

      //-----------------------------------------------------------------------
      // Initialisierung des Fensters
      //-----------------------------------------------------------------------
      Text = "Einfache Animation";
      ClientSize = new Size (iWidth, iHeight);
      DoubleBuffered = true;
      BackColor = Color.White;

      //-----------------------------------------------------------------------
      // Es ist kein guter Stil, im OnPaint die Pens und Brushes immer und
      // immer wieder neu zu erzeugen. Deshalb sollte man die Standard-Pens
      // und -Brushes aus den gleichnamigen .NET-Klassen verwenden. Oder wenn
      // da nichts passendes dabei ist, den Pen - wie hier - genau einmal im
      // Konstruktor erzeugen.
      //-----------------------------------------------------------------------
      _pen = new Pen (Color.Black, iLineWidth);

      //-----------------------------------------------------------------------
      // Initialisierung des Timers
      //-----------------------------------------------------------------------
      Timer t = new Timer ();
      t.Interval = 20;
      t.Tick += FormTick;
      t.Start ();
   }

   //==========================================================================
   protected void FormTick (Object objSender, EventArgs e)
   {
      //-----------------------------------------------------------------------
      // Aktualisieren des Zustands
      // Wir erhöhen einfach den Winkel und fangen bei 360° wieder bei null an.
      //-----------------------------------------------------------------------
      if (++_iAngle == 360) {
         _iAngle = 0;
      }

      //-----------------------------------------------------------------------
      // Neuzeichnen anstoßen
      //-----------------------------------------------------------------------
      Invalidate ();
   }

   //==========================================================================
   protected override void OnPaint (PaintEventArgs e)
   {
      Graphics g;
      Matrix   m;

      //-----------------------------------------------------------------------
      // Es ist wichtig, dass wir (zuerst) die Oberklasse machen lassen.
      //-----------------------------------------------------------------------
      base.OnPaint (e);

      //-----------------------------------------------------------------------
      // Entsprechend des aktuellen Winkels um die Mitte des Fensters rotieren.
      //-----------------------------------------------------------------------
      m = new Matrix ();
      m.RotateAt (_iAngle, ptfCenter);
      g = e.Graphics;
      g.Transform = m;

      //-----------------------------------------------------------------------
      // Damit das Gezeichnete freundlicher aussieht.
      //-----------------------------------------------------------------------
      g.SmoothingMode = SmoothingMode.AntiAlias;

      //-----------------------------------------------------------------------
      // Wenn wir es noch nicht gemacht haben, die Texthöhe berechnen.
      //-----------------------------------------------------------------------
      if (_iTextHeight == 0) {
         SizeF sizef = g.MeasureString ("360°", font);
         _iTextHeight = (int)Math.Round (sizef.Height);
      }

      //-----------------------------------------------------------------------
      // Den Winkel als Text zeichnen.
      //-----------------------------------------------------------------------
      g.DrawString (_iAngle.ToString ("d3") + "°",
                    font,
                    Brushes.Black,
                    iWidth  / 2 - iSmallRadius / 2,
                    iHeight / 2 + iRadius + iSmallRadius
                    - _iTextHeight / 2 + iLineWidth / 2);

      //-----------------------------------------------------------------------
      // Den zentralen Kreis zeichnen.
      //-----------------------------------------------------------------------
      g.DrawEllipse (_pen,
                     iWidth  / 2 - iRadius,
                     iHeight / 2 - iRadius,
                     2 * iRadius,
                     2 * iRadius);

      //-----------------------------------------------------------------------
      // Die Schaufeln zeichnen.
      // Das Ganze ist nämlich eine schematische Pelton-Turbine.
      //-----------------------------------------------------------------------
      for (int i = 0; i < iNumShovels; ++i) {
         m.RotateAt (360f/iNumShovels, ptfCenter);
         g.Transform = m;

         g.DrawArc (_pen,
                    iWidth  / 2 - iSmallRadius,
                    iHeight / 2 + iRadius,
                    2 * iSmallRadius,
                    2 * iSmallRadius,
                    90,
                    180);
      }
   }
}

//*****************************************************************************
internal static class App
{
   //==========================================================================
   private static void Main ()
   {
      Application.Run (new AnimationWindow ());
   }
}

Kompletter Code des dritten Beispielprogramms

PictureBox-Beispiel:


using System;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;

//*****************************************************************************
public class HexagonWindow : Form
{
   //--------------------------------------------------------------------------
   const int iBaseX      = 21;
   const int iBaseY      = 12;

   const int iCols       = 10;
   const int iRows       = 10;
   const int iWidth      = iCols*2*iBaseX+iBaseX+1;
   const int iHeight     = iRows*3*iBaseY+iBaseY+1;

   //--------------------------------------------------------------------------
   readonly Font _font   = new Font ("Arial", 9);

   //--------------------------------------------------------------------------
   private Bitmap     _bitmCanvasBehind;
   private Bitmap     _bitmCanvasAhead;

   //--------------------------------------------------------------------------
   private Graphics   _gCanvasBehind;
   private Graphics   _gCanvasAhead;

   //--------------------------------------------------------------------------
   private Brush      _brushBack;
   private Brush      _brushText;
   private Pen        _penBorder;
   private Pen        _penConnection;

   //--------------------------------------------------------------------------
   private PictureBox _picbCanvasBehind;
   private PictureBox _picbCanvasAhead;

   //==========================================================================
   /// <summary>
   ///    Konstruktor
   /// </summary>
   public HexagonWindow ()
   {
      Control ctrlCurr;
      Control ctrlPrev;
      Control ctrlCurrContainer;

      //-----------------------------------------------------------------------
      // Fenstertitel und Größe setzen
      //-----------------------------------------------------------------------
      Text = "Hexagon";
      ClientSize = new Size (iWidth, iHeight);
      MaximumSize = Size;
      MinimumSize = Size;

      //-----------------------------------------------------------------------
      // Bitmaps, Graphics & Co initialsieren
      //-----------------------------------------------------------------------
      _bitmCanvasBehind = new Bitmap (iWidth, iHeight,
                                      PixelFormat.Format24bppRgb);
      _bitmCanvasAhead  = new Bitmap (iWidth, iHeight,
                                      PixelFormat.Format32bppArgb);

      _gCanvasBehind = Graphics.FromImage (_bitmCanvasBehind);
      _gCanvasAhead  = Graphics.FromImage (_bitmCanvasAhead);

      _brushBack     = Brushes.White;
      _brushText     = Brushes.Black;
      _penBorder     = Pens.Black;
      _penConnection = Pens.Blue;

      //-----------------------------------------------------------------------
      // Steuerelemente erzeugen
      // Die PictureBox _picbCanvasAhead liegt transparent über
      // _picbCanvasBehind. Dadurch kann man die Verbindungslinien in
      // _picbCanvasAhead und die Hexagone in _picbCanvasBehind zeichnen,
      // ohne dass sich beide ins Gehege kommen.
      //-----------------------------------------------------------------------
      ctrlCurrContainer = this;

      //-----------------------------------------------------------------------
      ctrlCurr = _picbCanvasBehind = new PictureBox ();
      ctrlCurr.Dock = DockStyle.Fill;
      ctrlCurr.BackColor = Color.White;
      ((PictureBox)ctrlCurr).Image = _bitmCanvasBehind;
      ctrlCurrContainer.Controls.Add (ctrlCurr);
      ctrlPrev = ctrlCurr;

      //-----------------------------------------------------------------------
      ctrlCurrContainer = ctrlPrev;

      //-----------------------------------------------------------------------
      ctrlCurr = _picbCanvasAhead = new PictureBox ();
      ctrlCurr.Dock = DockStyle.Fill;
      ((PictureBox)ctrlCurr).Image = _bitmCanvasAhead;
      ctrlCurr.BackColor = Color.Transparent;
      ctrlCurrContainer.Controls.Add (ctrlCurr);
      ctrlPrev = ctrlCurr;

      //-----------------------------------------------------------------------
      // Karte initial zeichnen
      //-----------------------------------------------------------------------
      _gCanvasBehind.FillRectangle (_brushBack, 0, 0, iWidth, iHeight);
      for (int iX = 0; iX < iCols; ++iX) {
         for (int iY = 0; iY < iRows; ++iY) {
            DrawHexagon (_gCanvasBehind, iX, iY);
         }
      }

      //-----------------------------------------------------------------------
      // Testausgabe: Zeichnen einer Verbindungslinie
      //-----------------------------------------------------------------------
      DrawHexagonConnection (_gCanvasAhead, 2, 2, 2, 3);
   }

   //==========================================================================
   /// <summary>
   ///    Liefert zum (zweidimensionalen) Index die ClientKoordinaten
   ///    des Hexagons an dieser Position.
   /// </summary>
   protected Point GetHexagonPoint (int iX, int iY)
   {
      int iOffsetX;
      int iOffsetY;

      //-----------------------------------------------------------------------
      // Berechnen der Bildschirmkoordinaten anhand des Index ([iX, iY])
      // des Hexagons ...
      //-----------------------------------------------------------------------
      iOffsetX = iX*2*iBaseX;
      iOffsetY = iY*3*iBaseY;
      if (iY % 2 == 1) {
         iOffsetX += iBaseX;
      }

      return new Point (iOffsetX, iOffsetY);
   }

   //==========================================================================
   /// <summary>
   ///   Zeichnen einer Verbindungsline vom Mittelpunkt eines Hexagons
   ///   zu einem anderen.
   /// </summary>
   protected void DrawHexagonConnection (Graphics g,
                                         int iXFrom, int iYFrom,
                                         int iXTo, int iYTo)
   {
      Point ptFrom;
      Point ptTo;

      ptFrom = GetHexagonPoint (iXFrom, iYFrom);

      ptFrom.X += iBaseX;
      ptFrom.Y += 2*iBaseY;

      ptTo = GetHexagonPoint (iXTo, iYTo);

      ptTo.X += iBaseX;
      ptTo.Y += 2*iBaseY;

      g.DrawLine (_penConnection, ptFrom, ptTo);
   }

   //==========================================================================
   /// <summary>
   ///   (Neu-)Zeichnen eines Hexagons
   /// </summary>
   protected void DrawHexagon (Graphics g, int iX, int iY)
   {
      Point pt;

      int iOffsetX;
      int iOffsetY;

      GraphicsPath gpath;
      Region region;

      //-----------------------------------------------------------------------
      // Erstmal herauskriegen, wo das Hexagon gezeichnet werden soll.
      //-----------------------------------------------------------------------
      pt = GetHexagonPoint (iX, iY);
      iOffsetX = pt.X;
      iOffsetY = pt.Y;

      //-----------------------------------------------------------------------
      // Dann legen wir die sechs Eckpunkte des Hexagons an (wobei der
      // erste Punkt zweimal angegeben wird, damit das Polygon geschlossen
      // ist).
      //-----------------------------------------------------------------------
      Point[] apt =
      {
         new Point (iOffsetX + 0 * iBaseX, iOffsetY + 1 * iBaseY),
         new Point (iOffsetX + 1 * iBaseX, iOffsetY + 0 * iBaseY),
         new Point (iOffsetX + 2 * iBaseX, iOffsetY + 1 * iBaseY),
         new Point (iOffsetX + 2 * iBaseX, iOffsetY + 3 * iBaseY),
         new Point (iOffsetX + 1 * iBaseX, iOffsetY + 4 * iBaseY),
         new Point (iOffsetX + 0 * iBaseX, iOffsetY + 3 * iBaseY),
         new Point (iOffsetX + 0 * iBaseX, iOffsetY + 1 * iBaseY)
      };

      //-----------------------------------------------------------------------
      // Jetzt machen wir aus den Punkten einen (polygonen) Pfad.
      //-----------------------------------------------------------------------
      gpath = new GraphicsPath ();
      gpath.AddPolygon (apt);

      //-----------------------------------------------------------------------
      // Eigentliches Zeichnen
      //-----------------------------------------------------------------------
      g.DrawPath (_penBorder, gpath);

      //-----------------------------------------------------------------------
      // Testausgabe ((zweidimensionaler) Index der Hexagone)
      //-----------------------------------------------------------------------
      String strText;
      SizeF sizef;

      strText = String.Format ("[{0},{1}]", iX, iY);
      sizef = g.MeasureString (strText, _font);

      g.DrawString(strText,
                   _font,
                   _brushText,
                   iOffsetX+(2*iBaseX-1-sizef.Width)/2+1,
                   iOffsetY+(4*iBaseY-1-sizef.Height)/2+1);

      //-----------------------------------------------------------------------
      // Jetzt sagen wir der PictureBox noch, dass sich was geändert hat
      // (nämlich die den (polygonen) Pfad umschließende Region)
      //-----------------------------------------------------------------------
      region = new Region (gpath);
      _picbCanvasBehind.Invalidate (region);
   }
}

//*****************************************************************************
internal static class App
{
   //==========================================================================
   private static void Main ()
   {
      Application.Run (new HexagonWindow ());
   }
}

Hallo Community,

neulich ist die Frage aufgetaucht, ob dieses Tutorial noch aktuell ist. Ja, das ist es, unter anderem, weil es immer wieder mal aktualisiert wurde und wird. Und auch die verwendete Technik ist weiterhin aktuell. [EDIT]Das gilt auch im Dezember 2012 weiterhin und wird noch längere Zeit in die Zukunft gelten.[/EDIT]

In dem Tutorial geht es darum, grafische Inhalte auf den Schirm zu bekommen. Das Tutorial beschreibt dazu die auf GDI bzw. GDI+ basierenden Möglichkeiten des .NET-Frameworks. Natürlich gibt es mit DirectX, XNA, WPF u.a. seit längerem Alternativen und es werden im Laufe der Zeit sicher weitere Alternativen dazukommen, die alle ihre Berechtigung haben. Was im Tutorial beschrieben ist, ist und bleibt aber eine besonders einfache und durchaus sinnvolle Möglichkeit. Sie ist gut geeignet für Windows-Forms-Programme, eigene Controls, 2D-Grafik und einfache Spiele. Sie findet ihre Grenzen, wenn es um 3D, sehr schnell wechselnde oder sehr aufwändige Bildinhalte geht.

Im Forum finden sich diverse und teilweise sehr kontroverse Threads, die sich damit beschäftigen, welche Technik für welchen Anwendungsfall am besten geeignet ist. Deshalb brauchen und sollten wir das hier nicht vertiefen. Im Tutorial geht es um GDI, nicht um die Alternativen.

herbivore