KEIN Swing-Tutorial

Marco13

Top Contributor
Als kleinen Einstieg zum Punkt aus diesem Thread. Ich dachte da an einige einfache, compilierbare Schnipsel, jeweils jeweils auf minimale Weise (!) einige Funktionen anbieten, nach denen immer wieder mal gefragt wird.

GANZ grob:

- Einfach etwas zeichnen
(Frame mit von JPanel abgeleiteter Klasse mit überschriebener paintComponent)

- Mehrere verschiedene Objekte zeichnen
(Wie oben, aber mit einer List<Drawable> in der JPanel-Klasse)

- Anklickbare Objekte zeichnen
- Verschiebbare Objekte zeichnen
- Animierte Objekte zeichnen

...

Die Schwierigkeit bestünde da für mich weniger darin, das zu machen, sondern dem Abstraktionsdrang entgegenzuwirken und das ganze so rudimentär und minimalistisch wie möglich zu machen, damit jede einzelne "Stufe" noch nachvollziehbar bleibt, und man sich nicht mit den Dingen konfrontiert sieht, die eigentlich erst bei komplexeren Programmen relevant werden. Es ginge ja wirklich um Beispiele, wo man 100 Zeilen (notfalls auch unverstanden) rauskopiert und nur an so wenigen Stellen wie möglich solche Kommentare wie "// Hier dein Zeug zeichnen" mit eigenem Code ergänzt.
 

XHelp

Top Contributor
Ist der Thread als "Lieber Tagebuch, ich werde bald..." oder als "Schmeißt mal Vorschläge rein..." zu verstehen?
Man könnte ja ein Beispiel von "Systemauslastung-Graph-Zeichnen-Ding" brigen, wo man eben erkennen sollte, dass die pain-Methode nur dazu gedacht ist das was da ist zu zeichnen und die Berechnungen woanders erfolgen sollten.
 

Marco13

Top Contributor
Hm ich habe den Thread mal hier im Entwurfsbereich aufgemacht, damit es einen Platz gibt, wo ich oder jemand anderes bei Gelegenheit solche Schnipsel posten kann, und sie dann von anderen zerrissen werden können (in noch kleinere Schnipsel :D ) bevor sie in die FAQ wandern.... Nicht OK? ???:L

@XHelp: Das mit dem Graphen ist einerseits etwas spezifischer, als das, was ich meinte: Ich dachte wirklich an sowas wo z.B. einfach eine von JPanel abgeleitete Klasse in ein JFrame gelegt wird, und dort in der paintComponent eben steht
// Hier kannst du das Graphics zum Malen verwenden (und NUR hier ;) )
und wo eben NICHT irgendwo getGraphics aufgerufen wird, dafür aber super.paintComponent, und wo das GUI mit den SwingUtilities im EDT erstellt wird... Dann darauf aufbauen eben Beispiele wie oben angedeutet, z.B. auch sowas wie "Mit einem Buttonklick etwas zeichnen lassen"... Und wenn dann mal wieder jemand in einem AWT-Frame paint überschreibt oder im der actionPerformed mit getGraphics rumpfuscht, kann man ihn darauf verweisen, und er hat ein template, wo schonmal viele Fehler vermieden sind.

Andererseits ist eine Sache, die auch immer wieder angefragt wird: Ein Funktionsplotter :D ...
 

Quaxli

Top Contributor
Die Schwierigkeit bestünde da für mich weniger darin, das zu machen, sondern dem Abstraktionsdrang entgegenzuwirken und das ganze so rudimentär und minimalistisch wie möglich zu machen...

Aber nicht zu rudimentär, bitte. ;) Ein gewisser Grad an Abstraktion sollte schon enthalten sein. Von daher finde ich den Ansatz gut, das hier gemeinschaftlich zu erstellen und zu diskuiteren.
Das wären Beispiele, die sicherlich oft verlinkt würden. :D
 

Marco13

Top Contributor
Wenn man einfach nur etwas zeichnen will:
Java:
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

// Ein JPanel, bei dem die paintComponent-Methode überschrieben ist,
// so dass man in das JPanel etwas reinmalen kann.
class PaintPanel extends JPanel
{
	@Override
	public void paintComponent(Graphics graphics)
	{
		super.paintComponent(graphics);
		Graphics2D g = (Graphics2D)graphics;
		
		// Hier die Zeichenbefehle einfügen:
		g.setColor(Color.BLACK);
		g.drawString("Hello, Swing!", 60, 50);
	}
}


public class SwingSimplePaint 
{
	public static void main(String args[])
	{
		// Das Erzeugen des GUIs muss auf dem 
		// Swing-Thread ausgeführt werden:
		SwingUtilities.invokeLater(new Runnable() 
		{
			@Override
			public void run() 
			{
				// Fenster erstellen
				JFrame frame = new JFrame("Simple Paint");
				frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
				
				// PaintPanel erstellen und in das Fenster legen
				PaintPanel paintPanel = new PaintPanel();
				paintPanel.setPreferredSize(new Dimension(200, 100));
				frame.getContentPane().add(paintPanel);
				
				// Fenster zentrieren und anzeigen
				frame.pack();
				frame.setLocationRelativeTo(null);
				frame.setVisible(true);
			}
		});
	}
}


Wenn man durch einen Button-Klick etwas zeichnen will:
Java:
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

// Ein JPanel, bei dem die paintComponent-Methode überschrieben ist, und
// bei dem man ein Objekt setzen kann, das gezeichnet werden soll.
class ShapePaintPanel extends JPanel
{
	// Das Objekt, das gezeichnet werden soll
	private Shape objectToPaint = null;
	
	// Setzt das Objekt, das gezeichnet werden soll, und
	// löst ein Neuzeichnen aus.
	public void setObjectToPaint(Shape objectToPaint)
	{
		this.objectToPaint = objectToPaint;
		repaint();
	}
	
	@Override
	public void paintComponent(Graphics graphics)
	{
		super.paintComponent(graphics);
		Graphics2D g = (Graphics2D)graphics;
		
		// Hier wird das zu zeichnende Objekt gemalt:
		if (objectToPaint != null)
		{
			g.setColor(Color.BLACK);
			g.draw(objectToPaint);
		}
	}
}

//Die Hauptklasse für das GUI: Ein Panel, das die weiteren
//GUI-Komponenten enthält
class MainPanel extends JPanel
{
	public MainPanel()
	{
		setLayout(new BorderLayout());
		
		// PaintPanel erstellen und in die Mitte legen
		final ShapePaintPanel shapePaintPanel = new ShapePaintPanel();
		shapePaintPanel.setPreferredSize(new Dimension(200, 100));
		add(shapePaintPanel, BorderLayout.CENTER);
		
		// Button erstellen und and den unteren Rand legen
		JButton button = new JButton("Paint object");
		add(button, BorderLayout.SOUTH);
		
		// Wenn der Button geklickt wird, wird das zu zeichnende 
		// Objekt erstellt und an das ObjectPaintPanel übergeben
		button.addActionListener(new ActionListener() 
		{
			@Override
			public void actionPerformed(ActionEvent arg0) 
			{
				Shape objectToPaint = new Rectangle(50,50,100,20);
				shapePaintPanel.setObjectToPaint(objectToPaint);
			}
		});
	}
}


//Die Hauptklasse mit der main-Methode. 
public class SwingPaintOnButtonClick 
{
	public static void main(String args[])
	{
		// Das Erzeugen des GUIs muss auf dem 
		// Swing-Thread ausgeführt werden:
		SwingUtilities.invokeLater(new Runnable() 
		{
			@Override
			public void run() 
			{
				// Fenster erstellen
				JFrame frame = new JFrame("Paint on button click");
				frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
				frame.getContentPane().setLayout(new BorderLayout());
				
				// Hauptpanel erstellen und in den Frame legen
				MainPanel mainPanel = new MainPanel();
				frame.getContentPane().add(mainPanel, BorderLayout.CENTER);
				
				// Fenster zentrieren und anzeigen
				frame.pack();
				frame.setLocationRelativeTo(null);
				frame.setVisible(true);
			}
		});
	}
}


Wenn man verschiedene Objekte zeichnen will:
Java:
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Arc2D;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

//Ein Interface für alle Objekte, die gezeichnet werden können. 
interface Paintable
{
  void paintObject(Graphics2D g);
}


//Ein Smiley, der gezeichnet werden kann. 
class Smiley implements Paintable
{
  private int x;
  private int y;
  
  public Smiley(int x, int y)
  {
      this.x = x;
      this.y = y;
  }
  
