Advanced   Java   Services
Graphikprogrammierung
Back Next Up Home

Der Graphikkontext


Jeder graphischen Komponente, also jeder Klasse, die sich von Component ableitet ist ein Graphikkontext zugeordnet. Dieser Graphikkontext wird repräsentiert durch ein Objekt vom Typ Graphics. Mit Hilfe der Methoden der Klasse Graphics kann man auf die durch die Komponente dargestellte Fläche zeichnen. So bietet die Klasse Graphics etwa eine ganze Reihe von fill-Methoden an, mit denen man bestimmte geometrische Flächen wie Rechtecke ( mit fillRect() ) oder Ellipsen ( mit fillOval() ) usw. zeichnen kann. Will man nur die Randlinien der Flächen zeichnen, so nimmt man die entsprechenden draw-Methoden aus Graphics.

Wie erhält man ein GraphicObjekt


Die Klasse Graphics ist abstrakt, man kann also nicht mit einem Konstruktoraufruf arbeiten. Zudem braucht ein Graphikobjekt eine Zeichenfläche, für die es zuständig ist, also einen Graphikkontext. Deswegen führt der Weg zu einem Graphikobjekt über die Zeichenfläche, für die es zuständig ist. Die Zeichenfläche kann die sichtbare Fläche einer Komponente sein oder auch ein OffscreenImage, ein Bild im Hintergrund, das im Hauptspeicher gehalten wird. Ist die Zeichenfläche eine Komponente, so steht der Graphikkontext erst dann zur Verfügung, wenn die Komponente sichtbar geworden ist. Realisiert wird die Klasse Graphics durch die Implementierung sun.java2d.SunGraphics2D im JavaArchiv rt.jar.

graphicshierarchie.jpg

Bei einer Komponente gibt es zwei Möglichkeiten, zu einem Graphikkontext zu kommen. Da die erste Realisierung durch die Klasse sun.java2d.SunGraphics2D erfolgt, erhält man immer ein Objekt diesen Typs.

Zugriff von "außen" durch Aufruf der Methode getGraphics(), die jede Komponente von Component erbt


Zu beachten ist, daß obiger Aufruf erst dann nicht mehr null liefert, wenn die Komponente sichtbar geworden ist. Danach kann man mit dieser Methode jederzeit ein Graphicsobjekt erhalten, mit dem man auf die zugehörige Komponente zeichnen kann. Das folgende Applet verwendet ein BorderLayout und präsentiert ein Label im Zentrum und einen Button im Norden. In der Reaktionsmethode actionPerformed() zum Button holt man sich ein Graphicsobjekt. Mit diesem Graphicsobjekt wird dann das untere rechte Viertel des Labels mit einer Zufallsfarbe gestrichen und im oberen rechten Viertel der Text "Hallo" in der gleichen Farbe gesetzt.

Auszug aus dem Quellcode

// ---------------- vom ActionListener geforderte Methode ---------------- \\
public void actionPerformed (ActionEvent e)
{
   Graphics g = centerLabel.getGraphics();
   g.setColor( getRandomColor() );
   g.drawString("Hallo",50,50);
   Dimension dim = centerLabel.getSize() ;
   g.fillRect(dim.width/2, dim.height/2, dim.width/2, dim.height/2) ;
   g.dispose();
}

// liefert eine Zufallsfarbe
private Color getRandomColor()
{
   java.util.Random rd = new java.util.Random();
   return new Color( rd.nextInt(256), rd.nextInt(256), rd.nextInt(256) );
}

In dieser Situation sollte man einen Zeichenvorgang immer mit dispose() beenden. In der API heißt es hierzu:

When a Java program runs, a large number of Graphics objects can be created within a short time frame. Although the finalization process of the garbage collector also disposes of the same system resources, it is preferable to manually free the associated resources by calling this method rather than to rely on a finalization process which may not run to completion for a long period of time.

Die eben beschriebene Methode wird verwendet, wenn man den Graphikkontext zu einer Komponente aus der API braucht. Die folgende Methode erfordert den Einsatz von Vererbung.

