10.6 GUI- und Grafikprogrammierung
Anwendungen für grafische Benutzeroberflächen unterscheiden sich von Konsolenanwendungen dadurch, dass kein geordnetes Nacheinander von Ein- und Ausgabe stattfindet, sondern dass mehrere Bedienelemente parallel zur Verfügung stehen. Ein GUI-Programm muss also flexibel auf Befehle reagieren können, die in nicht vorhersagbarer Reihenfolge erfolgen.
Grafische Oberflächen und objektorientierte Programmierung passen vorzüglich zueinander: Die Bedienelemente der GUI (in den gängigen Grafikbibliotheken meist Widgets genannt[Anm.: Der Begriff widget selbst bedeutet eigentlich so viel wie »Dingsda«; die Verwendung im Bereich der GUIs wird allerdings meistens als Abkürzung für »window gadget«, also »Fenster-Apparatur«, erklärt.]) lassen sich sehr bequem als autonome Objekte realisieren, die wie in einem Baukastensystem zusammengefügt werden können. Die Benutzerbefehle werden den einzelnen Komponenten als Ereignisse (Events) mitgeteilt, auf die jedes Widget individuell reagieren oder über die es Nachrichten (Messages) an andere Elemente versenden kann. Eine der ersten objektorientierten Programmiersprachen, Smalltalk, wurde sogar eigens zur Implementierung der ersten GUI bei Xerox PARC entwickelt.
Trotz der unbestreitbaren Vorteile der OOP existieren einige GUI-Bibliotheken für imperative Programmiersprachen, vor allem für C. Sie verwenden verschiedene Tricks, um objektorientierte Verfahren wie die Nachrichtenübermittlung (Message-Passing) oder die Ereignisbehandlung (Event Handling) adäquat nachzubilden. Allerdings sind die weitaus meisten GUI-Toolkits objektorientiert.
In diesem Abschnitt wird eine besonders nützliche Bibliothek zur Programmierung grafischer Anwendungen vorgestellt: die plattformunabhängigen Java Foundation Classes (JFC). Mithilfe dieser Sammlung von Klassen zur Programmierung grafischer Oberflächen und Zeichnungen lassen sich zwar nicht alle Raffinessen der plattformspezifischen Desktops nutzen, aber dafür laufen JFC-Anwendungen unter den verschiedensten konkreten GUIs.
Die JFC bestehen im Wesentlichen aus den folgenden Packages und Bestandteilen:
- java.awt. Das Abstract Windowing Toolkit (AWT) ist der älteste Bestandteil der JFC, der bereits in der allerersten Java-Version enthalten war. Das AWT stellt sowohl grundlegende Zeichenfunktionen als auch einfache Widgets für grafische Benutzeroberflächen bereit. Der GUI-Teil wird inzwischen weitgehend durch das Swing-Toolkit ersetzt, die Zeichenfunktionalität durch die Java2D-API.
- java.awt.event. Dieses Unter-Package des AWT stellt grundlegende Interfaces zur Ereignisbehandlung für AWT-Komponenten zur Verfügung.
- javax.swing. Das umfangreiche Swing-Toolkit und seine Unter-Packages (zum Beispiel javax.swing.event zur erweiterten Ereignisbehandlung oder javax.swing. table mit mächtigen Funktionen zur Tabellendarstellung) bilden den Rahmen für die zeitgemäße Programmierung grafischer Oberflächen in Java. Alle Swing-Klassen sind von entsprechenden AWT-Vorfahren abgeleitet, aber erheblich flexibler und leistungsfähiger.
- Java2D. Die zentrale Java2D-Klasse Graphics2D wurde irritierenderweise im Package java.awt untergebracht. Dennoch handelt es sich um eine völlige Neuentwicklung, die erheblich mehr Funktionen zu bieten hat als die Vorgängerklasse java.awt.Graphics.
- java.applet. Formal gesehen gehören die Java-Applets zwar nicht zu den JFC, machen aber regen Gebrauch von deren Bestandteilen. Ein Java-Applet ist ein kleines Java-Programm, das in einem Webbrowser ausgeführt werden kann und auf jeden Fall aus einer grafischen Zeichenfläche besteht.
In der Praxis werden die alten AWT-Klassen oft mit den neuen Swing- und Java2D-Klassen gemischt angewandt, weil nicht alle Aspekte in den aktuelleren Modellen völlig neu entworfen wurden. Beispielsweise sind die Klassen, die in java.awt.event enthalten sind, in den meisten Fällen völlig ausreichend für das Event Handling.
Dieser Abschnitt konzentriert sich im Wesentlichen auf das AWT und die klassische Zeichen-API. Diese älteren Klassen sind zwar nicht so leistungsfähig wie Swing und Java2D, aber dafür mit beliebigen Java-Versionen kompatibel und einfacher zu verstehen.
10.6.1 Zeichnungen und Grafiken erstellen
Das Zeichnen von Linien, Füllungen und geometrischen Figuren ist eine recht einfache Angelegenheit. Die Zeichenfunktionen sind Methoden einer Instanz der Klasse java. awt.Graphics (beziehungsweise der abgeleiteten Klasse javax.swing.Graphics2D für erweiterte Funktionen). Um diese Methoden zu verwenden, benötigen Sie allerdings zunächst ein Container-Objekt, auf dessen Untergrund gezeichnet werden kann. Sämtliche Flächenobjekte wie java.awt.Frame (einfaches Fenster) oder java.awt.Panel (eine rechteckige Fläche) sind von der grundlegenden AWT-Container-Klasse java.awt.Canvas abgeleitet und besitzen deshalb ein integriertes Graphics-Objekt, auf das Sie mithilfe von container.getGraphics() zugreifen können. Ebenso ist jede Swing-Komponente mit einer Graphics2D-Instanz ausgestattet.
Die einfachste Art und Weise, eine Zeichenfläche zu erstellen und statische Zeichnungen hineinzumalen, besteht in der Erweiterung der Klasse Frame beziehungsweise JFrame (Swing-Fenster mit erweiterter Funktionalität). Wenn Sie in Ihrem eigenen Programm die Methode paint() der Elternklasse überschreiben, werden die darin enthaltenen Zeichenanweisungen automatisch ausgeführt, sobald ein Objekt der aktuellen Klasse erstellt und dessen Methode setVisible(true) aufgerufen wird. Diese Instanziierung erledigen Sie am sinnvollsten in der Methode main().
Hier sehen Sie ein Beispiel, das ein Fenster auf den Bildschirm zeichnet und ein rotes Rechteck, ein grünes Dreieck und einen blauen Kreis hineinzeichnet:
import java.awt.*;
public class EinfacheZeichnung extends Frame {
public static void main (String args[]) {
// Eine Instanz von EinfacheZeichnung erstellen
EinfacheZeichnung z = new EinfacheZeichnung();
// Größe und Titel des Fensters festlegen
z.setSize (480, 360);
z.setTitle ("Die erste Java-Zeichnung");
// Das Fenster anzeigen
z.setVisible(true);
}
public void paint (Graphics g) {
// Rotes Rechteck zeichnen
g.setColor (Color.RED);
g.fillRect (10, 10, 400, 150);
// Grünes Dreieck zeichnen
int[] xcoords = {150, 270, 30};
int[] ycoords = {180, 350, 350};
g.setColor (Color.GREEN);
g.fillPolygon (xcoords, ycoords, xcoords.length);
// Blauen Kreis zeichnen
g.setColor (Color.BLUE);
g.fillOval (250, 220, 120, 120);
}
}
Abbildung 10.3 zeigt zunächst das Ergebnis. Wie Ihnen beim Testen des Programms wahrscheinlich aufgefallen ist, lässt sich das erzeugte GUI-Fenster nicht durch Klick auf seinen Schließen-Button entfernen. Erst wenn Sie das zugehörige Konsolenprogramm per Tastenkombination + beenden, verschwindet auch das Fenster. Das liegt daran, dass AWT-Objekte nur dann Nachrichten wie Mausklicks oder Tastaturcodes verarbeiten, wenn Sie explizit das entsprechende Event Handling einrichten. Wie das funktioniert, erfahren Sie an späterer Stelle im Abschnitt »Event Handling«.
Die Methode main() erzeugt zunächst eine neue Instanz der Klasse, in der sie definiert wurde. Diese Vorgehensweise wurde auch schon an entsprechender Stelle bei der Hintergrundsuche eingesetzt; sie dient vor allem dazu, ein großes Problem zu lösen: Die Methode main() muss static deklariert werden, weil das laufende Programm selbst keine Instanz ist. Andererseits benötigen Sie eine Instanz, um mit den nicht statischen Eigenschaften und Methoden der Klasse arbeiten zu können. Wenn es Ihnen lieber ist, können Sie diese Funktionalität auch auf zwei Klassen in separaten Dateien verteilen, also eine separate »Programm«-Klasse mit einer main()-Methode schreiben, die eine Instanz der anderen Klasse erzeugt.
Abbildung 10.3 Eine einfache Zeichnung mithilfe von AWT-Klassen
Da die Klasse EinfacheZeichnung von Frame abgeleitet wurde, erbt sie die Methoden, die anschließend für die Instanz z aufgerufen werden: setSize(int breite, int hoehe) stellt die Fenstergröße in Pixeln ein, setTitle(String fenstertitel) bestimmt den Text, der in der Titelleiste angezeigt wird, und setVisible(true) zeigt das fertige Objekt auf dem Bildschirm an.
Interessanter sind die verschiedenen Methoden der Klasse Graphics, die in der Zeichenmethode paint() aufgerufen werden. Die wichtigsten werden in Tabelle 10.2 erläutert. Es handelt sich hier lediglich um die grundlegenden Methoden der AWT-Klasse Graphics, nicht um die erweiterten aus der Swing-Klasse Graphics2D.
Die Methode setColor() zum Einstellen der Zeichenfarbe existiert in zwei verschiedenen Varianten: Die Version, die einen int-Wert entgegennimmt, wird üblicherweise mit den symbolischen Konstanten der Grundfarben aufgerufen, die in der Klasse Color definiert sind, beispielsweise Color.RED, Color.WHITE oder Color.MAGENTA. Die andere Variante erwartet eine Instanz der Klasse Color als Argument; häufig wird mithilfe von new() eine anonyme Instanz erzeugt und übergeben. Der wichtigste Konstruktor von Color erwartet drei int-Werte, die den Rot-, den Grün- und den Blau-Anteil der gewünschten RGB-Farbe zwischen 0 und 255 angeben:
g.setColor (new Color (255, 153, 0)); // orange
Listing 10.2 enthält ein komplexeres Beispiel: Zunächst wird ein Koordinatensystem erstellt, anschließend werden eine Sinus- und eine Kosinuskurve hineingezeichnet. In Abbildung 10.4 sehen Sie die Ausgabe des Programms.
import java.awt.*;
public class SinusCosinus extends Panel {
public static void main (String args[]) {
SinusCosinus sc = new SinusCosinus();
Frame f = new Frame ("Sinus und Cosinus");
f.setSize (420, 420);
f.add (sc);
f.setVisible(true);
}
public void paint (Graphics g) {
// Grundlinien des Koordinatensystems zeichnen
g.setColor (Color.BLACK);
g.drawLine (200, 0, 200, 400);
g.drawLine (0, 200, 400, 200);
// Unterteilungen der Achsen zeichnen
for (int i = 20; i < 400; i += 20) {
g.drawLine (i, 198, i, 202);
g.drawLine (198, i, 202, i);
}
// Pfeilspitzen der Achsen zeichnen
g.drawLine (400, 200, 395, 195);
g.drawLine (400, 200, 395, 205);
g.drawLine (200, 0, 195, 5);
g.drawLine (200, 0, 205, 5);
g.setColor (Color.BLUE);
g.drawString ("y = sin (x)", 10, 30);
g.setColor (Color.RED);
g.drawString ("y = cos (x)", 10, 50);
// die eigentlichen Kurven zeichnen
double oldSinus = Math.sin (-10) * 20;
double oldCosinus = Math.cos (-10) * 20;
for (int i = 1; i < 400; i++) {
g.setColor (Color.BLUE);
double sinus = Math.sin
((double) (i - 200) / 20);
sinus *= 20;
g.drawLine (i-1, 200 -
(int)oldSinus, i,
200 - (int)sinus);
oldSinus = sinus;
g.setColor (Color.RED);
double cosinus = Math.cos
((double) (i - 200) / 20);
cosinus *= 20;
g.drawLine (i-1, 200 - (int)oldCosinus,
i, 200 - (int)cosinus);
oldCosinus = cosinus;
}
}
}
Listing 10.2 Koordinatensystem, Sinus- und Kosinuskurve in AWT
Beachten Sie zunächst, dass die Klasse SinusCosinus nicht von Frame abgeleitet ist, sondern von java.awt.Panel, einer rechteckigen Zeichenfläche. Das benötigte Fensterobjekt wird separat als Instanz von Frame erstellt. Der hier verwendete Konstruktor new Frame (String titel) setzt automatisch den Fenstertitel. Anschließend wird die SinusCosinus-Instanz mithilfe der Frame-Methode add() in das Fenster gesetzt. Diese Vorgehensweise garantiert, dass die linke obere Ecke des Zeichenbereichs auf jeden Fall innerhalb des Fensters zu sehen ist; bei einer direkten Ableitung von Frame wäre dies womöglich anders.
Die paint()-Methode benutzt nur wenige neue Anweisungen: Die statischen Methoden sin() und cos() der Klasse Math berechnen den Sinus beziehungsweise Kosinus eines Werts. Die Argumente müssen Winkel im Bogenmaß sein (360° = 2). Die Graphics-Methode drawString() schreibt den angegebenen Text auf eine Grafikfläche.
Verwirrend ist dagegen womöglich die erforderliche Umrechnung des Maßstabs: Der Wert 1 wird durch 20 Pixel dargestellt. Aus diesem Grund werden die Einteilungen des Koordinatensystems in Zwanzigerschritten gezeichnet; außerdem werden sämtliche Berechnungen mit 20 multipliziert. Ein weiteres Problem ergibt sich dadurch, dass die y-Werte in der Grafikprogrammierung Kopf stehen: Während sie beim kartesischen Koordinatensystem nach oben wachsen, liegt der Nullpunkt beim Computer in der linken oberen Ecke und wächst nach unten. Dass der Ursprung des Koordinatensystems auf (200|200) platziert wurde, ergibt weiteren Umrechnungsbedarf.
Das Zeichnen der Kurven selbst funktioniert folgendermaßen: In jedem Schleifendurchlauf wird mithilfe von drawLine() eine Linie vom Punkt des vorigen Durchlaufs (oldSinus beziehungsweise oldCosinus) zum aktuellen Punkt gezeichnet. Während die trigonometrischen Funktionen ziemlich exakt ausgerechnet werden müssen, um sie im Maßstab möglichst korrekt darzustellen, akzeptieren die AWT-Methoden nur ganzzahlige Werte. Deshalb finden einige Typecasting-Operationen von int nach double und umgekehrt statt.
Abbildung 10.4 Die Ausgabe des Sinus- und Kosinusprogramms
10.6.2 Animation
Ein weiterer Nutzen der AWT- und Swing-Zeichenklassen besteht in der Erstellung von Animationen. Das Grundprinzip ist leicht zu verstehen: Im zeitlichen Verlauf wird die Zeichenfläche erst mit einer Zeichnung versehen, die anschließend wieder von einer Füllung in Hintergrundfarbe verdeckt wird. Dies geschieht immer wieder.
Dieser theoretische Ansatz wird in der Praxis noch verbessert. Würden die verdeckende Füllung und die neue Zeichnung nacheinander auf eine sichtbare Fläche gezeichnet, würde die Animation flackern. Aus diesem Grund wird eine Technik namens Double-Buffering eingesetzt. Die nächste Phase der Animation, bestehend aus Lösch- und Neuzeichnungsvorgang, wird zunächst auf eine nicht sichtbare Zeichenfläche aufgetragen, den Buffer (eine Instanz der Klasse Image). Erst nach Fertigstellung wird das gesamte neue Bild auf die sichtbare Fläche kopiert.
Die Klasse Image können Sie übrigens auch zu einem anderen Zweck einsetzen: In eine Image-Instanz kann eine Bilddatei hineingeladen werden, die sich auf diese Weise in einer AWT-Anwendung anzeigen lässt. Unterstützt werden die klassischen Internet-Bilddateitypen GIF und JPEG. Beispielsweise sehen Sie hier, wie das Bild test.gif geladen und mithilfe der Graphics-Instanz g gezeichnet wird:
Toolkit tk = Toolkit.getDefaultToolkit();
Image test = tk.getImage ("test.gif");
g.drawImage (test, 0, 0, this);
Da der Umgang mit Bilddateien plattformspezifisch ist, wird zunächst eine Toolkit-Instanz erzeugt, die eine Referenz auf die konkreten Grafikfunktionen des verwendeten Betriebssystems bildet.
Im Übrigen wird für gewöhnlich ein eigener Thread mit der Ausführung der Animation beauftragt, damit sie möglichst gleichmäßig und unbeeindruckt von anderen Ereignissen ablaufen kann.
Listing 10.3 ist ein kleines Beispielprogramm, bei dem ein blauer »Ball« jeweils an den Fensterrändern abprallt, das heißt, die Richtung wird bei jedem Aufprall umgekehrt.
import java.awt.*;
public class BouncingBall extends Panel implements Runnable {
// Thread- und Buffer-Variablen:
private Image buffer;
private Graphics bg;
private Thread animation;
// die Eigenschaften der "Kugel":
private int x;
private int y;
private int xdir;
private int ydir;
private int sfaktor;
public static void main (String args[]) {
BouncingBall b = new BouncingBall();
b.init();
b.start();
Frame f = new Frame ("Bouncing Ball");
f.setSize (420, 420);
f.add (b);
f.setVisible(true);
}
private int getSFaktor () {
int xvm = Math.abs (x - 200);
int yvm = Math.abs (y - 200);
int f = (int)(xvm + yvm) / 2;
int sf = f / 20 + 1;
return sf;
}
public void init () {
// Werte initialisieren
/* Anmerkung: x und y dürfen nicht gleich sein,
sonst bewegt sich die "Kugel" nur zwischen
den Diagonalen, was langweilig wäre! */
x = 71;
y = 191;
xdir = 1;
ydir = 1;
}
public void start () {
animation = new Thread (this);
animation.start ();
}
public void run () {
while (true) {
try {
Thread.sleep (30);
}
catch (InterruptedException e) {
}
repaint ();
}
}
public void update (Graphics g) {
// Buffer bei Bedarf erzeugen
if (buffer == null) {
buffer = createImage (400, 400);
bg = buffer.getGraphics ();
}
// Auf den Buffer zeichnen
paint (bg);
// Den Buffer einblenden
g.drawImage (buffer, 0, 0, this);
}
public void paint (Graphics g) {
g.setColor (Color.YELLOW);
g.fillRect (0, 0, 400, 400);
g.setColor (Color.BLUE);
// die "Kugel" zeichnen
g.fillOval (x, y, 20, 20);
// Werte verändern
sfaktor = getSFaktor ();
g.setColor (Color.BLACK);
g.drawString ("" + sfaktor, 20, 20);
x += (xdir * sfaktor);
y += (ydir * sfaktor);
// Ränder prüfen und ggfls. Richtungen umkehren
if (x <= 0 || x >= 380)
xdir = -xdir;
if (y <= 0 || y >= 380)
ydir = -ydir;
}
}
Listing 10.3 Eine AWT-Animation, BouncingBall.java
Der Schlüssel zur Double-Buffering-Technik ist die Methode update(): Sie wird automatisch von der Methode repaint() aufgerufen. Als Erstes erfolgt hier ein Aufruf der Methode paint(), und zwar mit dem Graphics-Objekt des Buffers. Anschließend wird der Buffer mithilfe der Graphics-Methode drawImage() auf der eigentlichen Zeichenfläche platziert.
Damit die Animation nicht zu schnell abläuft, wird in run() zunächst die statische Thread-Methode sleep() aufgerufen. Das Argument ist eine Wartezeit in Millisekunden, die der Thread »schlafen« soll. Der Aufruf muss stets in einem try/catch-Block stehen, weil der Thread unter Umständen vorzeitig unterbrochen wird, was eine InterruptedException auslöst.
Die gezeigte Animation dürfte selbsterklärend sein. Interessant ist allenfalls die Methode getSFaktor(), die den Geschwindigkeitsfaktor in Abhängigkeit vom Abstand zu den Wänden berechnet, sodass der Ball zu den Rändern hin schneller zu werden scheint: Die Wände wirken ein wenig »magnetisch«. Zur Kontrolle wird der aktuelle Geschwindigkeitsfaktor jeweils angezeigt. Hier die entscheidenden Zeilen:
int f = (int)(xvm + yvm) / 2;
int sf = f / 20 + 1;
xvm und yvm sind dabei die Absolutwerte der Abstände von den Wänden.
10.6.3 Programmierung fensterbasierter Anwendungen
Die Hauptaufgabe von Fenstern besteht natürlich nicht darin, Zeichnungen oder Animationen anzuzeigen. Fenster stellen vielmehr den Rahmen zur Platzierung verschiedener Widgets dar, die mit Funktionalität versehen werden müssen.
Im AWT und in Swing stehen die einzelnen Arten von GUI-Komponenten als Klassen zur Verfügung. In der Regel werden entsprechende Instanzen erzeugt, anschließend wird die Methode add() der jeweils übergeordneten Komponente aufgerufen, um sie zu dieser hinzuzufügen. Auf diese Weise entsteht ein ineinander verschachteltes Gefüge von GUI-Bestandteilen.
Die wichtigsten AWT-Klassen werden in Tabelle 10.3 dargestellt. Zu vielen von ihnen gibt es abgeleitete Swing-Klassen, die durch ein vorangestelltes J gekennzeichnet werden (zum Beispiel JFrame zu Frame oder JButton zu Button).
Am Beispiel eines Fensters mit Menüleiste und zwei Buttons sehen Sie hier, wie eine solche Objekthierarchie aufgebaut wird:
import java.awt.*;
public class Test2 {
public static void main(String args[]) {
// Objekte erzeugen
Frame f = new Frame ("Menü und Buttons");
MenuBar mb = new MenuBar();
Menu m = new Menu ("Test");
MenuItem mi1 = new MenuItem ("Probe 1");
MenuItem mi2 = new MenuItem ("Probe 2");
Button b1 = new Button ("Info 1");
Button b2 = new Button ("Info 2");
// Objekte verknüpfen
// Menüpunkte zum Menü hinzufügen
m.add (mi1);
m.add (mi2);
// Menü zur Menüleiste hinzufügen
mb.add (m);
// Menüleiste und Buttons zum Frame hinzufügen
f.setMenuBar (mb);
f.add (b1);
f.add (b2);
f.setSize (400, 400);
f.setVisible(true);
}
}
Wenn Sie diesen Code ausführen, werden Sie feststellen, dass nicht beide Buttons zuverlässig angezeigt werden (wahrscheinlich wird sogar nur einer angezeigt). Das liegt daran, dass nicht festgelegt wurde, wie die beiden Buttons im Fenster angeordnet werden sollen. Für diese Anordnung sind spezielle Klassen zuständig, die als LayoutManager bezeichnet werden. Um die beiden Buttons beispielsweise nebeneinander zu setzen, müssen Sie über der Zeile f.add(b1); folgende Ergänzung vornehmen:
Um die Buttons dagegen untereinanderzustellen, lautet die Layoutanweisung so:
f.setLayout (new GridLayout (2, 1));
Das erste Argument des GridLayout-Konstruktors gibt also die Anzahl der Zeilen an, das zweite die Anzahl der Spalten. Nach der Einrichtung des LayoutManagers können Sie einfach nacheinander Elemente hinzufügen, die von links nach rechts und von oben nach unten nacheinander in das GridLayout eingesetzt werden.
Ein anderer bedeutender LayoutManager ist das BorderLayout. Es besteht aus fünf verschiedenen Anzeigebereichen, die durch die statischen symbolischen Konstanten BorderLayout.NORTH (oben), BorderLayout.SOUTH (unten), BorderLayout.WEST (links), BorderLayout.EAST (rechts) und BorderLayout.CENTER (Mitte) angegeben werden. Die Elemente, die sich oben und unten befinden, werden bis zum linken und rechten Rand durchgezogen; das linke, mittlere und rechte Element teilen sich den Platz dazwischen. Zum Beispiel:
Button b1 = new Button ("oben");
Button b2 = new Button ("unten");
Button b3 = new Button ("links");
Button b4 = new Button ("rechts");
Button b5 = new Button ("Mitte");
f.setLayout (new BorderLayout());
f.add (b1, BorderLayout.NORTH);
f.add (b2, BorderLayout.SOUTH);
f.add (b3, BorderLayout.WEST);
f.add (b4, BorderLayout.EAST);
f.add (b5, BorderLayout.CENTER);
Dies sind natürlich nur wenige Beispiele für die Verwendung von LayoutManagern; es gibt zahlreiche weitere Varianten, deren Behandlung hier zu weit führen würde.
LayoutManager können übrigens leicht ineinander verschachtelt werden: Erzeugen Sie eine Panel-Instanz, und weisen Sie ihr den zu verschachtelnden LayoutManager zu. Anschließend können Sie Elemente zu diesem Panel hinzufügen. Zu guter Letzt lässt sich das Panel auf einer übergeordneten Anzeigefläche platzieren, die ihr eigenes Layout besitzt. Die Beispiele im nächsten Abschnitt verwenden solche verschachtelten Layouts.
Event Handling
Nun wissen Sie zwar bereits, wie sich Buttons hinzufügen lassen, aber noch nicht, wie Sie Code schreiben können, der darauf reagiert. Sämtliches Event Handling wird im AWT durch sogenannte Listener erledigt, die im Hintergrund auf bestimmte Ereignisse »lauschen« und diese verarbeiten.
Konkret bedeutet das für die Programmierung, die passenden Listener-Interfaces zu implementieren und die in diesen Interfaces deklarierten Callback-Methoden bereitzustellen, die aufgerufen werden, wenn das entsprechende Ereignis eintritt. Einige gängige Listener-Interfaces sind:
- java.awt.ActionListener verarbeitet Button- und Menübefehle sowie Tastaturereignisse. Das Interface deklariert nur eine einzige Methode: void actionPerformed (ActionEvent e). Das übermittelte ActionEvent können Sie mithilfe von e.getActionCommand() untersuchen – Sie erhalten den String zurück, mit dem der Button oder das Menüelement beschriftet ist.
- java.awt.MouseListener ist für die Verarbeitung von Mausklicks und Rollover-Operationen zuständig. Leider
müssen Sie eine ganze Reihe von Methoden implementieren, um die Anforderungen dieses
Interface zu erfüllen. Falls Sie die Funktionalität einer dieser Methoden nicht benötigen,
kann der Methodenrumpf allerdings leer bleiben. Die einzelnen Methoden sind folgende:
- void mousePressed (MouseEvent e) – die Maustaste wurde gedrückt.
- void mouseReleased (MouseEvent e) – die Maustaste wurde wieder losgelassen.
- void mouseClicked (MouseEvent e) – ein vollständiger Mausklick aus der Kombination Drücken und Loslassen hat stattgefunden.
- void mouseEntered (MouseEvent e) – der Mauszeiger hat den Bereich der Komponente berührt.
- void mouseExited (MouseEvent e) – der Mauszeiger hat den Bereich der Komponente wieder verlassen.
Der konkrete Umgang mit diesen Methoden wird in den folgenden beiden Beispielen erläutert.
- MouseMotionListener registriert einfache Mausbewegungen in einer Komponente. Es müssen zwei Methoden
implementiert werden:
- void mouseMoved (MouseEvent e) – die Maus wurde bewegt.
- void mouseDragged (MouseEvent e) – die Maus wurde mit gedrückter Maustaste bewegt, also gezogen.
Die wichtigsten Methoden der erhaltenen MouseEvent-Instanz sind getX() und getY(); sie liefern die Koordinaten des Mauszeigers.
- WindowListener enthält eine Reihe von Methoden für die Behandlung wichtiger Fensterereignisse:
- void windowOpened (WindowEvent e) – wird unmittelbar nach der Erzeugung des Fensters aufgerufen.
- void windowActivated (WindowEvent e) – wird aufgerufen, wenn das Fenster durch Anklicken oder eine andere Methode zum aktiven Fenster wird.
- void windowDeactivated (WindowEvent e) – wird aufgerufen, sobald ein anderes Fenster aktiv wird.
- void windowIconified (WindowEvent e) – wird beim Minimieren des Fensters (eine plattformabhängige Funktion) aktiviert.
- void windowDeiconified (WindowEvent e) – wird beim Wiederherstellen des Fensters nach der Minimierung aufgerufen.
- void windowClosing (WindowEvent e) – wird aufgerufen, wenn der Schließen-Button des Fensters gedrückt wird. Meist erfolgt an dieser Stelle ein Aufruf der Methode dispose() des entsprechenden Fensters, um es tatsächlich zu schließen.
- void windowClosed (WindowEvent e) – wird automatisch nach einem Aufruf von dispose() aufgerufen, also nachdem das Fenster bereits geschlossen wurde. Eine gute Gelegenheit, um »aufzuräumen« und – falls dies das eigentliche Programmfenster war – das Programm durch Aufruf von System.exit(0) zu beenden.
Nachdem Sie einen Listener implementiert haben, müssen Sie ihn zu den Komponenten hinzufügen, deren Ereignisse er verarbeiten soll. Dafür gibt es eine Reihe von Methoden, etwa addActionListener(), addMouseListener, addMouseMotionListener() oder addWindowListener(). Das Argument dieser Methoden ist die jeweilige Instanz, die den Listener implementiert – sehr häufig this, wenn die aktuelle Klasse selbst den Listener bildet.
Das Beispiel in Listing 10.4 ist ein einfacher Rechner: In zwei Textfelder können Sie die beiden Zahlen eingeben, mit denen gerechnet werden soll. Ein Klick auf einen von vier Operations-Buttons führt die entsprechende Grundrechenart mit den beiden Zahlen aus und schreibt das Ergebnis in ein drittes Textfeld. Dank der Implementierung von WindowListener lässt sich das Programm einfach durch Schließen des Fensters beenden.
import java.awt.*;
import java.awt.event.*;
public class Rechner extends Frame implements ActionListener, WindowListener {
// GUI-Komponenten
private Panel eingabe1;
private Panel eingabe2;
private Panel ausgabe;
private TextField einTF1;
private TextField einTF2;
private TextField ausTF;
private Label ein1;
private Label ein2;
private Label aus;
private Panel buttonLeiste;
private Button plus;
private Button minus;
private Button mal;
private Button durch;
public static void main (String args[]) {
Rechner r = new Rechner();
r.setTitle ("AWT-Taschenrechner");
r.setSize (320, 240);
r.setVisible(true);
}
// Konstruktor
public Rechner() {
// Komponenten erzeugen
eingabe1 = new Panel();
eingabe2 = new Panel();
ausgabe = new Panel();
einTF1 = new TextField();
einTF2 = new TextField();
ausTF = new TextField();
ein1 = new Label ("Zahl 1:");
ein2 = new Label ("Zahl 2:");
aus = new Label ("Ergebnis:");
buttonLeiste = new Panel();
plus = new Button ("+");
minus = new Button ("-");
mal = new Button ("*");
durch = new Button ("/");
// Komponenten platzieren
eingabe1.setLayout (new GridLayout (1, 2));
eingabe1.add (ein1);
eingabe1.add (einTF1);
eingabe2.setLayout (new GridLayout (1, 2));
eingabe2.add (ein2);
eingabe2.add (einTF2);
ausgabe.setLayout (new GridLayout (1, 2));
ausgabe.add (aus);
ausgabe.add (ausTF);
buttonLeiste.setLayout (new GridLayout (1, 4));
buttonLeiste.add (plus);
buttonLeiste.add (minus);
buttonLeiste.add (mal);
buttonLeiste.add (durch);
this.setLayout (new GridLayout (4, 1));
this.add (eingabe1);
this.add (eingabe2);
this.add (buttonLeiste);
this.add (ausgabe);
// Event Handler registrieren
plus.addActionListener (this);
minus.addActionListener (this);
mal.addActionListener (this);
durch.addActionListener (this);
this.addWindowListener (this);
}
// ActionListener-Implementierung
public void actionPerformed (ActionEvent e) {
// Eingabewerte lesen
int z1 = Integer.parseInt (einTF1.getText());
int z2 = Integer.parseInt (einTF2.getText());
int erg = 0;
// Berechnung je nach Button
String cmd = e.getActionCommand();
if (cmd.equals ("+"))
erg = z1 + z2;
if (cmd.equals ("-"))
erg = z1 - z2;
if (cmd.equals ("*"))
erg = z1 * z2;
if (cmd.equals ("/"))
erg = z1 / z2;
// Ergebnis anzeigen
ausTF.setText ("" + erg);
}
// WindowListener-Implementierung
public void windowOpened (WindowEvent e) {
// Wird hier nicht benötigt
}
public void windowActivated (WindowEvent e) {
// Wird hier nicht benötigt
}
public void windowDeactivated (WindowEvent e) {
// Wird hier nicht benötigt
}
public void windowIconified (WindowEvent e) {
// Wird hier nicht benötigt
}
public void windowDeiconified (WindowEvent e) {
// Wird hier nicht benötigt
}
public void windowClosing (WindowEvent e) {
// Fenster schließen
this.dispose();
}
public void windowClosed (WindowEvent e) {
// Programm beenden
System.exit (0);
}
}
Listing 10.4 Ein AWT-basierter Taschenrechner, Rechner.java
Wenn Sie den Code lesen, werden Sie feststellen, dass die aktuelle Instanz der Klasse (this) im Konstruktor jeweils als Event Handler registriert wird: als ActionListener für die vier Buttons und als WindowListener für sich selbst. Der Rest des Programms sollte sich nach den vorigen Ausführungen und mithilfe der Kommentare von selbst erklären. In Abbildung 10.5 sehen Sie den fertigen Rechner.
Abbildung 10.5 Der AWT-Rechner bei der Division von 65.536 durch 2
Die Klassendeklaration des Rechners enthält die Implementierungen der beiden Interfaces ActionListener und WindowListener:
public class Rechner extends Frame
implements ActionListener, WindowListener {
Auf diese Weise können Sie den gewünschten Komponenten einfach this als Listener zuweisen. Beachten Sie, dass dies nicht in der Methode main() geschehen darf – da sie statisch ist, existiert hier keine Instanz. Erzeugen Sie also innerhalb von main() eine Instanz der aktuellen Klasse selbst:
Rechner r = new Rechner();
Schreiben Sie den Code zum Hinzufügen der Listener anschließend in einen Konstruktor. Beispielsweise stellt die folgende Zeile aus dem Konstruktor Rechner() den Action-Listener für den Plus-Button ein:
plus.addActionListener (this);
Noch deutlicher wird die Mausabfrage in Listing 10.5. Es handelt sich um ein kleines Programm zum Zeichnen mit der Maus, wobei Sie über ein Menü die Farbe wechseln, die Zeichenfläche löschen und das Programm beenden können.
import java.awt.*;
import java.awt.event.*;
public class Malen extends Frame implements Runnable, MouseMotionListener,
ActionListener, WindowListener {
// GUI-Komponenten
private MenuBar mbar;
private Menu bild;
private MenuItem loeschen;
private MenuItem beenden;
private Menu farben;
private MenuItem rot;
private MenuItem gruen;
private MenuItem blau;
private MenuItem cyan;
private MenuItem magenta;
private MenuItem gelb;
private MenuItem schwarz;
// Thread, Buffer
private Thread mal;
private Image buffer;
private Graphics bg;
// Globale Variablen
private int xnew, ynew; // Neue Koordinaten
private int xold, yold; // Alte Koordinaten
private boolean malt; // Maustaste oben/unten?
private Color farbe; // Aktuelle Malfarbe
public static void main (String args[]) {
Malen m = new Malen();
m.start(); // Thread starten
m.setTitle ("Kleines Malprogramm");
m.setSize (500, 500);
m.setVisible(true);
}
// Konstruktor
public Malen() {
// Komponenten erzeugen
mbar = new MenuBar();
bild = new Menu ("Bild");
loeschen = new MenuItem ("Löschen");
beenden = new MenuItem ("Beenden");
farben = new Menu ("Farbe");
rot = new MenuItem ("Rot");
gruen = new MenuItem ("Grün");
blau = new MenuItem ("Blau");
cyan = new MenuItem ("Cyan");
magenta = new MenuItem ("Magenta");
gelb = new MenuItem ("Gelb");
schwarz = new MenuItem ("Schwarz");
// Komponenten montieren
bild.add (loeschen);
bild.add (beenden);
mbar.add (bild);
farben.add (rot);
farben.add (gruen);
farben.add (blau);
farben.add (cyan);
farben.add (magenta);
farben.add (gelb);
farben.add (schwarz);
mbar.add (farben);
this.setMenuBar (mbar);
// Event Handler registrieren
this.addMouseMotionListener (this);
loeschen.addActionListener (this);
beenden.addActionListener (this);
rot.addActionListener (this);
gruen.addActionListener (this);
blau.addActionListener (this);
cyan.addActionListener (this);
magenta.addActionListener (this);
gelb.addActionListener (this);
schwarz.addActionListener (this);
this.addWindowListener (this);
// Werte initialisieren
xnew = 0;
ynew = 0;
xold = 0;
yold = 0;
malt = false;
farbe = Color.BLACK;
}
public void start() {
mal = new Thread (this);
mal.start();
}
public void run() {
while (true) {
try {
mal.sleep (3);
}
catch (InterruptedException e) {
}
repaint();
}
}
public void update (Graphics g) {
// Buffer bei Bedarf erzeugen
if (buffer == null) {
buffer = createImage (500, 500);
bg = buffer.getGraphics();
}
// Auf den Buffer zeichnen
paint (bg);
// Den Buffer einblenden
g.drawImage (buffer, 0, 0, this);
}
public void paint (Graphics g) {
if (malt) {
g.setColor (farbe);
g.drawLine (xold, yold, xnew, ynew);
g.drawLine (xold + 1, yold, xnew + 1, ynew);
g.drawLine (xold, yold + 1, xnew, ynew + 1);
g.drawLine (xold + 1, yold + 1, xnew + 1, ynew + 1);
}
}
// MouseMotionListener-Implementierung
public void mouseMoved (MouseEvent e) {
malt = false;
// "Alten" X- und Y-Wert setzen
xold = e.getX();
yold = e.getY();
}
public void mouseDragged (MouseEvent e) {
if (malt) {
// Alten X- und Y-Wert merken
xold = xnew;
yold = ynew;
} else {
// Ab hier neu malen
xold = e.getX();
yold = e.getY();
}
malt = true;
// Neuen X- und Y-Wert setzen
xnew = e.getX();
ynew = e.getY();
}
// ActionListener-Implementierung
public void actionPerformed (ActionEvent e) {
String cmd = e.getActionCommand();
if (cmd.equals ("Löschen")) {
bg.setColor (Color.WHITE);
bg.fillRect (0, 0, 500, 500);
}
if (cmd.equals ("Beenden")) {
this.dispose();
}
if (cmd.equals ("Rot")) {
farbe = Color.RED;
}
if (cmd.equals ("Grün")) {
farbe = Color.GREEN;
}
if (cmd.equals ("Blau")) {
farbe = Color.BLUE;
}
if (cmd.equals ("Cyan")) {
farbe = Color.CYAN;
}
if (cmd.equals ("Magenta")) {
farbe = Color.MAGENTA;
}
if (cmd.equals ("Gelb")) {
farbe = Color.YELLOW;
}
if (cmd.equals ("Schwarz")) {
farbe = Color.BLACK;
}
}
// WindowListener-Implementierung
public void windowOpened (WindowEvent e) {
// Wird hier nicht benötigt
}
public void windowActivated (WindowEvent e) {
// Wird hier nicht benötigt
}
public void windowDeactivated (WindowEvent e) {
// Wird hier nicht benötigt
}
public void windowIconified (WindowEvent e) {
// Wird hier nicht benötigt
}
public void windowDeiconified (WindowEvent e) {
// Wird hier nicht benötigt
}
public void windowClosing (WindowEvent e) {
// Fenster schließen
this.dispose();
}
public void windowClosed (WindowEvent e) {
// Programm beenden
System.exit (0);
}
}
Listing 10.5 Ein AWT-basiertes Malprogramm, Malen.java
Das eigentliche Malen findet wie bei der Animation auf einem Buffer statt, der jeweils eingeblendet wird. Die Wartezeit des Mal-Threads ist mit drei Millisekunden sehr kurz gewählt, damit auch relativ schnelle Mausbewegungen registriert werden. Andernfalls könnten in den gezeichneten Linien auffällige Lücken entstehen. Damit die Linien nicht zu dünn aussehen, werden übrigens vier im Quadrat nebeneinanderliegende Linien gezeichnet. Abbildung 10.6 zeigt das Malprogramm in Aktion.[Anm.: Die hübsche Schnecke hat meine Frau gezeichnet. Sie gibt Aufschluss über die Kompilier- und Ausführungsgeschwindigkeit des Programms, die Sie neben dessen eingeschränkter Featureliste noch einmal überdenken sollten, bevor Sie Ihr Adobe Photoshop bei eBay verkaufen und durch dieses Tool ersetzen.]
Abbildung 10.6 Das Malprogramm im Einsatz
10.6.4 Java-Applets
Ein Java-Applet ist ein spezielles kleines Java-Programm, das in einem Webbrowser ausgeführt werden kann. Gegenüber einer »normalen« Java-Anwendung besitzt es einige Besonderheiten:
- Jedes Applet muss von der Klasse java.applet.Applet abgeleitet werden. Da diese wiederum eine abgeleitete Klasse von java.awt.Panel ist, können Sie darin beliebige grafische Komponenten verwenden.
- Ein Applet benötigt keine main()-Methode. Falls Sie eine schreiben sollten, wird sie ohnehin nicht aufgerufen. Stattdessen kann ein Applet die besondere Methode init() besitzen, die unmittelbar nach dem Laden des Applets ausgeführt wird. Anders als main() in Java-Anwendungen ist init() keine statische Methode, weil ein Applet im Webbrowser gewissermaßen eine Instanz ist.
- Die Anzeigegröße des Applets wird von der Webbrowser-Umgebung festgelegt: Es wird in eine Webseite eingebettet, in der Höhe und Breite angegeben werden.
Wenn Sie sich an diese einfachen Regeln halten, lassen sich sämtliche AWT-Anwendungen aus diesem Abschnitt leicht in Applets umwandeln.
Das folgende kleine Applet besitzt lediglich eine paint()-Methode, die einen linearen Farbverlauf von Rot nach Grün berechnet und zeichnet:
import java.applet.*;
import java.awt.*;
public class GradientApplet extends Applet {
public void paint (Graphics g) {
int rot1 = 255;
int gruen1 = 0;
int blau1 = 0;
int rot2 = 0;
int gruen2 = 255;
int blau2 = 0;
int steps = 400;
double rotStep = (rot2 - rot1) / (double)steps;
double gruenStep = (gruen2 - gruen1)
/ (double)steps;
double blauStep = (blau2 - blau1)
/ (double)steps;
double ra = (double)rot1;
double ga = (double)gruen1;
double ba = (double)blau1;
for (int i = 1; i <= steps; i++) {
g.setColor (new Color ((int)ra, (int)ga,
(int)ba));
g.drawLine (0, i, 400, i);
ra += rotStep;
ga += gruenStep;
ba += blauStep;
}
}
}
Nach dem Kompilieren müssen Sie das Applet in ein HTML-Dokument einbetten. Die minimale Fassung eines solchen Dokuments sieht so aus:
<html>
<head>
<title>Farbverlaufs-Applet</title>
</head>
<body>
<applet code="GradientApplet.class" width="400"
height="400"></applet>
</body>
</html>
Wie das Einbetten von Applets detailliert funktioniert, wird in Kapitel 17, »Webseitenerstellung mit (X)HTML und CSS«, beschrieben.
Ihr Kommentar
Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.