  // Implementierung des Paintable-Interfaces 
  public void paintObject(Graphics2D g)
  {
      g.setColor(Color.YELLOW);
      g.fillOval(x, y, 50, 50);
      g.setColor(Color.BLACK);
      g.fillOval(x+15, y+15, 8, 8);
      g.fillOval(x+30, y+15, 8, 8);
      g.draw(new Arc2D.Double(x+10,y+10,30,30,190,160,Arc2D.OPEN));
  }
}



// Ein JPanel, bei dem die paintComponent-Methode überschrieben ist, und 
// das eine Liste von Objekten speichert, die gezeichnet werden können
class GeneralPaintPanel extends JPanel
{
	// Die Liste der Paintablen Objekte, und Methoden um solche
	// Objekte hinzuzufügen oder zu entfernen.
	private List<Paintable> paintables = new ArrayList<Paintable>();

	public void addPaintable(Paintable paintable)
	{
		paintables.add(paintable);
		repaint();
	}

	public void removePaintable(Paintable paintable)
	{
		paintables.remove(paintable);
		repaint();
	}
	
	@Override
	public void paintComponent(Graphics graphics)
	{
		super.paintComponent(graphics);
		Graphics2D g = (Graphics2D)graphics;
		g.setRenderingHint(
			RenderingHints.KEY_ANTIALIASING, 
			RenderingHints.VALUE_ANTIALIAS_ON);
		for (Paintable paintable : paintables)
		{
			paintable.paintObject(g);
		}
	}
}

//Die Hauptklasse für das GUI: Ein Panel, das die weiteren
//GUI-Komponenten enthält
class GeneralMainPanel extends JPanel
{
	public GeneralMainPanel()
	{
		setLayout(new BorderLayout());
		
		// PaintPanel erstellen und in die Mitte legen
		final GeneralPaintPanel paintPanel = new GeneralPaintPanel();
		paintPanel.setPreferredSize(new Dimension(400, 400));
		add(paintPanel, BorderLayout.CENTER);
		
		// Button erstellen und an den unteren Rand legen
		JButton button = new JButton("Paint object");
		add(button, BorderLayout.SOUTH);
		
		// Wenn der Button geklickt wird, wird das zu zeichnende 
		// Objekt erstellt und an das ObjectPaintPanel übergeben
		button.addActionListener(new ActionListener()
		{
			public void actionPerformed(ActionEvent e)
			{
				int x = (int)(Math.random()*(paintPanel.getWidth() - 100));
				int y = (int)(Math.random()*(paintPanel.getHeight() - 100));
				Smiley smiley = new Smiley(x,y);
				paintPanel.addPaintable(smiley);
			}
		});
		
	}
}

//Die Hauptklasse mit der main-Methode. 
public class SwingGeneralPaint
{
	// Das Erzeugen des GUIs muss auf dem 
	// Swing-Thread ausgeführt werden:
	public static void main(String args[])
	{
		SwingUtilities.invokeLater(new Runnable()
		{
			public void run()
			{
				// Fenster erstellen
				JFrame frame = new JFrame("Paint on button click");
				frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
				frame.getContentPane().setLayout(new BorderLayout());
				
				// Hauptpanel erstellen und in den Frame legen
				GeneralMainPanel mainPanel = new GeneralMainPanel();
				frame.getContentPane().add(mainPanel, BorderLayout.CENTER);
				
				// Fenster zentrieren und anzeigen
				frame.pack();
				frame.setLocationRelativeTo(null);
				frame.setVisible(true);
			}
		});
	}

}

Schon bei diesen Beispielen stellen sich viele Fragen: Sollte man zwischen den letzten beiden einen Unterschied machen? Wenn nicht, sollte man dann immer gleich "Paintable" oder überall nur Shapes verwenden? Wie hält man es damit für die weiteren Funktionen (anklickbar, verschiebbar, animiert (ohne den EDT zu blockieren ;) ) ... ?)

Vielleicht hat ja jemand Ideen.
 

XHelp

Top Contributor
Sollte man zwischen den letzten beiden einen Unterschied machen? Wenn nicht, sollte man dann immer gleich "Paintable" oder überall nur Shapes verwenden? Wie hält man es damit für die weiteren Funktionen (anklickbar, verschiebbar, animiert (ohne den EDT zu blockieren ;) ) ... ?)

Ich finde die Paintable-Variante irgendwie besser. Dann könnte man das stufenweise aufbauen. Im nächsten Schritt könnte man sowas wie
Java:
interface Clickable extends Paintable {
  boolean isResponsibleForPoint(Point position);
  void addMouseListener(MouseListener l);
  void removeMouseListener(MouseListener l);
  void fireMouseEvent(MouseEvent e);
}
machen. Zumal man da auch eine gewisse Grundlage für alle Beispiele schaffen kann und mehrmals benutzte Schnittstellen/Klassen einmalig zentral erklären kann.
Habe zu dem anklickbarem Teil eine Idee, muss ich aber noch eine Nacht darüber schlafen. Vllt wird die aber schon aus dem Interface deutlich.
 
Zuletzt bearbeitet:

Quaxli

Top Contributor
Schöne Beispiele. :) 2 Anmerkungen sind mir beim Überfliegen eingefallen.

1. Mir ist nicht ganz klar, warum Du grundsätzlich das Graphics-Object in ein Graphics2D castest? Außer bei den RenderingHints im letzten Beispiel sehe ich da keinen Vorteil? (Lasse mich aber gern eines besseren belehren :D)

2. Ich fände es gut, wenn die Beispiele aufeinander aufbauen. Für das GeneralPaintPanel im 3. Beispiel hast Du eine komplett neue Klasse. Evtl. wäre es lehrreicher für den Newbie, wenn man stattdessen das ShapePaintPanel erweitert/aufbohrt?
 

Marco13

Top Contributor
@L-ectron-X: Eine (schlechte :oops: ) Angewohnheit (seltsamerweise NUR bei dieser Methode) - werde ich ändern.

@XHelp: Viel "erklären" wollte ich eigentlich nicht ... nicht dass das doch noch ein Swing-Tutorial wird :mad: ;) (BTW: Das anklicken wäre im einfachsten Fall ja ein shape.contains(...) ?!). Ein ein Punkt, der für das Interface sprechen würde, wäre natürlich der des "Stufenweisen Aufbauens", und...
(auch....@Quaxli: ) ...dieser Gedanke der "Stufenweisen Erweiterung" steckte da schon ein bißchen drin. Ich aber bin nicht sicher, wie dafür de günstigste Strategie wäre. Eigentlich sollte es ja wirklich nur eine Art "Steinbruch" sein, wo man sich Copy&Paste-Vorlagen für eine spezifische(!) Zielsetzung rausholt - andererseits würde sich schon jetzt eine potentielle Kombinatorische Explosion andeuten: Zeichenbar, Zeichenbar+Klickbar, Zeichenbar+Animiert, Zeichenbar+Klickbar+Animiert... (Und DAS wäre ein Fall, wo man sowas nicht mit OSGi & Co lösen würde :D )... Aber vielleicht kann man diesen Gedanken noch ein bißchen besser ausnutzen... (Ich bin nicht sicher, wie man das formulieren soll, aber grob: ) ... ohne dass man bei der letzten "Stufe" sieht, dass es vorher schon andere Stufen gab...
 

XHelp

Top Contributor
So, nun habe ich die Nacht drüber geschlafen und es kommt mir mittlerweile gar nicht so richtig vor. Aber da der Thread in "Entwürfe" ist, packe ich das mal rein.
Habe einfach mal dein Smiley-Beispiel etwas umgebaut:
Java:
//Paintable, SwingGeneralPaint bleiben wie gehabt

//Ein Interface für alle Objekte, die gezeichnet und geklickt werden können. 
//Wobei sich durch den ganzen Listener-Kram eine abstrakte Klasse eher anbieten würde
public interface Clickable extends Paintable {
	boolean isResponsibleForPoint(Point position);
	void addMouseListener(MouseListener l);
	void removeMouseListener(MouseListener l);
	void fireMouseEvent(MouseEvent e);
}

//Ein Smiley, der gezeichnet und angeklickt werden kann. 
public class Smiley implements Clickable {
	private List<MouseListener> listenerList;
	private int x;
	private int y;
	private Color mainColor = Color.YELLOW;
	private final int DIAMETER = 50;

	public Smiley(int x, int y) {
		this.x = x;
		this.y = y;
		listenerList = new ArrayList<MouseListener>();
	}

	public void setRandomColor() {
		Random random = new Random();
		mainColor = new Color(random.nextInt(256), random.nextInt(256),
				random.nextInt(256));
	}