Zugriff von "innen" durch Überschreiben der paint()-Methode, die jede Komponente von Component erbt


Auch in diesem Fall erhält man ein Objekt vom Typ sun.java2d.SunGraphics2D. Dies geschieht meistens beim Entwickeln eigener Komponenten. Hierzu verwendet man in der Regel die Klasse Canvas und bildet eine Unterklasse von Canvas. Dabei sind jedoch gewisse Aufrufautomatismen zu beachten. Neben der Methode paint() spielen dabei auch die Methoden update() und repaint() eine Rolle. Die folgenden Graphik erläutert die Zusammenhänge.

repaint(), update(), paint(), setBackground(), setForeground(), invalidate(), validate() :

repaintupdate2.jpg

Die oben aufgezeigten Zusammenhänge kann man mit dem folgenden Applet nachvollziehen. Wenn man auf den Button drückt, dann öffnet sich ein Appletfenster. Es zeigt links eine TextArea und rechts ein Objekt einer Unterklasse von Canvas. Im Norden findet man vier Buttons, die jeweils die Methoden repaint(), bzw. setBackground(), bzw. setForeground() bzw. paint() der Unterklasse von Canvas aufrufen können. Die Aufrufe werden in der TextArea dokumentiert, um die Aufrufmechanismen zu erkennen. zudem werden die Aufrufe von invalidate() und validate() in der TextArea dokumentiert.

Canvas überschreibt die Methoden update() und paint() aus Component wie folgt:

public void update(Graphics g)
{
   g.clearRect(0, 0, width, height);
   paint(g);
}

public void paint(Graphics g)
{
   g.clearRect(0, 0, width, height);
}

clearRect() holt sich die aktuelle Hintergrundfarbe und übermalt damit die ganze Fläche, es "löscht" damit vorherige Zeichnungen. Man erkennt, daß update() die Methode paint() aufruft und daß so in der Defaultimplementierung gleich zweimal der Hintergrund mit der aktuellen Hintergrundfarbe übermalt wird. Eine sinnvolle Anwendung von Canvas wird daher paint() überschreiben ohne super.paint() zu rufen. Hier einige Ausschnitte aus dem Quellcode desd Applets. Zunächst die Reaktionsmethode für die Buttons.

// ---------------- vom ActionListener geforderte Methode ---------------- \\
public void actionPerformed (ActionEvent e)
{
   Object source = e.getSource();

   if(source== repaintButton)
      canvas.repaint();
   else if(source == setBackgrButton)
      canvas.setBackground( getRandomColor() );
   else if(source == setForegrButton)
      canvas.setForeground( getRandomColor() );
   else if(source == paintButton)
      canvas.paint( canvas.getGraphics() );
      //normalerweise nicht üblich, dient hier nur dazu
      //den paint-Aufruf zu dokumentieren !
}


// liefert eine Zufallsfarbe
private Color getRandomColor()
{
   java.util.Random rd = new java.util.Random();
   return new Color( rd.nextInt(256), rd.nextInt(256), rd.nextInt(256) );
}

Und hier die Canvasklasse.

class DemoCanvas extends Canvas
{
   private TextArea tear;
   private int countPaint, countUpdate, countSetBackground, countForeBackground;
   private int countRepaint, countValidate, countInvalidate;
   private boolean updateCalled, setBackgroundCalled, setForegroundCalled;

   public DemoCanvas(TextArea tear)
   {
      this.tear = tear;
   }

   public void update(Graphics g)
   {
      tear.append("update " + ++countUpdate + "\n");
      updateCalled=true;
      super.update(g);
   }

