Advanced
Java
Services
|
Graphikprogrammierung |
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.
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:
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() :
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();
}
}
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.