	// Implementierung des Clickable-Interfaces
	public void paintObject(Graphics2D g) {
		g.setColor(mainColor);
		g.fillOval(x, y, DIAMETER, DIAMETER);
		g.setColor(Color.BLACK);
		g.fillOval(x + 15, y + 15, 8, 8);
		g.fillOval(x + 30, y + 15, 8, 8);
		g.draw(new Arc2D.Double(x + 10, y + 10, 30, 30, 190, 160, Arc2D.OPEN));
	}

	public boolean isResponsibleForPoint(Point position) {
		float r = DIAMETER / 2;
		float center_x = x + r;
		float center_y = y + r;
		float dist = (float) Math.sqrt(Math.pow(position.x - center_x, 2)
				+ Math.pow(position.y - center_y, 2));
		return dist < r;
	}

	public void addMouseListener(MouseListener l) {
		listenerList.add(l);
	}

	public void removeMouseListener(MouseListener l) {
		listenerList.remove(l);
	}

	public void fireMouseEvent(MouseEvent e) {
		for (MouseListener currentListener : listenerList) {
			switch (e.getID()) {
			case MouseEvent.MOUSE_CLICKED:
				currentListener.mouseClicked(e);
				break;
			case MouseEvent.MOUSE_ENTERED:
				currentListener.mouseEntered(e);
				break;
			case MouseEvent.MOUSE_EXITED:
				currentListener.mouseExited(e);
				break;
			case MouseEvent.MOUSE_PRESSED:
				currentListener.mousePressed(e);
				break;
			case MouseEvent.MOUSE_RELEASED:
				currentListener.mouseReleased(e);
			}
		}
	}
}

// Ein JPanel, bei dem die paintComponent-Methode überschrieben ist, und 
// das eine Liste von Objekten speichert, die gezeichnet werden können
// Darüber hinaus ist es auch für das Weiterleiten der Mouseevents zuständig
public class GeneralPaintPanel extends JPanel {
	// Die Liste der Paintablen Objekte, und Methoden um solche
	// Objekte hinzuzufügen oder zu entfernen.
	// Die Mouseevents werden auch an die jeweiligen Objekten weitergeleitet
	private List<Clickable> clickables = new ArrayList<Clickable>();

	public GeneralPaintPanel() {
		addMouseListener(new MouseListener() {
			//Mouseevents an die zuständigen Objekte weiterleiten
			private void dispatchEvent(MouseEvent e) {
				for (Clickable clickable : clickables) {
					if (clickable.isResponsibleForPoint(e.getPoint())) {
						MouseEvent event = new MouseEvent((Component) e.getSource(), e.getID(),
								e.getWhen(), e.getModifiers(), e.getX(),
								e.getY(), e.getClickCount(),
								e.isPopupTrigger(), e.getButton());
						event.setSource(clickable);
						clickable.fireMouseEvent(event);
					}
				}
				repaint();
			}

			@Override
			public void mouseReleased(MouseEvent e) {
				dispatchEvent(e);
			}

			@Override
			public void mousePressed(MouseEvent e) {
				dispatchEvent(e);
			}

			@Override
			public void mouseExited(MouseEvent e) {
			}

			@Override
			public void mouseEntered(MouseEvent e) {
			}

			@Override
			public void mouseClicked(MouseEvent e) {
				dispatchEvent(e);
			}
		});
	}

	public void addPaintable(Clickable paintable) {
		clickables.add(paintable);
		repaint();
	}

	public void removePaintable(Paintable paintable) {
		clickables.remove(paintable);
		repaint();
	}

	@Override
	protected void paintComponent(Graphics graphics) {
		super.paintComponent(graphics);
		Graphics2D g = (Graphics2D) graphics;
		g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
				RenderingHints.VALUE_ANTIALIAS_ON);
		for (Clickable clickable : clickables) {
			clickable.paintObject(g);
		}
	}
}

//Die Hauptklasse für das GUI: Ein Panel, das die weiteren
//GUI-Komponenten enthält
public class GeneralMainPanel extends JPanel {
	public GeneralMainPanel() {
		setLayout(new BorderLayout());

		// PaintPanel erstellen und in die Mitte legen
		final GeneralPaintPanel paintPanel = new GeneralPaintPanel();
		paintPanel.setPreferredSize(new Dimension(400, 400));
		add(paintPanel, BorderLayout.CENTER);

		// Button erstellen und an den unteren Rand legen
		JButton button = new JButton("Paint object");
		add(button, BorderLayout.SOUTH);

		// Wenn der Button geklickt wird, wird das zu zeichnende
		// Objekt erstellt und an das ObjectPaintPanel übergeben
		button.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				int x = (int) (Math.random() * (paintPanel.getWidth() - 100));
				int y = (int) (Math.random() * (paintPanel.getHeight() - 100));
				Smiley smiley = new Smiley(x, y);
				smiley.addMouseListener(new MouseListener() {

					@Override
					public void mouseReleased(MouseEvent e) {
					}

					@Override
					public void mousePressed(MouseEvent e) {
					}

					@Override
					public void mouseExited(MouseEvent e) {
					}

					@Override
					public void mouseEntered(MouseEvent e) {
					}

					@Override
					public void mouseClicked(MouseEvent e) {
						Smiley smiley = (Smiley) e.getSource();
						smiley.setRandomColor();
					}
				});
				paintPanel.addPaintable(smiley);
			}
		});

	}
}
 

Quaxli

Top Contributor
@XHelp: So recht funktioniert Dein Beispiel aber nicht? Oder sagen wir so: Nicht bei mir. ;) Der MouseListener im GeneralPaintPanel schlägt bei mir nicht zu. Ich schau' da später nochmal genauer drauf.

Nachdem die Idee von Marco ja war, daß man die Dinger einfach kopiert und ausführen kann, fehlt mir zusätzlich noch die main-Methode.

Außerdem bin ich der Meinung, daß man Basis-Klassen verwenden sollte, wo es möglich ist, auch in einem NICHT-Tutorial ;)
Ich habe daher das Smiley mal von Rectangle erben lassen:

Java:
//Ein Smiley, der gezeichnet und angeklickt werden kann. 
class Smiley extends Rectangle implements Clickable {
	
    private ArrayList<MouseListener> listenerList;
    private Color mainColor = Color.YELLOW;
 
    public Smiley(int x, int y, int dia) {
        super(x,y,dia,dia);
        listenerList = new ArrayList<MouseListener>();
    }
 
    ....
 
    // Implementierung des Clickable-Interfaces
    public void paintObject(Graphics2D g) {
        g.setColor(mainColor);
        g.fillOval(x, y, width,height);
        g.setColor(Color.BLACK);
        g.fillOval(x + 15, y + 15, 8, 8);
        g.fillOval(x + 30, y + 15, 8, 8);
        g.draw(new Arc2D.Double(x + 10, y + 10, 30, 30, 190, 160, Arc2D.OPEN));
    }

    ....
}
 

Marco13

Top Contributor
Außerdem bin ich der Meinung, daß man Basis-Klassen verwenden sollte, wo es möglich ist, auch in einem NICHT-Tutorial ;)
Ich habe daher das Smiley mal von Rectangle erben lassen:

Darüber könnte jetzt eine lange philosophische Diskussion ausbrechen. Als erstes "Gegenargument" würde ich stichwortartig "Composition over Inheritance" einwerfen. Rectangle macht wirklich nicht viel, deswegen könnte es fast noch vertretbar sein, aber ... solche Methoden wie "reshape", "addPoint" oder "union" will man eigentlich nicht in einem Ding, dessen einzige und alleinige Aufgabe es ist, etwas zu repräsentieren, was einen Smiley auf den Bildschirm malt. (Threads in denen irgendwelche Sprites von JComponent erben, weil "...das mit setLocation und addMouseListener so praktisch ist" gibt es schon einige, da ist verständlicher, warum ich mit sowas Bauchschmerzen habe...)
 

Marco13

Top Contributor
Ja, wenn nur bestimmte Teile weiterdiskutiert werden, braucht man ja nicht alles zu posten, am Ende können die Beispiele ja als 1:1-Singe-C&P-Blöcke zusammengebastelt werden.
 

Marco13

Top Contributor
Die Frage wäre eher "Gibt es schon Fortschritte", aber in anbetracht des Datums, ja... :oops: Aber das ist eine gute Idee, ich kann da heute mal ein bißchen weiterbasteln...
 

Marco13

Top Contributor
Soo... ich habe nochmal weitergebastelt. Aber irgendwie sind die Fragen, wie man das am günstigsten strukturiert, für mich immer noch nicht geklärt. Das "einfachste Zeichnen" geht ja noch. Aber schon bei Zeichnen+Klicken stellen sich Fragen, von denen ich nicht weiß, ob sie egal sind oder nicht.