   public void paint(Graphics g)
   {
      tear.append("paint " + ++countPaint + "\n");

      if(updateCalled)
      {
         updateCalled=false;
         Dimension dim = getSize();
         g.setColor( getRandomColor() );
         g.drawString( "updateCalled" , 30,30 );
         g.fillRect(0, dim.height/2, dim.width/3, dim.height/2) ;
      }
      else if(setBackgroundCalled)
      {
         setBackgroundCalled=false;
         Dimension dim = getSize();
         Color backCol = getBackground() ;

         Color newCol = new Color( 255-backCol.getRed(), 255-backCol.getGreen(), 255-backCol.getBlue() );
         g.drawString( "setBackgroundCalled" , 30,30 );
         g.setColor( newCol );
         // unten mittleres drittel
         g.fillRect(dim.width/3, dim.height/2, dim.width/3, dim.height/2) ;
      }
      else if(setForegroundCalled)
      {
         setForegroundCalled=false;
         Dimension dim = getSize();
         Color foreCol = getForeground() ;
         Color newCol = new Color( 255-foreCol.getRed(), 255-foreCol.getGreen(), 255-foreCol.getBlue() );
         g.setColor( newCol );
         g.drawString( "setForegroundCalled" , 30,30 );
         // unten mittleres drittel
         g.fillRect(dim.width/3, dim.height/2, dim.width/3, dim.height/2) ;
      }
      else
      {
         Dimension dim = getSize();
         g.setColor( getRandomColor() );
         g.drawString( "paint direct", 50,50 );
         // unten rechtes drittel
         g.fillRect(2*dim.width/3, dim.height/2, dim.width/3, dim.height/2) ;
      }
   }

   public void setBackground(Color col)
   {
      tear.append("setBackground " + ++countSetBackground + "\n");
      setBackgroundCalled=true;
      super.setBackground(col) ;
   }

   public void setForeground(Color col)
   {
      tear.append("setForeground " + ++countForeBackground + "\n");
      setForegroundCalled=true;
      super.setForeground(col) ;
   }


   private Color getRandomColor()
   {
      java.util.Random rd = new java.util.Random();
      return new Color( rd.nextInt(256), rd.nextInt(256), rd.nextInt(256) );
   }

   public void repaint()
   {
      tear.append("repaint " + ++countRepaint + "\n");
      super.repaint();
   }

   public void validate()
   {
      tear.append("validate " + ++countValidate + "\n");
      super.validate();
   }

   public void invalidate()
   {
      tear.append("invalidate " + ++countInvalidate + "\n");
      super.invalidate();
   }
}



Überschreiben von update()


Normalerweise bedient man sich der Methode repaint(), um paint() aufzurufen, da repaint() keine Parameter benötigt. Da dies jedoch über die Methode update() geschieht, wird dabei jedesmal der Hintergrund neu gezeichnet. Dies ist in zwei Fällen störend. Zum einen verhindert es ein "Weitermalen" auf der Fläche, zum anderen führen schnell aufeinanderfolgende repaint() Aufrufe zu einem Flackern, da das Zeichnen des Hintergrunds relativ lange dauert. Für solche Fälle überschreibt man update() so, daß paint() direkt gerufen wird.

public void update(Graphics g)
{
   paint(g);
}

public void paint(Graphics g)
{
   // draw something
}

Das folgende Applet überschreibt update() um so ein Weitermalen zu erreichen.

Malen mit der Maus


Das Applet bringt ein kleines Malprogramm. Mit der linken Maustaste wird gezeichnet. Die Zeichnung kann mit dem clear-Button gelöscht werden.

Die Entwicklung eines solchen ZeichenCanvas ist eine Übung am Ende dieses Kapitels über AWT.

OffscreenImages


Obiges Applet hat einen kleinen Nachteil. das Kunstwerk, das man (vielleicht) vollbracht haben, wird sofort gelöscht, wenn der Browser etwa mimiert wird oder eine andere Anwendung den Fokus erhält. das liegt daran, daß wir direkt in den Canvas zeichnen. Könnte man stattdessen auf eine extra Leinwand zeichnen und diese im Canvas einfach nur bei Bedarf präsentieren, so blieben unserer Werke (zumindest für eine brwosersitzung) erhalten. Mit einem OffScreenimage ereiocht man genau dieses. Es gibt mehrere Möglichkeiten, zu einem Hintergrundbild zu kommen. der erste Weg führt über die graphische Komponente die wir benützen.

