Java Swing Rahmenprogramm | Ereignisse und Listener |
Die Grafikausgabe erfolgt bei grafischen Oberflächen anders als das Malen auf eine Leinwand. Es wird nicht einmal ein Kunstwerk erstellt, sondern die Zeichnung wird dann aufgebaut und später wieder neu aufgebaut, wenn ein Ereignis des Systems dies anfordert.
In einer Fensterumgebung sind diese Ereignisse das erstmalige Erstellen des Fensters und das Sichtbarwerden des Fensters, wenn es zuvor von einem anderen Fenster verdeckt oder teilverdeckt war. Dieses Ereignis kann das Programm selbst auslösen, wenn eine Änderung des Programmstatus das Neuzeichnen erforderlich macht.
Die Methode paint
Die Ereignisse muss der Programmierer nicht selbst überwachen. Es reicht, wenn das Programm die Klasse JFrame oder JPanelerweitert und dabei die Methode paint überschreibt. Die Methode paint wird nämlich immer dann aufgerufen, wenn das System oder das Programm ein Neuzeichnen erforderlich macht.Als Parameter erhält sie ein Objekt vom Typ Graphics2D, das aus Kompatibilitätsgründen Graphics heißt. Diese stellt quasi die Leinwand dar, deren Methoden die Zeichenprimitive sind.
Die folgende Methode paint wird eine grüne Ellipse in den Zeichenbereich des Rahmens schreiben. Die Koordinaten ergeben sich vom Nullpunkt links oben zur Breite und Höhe.
@Override public void paint(Graphics gr) { super.paint(gr); setColor(Color.green); gr.fillOval(0, 0, this.getWidth(), this.getHeight()); }Sowohl JFrame als auch JPanel bieten die Methode paint an. Allerdings benötigt JFrame einen Teil seines Inhalts zur Darstellung des Verschiebebalkens und des Fensterrahmens, so dass die Ellipse an allen Rändern beschnitten wird. Besonders oben ist das sehr deutlich sichtbar.
Es gibt zwei Lösungsansätze:
- Das Programm ermittelt den Bereich des JFrames, der sichtbar ist und passt die Zeichenkoordinaten an.
- Das Programm verwendet ein JPanel als Zeichenblatt und fügt dieses als Kind dem JFrame hinzu.
JFrame-Ränder ermitteln
Die Methode getInsets liefert eine Referenz auf Inset mit den Attributen left, right, top und bottom. In diesen befinden sich die Randbereiche, die nicht übermalt werden können.Mit deren Hilfe lassen sich nun die Werte x, y, breite und hoehe berechnen, in denen die Grafik darstellt werden kann.
@Override public void paint(Graphics gr) { super.paint(gr); gr.setColor(Color.green); Insets insets = getInsets(); int x = insets.left; int y = insets.top; int breite = getSize().width - insets.left - insets.right; int hoehe = getSize().height - insets.top - insets.bottom; gr.fillOval(x, y, breite, hoehe); }Dieser Lösungsansatz funktioniert zwar, wird aber schon dann sehr kompliziert, wenn zu der Grafik auch noch ein Button im Fensterrahmen integriert werden soll.
JPanel oder JFrame
Die Methode paint wird nicht in der Erweiterung von JFrame erstellt. Stattdessen wird ein JPanel dem Arbeitsbereich des JFrames hinzugefügt.import javax.swing.JFrame; public class MeinJFrame extends JFrame { public MeinJFrame() { this.setSize(400, 300); this.setVisible(true); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.add(new MeinJPanel()); } public static void main(String[] args) { new MeinJFrame(); } }Nun wird in der Erweiterung von JPanel die Methode paint überschrieben.
import java.awt.Color; import java.awt.Graphics; import javax.swing.JPanel; public class MeinJPanel extends JPanel { @Override public void paint(Graphics g) { super.paint(gr); setColor(Color.green); gr.fillOval(0, 0, this.getWidth(), this.getHeight()); } }Man kommt zum gleichen Ergebnis wie beim Einsatz von Inset. Allerdings erspart man sich das Errechnen der tatsächlichen Position, das bei Mausklicks wieder zurückgerechnet werden müsste.
Hinzu kommt, dass das JPanel selbst in ein Layout mit anderen Panels oder auch Kontrollelementen gelegt werden kann, ohne dass an den Grafikaufrufen etwas geändert werden muss. Langfristig ist dies also die sinnvollere Herangehensweise.
Aufruf von super.paint
In der Dokumentation wird empfohlen, in der überschriebenen paint-Methode zunächst super.paint aufzurufen. Es sorgt dafür, dass JFrame zunächst den Bildschirm putzt, bevor wir zeichnen.Wird der Aufruf von super.paint weggelassen, verhalten sich Linux und Windows interessanterweise unterschiedlich. Während Windows den Hintergrund vor jedem paint löscht, lässt Linux den alten Zustand stehen. Wird super.paint aufgerufen, verhalten sich beide gleich, weil die Basisklasse den Bildschirm löscht.
paint oder paintComponent
Man kann das Zeichnen bei JPanel sogar noch etwas optimieren, indem man statt paint die Methode paintComponent überschreibt. Das ist effizienter, da paint neben paintComponent auch paintBorder und paintChildren aufruft, was weder notwendig noch sinnvoll ist, wenn man das eigene JPanel nicht überschreibt oder add aufruft. Ansonsten funktioniert paint in Einzelfall überraschungsfreier. Siehe dazu:https://www.dreamincode.net/forums/blog/867/entry-2264-paint-vs-paintcomponent-a-resolution paintComponent kann nicht in JFrame überschrieben werden. Aber das wollten wir ja eh nicht mehr verwenden.
Es muss Farbe ins Heim
Über die Graphics-Referenz wird die Farbe für die nächsten Zeichenoperationen ausgewählt. Für den Umgang mit der Farbe gibt es zwei Aufrufe:- void setColor(Color c)
Mit setColor wird die Zeichenfarbe ausgewählt. Alle folgenden Zeichenoperationen erfolgen in dieser Farbe. - Color getColor()
Die Methode getColor liefert die zuletzt verwendete Farbe.
Color.black Color.blue Color.cyan Color.darkGray Color.gray Color.green Color.lightGray Color.magenta Color.orange Color.pink Color.red Color.white Color.yellow
Sollte keine dieser Farben passen, können RGB-Farben übergeben werden. Da setColor einen Parameter vom Typ Color erwartet, wird für die Übergabe per new ein Color erzeugt, dessen Konstruktor der RGB-Wert als Parameter übergeben wird.
setColor(new Color(0x00ff00));Die ersten beiden Stellen sind der Rot-Anteil. Der ist 00. Es folgt der Grün-Anteil, der mit FF der höchstmögliche Wert ist. Als dritter steht der Blau-Anteil auf 00, sodass wir ein kräftiges Grün erwarten dürfen.
Grafikoperationen
Die Methoden, die mit draw beginnen, wie beispielsweise drawRect für das Zeichnen eines Rechtecks, zeichnen die Umrisse, während die Methoden, die mit fill beginnen, wie beispielsweise fillRect, das innere der Figur ausfüllen.
Linien, Rechtecke und Polygone
- void drawLine(int x1, int y1, int x2, int y2)
Eine Linie wird mit durch Anfangs- und Endpunkt festgelegt. Diese werden als x- und y-Koordinate an die Methode drawLine übergeben.Das folgende Beispiel zeichnet fünf Linien, alle ausgehend vom Ursprung (0, 0). Die Linien breiten sich nach rechts aus. Die Endpunkte teilen die Höhe des Fensters gerecht untereinander auf.
public class LinePanel extends JPanel { static final int LINES = 5; @Override public void paint(Graphics gr) { for (int i=0; i<LINES; i++) { gr.drawLine(0, 0, getWidth(), i * (getHeight()/LINES)); } } }
Die Diagonale fehlt, da die Schleife bei 0 beginnt und bei <LINES aufhört. Soll die Diagonale mitgezeichnet werden, müsste man LINES+1 vergleichen oder die Bedingung auf <=LINES korrigieren.
- void drawRect(int x, int y, int breite, int hoehe)
void fillRect(int x, int y, int breite, int hoehe)
Ein Rechteck bestimmt sich durch seinen Ausgangspunkt, seine Breite und seine Höhe. Mit der Methode drawRect wird die Außenlinie eines Rechtecks gezeichnet. Die Methode fillRect füllt den angegebenen Rahmen mit der aktuellen Zeichenfarbe. - void drawRoundRect(int x, int y, int breite, int hoehe, int arcBreite, int arcHoehe)
void fillRoundRect(int x, int y, int breite, int hoehe, int arcBreite, int arcHoehe)
Weil Rechtecke einfach so furchtbar eckig sind, hat man in Java noch die Möglichkeit, Rechtecke für Weicheier zu erzeugen. Diese haben dann runde Ecken und daran kann man sich auch nicht so leicht verletzen.Die letzten beiden Parameter geben an, welche Höhe und Breite die gerundete Ecke haben soll. Natürlich gibt es dazu passend auch die Methode, die die Fläche füllt und die beginnt erwartungsgemäß mit fill.
- void draw3DRect(int x, int y, int breite, int hoehe, boolean vorn)
void fill3DRect(int x, int y, int breite, int hoehe, boolean vorn)
Ganz nett ist auch draw3DRect. Es entsteht der Eindruck, als wäre das Rechteck der Ebene nach vorn oder nach hinten enthoben, je nachdem, ob der letzte Parameter wahr ist. Natürlich gibt es eine passende fill-Methode. - void drawPolygon(int[] xPoints, int[] yPoints, int nPoints)
void fillPolygon(int[] xPoints, int[] yPoints, int nPoints)
Für unwägbarere Figuren können noch Polygone gezeichnet und gefüllt werden.Dazu wird ein Array der X-Positioen, eines für die Y-Positionen und als dritten Parameter die Anzahl der Punkte übergeben.
Kreise, Ellipsen, Ovale und Kreisausschnitte
Ein Kreis ist eigentlich nichts anderes als ein Oval, dessen Höhe und Breite gleich sind. Darum gibt es in Java auch keine eigene Funktion zum Zeichnen eines Kreises. Wenn man sehr pedantisch ist, ist das Oval von Java auch eigentlich eine Ellipse. Da die Ellipse aber ein Spezialfall des Ovals ist, ist die Bezeichnung Oval zumindest nicht falsch.
- void drawOval(int x, int y, int breite, int hoehe)
void fillOval(int x, int y, int breite, int hoehe)
Ensprechend erzeugt die Methode drawOval den Umriss und die Methode fillOval die Fläche eines Ovals oder eben eines Kreises, wenn die Parameter hoehe und breite gleich sind.
- void drawArc(int x, int y, int breite, int hoehe, int anfWinkel, int endWinkel)
Zeichnet einen Bogen in das beschriebene Rechteck. Der Winkel zählt ab 0 von der recht Position quasi bei 3 Uhr und bewegt sich entgegen dem Uhrzeigersinn einmal herum. Ein voller Kreist ist 360. Der Bogen ist nur dann ein Kreis, wenn Höhe und Breite gleich sind. Ansonsten ist es eine Ellipse. - void fillArc(int x, int y, int breite, int hoehe, int anfWinkel, int endWinkel)
Damit wird der Bogenwinkel mit der zuvor eingestellten Farbe gefüllt.
Texte malen
Um einen Text in eine Grafik zu malen, verwenden Sie die Methode drawString. Als Paramter übernimmt sie den String und die Koordinaten.
- void drawString(String str, int x, int y)
Wie bisher auch, wird die Farbe zuvor durch setColor gesetzt. Aber der besondere Vorteil der grafischen Oberflächen ist es, verschiedene Schriften, Größen und Attribute der Schrift verwenden zu können. Dazu müssen wir mit Fonts arbeiten.
- void setFont(Font font)
Die Methode setFont ändert zwar den Zeichensatz, erwartet aber als Parameter ein Objekt der Klasse Font. Wir müssen also erst einen Font zusammenbauen. Da der Font nur zum Setzen des Zeichensatzes verwendet wird, können wir das new direkt in die Parameterklammer setzen. Die Garbage Collection wird sich um die Reste kümmern.
setFont(new Font("Serif", Font.PLAIN, 24);
Nun wird eine Schrift ausgewählt, die Serif heißt und 24 Punkt groß ist. Der Vorteil der Serifenschrift für dieses Beispiel ist, dass nach der aktuellen Mode nur noch serifenlose Schriften verwendet werden und sie bei der Ausgabe den Unterschied anhand der Lesemuttern sehen können.
- Font getFont()
Der Aufruf von getFont liefert den aktuell verwendeten Zeichensatz.
Für das Darstellen von Zeichenketten wird häufig auch die Ausdehnung der Zeichenketten benötigt, beispielsweise um diesen mittig darzustellen. Zu diesem Zweck ermittelt man erst die FontMetrics.
- FontMetrics getFontMetrics()
Damit erhält man die FontMetrics des aktuell gesetzten Fonts, man kann aber auch einen Font übergeben und dessen FontMetrics ermitteln.
- FontMetrics getFontMetrics(Font f)
Die Klasse FontMetrics wiederum stellt Methoden zur Verfügung, um die Maße von dargestellten Zeichenketten zu ermitteln.
import javax.swing.JFrame; public class MainZeichensatz extends JFrame { public static void main(String[] args) { new MainZeichensatz(); } public MainZeichensatz() { setTitle("Zeichensatzinfo mit Java"); setSize(400, 300); setVisible(true); add(new PanelZeichensatz()); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } }Und nun das JPanel, das die eigentliche Arbeit tut.
import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import javax.swing.JPanel; public class PanelZeichensatz extends JPanel { public void paintComponent(Graphics g) { super.paintComponent(g); int breite = getWidth(); int hoehe = getHeight(); g.clearRect(0, 0, breite - 1, hoehe - 1); g.setColor(Color.black); g.setFont(new Font("FreeSerif", Font.PLAIN, 16)); FontMetrics fm = g.getFontMetrics(); int zeile = 1; String meldung = "Bildschirm: " + breite + " x " + hoehe + " Pixel"; g.drawString(meldung, 0, 0 + zeile * fm.getHeight()); zeile++; meldung = "Zeilenhöhe - getHeight(): " + fm.getHeight(); g.drawString(meldung, 0, 0 + zeile * fm.getHeight()); zeile++; meldung = "Ascent - getAscent(): " + fm.getAscent(); g.drawString(meldung, 0, 0 + zeile * fm.getHeight()); zeile++; meldung = "Descent - getDescent(): " + fm.getDescent(); g.drawString(meldung, 0, 0 + zeile * fm.getHeight()); zeile++; meldung = "Durchschuss - getLeading(): " + fm.getLeading(); g.drawString(meldung, 0, 0 + zeile * fm.getHeight()); zeile++; meldung = "Breite von \"Willemer\" - stringWidth(\"Willemer\"): " + fm.stringWidth("Willemer"); g.drawString(meldung, 0, 0 + zeile * fm.getHeight()); } }
Images
Bei vielen Spielen benötigt man Spielfiguren. Aber auch, wenn man Fotos in einem Programm darstellen will, wird man bei den Images landen. Ein Image wird aus einer Bilddatei gewonnen. Diese Bilddatei kann entweder vom Dateisystem geladen werden oder beispielsweise bei Spielen in der jar-Datei mit eingebunden werden.
Es muss das Package java.awt.Image eingebunden werden.
import java.awt.Image;
Zunächst wird ein Objekt der Klasse Image benötigt. Dieses muss natürlich mit einem Bild gefüllt werden. Im ersten Schritt wird also eine Bilddatei einem Objekt der Klasse Image zugeführt.
Die Applet-Klasse verfügt über die Methode getImage und kann eine Bilddatei direkt laden. Bei Applets kann eine URL für das Bild verwendet werden. So lädt die folgende Zeile ein Bild von einer Website.
Image img = getImage("http://www.seite.de/bild.jpg");
Falls die Bilder aber an der gleichen Stelle stehen wie die Anwendung, kann das Applet auch getCodeBase() einsetzen.
Image img = getImage(getCodeBase(),"meinbild.jpg");
Eine JFrame-Anwendung muss das Standard-Toolkit bemühen, um getImage aufzurufen.
Image img = Toolkit.getDefaultToolkit().getImage("meinbild.gif");
Nun verfügt die Anwendung über ein Image, das sie in einer Graphics-Umgebung mit der Methode drawImage darstellen kann.
paint(Graphics g) { ... g.drawImage(img, xPos, yPos, this);
Dieser Aufruf wird das Image img darstellen. Die Position wird durch xPos und yPos bestimmt. Das this im vierten Parameter bezieht sich auf den JFrame und informiert das Programm über den Status des Bildes. Aber auch in einem Applet passt ein this.
Eigentlich ist dieser Parameter vom Typ ImageObserver. Diese abstrakte Klasse wird von der Klasse Component implementiert. Insofern ist this an dieser Stelle fast immer richtig.
Neben der Möglichkeit, Images anzuzeigen, können auch Rechteckbereiche des Bildschirms copyArea kopiert werden.
- void copyArea(int x, int y, int breite, int hoehe, int dx, int dy)
Die Methode kopiert den Inhalt des beschriebenen Bereichs an die durch dx und dy angegebene Stelle.
Clipping
Mit dem Clipping kann man den Bereich eingrenzen, in dem gezeichnet werden soll. Der Programmierer definiert einen Clipping-Bereich, außerhalb dessen jegliche Malerei unterbunden wird.- void setClip(int x, int y, int breite, int hoehe)
Die Methode setClip sorgt für das Setzen der Grenzen. In seiner einfachsten Form übergibt man der Methode ein Rechteck. Wird Graphics2D verwendet, kann auch ein Shape als Parameter dienen. - void clipRect(int x, int y, int breite, int hoehe)
Schneidet den bisherigen Clippingbereich mit dem angegebenen Rechteck. - Rectangle getClipBounds()
Die Attribute x, y, width und height von Rectangle können direkt ausgelesen werden und ergeben den vom Clipping-Bereich umspannten Bereich. - boolean hitClip(int x, int y, int breite, int hoehe)
Man kann mit hitClip im Vorhinein prüfen, ob ein Rechteck innerhalb des Clipping-Bereichs liegt, indem man die Methode hitClip aufruft.
Modernisierung: Graphics2D
Die Grafikfähigkeiten in Java sind durch die 2D-Bibliothek erweitert worden. Die alte Schnittstelle wurde erhalten, so dass alle bisherigen Methoden auch bei 2D verwendbar sind. Sogar die Methode paint wurde beibehalten. Allerdings verbirgt sich nun hinter dem Parameter Graphics ein Graphics2D-Objekt, dass man innerhalb der ersten Zeile einfach per casting verwenden kann. Wenn man die ersten beiden Zeilen der Methode paint auf diese Weise anpasst, kann man den Rest der Methode einfach belassen.
public void paint(Graphics pg) { Graphics2D g = (Graphics2D) pg;
Die bisherigen Methoden arbeiten nach wie vor. Aber nun können die Ergänzungen von 2D hinzugefügt werden.
Grafische Objekte
Das Konzept ändert sich dahin, dass bei 2D grafische Objekte eingeführt werden. Linien beispielsweise sind nun nicht mehr nur die Koordinaten, die die drawLine-Methode als Parameter erhält, sondern eigenständige Objekte, die sich von der Klasse Line2D ableiten. Ein Linienobjekt wird angelegt, erhält beim Konstruktoraufruf seine Koordinaten und wird dann der überladenen Methode draw oder fill übergeben.Durch diese Veränderung des Konzepts können nun die Linien direkt nach dem Schnittpunkt mit einer anderen Linie gefragt werden.
Die Grundprimitive sind abstrakte Klassen.
- Line2D,
- Point2D,
- Rectangle2D,
- RoundRectangle2D,
- QuadCurve2D,
- Ellipse2D,
- Arc2D
Die Positionen werden den Objekten über den Konstruktor mitgegeben. Dann kann das Objekt einfach der Methode draw oder fill übergeben werden.
Die Positionen werden nicht mehr als ganzzahlige Werte behandelt, sondern als float oder double. Der Hintergrund ist, dass so eine Abstraktion von der tatsächlichen Hardware erreicht wird. Ein Bildschirm hat eine Auflösung von vielleicht 70 dpi. Aber auf Papier ist diese Auflösung wesentlich höher. Wird eine diagonale Linie auf die Bildschirmauflösung reduziert, hat sie bei der Papierausgabe bereits sichtbare Ecken.
Paint und Stroke
Wie bisher kann die Methode setColor verwendet werden, um die Farbe festzulegen. Neu ist die Möglichkeit Paint zu verwenden.
g.setPaint(new GradientPaint(0, 0, Color.blue, 50, 25, Color.green, true));
Mit Stroke kann die Dicke eines Striches verändert werden.
g.setStroke(new BasicStroke(5));
Die Strichstärke ist nun 5.
Java Swing Rahmenprogramm | Ereignisse und Listener |