Zum Beispiel: Sollte Clickable von Paintable erben? Eigentlich nicht. Die beiden Sachen haben nicht viel miteinander zu tun. Wenn man beides vermischt, wirkt es unstrukturiert, und wenn man es trennt, wird es so schnell so "aufwändig", dass es jemanden, der nur ein Snippet sucht, vielleicht schon erschlägt oder verwirrt.

Das ist nur ein Beispiel. Darüber hinaus und allgemeiner wäre die Frage: Sollen die Snippets so sein, dass sie auch ohne Vorwissen leicht nachvollziehbar sind? Auch wenn das bedeutet, dass die Struktur, die durch sie vorgegeben wird, schlecht erweiterbar ist?

Ich würde jetzt dazu tendieren, um der Einfachheit willen Dinge auch mal so zu machen, wie man sie eigentlich (in einem "echten Programm") nicht machen würde, und die vielleicht nicht von Eleganz, Allgemeingültigkeit und Erweiterbarkeit strotzen....
 

XHelp

Top Contributor
Zum Beispiel: Sollte Clickable von Paintable erben? Eigentlich nicht. Die beiden Sachen haben nicht viel miteinander zu tun.

Naja, du kannst aber nichts anklicken, was nicht auch angezeigt wird... Imho spricht das für das Vererben.

Ich bin auch für "einfach". Höhstens anschließend noch die Denkrichtung geben, wie man es hätte anders machen können.
 

Marco13

Top Contributor
Vielleicht denke ich da auch an eine zu strikte Separation Of Concerns - im speziellen ging es darum, dass die Objekte beim Zeichnen NUR als "Paintable" (und nicht als Clickable) bekannt sein müssten, und beim Anklicken NUR als "Clickable" (und nicht als Paintable). Sicher ist es um einige Größenordnungen einfacher und kompakter, wenn man beides vermischt, aber meine OO-Seele leidet da ein bißchen ... ;( :D

Ich dachte für diese Snippets dann auch schon an ein
Java:
interface SimpleObject extends Paintable, Clickable { }
damit man die Aspekte auch leichter kombinieren kann und so, und weil man dann eine [c]List<? extends Paintable>[/c] an das Zeichenpanel und eine [c]List<? extends Clickable>[/c] an den MouseListener übergeben könnte... Aber ... Bounded Wildcards bringen mich auch gelegentlich an die Grenze, ich vermute, dass das für solche Snippets dann ungeeignet wäre...
 

Marco13

Top Contributor
Wie auch immer. Hier mal das erste: Ein minimalistisches Programm, das ein Panel in einen Frame legt, wo man in der paintComponent etwas zeichnen kann.
Java:
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

// Ein JPanel, bei dem die paintComponent-Methode überschrieben ist,
// so dass man in das JPanel etwas reinmalen kann.
class PaintPanel extends JPanel
{
    // Die paintComponent-Methode wird immer aufgerufen,
    // wenn das Panel neu gezeichnet wird
    @Override
    protected void paintComponent(Graphics graphics)
    {
        super.paintComponent(graphics);
        Graphics2D g = (Graphics2D) graphics;

        // Hier die Zeichenbefehle einfügen:
        g.setColor(Color.BLACK);
        g.drawString("Hello, Swing!", 60, 50);
    }
}

public class SwingSimplePaint
{
    public static void main(String args[])
    {
        // Das Erzeugen des GUIs muss auf dem
        // Event-Dispatch-Thread ausgeführt
        // werden:
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                createAndShowGUI();
            }
        });
    }

    // Erzeugt ein Fenster, das ein PaintPanel enthält,
    // und macht das Fenster sichtbar
    private static void createAndShowGUI()
    {
        // Fenster erstellen
        JFrame frame = new JFrame("Simple paint");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        // PaintPanel erstellen und in das Fenster legen
        PaintPanel paintPanel = new PaintPanel();
        paintPanel.setPreferredSize(new Dimension(200, 100));
        frame.getContentPane().add(paintPanel);

        // Fenster zentrieren und anzeigen
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}
 

Marco13

Top Contributor
Das zweite: Ein Panel, das ein beliebiges Shape zeichnen kann. Das zu zeichnende Shape wird durch einen Buttonklick gesetzt.
Java:
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

// Ein JPanel, bei dem die paintComponent-Methode überschrieben ist, und
// bei dem man ein Objekt setzen kann, das gezeichnet werden soll.
class PaintPanel extends JPanel
{
    // Das Objekt, das gezeichnet werden soll
    private Shape objectToPaint = null;

    // Setzt das Objekt, das gezeichnet werden soll, und
    // löst ein Neuzeichnen aus.
    public void setObjectToPaint(Shape objectToPaint)
    {
        this.objectToPaint = objectToPaint;
        repaint();
    }

    // Die paintComponent-Methode wird immer aufgerufen,
    // wenn das Panel neu gezeichnet wird
    @Override
    protected void paintComponent(Graphics graphics)
    {
        super.paintComponent(graphics);
        Graphics2D g = (Graphics2D) graphics;

        // Hier wird das zu zeichnende Objekt gemalt:
        if (objectToPaint != null)
        {
            g.setColor(Color.BLACK);
            g.draw(objectToPaint);
        }
    }
}

// Die Hauptklasse für das GUI: Ein Panel, das die weiteren
// GUI-Komponenten enthält
class MainPanel extends JPanel
{
    public MainPanel()
    {
        setLayout(new BorderLayout());

        // PaintPanel erstellen und in die Mitte legen
        final PaintPanel paintPanel = new PaintPanel();
        paintPanel.setPreferredSize(new Dimension(200, 100));
        add(paintPanel, BorderLayout.CENTER);

        // Button erstellen und and den unteren Rand legen
        JButton button = new JButton("Set object");
        add(button, BorderLayout.SOUTH);

        // Wenn der Button geklickt wird, wird das zu zeichnende
        // Objekt erstellt und an das ObjectPaintPanel übergeben
        button.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent arg0)
            {
                Shape objectToPaint = new Rectangle(50, 50, 100, 20);
                paintPanel.setObjectToPaint(objectToPaint);
            }
        });
    }
}

// Die Hauptklasse mit der main-Methode.
public class SwingPaintOnButtonClick
{
    public static void main(String args[])
    {
        // Das Erzeugen des GUIs muss auf dem
        // Event-Dispatch-Thread ausgeführt
        // werden:
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                createAndShowGUI();
            }
        });
    }

    // Erzeugt ein Fenster, das ein PaintPanel enthält,
    // und macht das Fenster sichtbar
    private static void createAndShowGUI()
    {
        // Fenster erstellen
        JFrame frame = new JFrame("Paint on button click");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        // Haupt-Panel erstellen und in das Fenster legen
        MainPanel mainPanel = new MainPanel();
        mainPanel.setPreferredSize(new Dimension(200, 200));
        frame.getContentPane().add(mainPanel);

        // Fenster zentrieren und anzeigen
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}
 

Marco13

Top Contributor
Ein Beispiel mit "Paintable" interface, wo beliebige Objekte in eine Liste gelegt und gezeichnet werden können.
Java:
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Arc2D;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

// Ein Interface für alle Objekte, die gezeichnet werden können.
interface Paintable
{
    void paintObject(Graphics2D g);
}

// Ein Smiley, der gezeichnet werden kann.
class Smiley implements Paintable
{
    private int x;
    private int y;

    public Smiley(int x, int y)
    {
        this.x = x;
        this.y = y;
    }

    // Implementierung des Paintable-Interfaces
    @Override
    public void paintObject(Graphics2D g)
    {
        int x0 = x - 25;
        int y0 = y - 25;
        g.setColor(Color.YELLOW);
        g.fillOval(x0, y0, 50, 50);
        g.setColor(Color.BLACK);
        g.fillOval(x0 + 15, y0 + 15, 8, 8);
        g.fillOval(x0 + 30, y0 + 15, 8, 8);
        g.draw(new Arc2D.Double(x0 + 10, y0 + 10, 30, 30, 190, 160, Arc2D.OPEN));
    }

    @Override
    public String toString()
    {
        return "Smiley an Position (" + x + "," + y + ")";
    }
}

// Ein JPanel, bei dem die paintComponent-Methode überschrieben ist, und
// das eine Liste von Objekten speichert, die gezeichnet werden können
class PaintPanel extends JPanel
{
    // Die Liste der Paintablen Objekte, und Methoden um solche
    // Objekte hinzuzufügen oder zu entfernen.
    private List<Paintable> paintables = new ArrayList<Paintable>();

    public void addPaintable(Paintable paintable)
    {
        paintables.add(paintable);
        repaint();
    }

