Zeichnen in Swing Tutorial

Wildcard

Top Contributor
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]
 

Wildcard

Top Contributor
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:

Wildcard

Top Contributor
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

Gast
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:
 

Wildcard

Top Contributor
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:
 

Wildcard

Top Contributor
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:

Chris_1980

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

Wildcard

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

Lundner

Mitglied
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*
 

Marco13

Top Contributor
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/
 

Wildcard

Top Contributor
@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:
 

Lim_Dul

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

Daniel_L

Bekanntes Mitglied
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".
 

Wildcard

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

Daniel_L

Bekanntes Mitglied
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). :)
 

Quaxli

Top Contributor
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:
 

Wildcard

Top Contributor
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 :(
 

Leroy42

Top Contributor
Hab mir diesen Thread erst heute durchgelesen, deshalb erst jetzt mein Kommentar dazu:

Kompliment@Wildcard: :applaus:

Seine (ihre :shock: ) Vorgehensweise, dieses Thema aufzudröseln ist zwar
nichts für Eilige, aber er beschreibt dieses Thema anhand eines Laien,
der sich in einen Anfänger versetzen kann, und ihn erstmal auf falsche Fährten lenkt.

Diesen wohligmachenden Stil kenne ich noch von dem Verfasser unserer
damaligen Informatik-Bibel: "Algorithmen und Datenstrukturen" (Verfasser: Niklaus Wirth(*))

Auch er verstand es sehr gut, in seinem Buch, jemanden zuerst auf einmal auf
die falsche Fährte zu führen und dann, Schritt für Schritt, die Mysterica bei der
Programmierung zu entschleiern.

Dieses Buch hat meine Vorgehensweise beim Programmieren nachhaltig geprägt.

Wildcard? Wie alt bist du eigentlich?
(bitte, wenn überhaupt, via PN antworten :D )

PS: Ich hoffe mal, daß dieser Post, trotz meiner offensichtlich erkennbaren
Verfassung ( :shock: ), erhalten bleiben wird. :cool:

Ich verabschiede mich, dann vorerst mal...

P.S. Musste mal raus
P.P.S.: auch wenn mich AlArsenal jetztele Verdammich machten tutet.
P.P.S: Schönen Abend noch....



(*)
a) Professor an der ETH Zürich
b) Mit-Designer von Algol68
c) Erfinder von Pascal und Modula-II
d) ich meine dem hier
e) Zitat aus dem vorherigen Link:
Algorithmen und Datenstrukturen (1975), zum Klassiker geworden.
 
G

Guest

Gast
Hey Wildcard!

Schönes Tutorial, hat mir geholfen! Danke!

Würde mich wirklich über weitere Teile freuen. :)
 

Timmothey

Mitglied
Hallo!

Kennt ihr das "Problem", dass man, wenn man etwas gezeichnet hat und ein anderes Fenster das ganze überlagert oder man es rüber zieht, das Gezeichnete gelöscht, bzw wie weg radiert wird?

Ich habe hier diesen Thread überflogen und nichts dazu gefunden und für eine Google Recherche stell' ich mich nicht klug genug an.

Wenn jemand das schon mal hatte, würde ich mich freuen, wenn ihr mir helfen könntet :)

Danke!
 

Marco13

Top Contributor
Wenn du in der paintComponent-Methode zeichnest (und NUR da!) und der Event-Dispatch-Thread nicht blockiert ist, sollte das nicht passieren. Wenn BEIDE Bedingungen nicht erfüllt sind, poste ein compilierbares Programm, wo das Problem auftritt. Wenn du nicht weißt, ob sie erfüllt sind, sag bescheid.
 

André Uhres

Top Contributor
Timmothey hat gesagt.:
Kennt ihr das "Problem", dass man, wenn man etwas gezeichnet hat und ein anderes Fenster das ganze überlagert oder man es rüber zieht, das Gezeichnete gelöscht, bzw wie weg radiert wird?
Damit eine Swing Komponente jederzeit neu gezeichnet werden kann,
muss der Malcode unbedingt in der callback Methode "paintComponent(..)" stehen.
Diese wird vom System aufgerufen, wenn die Komponente beschädigt wurde.

Wenn du den Malcode auslagern willst, dann besteht die Möglichkeit, auf ein Image zu malen,
und in "paintComponent(..)" einfach nur dieses Image zu zeichnen (drawImage).
Hier ist ein Beispiel: DrawOnImage.java
 

siriuswhite

