Zeichnen in Swing - Tutorial

Wildcard

Top Contributor
Zeichnen in Swing

Dieses Tutorial wird sich mit dem generellen Vorgehen beim Zeichnen in Swing beschäftigen.
Zum Verständnis ist lediglich Java Grundwissen und erste Erfahrungen mit den Swing Komponenten (JFrame, JPanel,...) nötig.
Ziel des Tutorials ist die grundlegenden Mechanismen zu erläutern.
Erklärt werden diese an der beispielhaften Implementierung einer kleinen Applikation die geometrische Figuren in verschiedenen Farben zeichnet.
Während der Implementierung werden bewusst einige Fehler gemacht um auf die Fallen des Swing Toolkit hinzuweisen, es ist also sehr zu empfehlen das Tutorial zu ende zu lesen ;)

Zuerst brauchen wir eine Klasse die von JComponent erbt auf der wir später zeichnen.
[JAPI]JComponent[/JAPI] ist die Basisklasse aller Swing-Komponenten und hat nicht wesentlich mehr Funktionalität als dem Programmier eine Zeichenfläche zur Verfügung zu stellen.
Da dies unser erster Versuch ist, und wir naiv an die Sache herangehen nennen wir sie
NaivePaintingComponent

Wir möchten Geometrische Figuren (in Java Shapes) in verschiedenen Farben zeichnen, daher wird ein Member vom Typ [JAPI]Color[/JAPI] und ein Member vom Typ [JAPI]Shape[/JAPI] benötigt.

Um diese Member von außen später elegant setzen zu können fügen wir setter ein.



Falscher Ansatz:
Weiterhin sollte es eine Möglichkeit geben unserer Komponente zu sagen das sie jetzt zeichnen soll.
Nennen wir die Methode drawNow().
Sehen wir uns mal das Ergebnis an:

Java:
class NaivePaintingComponent extends JComponent
{
    private Shape shape;
    private Color c;
   
    public void drawNow() {
       
    }
   
    public void setColor(Color c) {
        this.c = c;
    }

    public void setShape(Shape shape) {
        this.shape = shape;
    }
   
}

Was noch fehlt ist das wichtigste: drawNow()

In Swing (sowie auch in AWT) wird immer mit einem Graphics Objekt gezeichnet.
Dieses Graphics Objekt hält grob gesagt eine vom Betriebssystem erhaltene Grafikresource mit der gezeichnet werden kann. Man kann sich ein Graphics Objekt also wie einen Pinsel vorstellen.
Was Swing von AWT unterscheidet ist, das man bei Swing von einem Lightweight Toolkit spricht.
In AWT bekommt jede Komponente ([JAPI]Label[/JAPI], [JAPI]Panel[/JAPI], [JAPI]TextField[/JAPI]) eine solche Betriebssystem Grafikresource mit der sie sich selbst zeichnen, dieses System nennt sich Heavyweight.
Swing geht einen flexibleren Weg und hat nur wenige Heavyweight-Komponenten ([JAPI]JFrame[/JAPI], [JAPI]JDialog[/JAPI], [JAPI]JWindow[/JAPI]). Man spricht dabei von Heavyweight Containern.
Diese Heavyweight Containern geben dann ihre eigene Grafikresource (gekapselt in Graphics Objekten) an ihre Kinder weiter.
Das führt dazu das eine Anwendung die einen nur JFrame benutzt auch nur eine Grafikresource vom Betriebssystem braucht, der Rest wird intern erledigt.
Somit ist nun klar das wir ein Graphics Objekt zum Zeichnen benötigen.
Der aufmerksame API Leser wird über eine getGraphics() Methode in jeder von [JAPI]JComponent[/JAPI] abgeleiteten Klasse stolpern die uns ein Graphics Objekt liefert.
Zu beachten ist allerdings das diese Methode durch die Lightweight Architektur nur dann ein Objekt zurück liefern kann wenn die Komponente bereits in einem Heavyweight Container liegt der eine solche Resource erhalten hat.
Wenn wir uns die Klasse [JAPI]Graphics[/JAPI] ansehen entdecken wir einige hilfreiche Methoden drawRect, drawOval, ... entdecken.
Für dieses Beispiel wurde allerdings mit den Shapes ein stärker Objekt orientierter Ansatz gewählt, daher nützen uns diese Methoden nicht viel.
Der Trick ist, das getGraphics in Swing Anwendungen eigentlich ein [JAPI]Graphics2D[/JAPI] Objekt liefert (das deutlich mehr Möglichkeiten als ein einfaches Graphics Objekt bietet) welches Graphics erweitert.