    public void removePaintable(Paintable paintable)
    {
        paintables.remove(paintable);
        repaint();
    }

    // Die paintComponent-Methode wird immer aufgerufen,
    // wenn das Panel neu gezeichnet wird
    @Override
    protected void paintComponent(Graphics graphics)
    {
        super.paintComponent(graphics);

        // Schalte Antialiasing ein (damit wirkt die
        // Zeichnung glatter)
        Graphics2D g = (Graphics2D) graphics;
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);

        // Zeichne alle Objekte
        for (Paintable paintable : paintables)
        {
            paintable.paintObject(g);
        }
    }
}

// Die Hauptklasse für das GUI: Ein Panel, das die weiteren
// GUI-Komponenten enthält
class MainPanel extends JPanel
{
    public MainPanel()
    {
        setLayout(new BorderLayout());

        // PaintPanel erstellen und in die Mitte legen
        final PaintPanel paintPanel = new PaintPanel();
        paintPanel.setPreferredSize(new Dimension(400, 400));
        add(paintPanel, BorderLayout.CENTER);

        // Button erstellen und an den unteren Rand legen
        JButton button = new JButton("Add object");
        add(button, BorderLayout.SOUTH);

        // Wenn der Button geklickt wird, wird das Objekt
        // erstellt und an das PaintPanel übergeben
        button.addActionListener(new ActionListener()
        {
            private final Random random = new Random(0);

            public void actionPerformed(ActionEvent e)
            {
                int x = 25 + random.nextInt(paintPanel.getWidth() - 25);
                int y = 25 + random.nextInt(paintPanel.getHeight() - 25);
                Smiley smiley = new Smiley(x, y);
                paintPanel.addPaintable(smiley);
            }
        });

    }
}

// Die Hauptklasse mit der main-Methode.
public class SwingGeneralPaint
{
    public static void main(String args[])
    {
        // Das Erzeugen des GUIs muss auf dem
        // Event-Dispatch-Thread ausgeführt
        // werden:
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                createAndShowGUI();
            }
        });
    }

    // Erzeugt ein Fenster, das ein MainPanel enthält,
    // und macht das Fenster sichtbar
    private static void createAndShowGUI()
    {
        // Fenster erstellen
        JFrame frame = new JFrame("General painting");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        // Haupt-Panel erstellen und in das Fenster legen
        MainPanel mainPanel = new MainPanel();
        mainPanel.setPreferredSize(new Dimension(500, 500));
        frame.getContentPane().add(mainPanel);

        // Fenster zentrieren und anzeigen
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}
 

Marco13

Top Contributor
Und hier noch eins mit "Clickable", wo man mit der Linken Maustaste Objekte setzen und mit der Rechten die Objekte wieder entfernen kann.
Java:
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.geom.Arc2D;
import java.awt.geom.Ellipse2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

// Ein Interface für alle Objekte, die gezeichnet werden können.
interface Paintable
{
    void paintObject(Graphics2D g);
}

// Ein Interface für alle Objekte, die gezeichnet und angeklickt
// werden können: Bietet eine Methode, die überprüft, ob das
// Objekt durch einen Mausklick an einer bestimmten Stelle
// getroffen wurde.
interface Clickable extends Paintable
{
    boolean isHitBy(int mx, int my);
}

// Ein Smiley, der gezeichnet und angeklickt werden kann.
class Smiley implements Clickable
{
    private int x;
    private int y;

    public Smiley(int x, int y)
    {
        this.x = x;
        this.y = y;
    }

    // Implementierung des Paintable-Interfaces
    @Override
    public void paintObject(Graphics2D g)
    {
        int x0 = x - 25;
        int y0 = y - 25;
        g.setColor(Color.YELLOW);
        g.fillOval(x0, y0, 50, 50);
        g.setColor(Color.BLACK);
        g.fillOval(x0 + 15, y0 + 15, 8, 8);
        g.fillOval(x0 + 30, y0 + 15, 8, 8);
        g.draw(new Arc2D.Double(x0 + 10, y0 + 10, 30, 30, 190, 160, Arc2D.OPEN));
    }

    // Implementierung des Clickable-Interfaces
    @Override
    public boolean isHitBy(int mx, int my)
    {
        // Gib zurück, ob dieser Smiley (beschrieben durch
        // einen Kreis) den angeklickten Punkt enthält
        Shape smileyShape = new Ellipse2D.Double(x - 25, y - 25, 50, 50);
        return smileyShape.contains(mx, my);
    }

    @Override
    public String toString()
    {
        return "Smiley an Position (" + x + "," + y + ")";
    }
}

// Ein JPanel, bei dem die paintComponent-Methode überschrieben ist, und
// das eine Liste von Objekten speichert, die gezeichnet und angeklickt
// werden können
class PaintPanel extends JPanel
{
    // Die Liste der Clickablen Objekte, und Methoden um solche
    // Objekte hinzuzufügen oder zu entfernen.
    private List<Clickable> clickables = new ArrayList<Clickable>();

    public void addClickable(Clickable clickable)
    {
        clickables.add(clickable);
        repaint();
    }

    public void removeClickable(Clickable clickable)
    {
        clickables.remove(clickable);
        repaint();
    }

    // Gibt eine unveränderliche Liste aller anklickbaren
    // Objekte zurück
    public List<Clickable> getClickables()
    {
        return Collections.unmodifiableList(clickables);
    }

    // Die paintComponent-Methode wird immer aufgerufen,
    // wenn das Panel neu gezeichnet wird
    @Override
    protected void paintComponent(Graphics graphics)
    {
        super.paintComponent(graphics);

        // Schalte Antialiasing ein (damit wirkt die
        // Zeichnung glatter)
        Graphics2D g = (Graphics2D) graphics;
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);

        // Zeichne alle Objekte
        for (Paintable paintable : clickables)
        {
            paintable.paintObject(g);
        }
    }
}

// Die Hauptklasse für das GUI: Ein Panel, das die weiteren
// GUI-Komponenten enthält
class MainPanel extends JPanel
{
    public MainPanel()
    {
        setLayout(new BorderLayout());

        // PaintPanel erstellen und in die Mitte legen
        final PaintPanel paintPanel = new PaintPanel();
        paintPanel.setPreferredSize(new Dimension(400, 400));
        add(paintPanel, BorderLayout.CENTER);

        // Erstelle einen MouseListener, der aufgerufen wird,
        // wenn man auf das PaintPanel klickt:
        MouseListener mouseListener = new PaintMouseListener(paintPanel);
        paintPanel.addMouseListener(mouseListener);

    }
}

// Ein MouseListener, der bei einem Mausklick alle
// anklickbaren Objekte eines PaintPanels überprüft,
// ob sie durch den Mausklick getroffen wurden.
class PaintMouseListener implements MouseListener
{
    private PaintPanel paintPanel;

    public PaintMouseListener(PaintPanel paintPanel)
    {
        this.paintPanel = paintPanel;
    }

    @Override
    public void mouseClicked(MouseEvent e)
    {
        System.out.println("Maus wurde geklickt an Position "+e.getPoint());
        if (e.getButton() == MouseEvent.BUTTON1)
        {
            Clickable clickable = new Smiley(e.getX(), e.getY());
            System.out.println("Linke Maustaste - füge hinzu: " + clickable);
            paintPanel.addClickable(clickable);
        }
        else if (e.getButton() == MouseEvent.BUTTON3)
        {
            List<Clickable> clickedObjects = new ArrayList<Clickable>();
            for (Clickable clickable : paintPanel.getClickables())
            {
                if (clickable.isHitBy(e.getX(), e.getY()))
                {
                    clickedObjects.add(clickable);
                }
            }
            System.out.println("Rechte Maustaste - entferne: " + clickedObjects);
            for (Clickable clickable : clickedObjects)
            {
                paintPanel.removeClickable(clickable);
            }
        }
    }

    @Override
    public void mouseEntered(MouseEvent e)
    {}

    @Override
    public void mouseExited(MouseEvent e)
    {}

    @Override
    public void mousePressed(MouseEvent e)
    {}

    @Override
    public void mouseReleased(MouseEvent e)
    {}
}

