Java Grafikprogrammierung
Willemers Informatik-Ecke
Java Swing Rahmenprogramm Ereignisse und Listener

Die Grafikausgabe erfolgt bei grafischen Oberflächen anders als das Malen auf eine Leinwand. Es wird nicht einmal ein Kunstwerk erstellt, sondern die Zeichnung wird dann aufgebaut und später wieder neu aufgebaut, wenn ein Ereignis des Systems dies anfordert.

In einer Fensterumgebung sind diese Ereignisse das erstmalige Erstellen des Fensters und das Sichtbarwerden des Fensters, wenn es zuvor von einem anderen Fenster verdeckt oder teilverdeckt war. Dieses Ereignis kann das Programm selbst auslösen, wenn eine Änderung des Programmstatus das Neuzeichnen erforderlich macht.

Die Methode paint

Die Ereignisse muss der Programmierer nicht selbst überwachen. Es reicht, wenn das Programm die Klasse JFrame oder JPanelerweitert und dabei die Methode paint überschreibt. Die Methode paint wird nämlich immer dann aufgerufen, wenn das System oder das Programm ein Neuzeichnen erforderlich macht.

Als Parameter erhält sie ein Objekt vom Typ Graphics2D, das aus Kompatibilitätsgründen Graphics heißt. Diese stellt quasi die Leinwand dar, deren Methoden die Zeichenprimitive sind.

Die folgende Methode paint wird eine grüne Ellipse in den Zeichenbereich des Rahmens schreiben. Die Koordinaten ergeben sich vom Nullpunkt links oben zur Breite und Höhe.

@Override
public void paint(Graphics gr) {
    super.paint(gr);
    setColor(Color.green);
    gr.fillOval(0,  0, this.getWidth(), this.getHeight());
}
Sowohl JFrame als auch JPanel bieten die Methode paint an. Allerdings benötigt JFrame einen Teil seines Inhalts zur Darstellung des Verschiebebalkens und des Fensterrahmens, so dass die Ellipse an allen Rändern beschnitten wird. Besonders oben ist das sehr deutlich sichtbar.

Es gibt zwei Lösungsansätze:

Wir werden beide Ansätze in den beiden folgenden Abschnitten durchspielen.

JFrame-Ränder ermitteln

Die Methode getInsets liefert eine Referenz auf Inset mit den Attributen left, right, top und bottom. In diesen befinden sich die Randbereiche, die nicht übermalt werden können.

Mit deren Hilfe lassen sich nun die Werte x, y, breite und hoehe berechnen, in denen die Grafik darstellt werden kann.

@Override
public void paint(Graphics gr) {
    super.paint(gr);
    gr.setColor(Color.green);
    Insets insets = getInsets();
    int x = insets.left;
    int y = insets.top;
    int breite   = getSize().width  - insets.left - insets.right;
    int hoehe   = getSize().height - insets.top  - insets.bottom;
    gr.fillOval(x, y, breite, hoehe);
}
Dieser Lösungsansatz funktioniert zwar, wird aber schon dann sehr kompliziert, wenn zu der Grafik auch noch ein Button im Fensterrahmen integriert werden soll.

JPanel oder JFrame

Die Methode paint wird nicht in der Erweiterung von JFrame erstellt. Stattdessen wird ein JPanel dem Arbeitsbereich des JFrames hinzugefügt.
import javax.swing.JFrame;

public class MeinJFrame extends JFrame {
    public MeinJFrame() {
        this.setSize(400, 300);
        this.setVisible(true);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.add(new MeinJPanel());
    }
    public static void main(String[] args) {
        new MeinJFrame();
    }
}
Nun wird in der Erweiterung von JPanel die Methode paint überschrieben.
import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JPanel;

public class MeinJPanel extends JPanel {
    @Override
    public void paint(Graphics g) {
        super.paint(gr);
        setColor(Color.green);
        gr.fillOval(0,  0, this.getWidth(), this.getHeight());
    }
}
Man kommt zum gleichen Ergebnis wie beim Einsatz von Inset. Allerdings erspart man sich das Errechnen der tatsächlichen Position, das bei Mausklicks wieder zurückgerechnet werden müsste.

Hinzu kommt, dass das JPanel selbst in ein Layout mit anderen Panels oder auch Kontrollelementen gelegt werden kann, ohne dass an den Grafikaufrufen etwas geändert werden muss. Langfristig ist dies also die sinnvollere Herangehensweise.

Aufruf von super.paint

In der Dokumentation wird empfohlen, in der überschriebenen paint-Methode zunächst super.paint aufzurufen. Es sorgt dafür, dass JFrame zunächst den Bildschirm putzt, bevor wir zeichnen.

Wird der Aufruf von super.paint weggelassen, verhalten sich Linux und Windows interessanterweise unterschiedlich. Während Windows den Hintergrund vor jedem paint löscht, lässt Linux den alten Zustand stehen. Wird super.paint aufgerufen, verhalten sich beide gleich, weil die Basisklasse den Bildschirm löscht.