Ein OffScreenImage mit component.createImage(int width, int height)

Dieser Weg erscheint zunächst einfach. Die Methode createImage() liefert einem mit einem Aufruf ein ImageObjekt. Der Typ ist BufferedImage. Will man ein Image, das den ganzen Monitor ausfüllen kann, so kann man sich die Bildschirmgröße mit Hilfe der Klasse Toolkit besorgen.

Das zweite Statement hat jedoch seine Tücken. Es liefert nämlich erst dann einen Wert ungleich null, wenn die Komponente sichtbar ist. Im Konstruktor des eigenen Canvas untergebracht, liefert diese Methode also immer null, da zu diesem Zeitpunkt die Komponente noch nicht sichtbar ist. Bei einer Applikation darf man createImage() erst nach einem setVisible() für die toplevel-Komponente rufen. Auch bei einem Applet muß man aufpassen. Auch hier erhält man bei einem createImage()-Aufruf im Konstruktor null. Erst in init() oder start() führt der Aufruf zum Erfolg. Hier das Applet, das diese Technik verwendet. Es ist im Prinzip das gleiche Malapplet, diesmal aber zeichnet man auf ein Hintergrundbild. Wenn Sie abwechselnd auf die beiden Applets mit der Maus malen, werden Sie schnell den Unterschied sehen.

Der so erhaltene Typ des BufferedImage ist BufferedImage.TYPE_INT_RGB . Es unterstützt das Standardfarbmodell RGB, hat keine Transparenz und ist weiß.

Ein OffScreenImage über GraphicsConfiguration

Der zweite Weg ist zwar ein wenig aufwendiger, dafür aber braucht man sich keine Gedanken darüber machen, wann die Komponente sichtbar wird. Man erhält sofort ein von der Komponente unabhängiges Imageobjekt vom Typ BufferedImage.

Wieder ist der Typ des BufferedImage BufferedImage.TYPE_INT_RGB. Es unterstützt das Standardfarbmodell RGB, hat keine Transparenz, ist aber diesmal schwarz. Das folgende Applet arbeitet mit diesem Modell. Als Zeichenfarbe wurde zunächst weiß gewählt.


Ein OffScreenImage mit dem Konstruktor von BufferedImage

Am einfachsten ist es, direkt ein Objekt der Klasse BufferedImage zu erzeugen. Im Gegensatz zu Image ist BufferedImage eine reale Klasse. Wir verwenden den einfachsten Konstruktor:

Für imageType stehen eine Reihe von statischen Konstanten der Klasse zur Verfügung. Die gebräuchlichsten sind

die das vertraute RGB-Modell bezeichnen. Wählt man TYPE_INT_RGB, so erhält man ein Bild mit einem scharzen Hintergrund, bei TYPE_INT_ARGB ergibt sich ein Bild mit weißem Hintergrund.

Codierung

Zwei Fragen müssen noch geklärt werden. Wie zeichnet man in das Hintergrundbild und wie stellt man das Hintergrundbild dar. Hierzu ein kleiner Ausschnitt aus dem Applet:

// -------------------------- paint(Graphics g) -------------------------- \\
public void paint(Graphics g)
{
   if(offScreenImage!=null)
      g.drawImage(offScreenImage, 0, 0, this);
}

// -------------------------- paint(Image img) --------------------------- \\
public void paint(Image img)
{
   if (img!=null && mousePos != null )
   {
      Graphics g = img.getGraphics();
      g.setColor(paintColor);
      g.fillOval(mousePos.x-2, mousePos.y-2 , 4, 4 );
      g.dispose();
   }
   repaint();
}

Mit einer eigenen paint(Image img)-Methode wird hier in das Hintergrundbild gezeichnet. Diese Methode wiederum ruft mit Hilfe von repaint() das ererbte paint() auf. paint(Graphics g) zeichnet dann das Hintergrundbild mit Hilfe einer der drawImage()-Methoden. Ein EreignisHandler für Mausereignisse ruft dann die paint(Image img)-Methode und initiiert so den Vorgang.

top Back Next Up Home