In Graphics2D ist nun eine Methode draw zu finden der man ein Shape übergeben kann.
Der resultierende Code könnte nun so aussehen:
Java:
//das Graphics Objekt in ein Graphics2D Objekt casten
Graphics2D g2d = (Graphics2D) getGraphics();
//die gewünschte Farbe setzen
g2d.setColor(c);
//die gewählte Figur zeichnen
g2d.draw(shape);

Der komplette Code:
Java:
class NaivePaintingComponent extends JComponent
{
    private Shape shape;
    private Color c;
   
    public void drawNow() {
        Graphics2D g2d = (Graphics2D) getGraphics();
        g2d.setColor(c);
        g2d.draw(shape);
       
    }
   
    public void setColor(Color c) {
        this.c = c;
    }

    public void setShape(Shape shape) {
        this.shape = shape;
    }
}

Als nächstes benötigen wir einen JFrame auf den wir unsere neue Komponente setzen können.

Java:
public class PaintInSwing
{
    //unsere frisch gebackene Komponente
    private NaivePaintingComponent paintingComponent = new NaivePaintingComponent();
   
    /**
     * Im Konstruktor wird die übliche Arbeit erledigt um den JFrame zu öffnen
     * und die Komponenten zu initialisieren
     */
    public PaintInSwing() {
        //einen JFrame erzeugen
        JFrame frame = new JFrame("Selbst Zeichnen mit Swing");
        //ein hübsches Layout setzen
        frame.setLayout(new BorderLayout());
        //dafür sorgen das das Programm beendet wird wenn man das 'X' anklickt
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        //eine JComponent hat keine Ahnung davon was man auf ihr zeichnen möchte.
        //Der LayoutManager hat also keine Möglichkeit die passende Größe für unser
        //Objekt festzustellen und würde von (0,0) ausgehen.
        //Daher helfen wir etwas nach und setzen die gewünschte Größe händisch
        paintingComponent.setPreferredSize(new Dimension(300,300));
        //unsere Komponente wird mittig im JFrame platziert
        frame.add(paintingComponent,BorderLayout.CENTER);
       
        //in den unteren Bereich des Frames packen wir einige
        //Steuerelemente die wir der Übersicht wegen in einer
        //eigenen Methode erstellen und initialisieren
        frame.add(createControls(),BorderLayout.SOUTH);
       
        //der Frame enthält nun alle benötigten Komponenten
        //und kann nun seine minimale Größe berechnen
        frame.pack();
        //und noch den Frame sichtbar machen und zentrieren
        frame.setVisible(true);
        frame.setLocationRelativeTo(null);
    }
    /**
     * hier wird ein JPanel erzeugt auf das wir alle
     * Steuerelemente legen
     * @return ein JPanel das alle Steuerelemente enthält
     */
    private Component createControls() {
        //ein einfaches FlowLayout soll für unser Beispiel genügen
        JPanel panel = new JPanel(new FlowLayout());
       
        //Ein Array mit den 3 Grundfarben wird erstellt und in
        //eine Combobox übergeben.
        //damit können wir später die Farbe der Zeichnung bestimmen
        Object[] colors = {Color.RED,Color.BLUE,Color.GREEN};
        final JComboBox colorBox = new JComboBox(colors);
        panel.add(colorBox);
       
        //Als nächstes ein Array mit Shapes (Figuren).
        //Der Einfachheit halber setzen wir die Position und Größe
        //für alle Objekte fest.
        //Die toString Methode wird hier überschrieben damit die Auswahl
        //in der Combobox besser lesbar ist.
        Object[] shapes = {
                new Ellipse2D.Float(10f,10f,100f,100f) {public String toString() {return "Ellipse";}},
                new RoundRectangle2D.Float(10f,10f,100f,100f,20f,20f) {public String toString() {return "Abgerundetes Rechteck";}},
                new Rectangle2D.Float(10f,10f,100f,100f) {public String toString() {return "Rechteck";}}
                };
        //Mit der ComboBox können wir bestimmen welche Figur gezeichnet werden soll
        final JComboBox shapeBox = new JComboBox(shapes);
        panel.add(shapeBox);
       
        //als letztes noch ein Button mit dem die gewählte Figur gezeichnet wird
        JButton paintNow = new JButton("Zeichnen");
        panel.add(paintNow);
        paintNow.addActionListener(new ActionListener() {
       
            public void actionPerformed(ActionEvent e) {
                //wir teilen unserer Zeichenkomponente die gewählte Farbe mit
                paintingComponent.setColor((Color)colorBox.getSelectedItem());
                //wir teilen unserer Zeichenkomponente mit welche Figur wir haben möchten
                paintingComponent.setShape((Shape)shapeBox.getSelectedItem());
                //jetzt soll gezeichnet werden
                paintingComponent.drawNow();
            }
       
        });
       
        return panel;
    }
    public static void main(String[] args)
    {
        new PaintInSwing();
    }
}