Aktives Mitglied
Find das Tutorial super,vor allem da ich meistens mit älteren Tutorials zu tun hab da ich kein gutes finde ausser welchen die noch von 2003 oder sowas sind. Zum Thema dass man überall "benutze NICHT!!! getGraphics() " einsetzen soll,ich glaube das würd die leser mehr nerven als von getGraphics() abzubringen.Ich würd jedenfalls eher mit demLesen aufhören wenn ständig kommt NICHT GetGraphics() BENUTZEN.
 

Marco13

Top Contributor
Das mit dem "nicht getGraphics benutzen" war ja nur als Hinweis darauf, dass das (nicht wirklich ernsthaft "überall eingestreut" :roll: , sondern an den entsprechenden Stellen oder einmal) an "prominanter Stelle" stehen sollte, weil es ja ein SEHR häufiger Fehler ist. Und wenn, dann auch richtig - also nicht nur das als kurzes, knackiges Bibel-ähnliches Gebot formulierte "Thous halt not call getGraphics", sondern sowas wie "getGraphics auf Components aufzurufen ist fast immer falsch" .... Nicht so drauf rumhacken :wink:
 

André Uhres

Top Contributor
Auch eine ungünstige Formulierung (man fragt sich dann, wann es richtig ist, und die Antwort würde für Anfänger wohl wieder zu weit führen).
Hier ist eine mögliche positive Formulierung:

Swing benutzt einen "callback" ("Wiederholungsbesuch") Mechanismus zum Zeichnen.
Das heißt, daß ein Programm den Darstellungscode der Komponente innerhalb einer bestimmten
überschriebenen Methode setzen sollte, und das Toolkit ruft diese Methode auf, wenn es Zeit ist zu malen.
Die zu überschreibende Methode ist in javax.swing.JComponent:

Java:
protected void paintComponent(Graphics g)
 
Zuletzt bearbeitet von einem Moderator:

Marco13

Top Contributor
Das würde aber niemanden davon abhalten, "mal kurz" in irgendeinem "game loop" getGraphics().drawSomeWeirdStuff() aufzurufen. Aber vermutlich wird selbst das beste Tutorial der Welt uns nicht vor Theads bewahren, in denen das gemacht wird. Diejenigen, die diese Threads starten, haben im Zweifelsfall einfach noch kein entsprechendes Tutorial gelesen :roll:
 

semi2183

Mitglied
Gut gemacht.
Ich beschäftige mich erst seit kurzen mit der grafischen Programmierung, und konnte es gut verstehen.
Ich finde auch den Weg nicht schlecht. Würde aber im "faschen Code" Kommentare setzen. Wenn man dann Copy& Past beim falshen Teil macht, wird man dann bei der Fehler Suche vielleicht den Fehler besser erkennen.

Außerdem wenn ich es eillig habe und nur schnell Code suche, lese ich aber zumindestens die ersten Zeilen in einem HowTo um zu sehen was drin steht und ob sich die Suche im Howto lohnt. Und wenn das Vorgehen mit dem flaschen dem richtige Ansatz am Anfang erwähnt wird, ist das ausreichend. Ich würde es an deinen Stelle nicht ändern.
 

Ebenius

Top Contributor
Ich glaube, Wildcard wollte hier noch einige Änderungen machen. Die FAQ sollten, soweit ich weiß, keine Diskussionen beinhalten sondern in sich geschlossene Erklärungen. Dieses Thema ist für einen FAQ-Eintrag meiner Meinung nach zu durcheinander.

Ebenius
 

DaFuchs

Neues Mitglied
Hallo Wildcard,

ich finde das Tutorial auch gut, allerdings sollte nicht so rigoros auf den falschen Ansatz eingegangen werden.

Für die Zukunft würde ich mir wünschen
- Unterschied zwischen AWT und Swing,
- warum paintComponent anstatt paint,
- was hat es mit repaint auf sich,
- wo soll setPreferredSize aufgerufen werden

viele grüße
 

U2nt

Bekanntes Mitglied
Wenns möglich wäre vielleicht das "Tutorial" als PDF verpacken?

(Oder hab ich einen Link übersehen? Hoffe nich :p)

Ansonsten super Tutorial!
 

JBuntu

Mitglied
Jo die paint Methoden von JComponent zu überschreiben ist eine verkorkste Angelegenheit, da sich alles ineinander verschachtelt. Viel einfacher ist es sich das Graphics Objekt des Frame zu angeln und sich eigene Klassen(z.B.Kreis) schreiben welche das Graphics Object zum zeichnen nutzen.
Code:
Frame.getContentPane().getGraphics()
Code:
Graphics2D g= (Graphics2d)  Frame.getContentPane().getGraphics();

