Zeichnen in Swing Tutorial

Bitte aktiviere JavaScript!
Wie der aufmerksame Leser wohl mitbekommen hat gibt es enorm viele Probleme beim zeichnen mit Swing.
Ich halte den derzeitigen FAQ Beitrag für nicht ausreichend, aber wenn ihr anderer Meinung seid nehme ich das hin :wink:
Sofern ich nicht der einzige bin der die Meinung vertritt das man da noch ein paar Dinge hinzufügen/verbessern könnte wäre ich bereit einen neuen Beitrag zu verfassen.
Was sollte eurer Meinung nach behandelt werden, wo die Schwerpunkte gesetzt werden?
  • - zeichnen in paintComponent (und warum kein getGraphics)?
    - EDT?
    - Funktionsweise des Swing Toolkits (lightweight Architektur usw.)?
    - performance?
    - Aufbau eines eigenen Lightweight Zeichenframeworks?
    - Buffer Strategien,Page Flipping, usw?
    - Draw2D API?
    - Affine Transformationen?
    - andere Vorschläge?
[Edit by Beni: verschoben nach AWT/Swing, als wichtig markiert]
 
A

Anzeige




Schau mal hier —> (hier klicken)
Eine großartige Idee!
Fang einfach an, du kennst die Hauptproblematiken.
Vielleicht könnte man dazu auch unseren Forum-Uropi mit ins Boot bekommen.
 
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.
JComponent 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 Color und ein Member vom Typ Shape benötigt.

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

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 (Label, Panel, TextField) 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 (JFrame, JDialog, JWindow). 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 JComponent 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ückliefern kann wenn die Komponente bereits in einem Heavyweight Container liegt der eine solche Resource erhalten hat.
Wenn wir uns die Klasse Graphics 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 Graphics2D 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 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.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 Einfluß.
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.
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 Trugschluß, weshalb hier so detailiert 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 Neuzeichnen 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 NaivePaintingComponent 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 ActionListener 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 NaivePaintingComponent paintingComponent = new NaivePaintingComponent();
	
	/**
	 * 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 NaivePaintingComponent 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:
So, erster Teil ist oben. Wenn ihr was zu nörgeln habt sagt bescheid.
Wenn Bedarf besteht mache ich noch einen zweiten Teil.
Ich könnte mir beispielsweise vorstellen Threads und Animationen anhand eines kleinen Pong Spiels zu erläutern.
 
B

Beni

Grundsätzlich ganz gut, aber IMHO ist der "falsche" und der "richtige" Teil nicht klar genug markiert. Wie wäre es mit zwei grossen Titeln "Naiver & falscher Ansatz" und "Richtiger Ansatz"?

P.S. ein oder zwei Tippfehler hats noch :bae:
 
Beni hat gesagt.:
Grundsätzlich ganz gut, aber IMHO ist der "falsche" und der "richtige" Teil nicht klar genug markiert. Wie wäre es mit zwei grossen Titeln "Naiver & falscher Ansatz" und "Richtiger Ansatz"?
Ja, da hast du recht. Mal sehen wie sich das klarer trennen lässt.

P.S. ein oder zwei Tippfehler hats noch :bae:
Dachte ich mir. War zu faul mir das nochmal durch zu lesen :cool:
 
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.
JComponent 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 Color und ein Member vom Typ Shape 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 (Label, Panel, TextField) 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 (JFrame, JDialog, JWindow). 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 JComponent 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 Graphics 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 Graphics2D 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 ActionListener 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:
Hi Wildcard,

ich finde du hast das echt super erklärt. :toll:
Was sich aber imo noch ganz toll machen würde, wäre eine Erläuterung wie man trotz passiven Zeichnens verschiedene Bilder bzw. Animationen dargestellt bekommt.
 
Chris_1980 hat gesagt.:
Was sich aber imo noch ganz toll machen würde, wäre eine Erläuterung wie man trotz passiven Zeichnens verschiedene Bilder bzw. Animationen dargestellt bekommt.
Danke für das Lob.
Habe ich mir auch vorgenommen, aber erst im zweiten Teil. Sonst wird's zu viel auf einmal.
 
sehr geniales tutorial! für den anfänger aber doch etwas umfangreich ;-) schwer das wesentliche zu finden, aber so wird man wenigstens gezwungen, sich alles durchzulesen und zu verstehen *daumenhoch*
 
Die Idee an sich ist gut, aber ... (sorry, ich hab an ALLEM was auzusetzen) ... ich weiß nicht, ob es grundsätzlich didaktisch klug ist, in der ersten Hälfte ausführlichst den "Falschen Ansatz" zu erklären. Man soll z.B. auch nie absichtlich etwas falsches an eine Tafel schreiben, weil es jemand abschreibt, und später für richtig hält - übertragen auf dieses Beispiel: Irgendjemand scrollt garantiert ein Stück runter, sieht dann das fettgedruckte getGraphics, darunter irgendwas mit ... "Lightweight Architektur ... Heavyweight Container ... Resource"... Fachgesimpel - wo ist der Code? Ah, da: "Der komplette Code" - den kopier' ich mir einfach mal da raus.... Kompilieren, starten, """funktioniert""".

Aber ändern mußt du das nicht. Ich sage jetzt einen Satz mit "Ich hätte...", aber weiß, dass, egal, was man in so einen Tutorial schreibt, immer jemand mit einem "Ich hätte..."- Satz antworten wird :wink: Also: Ich hätte nur den richtigen Ansatz beschrieben, und zwischendurch immer wieder das "erste Gebot des Swing-Zeichnens" eingesteut: Thou shalt NOT call getGraphics() !!!

Als kleine Ergänzung: For the users that englisch can, could you insert a link to the site where the painting explained becomes :wink:
http://java.sun.com/products/jfc/tsc/articles/painting/
 
@Marco13
Ich hab mir auch recht schwer damit getan es auf diese Weise zu schreiben, da mir durchaus bewusst ist das viele dazu neigen Text nicht zu ende zu lesen sondern bis sie für sie verwertbare Informationen gefunden haben.
Der Grund warum ich es trotzdem so getan habe ist, das es mir nicht nur darum ging richtiges Zeichnen zu erklären, sondern schlicht und ergreifend keine Lust mehr habe immer auf's neue das getGraphics Thema durchzukauen.
Wenn es nicht so unglaublich oft falsch gemacht werden würde, wäre ich niemals darauf eingegangen.
Ein simples "Thou shalt NOT call getGraphics() !!! " ist in meinen Augen nicht ausreichend, da immer wieder die Frage nach dem warum kommt.
Den falschen Ansatz aus der Entwicklung der Anwendung herauszulösen und seperat zu behandeln, damit wäre ich einverstanden.
Den Link werde ich dazu packen. Gute Idee.
Ansonsten Danke für die Kritik :toll:
 
Ich muss marco13 zustimmen.

Ich würde die richtige Methode mit Begründungen erklären und dann hinten noch Absäzte einfügen:

Warum XY schlecht ist.
 
Ich finde das Tutorial auch sehr gelungen, aber auch den Ansatz, die Probleme vorweg zu schicken und somit erst den falschen Ansatz zu erklären.

Allerdings liest sich der Text so fließend, dass man gar nicht weiß, warum es falsch ist. Ich würde die Begründung vom Ende des Falsch-Teils an den Anfang setzen bzw. dort kurz beschreiben, und am Ende noch mal als Erinnerung darauf zurückkommen.

Dann weiß man: "Ah, das ist falsch, aber die Logik weshalb wird auch klar, sodass man aus den Fehlern lernen kann".
 
Sorry nochmal an alle das es so lange dauert, ich bin zur Zeit leider sehr beschäftigt und Antworte hier im Forum(wider besseres Wissen) eigentlich nur um mich von den wichtigen Sachen abzulenken. :oops:
Die überarbeitete Fassung kommt definitiv (vermutlich auch einige weitere Teile) sobald die dringenden Sachen aus dem Weg sind.
 
Mach dir kein Stress. Ich find's ja schon gut, dass sich jemand überhaupt die Mühe macht. Vielleicht wird ja irgendwann eine richtig umfangreiche Tutorial-Sammlung draus, wenn auch andere mithelfen (können). :)
 
Um in die gleiche Kerbe zu schlagen: Ich habe beim ersten Überfliegen mich auch gewundert, warum Du getGraphics() nimmst. :wink: Aber sonst finde ich das Ganze sehr gelungen :applaus:

Was meiner Ansicht nach noch mit rein sollte (evtl. etwas später): Animation bzw. Bewegung des Shapes. Das gehört bei vielen zusammen und würde das eine oder andere Problem gleich mit erschlagen. Wobei man dann auch noch gleich eine Steuerung reinbasteln könnte. Aber dann nimmt das Ganze wohl kein Ende :bae:
 
Quaxli hat gesagt.:
Was meiner Ansicht nach noch mit rein sollte (evtl. etwas später): Animation bzw. Bewegung des Shapes. Das gehört bei vielen zusammen und würde das eine oder andere Problem gleich mit erschlagen. Wobei man dann auch noch gleich eine Steuerung reinbasteln könnte. Aber dann nimmt das Ganze wohl kein Ende :bae:
Das wird ein zweiter Teil. Leider kann das alles in frühstens 6 Wochen passieren. Zur Zeit schwimmen mir trotz regelmäßiger 12 Stunden Tage etwas die Felle davon :(
 
A

Anzeige




Vielleicht hilft dir das hier weiter: (klicke hier)
Passende Stellenanzeigen aus deiner Region:

Neue Themen

Oben