Für einfache graphische Probleme bietet sich in Java das Turtle-Objekt an. Vor allem im Anfangsunterricht kann mit diesem Objekt sehr einfach gezeichnet werden.

Für komplexere grafische Ausgaben in Java stehen verschiedene Klassen zur Verfügung. Verwendet man in einer GUI-Anwendung AWT-Klassen sollte als Zeichenfläche die Klasse Canvas verwendet werden. Verwendet man SWING-Klassen sollte man für grafische Ausgaben auf die Klasse JComponent (als Oberklasse aller SWING-Klassen) oder einfacher auf die die Graphic-Komponente der Klasse JPanel zurückgreifen.

Die konkrete Umsetzung ist in wenigen Punkten von der Entwicklungsumgebung abhängig. Im JavaEditor, der gerade im Anfangsunterricht verwendet wird, wird standardmäßig mit dem null-Layout gearbeitet – einzelne SWING-Elemente können so einfach platziert werden und (über den Quelltext) eingebundene SWING-Elemente können relativ leicht ergänzt werden. In professionellen Entwicklungsumgebungen, wie z.B. eclipse oder intelliJ IDEA, greift man in der Regel auf Layoutmanager zurück. Für beide Varianten sind im Folgenden Beispiele angegeben …

Zeichnen mit JPanel (SWING; JavaEditor)

Das eigentliche Zeichnen (auf einem JPanel) übernimmt die Methode paintComponent(). Diese Methode wird immer dann vom Betriebssystem / der Java Virtual Machine aufgerufen, wenn sich das Windowsfenster neu zeichnen muss, wenn es z. B. von einem anderen Fenster verdeckt war. Für das Betriebssystem stellt der gesamte Bildschirm eine einzige Grafikfläche dar, für uns sieht es jedoch so als, als gebe es Fenster, Buttons usw.. Wenn dann also die JVM die paintComponent() auruft, wird dort ein Graphics-Objekt übergeben, das praktisch den Bereich auf dem Bildschirm repräsentiert, wo hin gezeichnet werden kann.

In der Originalklasse JPanel ist die Methode paintComponent() allerdings leer, d. h. soll etwas gezeichnet werden, muss die Methode paintComponent() überschrieben werden. Alles, was gezeichnet werden soll MUSS in dieser Methode stehen. Es genügt auch nicht nur die Elemente zu zeichnen, die evtl. hinzukommen … paintComponent() merkt sich nicht, was auf ihr gezeichnet wurde.

Da die Methode paintComponent() sehr häufig aufgerufen wird, ist es zwingend notwendig, innerhalb dieser Methode möglichst wenig Anweisungen abarbeiten zu lassen: In paintComponent() wird nur gezeichnet. Berechnungen u. ä. sollten hier nicht vorgenommen werden.

Einfaches Beispiel

Das Zeichnen auf einem JPanel soll an einem einfachen Beispiel erläutert werden: auf der Zeichenfläche soll ein Rechteck gezeichnet werden.

Im Java-Editor kann ein einfacher JFrame (PaintSWING) erzeugt werden, der lediglich ein Objekt JPanel (zeichenflaeche) enthält. Das Ergebnis könnte so aussehen:

 import java.awt.*;
 import java.awt.event.*;
 import javax.swing.*;
 import javax.swing.event.*;
 
 public class PaintSWING extends JFrame {
   // Anfang Attribute
   private JPanel zeichenflaeche = new JPanel(null);
  
   // Ende Attribute
 
   public PaintSWING(String title) {
     // Frame-Initialisierung
     super(title);
     setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
     int frameWidth = 520;
     int frameHeight = 543;
     setSize(frameWidth, frameHeight);
     Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
     int x = (d.width - getSize().width) / 2;
     int y = (d.height - getSize().height) / 2;
     setLocation(x, y);
     Container cp = getContentPane();
     cp.setLayout(null);
  
     // Anfang Komponenten
     zeichenflaeche.setBounds(7, 2, 500, 500);
     zeichenflaeche.setBackground(Color.WHITE);
     cp.add(zeichenflaeche);
     // Ende Komponenten
 
     setResizable(false);
     setVisible(true);
   }
 
   // Anfang Methoden
   
   // Ende Methoden
 
   public static void main(String[] args) {
     new PaintSWING("Zeichnen in SWING");
   }
 }