kompilieren, ausführen, testen

Das sieht doch schon ganz gut aus. Alles wird wie gewünscht gezeichnet.
Ein paar Probleme bleiben allerdings.
Zeichnet man zum Beispiel ein Rechteck und anschließend eine Ellipse ist das Rechteck immer noch zu sehen.
Der Grund dafür ist, das die alte Zeichnung nicht gelöscht wird.
Wir müssen also dafür sorgen das vor jedem Zeichnen die Fläche wieder geleert wird.
Dafür kann beispielsweise die Methode clearRect eines Graphics Objekts verwendet werden.
Java:
public void drawNow() {
    Graphics2D g2d = (Graphics2D) getGraphics();
    g2d.clearRect(0, 0, getWidth(), getHeight());
    g2d.setColor(c);
    g2d.draw(shape);
}
Auf den ersten Blick scheint nun alles zu funktionieren.
Leider nur auf den ersten Blick.
Versucht man beispielsweise die Größe des Fenster zu verändern, das Fenster zu minimieren, oder schiebt ein anderes Fenster über unsere Zeichnung, so fällt auf das sie verschwindet.

Der Grund dafür ist ganz einfach der, das nur der Windowmanager des Betriebssystem weiß wann wir unser Bild neu zeichnen müssen. Wir selbst haben darauf keinen Einfluss.
Da Java plattformunabhängig ist sind wir zu weit vom Betriebssystem entfernt um diese Entscheidung selbst treffen zu können.
Hier kommt das AWT/Swing Toolkit zum Einsatz. Das Betriebssystem teilt dem Toolkit mit wann welcher Bereich der Anwendung neu gezeichnet werden muss.
Aus eben diesem Grund ist der scheinbar offensichtlich Weg über getGraphics falsch.