Java:
import javax.swing.*;
import java.awt.*;
/**
 *
 * @author Nature One
 */
class Kreis
{
    Graphics G;
    int x,y,r;
    public Kreis(Graphics g)
    {
        G=g;
    }
    public void init(int px,int py,int pz)
    {
        x=px;y=py;r=pz;
    }
    public void zeichne()
    {
        G.drawOval(x, y,r, r);
    }
}
public class JZeichnen extends JFrame
{
    Kreis K;
    public JZeichnen()
    {
        setBounds(0,0,600,400);
        K=new Kreis(getContentPane().getGraphics());
    }
}
 

eRaaaa

Top Contributor
Du hast die erste Seite nicht gelesen oder?
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.

(p.s.: Variablen schreibt man klein)
 

JBuntu

Mitglied
Eine JComponent Klasse bekommt durch Frame.add(Component) genauso wie die Klasse Kreis eine Referenz/Parameter Graphics übergeben. Die paint Methode von JComponent referenziert also nur die GLEICHE Instanz Graphics des Frames. Die paint Methode wird natürlich durch das WindowEvent Onactivated,Onresize automatisch aufgerufen. Wenn man selber sauber Zeichnen will sollte man die Umleitung über JComponent vermeiden, denn diese belastet die Laufzeit negativ. Man benötigt keine JComponents oder JPanels wenn man mit swing zeichnen möchte. Natürlich musst du noch nen WindowListener implementieren damit auch hier das Programm immer weiß, wann neu gezeichnet werden muss.
 

Ebenius

Top Contributor
Zuletzt bearbeitet:

André Uhres

Top Contributor
Natürlich musst du noch nen WindowListener implementieren damit auch hier das Programm immer weiß, wann neu gezeichnet werden muss.

Bei deinem Ansatz müssten wir das Toolkit (AWT/Swing) zum Teil neu erfinden. Wenn die Komponente beschädigt wurde und repariert werden muss (zum Beispiel wenn etwas verschoben wurde, das vorher die Komponente verdeckte, und ein vorher verdeckter Teil der Komponente wird sichtbar), dann würde zudem ein WindowListener wohl kaum etwas nützen. Solche "Reparaturen" sind jedoch ohne Weiteres möglich, wenn wir korrekten Gebrauch von den "callback" Methoden machen, die das Toolkit für's Zeichnen zur Verfügung stellt (paint/paintComponent). In diesen Methoden bekommen wir außerdem automatische Clip Informationen, die uns leistungsfähigen Malcode ermöglichen, wenn es darum geht, komplizierte Komponenten darzustellen. Das sind nur ein paar der zahllosen Argumente, die für den korrekten Gebrauch von paint/paintComponent sprechen.
 

beastofchaos

Bekanntes Mitglied
VLl. noch etwas, was man brigen könnte, wären Area. Habe über Google kein Tutorial gefunden, dabei ist die Klasse echt super, um z.B. ein Rechteck vom anderen abzuziehen.

Hier eine Seite von "Java ist auch eine Insel", die das zumindest anschneidet, aber kein Beispiel bringt.
Galileo Computing :: Java ist auch eine Insel – 20.4 Geometrische Objekte

Hier mal kurzes Programm, mit dem von einer vorhanden Area-Fläche mit Mausziehen eine Fläche abgezogen wird. Funktioneirt nur aber in die positive Richtung des Koordinatensystem. Beim Zeichnen, müsst man das halt immer umschieben, aber das sind auch nur ein paar Zeilen Code ;)

Java:
package managment;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;

@SuppressWarnings("serial")
class Tester extends JPanel{
	private Rectangle selShape;
	private Ellipse2D.Double subShape;
	
	private Timer timerMove;
    private int dashPhase;     // Wenn durch Timer erhöht oder verringert wird, "bewegt" sich die Umrandung
	
	private Tester(){
		super();
		setBackground(Color.white);
		selShape = new Rectangle(25, 25, 370, 240);
		subShape = new Ellipse2D.Double(100, 50, 190, 200);
		
		dashPhase = 0;
		ActionListener taskPerformer = new ActionListener() {
            public void actionPerformed(ActionEvent evt) {
            	dashPhase += 2;
            	if (dashPhase >= 10){
            		dashPhase = 0;
            	}
            	Tester.this.repaint();
            }
        };
        timerMove = new Timer(100, taskPerformer);
        timerMove.start();
		
		addMouseListener(new MouseActions());
		addMouseMotionListener(new MouseActions());
	}
	