Da auf unserem JPanel allerdings nur durch die Originalmethode paintComponent() gezeichnet wird, die im Original leer ist, müssen wir uns eine eigene Klasse ableiten, in der wir dann unsere Elemente zeichnen können:

 import java.awt.*;
 import java.awt.event.*;
 import javax.swing.*;
 import javax.swing.event.*;
 
 public class MyCanvasPanel extends JPanel {
 
   public MyCanvasPanel() {              // Construktor der Klasse
   }
   
   @Override
   public void paintComponent(Graphics g) {
     super.paintComponent(g);            
     // meine Zeichnung
     g.fillRect(100,100,150,250);
   }
 }

In dieser Klasse MyCanvasPanel überschreiben wir die Originalemethode paintComponent(). Dabei weisen wir zuerst mit super.paintComponent(g); an, dass alle notwendigen Aktionen der Vorfahrklasse ausgeführt werden. Danach zeichnen wir unser Rechteck.

Nun müssen wir noch die Vereinbarung unseres JPanel in PaintSWING abändern, da unser Objekt zeichenflaeche nicht von JPanel sondern von MyCanvasPanel abgeleitet wird:

private MyCanvasPanel zeichenflaeche = new MyCanvasPanel();

Wenn wir dieses Programm starten (Die Klasse MyCanvasPanel sollte im gleichen Verzeichnis stehen wie PaintSWING!!!) sehen wir, dass (obwohl wir eigentlich nie zeichnen) ein Rechteck gezeichnet wird. Beim Aufbau des Fensters wird automatisch die Methode paintComponent aufgerufen und daher unser Rechteck gezeichnet. Ein solcher Aufruf erfolgt immer, wenn ein Teil der Zeichenfläche neu gezeichnet werden muss (z. B. beim Vergrößern / Verkleinern des Fensters, beim Überblenden usw.). Mit der Methode repaint() kann ein Neuzeichnen erzwungen werden, repaint() ruft zum eigentlichen Zeichnen jedoch ebenfalls die Methode paintComponent auf.

Zeichnen mit Datenübergabe an MyCanvasPanel

In den meisten Programmen wird jedoch mehr verlangt als ein einfaches Rechteck zu zeichnen und dies auch nur beim (normalen) Neuzeichnen des Panels.

Wollen wir z. B. eine Feld mit 20 x 20 Einzelfeldern zeichnen, die jeweils verschiedene Farben haben können, müssen wir im Hauptprogramm die Möglichkeit haben, diese Felder individuell zu belegen, z. B. durch einen byte-Wert, der ausgewertet werden kann. In unserer MyCanvasPanel-Klasse müssen wir jedoch auf diese Daten zugreifen können um u. a. die Farben jeweils zu setzen.

Um dieses Vorhaben umzusetzen müssen wir im Hauptprogramm die notwendige Datenstruktur vereinbaren:

private byte[][] myColorField = new byte[20][20];

In der Klasse MyCanvasPanel müssen wir ebenfalls ein Attribut für unser Feld vereinbaren – im Konstruktor weisen wir mit this.feld = feld das Feld aus dem Hauptprogramm der Variablen Feld zu, d. h. die Attribute myColorField im Hauptprogramm und feld in MyCanvasPanel greifen auf den selben Speicherplatz zu!!!

public class MyCanvasPanel extends JPanel {
 
   private byte[][] feld;
 