Richtiger Ansatz:
Anstatt sich aktiv ein Graphics Objekt zu holen und anzufangen zu zeichnen muss der Swing Programmierer passiv zeichnen, also darauf warten das AWT/Swing unsere Objekte zum zeichnen auffordert, denn nur AWT/Swing wissen wann das nötig ist.
Dies ist ein sehr häufiger Trugschluss, weshalb hier so detailliert auf die Problematik eingegangen wurde.
Bleibt die Frage wie man es denn richtig macht.
Jede JComponent verfügt über eine Methode paintComponent(Graphics g).
Wie man sieht wird hier das benötigte Graphics Objekt direkt als Parameter übergeben.
Diese Methode ist nicht dazu da das sie von einem Programmierer aufgerufen wird, stattdessen übernimmt diese Aufgabe das Toolkit.
Vereinfacht gesagt:
Wann immer AWT/Swing der Meinung ist das ein Neu zeichnen erforderlich ist wird diese Methode aufgerufen.
Bemerkenswert an dieser Tatsache ist, das wir also offensichtlich keine Kontrolle darüber haben wann und wie oft diese Methode tatsächlich aufgerufen wird.
Aus diesem Grund ist es sehr wichtig das keinerlei Logik in einer paintComponent hinterlegt werden darf.
Weiterhin kann es passieren das die Methode sehr oft aufgerufen wird, daher gilt es unbedingt teure Operationen wie Objekterzeugung zu vermeiden.
Das daraus resultierende Fazit lautet:
In paintComponent wird nur gezeichnet

Setzen wir diese Erkenntnis nun in die Tat um und überschreiben die paintComponent Methode in unserer Klasse:

Java:
class PaintingComponent extends JComponent
{
    private Shape shape;
    private Color c;
   
    @Override
    protected void paintComponent(Graphics g) {
        //dient dazu den Hintergrund zu säubern wie wir es vorher bereits mit
        //clearRect getan haben.
        super.paintComponent(g);
        //AWT/Swing bestimmt wann paintComponent aufgerufen wird, wir müssen
        //nun also überprüfen ob shape und color noch gar nicht gesetzt wurden
        if(shape!=null && c!=null)
        {
            Graphics2D g2d = (Graphics2D)g;
            g2d.setColor(c);
            g2d.draw(shape);
        }
    }
   
    public void setColor(Color c) {
        this.c = c;
    }

    public void setShape(Shape shape) {
        this.shape = shape;
    }
}

Da wir nun nicht mehr aktiv Zeichnen können muss eine Methode her mit der wir Swing mitteilen können das eine Komponente neu gezeichnet werden muss.
In diesem Beispiel ist das der Fall wenn sich auf den Knopf gedrückt wurde.
Für diese Notwendigkeit verfügt jede JComponent über eine repaint() Methode.
repaint ist weniger als Befehl sondern mehr als freundliche Bitte zu verstehen.
Damit wird dem Toolkit mitgeteilt das die Komponente bei nächster Gelegenheit aktualisiert werden möchte.
Bauen wir das in unseren [JAPI]ActionListener[/JAPI] ein:
Java:
public void actionPerformed(ActionEvent e) {
    //wir teilen unserer Zeichenkomponente die gewählte Farbe mit
    paintingComponent.setColor((Color)colorBox.getSelectedItem());
    //wir teilen unserer Zeichenkomponente mit welche Figur wir haben möchten
    paintingComponent.setShape((Shape)shapeBox.getSelectedItem());
    //jetzt soll gezeichnet werden
    paintingComponent.repaint();
}

Nun sollte alles korrekt funktionieren.
Zum Abschluß nochmal den kompletten Code:
Java:
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;

import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;


public class PaintInSwing
{
    //unsere frisch gebackene Komponente
    private PaintingComponent paintingComponent = new PaintingComponent();
   