// Die Hauptklasse mit der main-Methode.
public class SwingPaintAndClick
{
    public static void main(String args[])
    {
        // Das Erzeugen des GUIs muss auf dem
        // Event-Dispatch-Thread ausgeführt
        // werden:
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                createAndShowGUI();
            }
        });
    }

    // Erzeugt ein Fenster, das ein MainPanel enthält,
    // und macht das Fenster sichtbar
    private static void createAndShowGUI()
    {
        // Fenster erstellen
        JFrame frame = new JFrame("Paint and click");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        // Haupt-Panel erstellen und in das Fenster legen
        MainPanel mainPanel = new MainPanel();
        mainPanel.setPreferredSize(new Dimension(500, 500));
        frame.getContentPane().add(mainPanel);

        // Fenster zentrieren und anzeigen
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

Als nächstes würde vielleicht was mit "Dragging"/Verschieben dazukommen, oder was mit einer Animation im eigenen Thread, oder etwas, was in Richtung eines "Zeichenprogrammes" geht. Vermutlich das mit der Animation, weil das ja sehr oft falsch gemacht/nachgefragt wird.
 

Marco13

Top Contributor
Hier etwas mit "Paintable" und "Movable", wo das zu zeichnende Objekt auf Buttonklick hin in einem eigenen Thread animiert wird. Der wichtige Punkt (nämlich, wie man die Animation in einem eigenen Thread und NICHT im EDT macht) ist mit "WICHTIG" hervorgehoben und kurz erläutert.

Java:
package basic05;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Arc2D;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

// Ein Interface für alle Objekte, die gezeichnet werden können.
interface Paintable
{
    void paintObject(Graphics2D g);
}

// Ein Interface für alle Objekte, die gezeichnet und bewegt
// werden können
interface Movable extends Paintable
{
    void moveTo(int px, int py);
}

// Ein Smiley, der gezeichnet und bewegt werden kann.
class Smiley implements Movable
{
    private int x;
    private int y;

    public Smiley(int x, int y)
    {
        this.x = x;
        this.y = y;
    }

    // Implementierung des Paintable-Interfaces
    @Override
    public void paintObject(Graphics2D g)
    {
        int x0 = x - 25;
        int y0 = y - 25;
        g.setColor(Color.YELLOW);
        g.fillOval(x0, y0, 50, 50);
        g.setColor(Color.BLACK);
        g.fillOval(x0 + 15, y0 + 15, 8, 8);
        g.fillOval(x0 + 30, y0 + 15, 8, 8);
        g.draw(new Arc2D.Double(x0 + 10, y0 + 10, 30, 30, 190, 160, Arc2D.OPEN));
    }

    // Implementierung des Movable-Interfaces
    @Override
    public void moveTo(int px, int py)
    {
        this.x = px;
        this.y = py;
    }

    @Override
    public String toString()
    {
        return "Smiley an Position (" + x + "," + y + ")";
    }
}

// Ein JPanel, bei dem die paintComponent-Methode überschrieben ist, und
// das ein Objekt speichert, das gezeichnet und bewegt werden kann
class PaintPanel extends JPanel
{
    // Das Objekt, das gezeichnet und bewegt werden kann 
    private Movable movable;

    public void setMovable(Movable movable)
    {
        this.movable = movable;
        repaint();
    }

    // Die paintComponent-Methode wird immer aufgerufen,
    // wenn das Panel neu gezeichnet wird
    @Override
    protected void paintComponent(Graphics graphics)
    {
        super.paintComponent(graphics);

        // Schalte Antialiasing ein (damit wirkt die
        // Zeichnung glatter)
        Graphics2D g = (Graphics2D) graphics;
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);

        // Zeichne das Objekt
        if (movable != null)
        {
            movable.paintObject(g);
        }
    }
}

// Die Hauptklasse für das GUI: Ein Panel, das die weiteren
// GUI-Komponenten enthält
class MainPanel extends JPanel
{
    private final PaintPanel paintPanel;
    private final Movable movable;
    
    public MainPanel()
    {
        setLayout(new BorderLayout());

        // PaintPanel erstellen und in die Mitte legen
        paintPanel = new PaintPanel();
        paintPanel.setPreferredSize(new Dimension(400, 400));
        add(paintPanel, BorderLayout.CENTER);
        
        // Button erstellen und and den unteren Rand legen
        JButton button = new JButton("Animate");
        add(button, BorderLayout.SOUTH);

        // Erstelle das bewegbare Objekt und weise es
        // dem PaintPanel zu
        movable = new Smiley(100, 200);
        paintPanel.setMovable(movable);
        
        // Wenn der Button geklickt wird, wird das bewegbare
        // Objekt mit einem eigenen Thread bewegt
        button.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent arg0)
            {
                // WICHTIG: Hier NICHT direkt die Animation durchführen! 
                // Dadurch würde der Event-Dispatch-Thread für die Dauer 
                // der Animation blockiert werden. Der Event-Dispatch-
                // Thread ist für Benutzereingaben (wie Button-Klicks) 
                // und für das Neuzeichnen verantwortlich, darum würde
                // man die Animation dann nicht sehen
                //animateObject(); // FALSCH!
                
                // Stattdessen die Animation in einem neuen Thread
                // ausführen lassen:
                animateObjectInOwnThread();
            }
        });
    }

    // Erstelle und starte einen neuen Thread, der 
    // die 'animateObject'- Methode ausführt
    private void animateObjectInOwnThread()
    {
        Thread thread = new Thread(new Runnable()
        {
            @Override
            public void run()
            {
                animateObject();
            }
        });
        thread.start();
    }
    
    // Bewegt das Objekt einmal hin und her
    private void animateObject()
    {
        for (int x=100; x<300; x+=5)
        {
            // Setze die neue Position, löse ein Neuzeichnen
            // aus, und warte einen Moment
            movable.moveTo(x, 200);
            paintPanel.repaint();
            try
            {
                Thread.sleep(30);
            }
            catch (InterruptedException e)
            {
                Thread.currentThread().interrupt();
            }
        }
        for (int x=300; x>=100; x-=5)
        {
            movable.moveTo(x, 200);
            paintPanel.repaint();
            try
            {
                Thread.sleep(30);
            }
            catch (InterruptedException e)
            {
                Thread.currentThread().interrupt();
            }
        }
    }
}


// Die Hauptklasse mit der main-Methode.
public class SwingSimpleAnimation
{
    public static void main(String args[])
    {
        // Das Erzeugen des GUIs muss auf dem
        // Event-Dispatch-Thread ausgeführt
        // werden:
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                createAndShowGUI();
            }
        });
    }

    // Erzeugt ein Fenster, das ein MainPanel enthält,
    // und macht das Fenster sichtbar
    private static void createAndShowGUI()
    {
        // Fenster erstellen
        JFrame frame = new JFrame("Paint and animate");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        // Haupt-Panel erstellen und in das Fenster legen
        MainPanel mainPanel = new MainPanel();
        mainPanel.setPreferredSize(new Dimension(500, 500));
        frame.getContentPane().add(mainPanel);

        // Fenster zentrieren und anzeigen
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}


Als nächstes würde ich ggf. mal schauen, wie man so eine Animation noch ein bißchen "Pimpen" kann - also ein möglichst kleines, allgemeines Snippet, in dem man eine kontinuierliche Animation mit Start/Stop/Pause kontrollieren kann. Einschließlich des Enablens/Disablens der Buttons: Im obigen Beispiel kann man durch mehrfaches Klicken des Buttons ja unschöne Effekte erzielen - das wäre da speziell noch leicht zu beheben, aber ich schau' mal, ob man's auch allgemeiner beschreiben kann...
 

Marco13

Top Contributor
Hmja... man könnte da vielleicht noch einiges etwas kürzer machen ("Das mit dem SwingUtilities braucht man nicht, man kann das direkt in der main erstellen" ;) ) aber... es ist eben (leider?) so, dass manche "Best Practices", "guter Stil" (oder wie auch immer man das nennen will) u.U. aufwändiger sind.

In diesem speziellen Fall... ist es aber schon fast wieder was anderes: Die (mindestens) 83 Gründe, warum man Sprites nicht von JComponent erben lassen sollte, hatte ich mal in http://www.java-forum.org/java-basi...694-pingpong-selber-schreiben.html#post394005 zusammengesucht... Auch wenn man es als "Designentscheidung" ansehen könnte, sollte man es IMHO nicht machen (und auch einem Anfänger nicht suggerieren, dass das eine "pauschal tolle" Lösung sei), sofern nicht übergeordnete, driftige (!) Gründe dafür sprechen Wenn man es machen würde, müßte/sollte man ggf. (ähnlich wie beim DefaultTableCellRenderer) sowas machen wie
Java:
    /**
     * Overridden for performance reasons.
     * See the <a href="#override">Implementation Note</a> 
     * for more information.
     *
     * @since 1.5
     */
    public void invalidate() {}
    public void validate() {}
    public void revalidate() {}
    public void repaint(long tm, int x, int y, int width, int height) {}
    public void repaint(Rectangle r) { }
    public void repaint() {}
was aber auch wieder schwer zu vermitteln wäre ;)
 

André Uhres