  @Override
  protected void paintComponent( Graphics g )
  {
	super.paintComponent(g);
    Graphics2D g2 = (Graphics2D) g;
    Area area1 = new Area(selShape);
    Area area2 = new Area(updateShape(subShape));
    area1.subtract(area2);
    
    Composite oldComposite = g2.getComposite();
    RenderingHints oldRenders = g2.getRenderingHints();
    Stroke oldStroke = g2.getStroke();
    
    g2.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // Beim Zeichnen Kurven glätten
    g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.2f));  // Auf 20% durchsichtig machen
    g2.setColor(Color.blue);
    g2.fill(area1);
    
    g2.setStroke(new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{5, 5}, dashPhase));  // Rand "gedasht" machen
    g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.7f));  // Auf 70% durchsichtig machen
    g2.setColor(Color.black);
    g2.draw(area1);
    
    g2.setComposite(oldComposite);
    g2.setRenderingHints(oldRenders);
    g2.setStroke(oldStroke);
  }

  	private Ellipse2D.Double updateShape(Ellipse2D.Double shape) {
  		Rectangle bounds = new Rectangle();
  		bounds.setFrame(shape.x, shape.y, shape.width, shape.height);
  		if (bounds.width < 0){
  			bounds.x += bounds.width;
  			bounds.width = bounds.width * -1;
  		}
  		if (bounds.height < 0){
  			bounds.y += bounds.height;
  			bounds.height = bounds.height *  -1;
  		}
  		return new Ellipse2D.Double(bounds.x, bounds.y, bounds.width, bounds.height);
  	}

	public static void main( String[] args )
	  {
	    JFrame f = new JFrame("Selektieren");
	    f.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
	    f.setSize( 500, 420 );
	    f.setLocationRelativeTo(null);
	    f.add(new Tester());
	    f.setVisible( true );
	  }
  
	private class MouseActions extends MouseAdapter{
		@Override
		public void mousePressed(MouseEvent e){
			subShape = new Ellipse2D.Double(e.getX(), e.getY(), 0, 0);
			Tester.this.repaint();
		}
		@Override
		public void mouseDragged(MouseEvent e){
			subShape.width = e.getX() - subShape.x;
			subShape.height = e.getY() - subShape.y;
			Tester.this.repaint();
		}
	}
}

Gruß, Thomas

edit: Ich hab den "kurzen" Code noch eingefügt, damit man auch in den negativen Bereich des Kooridantensystems kann. Es hat lange gedauert, weil getBounds() von dem Ellipse2D.Double-Objekt immer ein neues Rectangle(0, 0, 0, 0) ausgespuckt hatte, sobald es im Minus war.
->
Java:
    public Rectangle getBounds() {
	double width = getWidth();
	double height = getHeight();
	if (width < 0 || height < 0) {
	    return new Rectangle();
	}
	double x = getX();
	double y = getY();
	double x1 = Math.floor(x);
	double y1 = Math.floor(y);
	double x2 = Math.ceil(x + width);
	double y2 = Math.ceil(y + height);
	return new Rectangle((int) x1, (int) y1,
				      (int) (x2 - x1), (int) (y2 - y1));
    }
Da hat wohl ein Java-Entwickler n bissl geschlampt oder eine andere Vorstellung eines Rechtecks(im Original ist es noch nicht mal eingerückt :p).
 
Zuletzt bearbeitet:

André Uhres

Top Contributor
Hallo Thomas,

das Thema ist zweifellos interessant, hat aber kaum etwas mit Swing zu tun. Es würde jedoch gut in ein Java2D Tutorial passen, wie zum Beispiel hier: Advanced Topics in Java2D

Gruß,
André

Tipp: [c]bounds.width = bounds.width * -1;[/c] können wir auch so kürzen: [c]bounds.width *= -1;[/c]
 
Zuletzt bearbeitet:

beastofchaos

Bekanntes Mitglied
Ich weiß :p Bevor ich von dem getBounds()-Trick wusste hab ich erstmal alles wieder so aufwendig geschrieben (a*=1 -> a = a * 1). Weil ich vorher noch nie das ausprobiert hatte mti mal, dachte ich, dass das iwie anders funktioniet :DD

Und danke für die Seite, werd ich mir auch mal anschaun