    /**
     * Im Konstrukor wird die übliche Arbeit erledigt um den JFrame zu öffnen
     * und die Komponenten zu initialisieren
     */
    public PaintInSwing() {
        //einen JFrame erzeugen
        JFrame frame = new JFrame("Selbst Zeichnen mit Swing");
        //ein hübsches Layout setzen
        frame.setLayout(new BorderLayout());
        //dafür sorgen das das Programm beendet wird wenn man das 'X' anklickt
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        //eine JComponent hat keine Ahnung davon was man auf ihr zeichnen möchte.
        //Der LayoutManager hat also keine Möglichkeit die passende Größe für unser
        //Objekt festzustellen und würde von (0,0) ausgehen.
        //Daher helfen wir etwas nach und setzen die gewünschte Größe händisch
        paintingComponent.setPreferredSize(new Dimension(300,300));
        //unsere Komponente wird mittig im JFrame plaziert
        frame.add(paintingComponent,BorderLayout.CENTER);
       
        //in den unteren Bereich des Frames packen wir einige
        //Steuerelemente die wir der Übersicht wegen in einer
        //eigenen Methode erstellen und initialisieren
        frame.add(createControls(),BorderLayout.SOUTH);
       
        //der Frame enthält nun alle benötigten Komponenten
        //und kann nun seine minimale Größe berechnen
        frame.pack();
        //und noch den Frame sichtbar machen und zentrieren
        frame.setVisible(true);
        frame.setLocationRelativeTo(null);
    }
    /**
     * hier wird ein JPanel erzeugt auf das wir alle
     * Steuerelemente legen
     * @return ein JPanel das alle Steuerelemente enthält
     */
    private Component createControls() {
        //ein einfaches FlowLayout soll für unser Beispiel genügen
        JPanel panel = new JPanel(new FlowLayout());
       
        //Ein Array mit den 3 Grundfarben wird erstellt und in
        //eine Combobox übergeben.
        //damit können wir später die Farbe der Zeichnung bestimmen
        Object[] colors = {Color.RED,Color.BLUE,Color.GREEN};
        final JComboBox colorBox = new JComboBox(colors);
        panel.add(colorBox);
       
        //Als nächstes ein Array mit Shapes (Figuren).
        //Der Einfachheit halber setzen wir die Position und Größe
        //für alle Objekte fest.
        //Die toString Methode wird hier überschrieben damit die Auswahl
        //in der Combobox besser lesbar ist.
        Object[] shapes = {
                new Ellipse2D.Float(10f,10f,100f,100f) {public String toString() {return "Ellipse";}},
                new RoundRectangle2D.Float(10f,10f,100f,100f,20f,20f) {public String toString() {return "Abgerundetes Rechteck";}},
                new Rectangle2D.Float(10f,10f,100f,100f) {public String toString() {return "Rechteck";}}
                };
        //Mit der ComboBox können wir bestimmen welche Figur gezeichnet werden soll
        final JComboBox shapeBox = new JComboBox(shapes);
        panel.add(shapeBox);
       
        //als letztes noch ein Button mit dem die gewählte Figur gezeichnet wird
        JButton paintNow = new JButton("Zeichnen");
        panel.add(paintNow);
        paintNow.addActionListener(new ActionListener() {
       
            public void actionPerformed(ActionEvent e) {
                //wir teilen unserer Zeichenkomponente die gewählte Farbe mit
                paintingComponent.setColor((Color)colorBox.getSelectedItem());
                //wir teilen unserer Zeichenkomponente mit welche Figur wir haben möchten
                paintingComponent.setShape((Shape)shapeBox.getSelectedItem());
                //jetzt soll gezeichnet werden
                paintingComponent.repaint();
            }
       
        });
       
        return panel;
    }
    public static void main(String[] args)
    {
        new PaintInSwing();
    }
}

class PaintingComponent extends JComponent
{
    private Shape shape;
    private Color c;
   
    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        if(shape!=null && c!=null)
        {
            Graphics2D g2d = (Graphics2D)g;
            g2d.setColor(c);
            g2d.draw(shape);
        }
    }
   
    public void setColor(Color c) {
        this.c = c;
    }

    public void setShape(Shape shape) {
        this.shape = shape;
    }
}
 
Zuletzt bearbeitet von einem Moderator:

Oben