   public MyCanvasPanel(byte[][] feld) {
       this.feld = feld;
   }

Damit der Konstruktor des Panel auch korrekt aufgerufen wird, müssen wir die entsprechende Zeile im Hauptprogramm abändern:

private MyCanvasPanel zeichenflaeche = new MyCanvasPanel(myColorField);

Nun können wir in der Methode paintComponent() der Klasse MyCanvasPanel auf das Feld zugreifen und entsprechend zeichnen:

public void paintComponent(Graphics g) {
    super.paintComponent(g);
  
    for (int x=0; x<19; x++) {
    for (int y=0; y<19; y++) {
      switch (feld[y][x]) {
        case 1 :
          g.setColor(new Color(0,0,0));  // schwarz
          break;
        case 2 :
        ...
      }
    }
  }

In der geschachtelten Schleife wird nun für jedes der 20×20-Felder der gespeicherte Wert ausgelesen, eine entsprechende Zeichenfarbe gewählt und das Feld gezeichnet.

Aktiver Aufruf zum Zeichnen

Bisher haben wir uns darauf verlassen, dass unsere Zeichnung (irgendwann) automatisch durch einen Aufruf von paintComponent() gezeichnet wird. Dies kann aber unter Umständen länger dauern als wir wollen, z. B. sollte eine Änderung der Farbe eines Feldes sofort angezeigt werden …

Mit der Methode repaint() haben wir die Möglichkeit an jeder beliebigen Stelle im Hauptprogramm ein Neuzeichnen von MyCanvasPanel (zum nächstmöglichen Zeitpunkt) zu erzwingen:

zeichenflaeche.repaint();

Zu langsam?

… natürlich zu langsam. Da wir alle Abfragen und Zeichenanweisungen in der Methode paintComponent durchführen, diese aber gerade in wichtigen Situationen sehr häufig hintereinander aufgerufen wird, kann es zu einem „Stau“ bei der Abarbeitung kommen …

Eine Lösung für dieses Problem kann die Verwendung eines BufferedImage sein, diese Variante findet auch in Spielen häufig Verwendung. Für unsere – im Unterricht anfallenden Probleme – dürfte die oben beschriebene Variante allerdings mehr als ausreichend sein …

Zeichnen mit JPanel (SWING; intelliJ IDEA)

Das eigentliche Zeichnen wird hier identisch realisiert, jedoch muss die grafikfähige Komponente etwas anders eingebunden werden. Professionelle Entwicklungsumgebungen nutzen Layoutmanager (das null-Layout wird hier nicht gerne gesehen und ist z. B. im GUI-Editor in intelliJ IDEA nicht anwählbar). Um GUIs mit Hilfe von Layouts zu erstellen, sollte man sich die verschiedenen Möglichkeiten aneignen (z. B. HIER oder HIER).

Als Zeichenklasse MyCanvasPanel nehmen wir die gleiche Klasse wie oben – hier ändert sich nichts. Auch in der Aufgabenstellung soll sich nicht ändern … wieder sollen es 20 x 20 farbige Felder sein.

Da man für jedes Panel als Container ein Layout vereinbaren kann, könnte man nun auf einem mainPanel (JPanel) z. B. ein BorderLayout vereinbaren. Auf diesem Panel platziert man dann zwei weitere JPanel, eines für die Zeichenfläche (graphicPanel; BorderLayout) und eines für weitere Komponenten, z. B. Buttons und Textfelder für andere Aufgaben (controlPanel). Das controlPanel könnte dann auch wieder vom GridLayoutManager verwaltet werden – hier können die einzelnen Komponenten gut angeordnet werden. Dies kann recht komfortabel mit dem Swing UI Designer erledigt werden.

Nun müssen wir nur noch die Zeichenfläche zuweisen. Dafür erzeugen wir unsere Zeichenfläche (JPanel (MyCanvasPanel); zeichenflaeche) und fügen diese dem graphicPanel hinzu:

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

public class MainGUI {

    private byte[][] myColorField = new byte[20][20];

    private JPanel mainPanel;
    private JPanel controlPanel;
    private JPanel graphicPanel;
    private MyCanvasPanel zeichenflaeche= new MyCanvasPanel(myColorField); 

    public static void main(String[] args) {
        JFrame frame = new JFrame("Zeichnen mit Swing");
        frame.setContentPane(new MainGUI().mainPanel);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    public MainGUI() {
        // Zeichenfläche hinzufügen
        zeichenflaeche.setPreferredSize(new Dimension(500,500));
        graphicPanel.add(zeichenflaeche);
        ...

Da das (von mir verwendete) BorderLayout mit frame.pack den gesamten Frame hinsichtlich der in den Komponenten angegebenen Eigenschaft prefered sizes packt, muss man der Zeichenfläche vorab die gewünschte Größe zuteilen.

Sicher gibt es hier auch die Möglichkeit andere Layouts zu verwenden oder z. B. die Zeichenfläche direkt auf das mainPanel zu legen. Gerade beim Letzteren empfiehlt sich jedoch eine klare Strukturierung.

Links zum Thema

In verschiedenen Foren und Wikis gibt es eine Menge Informationen, die über die hier dargestellten Möglichkeiten (weit) hinausgehen und tiefere Einblicke vermitteln. Die folgende Liste bietet dafür einen Einstieg …

Schlagwörter: