Java Grafikprogrammierung
Willemers Informatik-Ecke
Die Grafikausgabe erfolgt nicht, wie das Malen auf eine Leinwand. Eine Swing-Anwendung wird seine Grafik dann erzeugen, wenn es vom System dazu aufgefordert wird. Der Hintergrund ist, dass in einer Fensterumgebung eine Anwendung leicht mal von einem anderen Fenster überdeckt wird. Wird es dann wieder nach vorn geklickt, muss es den Fensterinhalt wieder aufbauen.

Die JFrame-Klasse enthält eine Methode paint, die aufgerufen wird, wenn der Fensterinhalt neu gestaltet werden muss. Eine Applikation, die selbst grafisch tätig wird, überschreibt diese Methode.

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

Beispiel:

Das folgende Beispiel malt ein gelbes Oval, das sich in der Größe am Fenster orientiert. Innerhalb des Ovals malt die Anwendung einen Text, indem die Größe des Zeichenbereichs in Pixeln angegeben wird.

In main wird das eine Instanz der eigenen Klasse erzeugt. Daraufhin erledigt der Konstruktor die Arbeiten am Rahmenfenster. Er setzt die CloseOperation, setzt den Titel und die Größe und macht es schließlich sichtbar. Das wirklich Spannende passiert in der überschriebenen Methode paint.

import javax.swing.*;
import java.awt.*;

public class MyGraphics extends JFrame {

    public static void main(String[] args) {
        MyGraphics fenster = new MyGraphics();
    }

    public MyGraphics() {
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setTitle("Zeichnen mit Java");
        setSize(400, 300);
        setVisible(true);
    }

    @Override // @Override ermöglicht dem Compiler die Kontrolle
    public void paint(Graphics g) {
        super.paint(g);
        Insets insets = getInsets();
        int originX = insets.left;
        int originY = insets.top;
        int breite   = getSize().width  - insets.left - insets.right;
        int hoehe   = getSize().height - insets.top  - insets.bottom;
        g.setColor(Color.yellow);
        g.fillOval(originX, originY, breite-1, hoehe-1);
        g.setColor(Color.black);
        String meldung  = "" + breite + " x " + hoehe + " Pixel";
        g.drawString(meldung, breite/2, hoehe/2);
    }
}

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

JPanel oder JFrame

Mit getInsets wird der Arbeitsbereich des JFrame ermittelt. Ansonsten bemalt das Programm den Rahmen, was sich JFrame allerdings nicht gefallen lassen würde. Die Ellipse würde abgeschnitten.

Das führt aber dazu, das die paint-Methode ständig mit eigenen Koordinaten rechnen muss. Einfacher und sauberer funktioniert das, indem man ein JPanel in den Arbeitsbereich des JFrames steckt.

import javax.swing.JFrame;

public class MainFrame extends JFrame {

    public static void main(String[] args) {
        // Main fenster = new Main();
        // Da wir die Referenz fenster nicht mehr brauchen, reicht auch:
        new MainFrame();
    }

    public MainFrame() {
        this.setSize(400, 300);
        // Mit add wird unser eigenes JPanel eingefügt
        this.add(new MyPanel());
        this.setVisible(true);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }
}
Nun wird in unserer Erweiterung von JPanel die Methode paint überschrieben.
import java.awt.Color;
import java.awt.Graphics;

import javax.swing.JPanel;

public class MyPanel extends JPanel {
    @Override
    public void paint(Graphics g) {
        super.paint(g);
        int breite   = getWidth();
        int hoehe   = getHeight();
        g.setColor(Color.yellow);
        g.fillOval(0, 0, breite, hoehe);
        g.setColor(Color.black);
        String meldung  = "" + breite + " x " + hoehe + " Pixel";
        g.drawString(meldung, breite/2, hoehe/2);
    }
}
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

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 dies nicht passen, können die RGB-Farben dem Konstruktor von Color übergeben werden.

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.

Grafische Fähigkeiten

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

Man kann nicht überall malen. Beispielsweise muss man einen Rand freihalten oder möchte verhindern, dass in den gerade mühsam gezeichneten Bereich hineingemalt wird. Der Programmierer könnte natürlich ausrechnen, ob sich die gemalte Figur vielleicht außerhalb des Zielgebietes ausdehnt. Aber es geht auch einfacher, indem ein Clipping-Bereich definiert wird, 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 so erhalten, 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. Aber nun können die Ergänzungen von 2D hinzugefügt werden.

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 heißen Line2D, Point2D, Rectangle2D, RoundRectangle2D, QuadCurve2D, Ellipse2D, Arc2D und sind abstrakte Klassen. Die implementierten Klassen der Klasse Line2D lauten 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.

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.