Gruß, Thomas
 

GUI-Programmer

Top Contributor
So, da ich mich nun schon sehr oft mit dem Thema "Zeichnen in Swing" beschäftigt habe, bzw. immer wieder neue Fragen hier im Forum dazu aufkamen, will ich nun mal eine weitere Möglichkeit (evtl. sogar eine Art von Verbesserungsvorschlag) vorstellen. Zweck dieser ist es in einem beliebigen Teil des Programms etwas Neues zeichnen lassen zu können, wie als würde man
Code:
getGraphics()
aufrufen und auf das erhaltene Graphics Objekt zeichnen, nur eben ohne der Verwendung von
Code:
getGraphics()
.

Das Ganze kann auf folgender Art und Weise aufgebaut sein:
  1. Man benötigt ein Interface oder eine abstrakte Klasse. Diese/s enthält eine Methode mit Graphics bzw. Graphics2D als Parameter [hier:
    Code:
    paintObject(Graphics2D g2)
    ]. Der gesamte Inhalt dieser Methode wird später in der
    Code:
    paintComponent(Graphics g)
    des entsprechenden JPanels/JComponents gezeichnet.

  2. Als nächstes benötigt man eine Liste "vom Typ" des Interfaces (/der abstrakten Klasse) [hier:
    Code:
    List<Paintable> paintObjects;
    ]. Diese Liste dient zur Speicherung "der zu zeichnenden Sachen/Dinge/Objekte".

  3. Schließlich braucht man als Letztes noch eine Klasse, die für das Zeichnen zuständig ist, also mit
    Code:
    paintComponent(Graphics g)
    - Methode. D.h. sie sollte von JPanel oder JComponent erben. Kann sowohl eine "normale" Klasse als auch eine innere sowie eine anonyme sein [hier:
    Code:
    DrawingArea
    ]. In deren
    Code:
    paintComponent(Graphics g)
    - Methoden werden dann alle "Paintables" der Liste gezeichnet.
Code:

Das Interface:
Java:
import java.awt.Graphics2D;

public interface Paintable {
	public void paintObject(Graphics2D g2);
}
Die Klasse mit der Liste, zugleich die Zeichen - Klasse:
Java:
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class DrawingArea extends JPanel {
	private List<Paintable> paintObjects;
	
	public DrawingArea() {
		paintObjects = new ArrayList<Paintable>();
		setPreferredSize(new Dimension(600, 400));
	}
	
	@Override
	protected void paintComponent(Graphics g) {
		super.paintComponent(g);
		Graphics2D g2 = (Graphics2D) g;
		if(paintObjects != null) {
			for(Paintable paintable : paintObjects) {
				paintable.paintObject(g2);
			}
		}
	}
	
	public List<Paintable> getPaintObjects() {
		return paintObjects;
	}
	
	public void setPaintObjects(List<Paintable> paintObjects) {
		this.paintObjects = paintObjects;
		repaint();
	}
	
	private static void createTestFrame() {
		// create the frame
		JFrame frame = new JFrame("Test");
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		DrawingArea drawingArea = new DrawingArea();
		frame.setContentPane(drawingArea);
		frame.pack();
		frame.setLocationRelativeTo(null);
		frame.setVisible(true);
		
		// add some Paintables to the drawingArea
		// this could be done enywhere in the program, maybe in
		// some methodes from EventListeners
		Paintable p1 = new Paintable() {
			@Override
			public void paintObject(Graphics2D g2) {
				g2.drawString("TestString", 30, 30);
				g2.setColor(Color.BLUE);
				g2.fillRect(50, 50, 500, 300);
			}
		};
		drawingArea.getPaintObjects().add(p1);
		
		Paintable p2 = new Paintable() {
			@Override
			public void paintObject(Graphics2D g2) {
				g2.setColor(Color.YELLOW);
				g2.fillOval(200, 150, 200, 120);
				g2.setColor(Color.BLACK);
				g2.fillOval(250, 180, 20, 15);
				g2.fillOval(320, 180, 20, 15);
			}
		};
		drawingArea.getPaintObjects().add(p2);
		
		// after adding Paintables repaint the drawingArea
		drawingArea.repaint();
	}
	
	public static void main(String[] args) {
		EventQueue.invokeLater(new Runnable() {
			public void run() {
				createTestFrame();
			}
		});
	}
}

[EDIT]ArrayList wurde zu List[/EDIT]
 
Zuletzt bearbeitet:

Neue Themen


Oben