Wie kann man in Java zeichnen?

Aus Informatik
Wechseln zu: Navigation, Suche

Für 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 Klasse JPanel zurückgreifen.


Zeichnen mit JPanel (SWING)

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 einfaches 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 20x20-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 ausreichend sein ...


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 ...