paint oder paintComponent

Man kann das Zeichnen bei JPanel sogar noch etwas optimieren, indem man statt paint die Methode paintComponent überschreibt. Das ist effizienter, da paint neben paintComponent auch paintBorder und paintChildren aufruft, was weder notwendig noch sinnvoll ist, wenn man das eigene JPanel nicht überschreibt oder add aufruft. Ansonsten funktioniert paint in Einzelfall überraschungsfreier. Siehe dazu:

https://www.dreamincode.net/forums/blog/867/entry-2264-paint-vs-paintcomponent-a-resolution paintComponent kann nicht in JFrame überschrieben werden. Aber das wollten wir ja eh nicht mehr verwenden.

Es muss Farbe ins Heim

Über die Graphics-Referenz wird die Farbe für die nächsten Zeichenoperationen ausgewählt. Für den Umgang mit der Farbe gibt es zwei Aufrufe: Die Klasse Color stellt die Standardfarben als statische Konstanten zur Verfügung. Sie können mit den folgenden Bezeichnungen verwendet werden.

Color.black
Color.blue
Color.cyan
Color.darkGray
Color.gray
Color.green
Color.lightGray
Color.magenta
Color.orange
Color.pink
Color.red
Color.white
Color.yellow

Sollte keine dieser Farben passen, können RGB-Farben übergeben werden. Da setColor einen Parameter vom Typ Color erwartet, wird für die Übergabe per new ein Color erzeugt, dessen Konstruktor der RGB-Wert als Parameter übergeben wird.

setColor(new Color(0x00ff00));
Die ersten beiden Stellen sind der Rot-Anteil. Der ist 00. Es folgt der Grün-Anteil, der mit FF der höchstmögliche Wert ist. Als dritter steht der Blau-Anteil auf 00, sodass wir ein kräftiges Grün erwarten dürfen.

Grafikoperationen

Die Methoden, die mit draw beginnen, wie beispielsweise drawRect für das Zeichnen eines Rechtecks, zeichnen die Umrisse, während die Methoden, die mit fill beginnen, wie beispielsweise fillRect, das innere der Figur ausfüllen.

Linien, Rechtecke und Polygone

Kreise, Ellipsen, Ovale und Kreisausschnitte

Ein Kreis ist eigentlich nichts anderes als ein Oval, dessen Höhe und Breite gleich sind. Darum gibt es in Java auch keine eigene Funktion zum Zeichnen eines Kreises. Wenn man sehr pedantisch ist, ist das Oval von Java auch eigentlich eine Ellipse. Da die Ellipse aber ein Spezialfall des Ovals ist, ist die Bezeichnung Oval zumindest nicht falsch.

Texte malen

Um einen Text in eine Grafik zu malen, verwenden Sie die Methode drawString. Als Paramter übernimmt sie den String und die Koordinaten.

import javax.swing.JFrame;

public class MainZeichensatz extends JFrame {

    public static void main(String[] args) {
        new MainZeichensatz();
    }

    public MainZeichensatz() {
        setTitle("Zeichensatzinfo mit Java");
        setSize(400, 300);
        setVisible(true);
        add(new PanelZeichensatz());
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }
}
Und nun das JPanel, das die eigentliche Arbeit tut.
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import javax.swing.JPanel;

public class PanelZeichensatz extends JPanel {
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        int breite = getWidth();
        int hoehe = getHeight();
        g.clearRect(0, 0, breite - 1, hoehe - 1);
        g.setColor(Color.black);
        g.setFont(new Font("FreeSerif", Font.PLAIN, 16));
        FontMetrics fm = g.getFontMetrics();
        int zeile = 1;
        String meldung = "Bildschirm: " + breite + " x " + hoehe + " Pixel";
        g.drawString(meldung, 0, 0 + zeile * fm.getHeight());
        zeile++;
        meldung = "Zeilenhöhe - getHeight(): " + fm.getHeight();
        g.drawString(meldung, 0, 0 + zeile * fm.getHeight());
        zeile++;
        meldung = "Ascent - getAscent(): " + fm.getAscent();
        g.drawString(meldung, 0, 0 + zeile * fm.getHeight());
        zeile++;
        meldung = "Descent - getDescent(): " + fm.getDescent();
        g.drawString(meldung, 0, 0 + zeile * fm.getHeight());
        zeile++;
        meldung = "Durchschuss - getLeading(): " + fm.getLeading();
        g.drawString(meldung, 0, 0 + zeile * fm.getHeight());
        zeile++;
        meldung = "Breite von \"Willemer\" - stringWidth(\"Willemer\"): " + fm.stringWidth("Willemer");
        g.drawString(meldung, 0, 0 + zeile * fm.getHeight());
    }
}

Images

Bei vielen Spielen benötigt man Spielfiguren. Aber auch, wenn man Fotos in einem Programm darstellen will, wird man bei den Images landen. Ein Image wird aus einer Bilddatei gewonnen. Diese Bilddatei kann entweder vom Dateisystem geladen werden oder beispielsweise bei Spielen in der jar-Datei mit eingebunden werden.

Es muss das Package java.awt.Image eingebunden werden.

import java.awt.Image;

Zunächst wird ein Objekt der Klasse Image benötigt. Dieses muss natürlich mit einem Bild gefüllt werden. Im ersten Schritt wird also eine Bilddatei einem Objekt der Klasse Image zugeführt.

Die Applet-Klasse verfügt über die Methode getImage und kann eine Bilddatei direkt laden. Bei Applets kann eine URL für das Bild verwendet werden. So lädt die folgende Zeile ein Bild von einer Website.

Image img = getImage("http://www.seite.de/bild.jpg");

Falls die Bilder aber an der gleichen Stelle stehen wie die Anwendung, kann das Applet auch getCodeBase() einsetzen.

Image img = getImage(getCodeBase(),"meinbild.jpg");

Eine JFrame-Anwendung muss das Standard-Toolkit bemühen, um getImage aufzurufen.

Image img = Toolkit.getDefaultToolkit().getImage("meinbild.gif");

Nun verfügt die Anwendung über ein Image, das sie in einer Graphics-Umgebung mit der Methode drawImage darstellen kann.

paint(Graphics g)  {
   ...
   g.drawImage(img, xPos, yPos, this);

Dieser Aufruf wird das Image img darstellen. Die Position wird durch xPos und yPos bestimmt. Das this im vierten Parameter bezieht sich auf den JFrame und informiert das Programm über den Status des Bildes. Aber auch in einem Applet passt ein this.

Eigentlich ist dieser Parameter vom Typ ImageObserver. Diese abstrakte Klasse wird von der Klasse Component implementiert. Insofern ist this an dieser Stelle fast immer richtig.

Neben der Möglichkeit, Images anzuzeigen, können auch Rechteckbereiche des Bildschirms copyArea kopiert werden.

Clipping

Mit dem Clipping kann man den Bereich eingrenzen, in dem gezeichnet werden soll. Der Programmierer definiert einen Clipping-Bereich, außerhalb dessen jegliche Malerei unterbunden wird.

Modernisierung: Graphics2D

Die Grafikfähigkeiten in Java sind durch die 2D-Bibliothek erweitert worden. Die alte Schnittstelle wurde erhalten, so dass alle bisherigen Methoden auch bei 2D verwendbar sind. Sogar die Methode paint wurde beibehalten. Allerdings verbirgt sich nun hinter dem Parameter Graphics ein Graphics2D-Objekt, dass man innerhalb der ersten Zeile einfach per casting verwenden kann. Wenn man die ersten beiden Zeilen der Methode paint auf diese Weise anpasst, kann man den Rest der Methode einfach belassen.

public void paint(Graphics pg) {
    Graphics2D g = (Graphics2D) pg;

Die bisherigen Methoden arbeiten nach wie vor. Aber nun können die Ergänzungen von 2D hinzugefügt werden.

Grafische Objekte

Das Konzept ändert sich dahin, dass bei 2D grafische Objekte eingeführt werden. Linien beispielsweise sind nun nicht mehr nur die Koordinaten, die die drawLine-Methode als Parameter erhält, sondern eigenständige Objekte, die sich von der Klasse Line2D ableiten. Ein Linienobjekt wird angelegt, erhält beim Konstruktoraufruf seine Koordinaten und wird dann der überladenen Methode draw oder fill übergeben.

Durch diese Veränderung des Konzepts können nun die Linien direkt nach dem Schnittpunkt mit einer anderen Linie gefragt werden.

Die Grundprimitive sind abstrakte Klassen.

Die implementierten Klassen der Klasse Line2D lauten beispielsweise Line2D.Double und Line2D.Float.

Die Positionen werden den Objekten über den Konstruktor mitgegeben. Dann kann das Objekt einfach der Methode draw oder fill übergeben werden.

Die Positionen werden nicht mehr als ganzzahlige Werte behandelt, sondern als float oder double. Der Hintergrund ist, dass so eine Abstraktion von der tatsächlichen Hardware erreicht wird. Ein Bildschirm hat eine Auflösung von vielleicht 70 dpi. Aber auf Papier ist diese Auflösung wesentlich höher. Wird eine diagonale Linie auf die Bildschirmauflösung reduziert, hat sie bei der Papierausgabe bereits sichtbare Ecken.

Paint und Stroke

Wie bisher kann die Methode setColor verwendet werden, um die Farbe festzulegen. Neu ist die Möglichkeit Paint zu verwenden.

g.setPaint(new GradientPaint(0, 0, Color.blue, 50, 25, Color.green, true));

Mit Stroke kann die Dicke eines Striches verändert werden.

g.setStroke(new BasicStroke(5));

Die Strichstärke ist nun 5.


Java Swing Rahmenprogramm Ereignisse und Listener