Top Contributor
Ich hatte mir diese Reaktion schon erwartet. Als Kompromiss könnte man dann vielleicht den Code so anpassen, dass die Verbindung zu Swing erkennbar wird. Zum Beispiel so (als Alternative für Dein letztes Beispiel):
Java:
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Arc2D;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

interface PComponent {//bei JComponent weglassen

    public void paintComponent(final Graphics g);

    public void setLocation(final int x, final int y);

    public int getX();

    public int getY();
}

// Ein Smiley, der bewegt werden kann
final class Smiley implements PComponent {//oder extends JComponent

    private int x;//bei JComponent weglassen
    private int y;//bei JComponent weglassen

    Smiley(final int x, final int y) {
        this.x = x;//setLocation(x, y);
        this.y = y;//setSize(50, 50);
    }

    // Implementierung der paintComponent Schnittstelle
    @Override
    public void paintComponent(final Graphics g) {
        int x0 = getX();//bei JComponent weglassen
        int y0 = getY();//bei JComponent weglassen
        g.setColor(Color.YELLOW);
        g.fillOval(x0, y0, 50, 50);
        g.setColor(Color.BLACK);
        g.fillOval(x0 + 15, y0 + 15, 8, 8);
        g.fillOval(x0 + 30, y0 + 15, 8, 8);
        ((Graphics2D) g).draw(new Arc2D.Double(x0 + 10, y0 + 10, 30, 30, 190, 160, Arc2D.OPEN));
    }

    @Override
    public String toString() {
        return "Smiley at position (" + getX() + "," + getY() + ")";
    }

    @Override
    public int getX() {//bei JComponent weglassen
        return x;
    }

    @Override
    public int getY() {//bei JComponent weglassen
        return y;
    }

    @Override
    public void setLocation(int x, int y) {//bei JComponent weglassen
        this.x = x;
        this.y = y;
    }
}

class PaintPanel extends JPanel {//bei JComponent weglassen

    private PComponent component;

    @Override
    protected void paintComponent(final Graphics g) {
        super.paintComponent(g);
        component.paintComponent(g);
    }

    public void add(final PComponent component) {
        this.component = component;
    }
}

// Die Hauptklasse für das GUI: Ein Panel, das die weiteren
// GUI-Komponenten enthält
class MainPanel extends JPanel {

    private final PaintPanel paintPanel;//JPanel
    private final PComponent component;//Component
    private final JButton button;

    MainPanel() {
        setLayout(new BorderLayout());

        // Ein JPanel erstellen, das ein Objekt speichert, das bewegt werden kann
        paintPanel = new PaintPanel();//new JPanel(null);
        paintPanel.setPreferredSize(new Dimension(400, 400));
        // paintPanel in die Mitte legen
        add(paintPanel, BorderLayout.CENTER);

        // Button erstellen und and den unteren Rand legen
        button = new JButton("Animate");
        add(button, BorderLayout.SOUTH);

        // Erstelle das bewegbare Objekt und weise es dem paintPanel zu
        component = new Smiley(100, 200);
        paintPanel.add(component);

        // Wenn der Button geklickt wird, wird das bewegbare
        // Objekt mit einem eigenen Thread bewegt
        button.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(final ActionEvent arg0) {
                // WICHTIG: Hier NICHT direkt die Animation durchführen! 
                // Dadurch würde der Event-Dispatch-Thread für die Dauer 
                // der Animation blockiert werden. Der Event-Dispatch-
                // Thread ist für Benutzereingaben (wie Button-Klicks) 
                // und für das Neuzeichnen verantwortlich, darum würde
                // man die Animation dann nicht sehen
                //animateObject(); // FALSCH!

                // Stattdessen die Animation in einem neuen Thread
                // ausführen lassen:
                animateObjectInOwnThread();
            }
        });
    }

    // Erstelle und starte einen neuen Thread, der 
    // die 'animateObject'- Methode ausführt
    private void animateObjectInOwnThread() {
        Thread thread = new Thread(new Runnable() {

            @Override
            public void run() {
                button.setEnabled(false);
                animateObject();
                button.setEnabled(true);
            }
        });
        thread.start();
    }

    // Bewegt das Objekt einmal hin und her
    private void animateObject() {
        int x1 = component.getX();
        int y1 = component.getY();
        for (int x = x1; x < x1 + 100; x += 5) {
            // Setze die neue Position und warte einen Moment
            moveAndWait(x, y1, 30);
        }
        for (int x = x1 + 100; x >= x1; x -= 5) {
            // Setze die neue Position und warte einen Moment
            moveAndWait(x, y1, 30);
        }
    }

    private void moveAndWait(final int x, final int y, final int delay) {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                component.setLocation(x, y);
                paintPanel.repaint();//bei JComponent weglassen
            }
        });
        try {
            Thread.sleep(delay);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

// Die Hauptklasse mit der main-Methode.
public class SwingSimpleAnimation {

    private SwingSimpleAnimation() {
    }

    public static void main(final String args[]) {
        // Das Erzeugen des GUIs muss auf dem
        // Event-Dispatch-Thread ausgeführt werden:
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                createAndShowGUI();
            }
        });
    }

    // Erzeugt ein Fenster, das ein MainPanel enthält,
    // und macht das Fenster sichtbar
    private static void createAndShowGUI() {
        // Fenster erstellen
        JFrame frame = new JFrame("Drag and animate");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        // Haupt-Panel erstellen und in das Fenster legen
        MainPanel mainPanel = new MainPanel();
        mainPanel.setPreferredSize(new Dimension(500, 500));
        frame.getContentPane().add(mainPanel);

        // Fenster zentrieren und anzeigen
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

Es ist ja nicht so, dass Swing generell "worst practice" ist :D.

Gruß,
André
 

André Uhres

Top Contributor
Hier würde ich außerdem für die Animation einen Swing-Timer vorschlagen. Die einzige Funktion einer Animation besteht ja gewöhnlich darin, die Anzeige zu ändern, was in der Regel nur sehr wenig Zeit beansprucht.

Mit einem Swing-Timer wird die Funktion automatisch im Event Dispatch Thread ausgeführt und ist somit threadsicher. Allerdings, wenn die Funktion eine Weile dauern könnte, sollten wir einen SwingWorker in Betracht ziehen (anstelle oder zusätzlich zum Timer), damit wir den Event Dispatch Thread nicht lahm legen.

Um den Swing-Timer hier benutzen zu können, müssen wir die Animationsfunktion etwas anpassen, da in diesem Fall die beiden for-Schleifen naturgemäß wegfallen. Wir benötigen somit eine Möglichkeit, Hin- und Rückweg zu unterscheiden sowie das Ende der Animation abzufragen. Das könnten wir zum Beispiel mit einem einfachen Zähler verwirklichen. Etwa so:

Java:
    private void animateObjectInOwnThread() {
        button.setEnabled(false);
        final int x1 = component.getX();
        final int y1 = component.getY();
        final int start = x1;
        final int end = x1 + 200;
        //Eine einfache Methode, den Animations-Thread zu verwirklichen
        //besteht darin, einen Swing-Timer zu benutzen:
        timer = new Timer(30, new ActionListener() {

            private int count, x = start, speed = 5;

            @Override
            public void actionPerformed(ActionEvent e) {
                animateObject();
            }

            private void animateObject() {
                // Bewegt das Objekt einmal hin und her
                int maxCount = 2;
                if (x <= start || x >= end) {
                    count++;
                }
                if (count > maxCount) {
                    timer.stop();
                    button.setEnabled(true);
                    return;
                }
                if (count % 2 == 1) {
                    x += speed;//hin
                } else {
                    x -= speed;//zurück
                }
                // Setze die neue Position:
                component.setLocation(x, y1);
                paintPanel.repaint();//bei Smiley=JComponent: repaint weglassen
            }
        });
        timer.start();
    }
Gruß,
André
 
Zuletzt bearbeitet:

Marco13

Top Contributor
Hmja... was davon besser ist, hängt sicher vom konkreten Anwendungsfall ab. Im Falle dieser Snippets war ein Anwendungsfall auch: Jemand hat mal wieder was gemacht wie
Java:
public void actionPerformed(ActionEvent e) 
{
    for (int x=123; x<456; x++)
    {
         something.setPostion(x);
         try 
         {
              Thread.sleep(78);
         }
         catch (InterruptedException e)
         {
             System.exit(-666); // KREISCH!
         }
    }
}
mit der Frage: "Hilfe, das mal iwie nur den letzen Zustant ;( " und man will auf ein Snippet verlinken, wo der Unterschied zwischen
doit();
und
doitInOwnThread();
leicht erkennbar ist. Dass man dann ggf. noch aufpassen muss, "setPosition" nur auf dem EDT aufzurufen (wenn man als Sprite eine JComponent verwendet hat :bae: ) könnte man vielleicht noch erwähnen...

Diese ganzen Erkenntnisse bzw. Alternativen könnte man dann auch mal als Copy+Paste-Snippets zusammenfügen... :reflect:
 

André Uhres

Top Contributor
Dass man dann ggf. noch aufpassen muss, "setPosition" nur auf dem EDT aufzurufen (wenn man als Sprite eine JComponent verwendet hat :bae: ) könnte man vielleicht noch erwähnen...

Aufpassen muss man auch, wenn man als Sprite keine JComponent verwendet hat :bae:: z.B. "setEnabled(true)" sollte man auch nur auf dem EDT aufrufen, für den Fall, wo man nach der Animation einen JButton damit wieder freigeben will. Auch aus diesem Grund ist der Swing-Timer hier günstiger.

Gruß,
André
 

Marco13

Top Contributor
Ja, das setEnabled ist mir auch ins Auge gesprungen: Da muss man bei einem eigenen Thread natürlich nochmal "besonders" aufpassen. Aber etwas weiter oben hatte ich das ja schon angedeutet, im Zusammenang mit einem Beispiel, wo man eine Animation mit Start/Pause/Stop steuern kann. Ich denke, da ist ein Thread dann schon einfacher (vielleicht aber auch nicht - hab' noch nicht genauer drüber nachgedacht, wie man das mit einem SwingTimer machen würde...)
 

André Uhres

Top Contributor
Über die Methoden stop(), start() und restart() können wir den Timer beliebig oft anhalten und wieder starten. Somit sind die Start/Pause/Stop Buttons kein Problem. Beim Stop Button muss man eventuell darauf achten, die Grafik wieder in den Anfangszustand zu bringen (im vorliegenden Beispiel: den Smiley wieder dorthin setzen, wo er am Anfang war).

Gruß,
André
 

Marco13

Top Contributor
Tatsächlich... bisher hatte ich nur den Fall, wo es nicht darum ging, in einem festgelegten Invervall Events zu feuern, sondern (ohne sleep zwischendrin) so schnell wie möglich so viele Daten wie möglich zu verarbeiten. Aber es stimmt: Für die meisten Anwendungsszenarien, nach denen so gefragt wird, wäre der Aufwand da mit einem Swing Timer wohl geringer.
 

André Uhres

Top Contributor
bisher hatte ich nur den Fall, wo es nicht darum ging, in einem festgelegten Intervall Events zu feuern
Das festgelegte Intervall kann sogar hinauszögern bzw. einen Zahn zulegen :D:
Java:
if (count % 2 == 1) {
    x += speed;//hin
    timer.setDelay(60);//zögernd
} else {
    x -= speed;//zurück
    timer.setDelay(10);//zügig
}
Gruß,
André
 

Marco13

Top Contributor
Gut. Du weißt, was zu tun ist: Das alles jetzt schön in ein KSKB verpacken, mit Slidern für die Geschwindigkeit und Buttons, und mit Anfängergeeigneten Komentaren ... :bae: ;)

Ich stelle ja nichts von dem, was du sagst, in Frage. (Naja... das mit den JComponent-Sprites ein bißchen... :oops: ). Aber das Ziel war ja, KEIN Swing-Tutorial zu basteln, wo von Grund auf alle Möglichkeiten vermittelt, gegenübergestellt und diskutiert werden, sondern was, was jemand einfach rauskopieren und compilieren kann, und wo er an der mit [c]// HIER DEIN CODE[/c] markierten Stelle loslegen kann, und schonmal die gröbsten Schnitzer, die bei unbedachtem Hinarbeiten auf sein Ziel möglich sind, vermieden werden. Es stimmt ja alles, was du sagst: Für manche (vielleicht sogar viele :oops: ) Einsatzgebiete wäre ein Swing-Timer "besser". Für manche (IMHO aber sehr wenige) könnte ein JComponent-Sprite "besser" sein. Das zu entscheiden kann aber sehr schwierig sein, wenn man das Ziel des Suchenden nicht kennt.

Es wäre eigentlich cool wenn es (hier im Forum, oder sonstwo) eine Schnipsel-Sammlung gäbe. Teilweise gibt es die mit den FAQ, teilweise mit oft verlinkten Beiträgen, aber wirklich "systematisch" gibt's das nicht. Irgendwann hatte ich mir mal überlegt, aus den "*Test.java"-Dateien auf meiner Platte so eine Verstichwortete Snippet-Sammlung zu basteln, aber ... das mache ich, wenn ich mal keinen Job und keine Projektideen mehr habe ... und... vorher würde immernoch die Projektidee eines Snippet-Verwaltungs-Programmes stehen :rolleyes: ;)
 

André Uhres

Top Contributor
Aber das Ziel war ja, KEIN Swing-Tutorial zu basteln, wo von Grund auf alle Möglichkeiten vermittelt, gegenübergestellt und diskutiert werden

Hier müssen wir meiner Meinung nach das Ziel im Sinn behalten, um nicht zu sehr auszuschweifen. Das Ziel ist ja grob gesagt, eine allgemeine Antwort auf eine allgemeine Swing-Frage zu geben. Die allgemeine Antwort auf jede Swing-Frage muss lauten, vorhandene Komponenten und Klassen zu nutzen, so dass wir das Rad nicht neu erfinden müssen. Wenn aus irgendeinem Grund die vorhandenen Klassen unsere Wünsche nicht erfüllen, dann kann man nach Alternativen suchen, aber wir schauen nicht zuerst nach Alternativen.

Demnach sind Alternativen (wie etwa eine Swing-Komponente von Grund auf neu bauen, eine Animation ohne den dafür vorgesehenen Swing-Timer verwirklichen, usw.) in dem hier gegebenen Rahmen nur kurz zu erwähnen, wobei man gegebenenfalls auf einen entsprechenden Artikel oder ein Tutorial hinweisen kann.

Nach Deinem letzten Beitrag glaube ich allerdings, dass unsere Standpunkte hier so weit auseinander gehen, dass wir uns kaum einigen können. Da die Idee ursprünglich von Dir stammt, überlasse ich jetzt besser Dir die weiteren Ausführungen, falls zu überhaupt noch Zeit dafür findest :).

Gruß,
André
 

Marco13

Top Contributor
Was dich zur Vermutung gebracht hat, unsere Standpunkte würden auseinander gehen, ist mir nicht ganz klar ???:L Das mit dem Swing-Timer war ein berechtigter Einwand, und das Animationsbeispiel auf Basis eines solchen zu bauen wäre sicher nich verkehrt. Man könnte höchstens sagen, dass es nicht unmittelbar dem ursprünglichen Ziel zuträglich ist, Alternativen nur aufzulisten oder anzusprechen, oder zu "kombinieren" (mit Bemerkungen wie "Hier ggf. das repaint weglassen") ohne sie dann zu "konsistenten" Beispielen zusammenzufassen - weil dann derjenige, der nur ein KSKB sucht, die Teile zusammenkopieren muss (die dann ggf. nicht zusammenpassen) und verstehen muss, wo die Unterschiede sind. Aber das heißt ja nicht, dass die Erwähnung von Alternativen (und ggf. eine hitzige Diskussion über Vor- und Nachteile :D ) unangebracht wäre. Ich fände es gut, wenn ich nicht der einzige wäre, der hier KSKBs einfügt. ("Betriebsblindheit" im Sinne von unbeabsichtigt übergangenen Alternativen kann ich für mich nicht ausschließen - am konkreten Beispiel: Ich hatte bisher kaum Anlass, einen Swing-Timer zu verwenden, aber jezt werde ich das an manchen Stellen als Option in Erwägung ziehen, wo ich das vorher vielleicht nicht gemacht hätte - und das schon ganz konkret bei etwas, woran ich gerade bastle).
 

André Uhres

Top Contributor
Marco, warum verschiebst Du die Sache nicht einfach ins Byte-Welt Wiki? Das Wiki Format ist für mich günstiger: ich sehe sofort eine zusammenhängende Seite auf dem letzten Stand. Ich sehe, wie sie sich entwickelt und kann parallel eine Seite mit Diskussionen führen, so dass die "richtige" Seite nicht aufgebläht wird. Ich habe dann immer gleich die letzte Version als Diskussionsbasis zur Hand, ohne in einem endlosen Forum-Thread danach suchen zu müssen. Ich habe im Byte-Welt Wiki schon die Kategorie:Java-Codeschnipsel angelegt, die Du vielleicht auch benutzen willst.

Gruß,
André
 

Neue Themen


Oben