Java-Einführung von Hubert Partl


User-Interfaces


Graphical User Interfaces (GUI)

In der zeilen-orientierten prozeduralen Programmierung muss der Benutzer das Programm richtig "bedienen". Wie kann ich das User-Interface so gestalten, dass das Programm dem Menschen dient?
Bei Programmen mit graphischen Benutzungsoberflächen (GUI) kommt es auf das Zusammenspiel von 3 Bereichen an:

In sehr einfachen Fällen können alle diese Aspekte innerhalb des selben Programms (der selben Klasse) behandelt werden.

Für komplexere Anwendungen wird aber empfohlen, diese 3 Bereiche getrennt zu programmieren und damit die Vorteile der objekt-orientierten Programmierung auszunützen (siehe das Kapitel über Objekte und Klassen). Professionelle Software-Hersteller setzen für die Programmierung der drei Bereiche oft sogar verschiedene Personen ein, die jeweils Spezialisten für ihren Bereich sind.

Abstract Windowing Toolkit (AWT)

Das Abstract Windowing Toolkit (AWT) ist ein Package, das Klassen für die Zusammenstellung und Verwendung von graphischen Benutzungsoberflächen (GUI) enthält. Solche GUIs bestehen aus Fenstern, Menüs, Eingabefeldern, Buttons und dergleichen, und die Steuerung durch den Benutzer erfolgt meistens mit Tastatur (Keyboard) und Maus oder mit ähnlichen Hardware-Komponenten wie z.B. Trackballs, Touchscreen oder Spracheingabe.

Das Wort abstrakt (abstract) im Namen AWT deutet darauf hin, dass in diesen Klassen nur die plattformunabhängigen wesentlichen Eigenschaften der Komponenten definiert sind und für die konkrete Darstellung dann die am jeweiligen Rechner vorhandenen Systemkomponenten ("Peers") mit den auf diesem Rechner üblichen Layouts, Aussehen und Funktionalitäten ("look and feel") verwendet werden. Ein im Java-Programm mit AWT definierter Button wird also auf einem PC wie normale Windows-Buttons, auf einem Macintosh wie normale Apple-Buttons und unter Unix wie normale Motif- oder CDE-Buttons aussehen, und das Gleiche gilt für Fenster, Scrollbars, Menüs etc.

Dies hat für den Benutzer den Vorteil, dass er auf seinem Computersystem immer die selben, gewohnten GUI-Komponenten vorfindet, unabhängig von der Applikation. Es hat aber den Nachteil, dass die gleiche Applikation auf verschiedenen Computersystemen dadurch in den Details verschieden aussieht und dass im Fall von Applets keine genaue Anpassung der Größen, Farben, Schriftarten und Graphik-Elemente an die umgebende Web-Page möglich ist.

Deshalb unterstützen neuere Java-Versionen (Swing, JDK 1.2) zusätzlich zu den Peer-Komponenten auch sogenannte "leichte" (light weight) Komponenten, die komplett in Java geschrieben sind und bei denen die Layout-Details genauer definiert werden können. Damit kann man Applets und Applikationen so gestalten, dass jede Anwendung plattformübergreifend ihr eigenes Look-and-Feel hat.

Beide Methoden haben ihre Vor- und Nachteile.

Die Swing-Komponenten sind Teil der sogenannten Java Foundation Classes (JFC). Sie können beim JDK 1.1 zusätzlich installiert werden und sind ab JDK 1.2 Teil des JDK. Sie werden von den meisten Web-Browsern noch nicht unterstützt.

Für AWT Version 1.1 müssen die Packages java.awt und java.awt.event importiert werden, für Swing zusätzlich das Package javax.swing.

AWT-Komponenten (Component)

Die folgenden Komponenten stehen in allen AWT-Versionen zur Verfügung. Sie sind Peer-Komponenten, d.h. das Aussehen und die Funktionalität entsprechen immer dem jeweiligen Rechnersystem.

Frame

ein Fenster mit Rahmen, das also vom Benutzer vergrößert, verkleinert, verschoben, geschlossen werden kann (hat nichts mit dem gleichnamigen HTML-Tag zu tun). Konstruktor:
new Frame(String)

Panel

ein Bereich innerhalb eines Fensters oder eines anderen Panels, ohne eigenen Rahmen. Dies dient vor allem dazu, ein Fenster oder Applet in mehrere Bereiche zu unterteilen, in denen dann weitere Komponenten mit verschiedenen Layout-Managern angeordnet werden. Konstruktor:
new Panel()
oder new Panel(LayoutManager)

Canvas

eine Fläche, die für graphische Ein- und Ausgabe verwendet werden kann. Der Inhalt und die Größe müssen vom Programmierer definiert werden. Zu diesem Zweck wird meist eine Subklasse von Canvas definiert, in der die entsprechenden Methoden überschrieben werden, insbesondere die paint-Methode (siehe unten). Konstruktor:
new Canvas()

Label

ein einfaches Feld für die Darstellung eines einzeiligen Textes. Konstruktor:
new Label(String)

Button

ein Knopf mit Text, der vom Benutzer mit der Maus gedrückt oder angeklickt werden kann. Es wird dringend empfohlen, Buttons nur dann zu verwenden, wenn das Anklicken tatsächlich etwas bewirkt, und für Texte, die man nicht anklicken soll, stattdessen Labels zu verwenden. Außerdem wird empfohlen, Buttons immer in ihrer natürlichen Größe anzulegen (also z.B. mit dem Flow-Layout-Manager) und nicht größer werden zu lassen, damit sie vom Benutzer immer sofort als typische Buttons erkannt werden. Konstruktor:
new Button(String)

Checkbox

ein Schalter mit Beschriftung, der ein- oder ausgeschaltet werden kann. Checkboxes können auch in einer CheckboxGroup zusammengefasst werden, dann bewirkt das Einschalten einer Checkbox automatisch das Ausschalten der anderen (radio button). Konstruktoren:
new Checkbox (String, boolean)
new CheckboxGroup()
new Checkbox (String, boolean, CheckboxGroup)

Choice, List

Listen von Texten, von denen einer bzw. mehrere ausgewählt werden können. Konstruktoren:
new Choice()
new List(int)

TextField

ein einzeiliges Feld für Tastatur-Eingaben. Konstruktor:
new TextField (columns)
oder new TextField (String, columns)

TextArea

ein mehrzeiliges Feld für Tastatur-Eingaben. Konstruktor:
new TextArea (rows, columns)
oder new TextArea (String, rows, columns)

Dialog

ein kleines Fenster mit einem Text, das eventuell auf eine Aktion des Benutzers wartet (z.B. eine Fehlermeldung, die erst dann verschwindet, wenn der Benutzer sie anklickt). Konstruktor:
new Dialog (Frame, String, true)

FileDialog

ein Menü zum Auswählen einer Datei. Konstruktoren:
new FileDialog (Frame, String, FileDialog.LOAD)
new FileDialog (Frame, String, FileDialog.SAVE)

MenuBar, Menu, MenuItem, CheckboxMenuItem, PopupMenu

Komponenten zum Anlegen einer Menü-Leiste in einem Frame und von Haupt-Menüs, Unter-Menüs, Help-Menüs oder Popup-Menüs sowie von Menü-Items, die ausgewählt werden können. Konstruktoren:
new MenuBar()
new Menu(String)
new MenuItem(String)
new MenuItem (String, MenuShortcut)
new MenuShortcut(keycode)
new MenuShortcut (keycode, shift)
new CheckboxMenuItem (String, boolean)
new PopupMenu(String)

Scrollbar

ein horizontaler oder vertikaler Scrollbar. Konstruktoren:
new Scrollbar (Scrollbar.HORIZONTAL)
new Scrollbar (Scrollbar.VERTICAL)
new Scrollbar (orientation, value, size, min, max)

ScrollPane

ein Rahmen mit Scrollbars zu einem zu großen Panel oder Canvas. Konstruktor:
new ScrollPane()
oder new ScrollPane (ScrollPane.policy)

Methoden

Alle Komponenten haben die folgenden Methoden:

Für die einzelnen Komponenten gibt es meist noch weitere, spezielle Methoden, wie z.B.

und andere. Die Details finden Sie in der Online-Dokumentation (API).

Frames, Panels, Menüs und dergleichen sind Container, d.h. sie können andere Komponenten enthalten (eventuell auch mehrfach geschachtelt), und enthalten zu diesem Zweck weitere Methoden, siehe das Kapitel über Container.

Andere Komponenten:

Falls Sie mit dem Layout oder der Funktionsweise der im AWT bzw. in Swing vorgesehenen Komponenten nicht zufrieden sind, können Sie mit Hilfe eines Canvas beliebige eigene Komponenten definieren, z.B. spezielle Buttons, die ein Bild statt der Beschriftung enthalten oder die ihr Aussehen oder ihre Beschriftung verändern, wenn sie von der Maus erreicht, gedrückt oder angeklickt werden oder wenn bestimmte andere Aktionen erfolgreich oder nicht erfolgreich abgelaufen sind.

Container

Frames, Panels, ScrollPanes, Menüs und dergleichen sind Container, d.h. sie können andere Komponenten enthalten (eventuell auch mehrfach geschachtelt), die dann in einer bestimmten Anordnung innerhalb des Containers dargestellt werden. Zu diesem Zweck enthalten Container - zusätzlich zu den oben angeführten, für alle Komponenten gültigen Methoden - die folgenden Methoden:

Ein typisches Beispiel für die Anwendung dieser Methoden finden Sie weiter unten. Hier nur eine kurze Skizze:

Frame f = new Frame("Titlebar");
f.setLayout( new FlowLayout() );
Button b = new Button("Press me!");
f.add(b);
f.setSize(400,300);
f.setVisible(true);

Layout-Manager

Der Layout-Manager dient dazu, die einzelnen Komponenten innerhalb eines Containers anzuordnen und die Größe des Containers zu bestimmen. Er wird mit

setLayout ( new LayoutManagerName() )

vereinbart. Wenn man mit keinem der vordefinierten Layout-Manager zufrieden ist, kann man

setLayout(null)

angeben und muss dann alle Komponenten selbst mit setLocation, setSize, setBounds innerhalb des Containers mit absoluten Pixel-Angaben positionieren. Damit verliert man aber den Vorteil der Layout-Manager, dass die Komponenten bei Größenänderungen automatisch wieder richtig angeordnet werden.

Die wichtigsten und einfachsten Layout-Manager sind:

FlowLayout

setLayout ( new FlowLayout() )

Die Elemente werden zeilenweise nebeneinander angeordnet, der Reihe nach von links nach rechts; falls die Breite nicht ausreicht, dann in mehreren Zeilen. Standardmäßig werden die Zeilen zentriert, man kann aber im Konstruktor auch mit einem Parameter angeben, dass sie links- oder rechtsbündig ausgerichtet werden sollen, z.B. mit

setLayout ( new FlowLayout(FlowLayout.LEFT) )

BorderLayout

setLayout ( new BorderLayout() )

Der Container wird in 4 Randbereiche und einen Bereich für den restlichen Platz in der Mitte eingeteilt, kann also höchstens 5 Elemente aufnehmen. Diese Elemente sind meistens Panels, die dann ihrerseits einzelne Komponenten enthalten und diese mit geeigneten Layout-Managern anordnen.

In der add-Methode muss mit einem zusätzlichen Parameter angegeben werden, welchen der 5 Bereiche die Komponente ausfüllen soll:

Man muss nicht immer alle 5 Bereiche verwenden, oft wird das Border-Layout nur dazu verwendet, 2 oder 3 Komponenten neben- oder übereinander anzuordnen.

GridLayout

setLayout ( new GridLayout (n, m) )

Der Container wird in n mal m gleich große Bereiche in n Zeilen und m Spalten aufgeteilt, und die Komponenten werden in der Reihenfolge der add-Aufrufe Zeile für Zeile in dieses Gitter eingefügt und alle auf die gleiche Größe vergrößert.

Damit kann man z.B. eine Zeile oder eine Spalte von gleich großen Buttons erzeugen.

Ab JDK 1.1 gibt es auch ein GridBagLayout, mit dem man komplexere Gitter-Strukturen definieren kann.

Ab JDK 1.2 gibt es auch ein BoxLayout, mit dem man verschieden große Komponenten in Zeilen, Spalten oder Gitter-Strukturen anordnen kann. Dazu muss man deren Größen jeweils mit getPreferredSize angeben.

CardLayout

CardLayout cards = new CardLayout();
container.setLayout (cards);

Es wird jeweils nur eine der Komponenten im Container angezeigt, alle anderen Komponenten sind unsichtbar, wie beim Aufschlagen von Spielkarten oder bei einem Dia-Vortrag.

Mit container.add (Component, String) wird mit dem String-Parameter ein Name für die Komponente vereinbart. Die Reihenfolge der add-Aufrufe ist relevant.

Mit cards.show(container, String) wird die Komponente mit dem angegebenen Namen angezeigt.

Mit cards.next(container) wird die jeweils nächste Komponente (in der Reihenfolge der add-Aufrufe) angezeigt.

Mit cards.first(container) und cards.last(container) wird die erste bzw. letzte Komponente angezeigt.

Dynamische Layout-Änderungen

Wenn sich die Größe des Containers ändert, ordnet der Layout-Manager die darin enthaltenen Komponenten automatisch neu an.

Wenn sich die Größe einer Komponente ändert, kann man nach dem folgenden Schema erreichen, dass der Layout-Manager die Komponenten im Container richtig anordnet und anzeigt:

component.invalidate();
// change component
container.validate();
Dabei kann man den Container entweder direkt angeben oder mit component.getParent() erhalten. Das validate() im Container bewirkt auch ein validate() für die Komponente und ein doLayout() für den Container.

Übung: Layout-Manager

Erstellen Sie ein einfaches GUI (1 Frame mit 2 Buttons, 1 Label, 1 TextField) und probieren Sie 3 verschiedene Layout-Manager aus; vorerst ohne Event-Handling, Abbruch mit Ctrl-C im Befehlsfenster.

Farben (Color)

Die Vordergrundfarbe (für Texte und Linien) und Hintergrundfarbe von Komponenten können mit den folgenden Methoden festgelegt werden:

setForeground (Color.xxx);
setBackground (Color.xxx);

Diese Methoden müssen immer paarweise aufgerufen werden, d.h. man muss immer beide Farben festlegen, man kann sich nicht darauf verlassen, dass der Benutzer eine bestimmte Hintergrundfarbe wie weiß oder hellgrau eingestellt hat. Wenn man z.B. nur den Vordergrund auf blau setzt, ein Benutzer aber aus irgendwelchen Gründen für seinen Bildschirm weiße Schrift auf blauem Grund eingestellt hat, dann bekommt er blau auf blau.

Die Farbe kann auf 3 Arten angegeben werden:

Die Farben von Flächen, Linien und Schriften in Graphics-Objekten werden nicht mit setForeground und setBackground sondern mit setColor angegeben (siehe unten).

Nach dem folgenden Schema können Sie die Farben im Applet auf die Farben der Web-Page abstimmen: Der HTML-Angabe

<body bgcolor="#FFCC99" text="#006633" ...>
entsprechen zum Beispiel die Java-Angaben
Color backColor = new Color (0xFF, 0xCC, 0x99);
Color textColor = new Color (0x00, 0x66, 0x33);
setBackground (backColor);
setForeground (textColor);
oder analog mit den dezimalen Angaben 0, 51, 102, 153, 204, 255, oder gleich mit den String-Angaben:
Color backColor = Color.decode ("#FFCC99");
Color textColor = Color.decode (#006633);
setBackground (backColor);
setForeground (textColor);

Ab JDK 1.1 kann man mit statischen Feldern der Klasse SystemColor auf die vom Benutzer jeweils auf seinem Rechner eingestellten Standardfarben zugreifen und die Farben im Applet auf die Farben in den anderen Bildschirmfenstern abstimmen. Beispiele:

für Frame, Panel, Applet, Label:
setBackground (SystemColor.window);
setForeground (SystemColor.windowText);

für TextField, TextArea:
setBackground (SystemColor.text);
setForeground (SystemColor.textText);

für Button u.dgl.:
setBackground (SystemColor.control);
setForeground (SystemColor.controlText);

und analog für Menüs, Scrollbars und andere Komponenten sowie für spezielle Farbgebungen (highlight, inactive u.dgl.).

Schriften (Font)

Die Schriftart und Schriftgröße von Komponenten kann mit der folgenden Methode festgelegt werden:

Font f = new Font (Fontname, Fonttyp, Fontsize);
setFont (f);

Fontnamen sind "Serif", "SansSerif" und "Monospaced" (in Java 1.0: "TimesRoman", "Helvetica" und "Courier").

Fonttypen sind Font.PLAIN (normal), Font.BOLD (fett), Font.ITALIC (kursiv) oder die Summe Font.BOLD+Font.ITALIC.

Die Fontgröße wird als ganze Zahl in Points (nicht in Pixels) angegeben, eine typische Fontgröße ist 12.

Größenangaben

Bei den meisten Komponenten ergibt sich ihre richtige Größe aus ihrem Inhalt (Textstring bei einem Label oder Button, Summe der Komponenten bei einem Container) und muss daher vom Programmierer nicht explizit angegeben werden.

Bei einem Fenster (Frame oder Dialog) muss die Größe explizit angegeben werden:

Bei einem Zeichenobjekt (Canvas) muss die Größe ebenfalls explizit angegeben werden, und zwar mit Hilfe der folgenden Methoden, die vom Layout-Manager abgefragt werden, um den benötigten Platz festzustellen:

Es gibt auch weitere Spezialfälle, in denen man dem Layout-Manager mit solchen expliziten Größenangaben helfen kann, die richtige Größe zu finden. Es hängt vom jeweils verwendeten Layout-Manager ab, ob und wie er diese Größenangaben verwendet und umsetzt.

Bei allen Komponenten und Containern kann man jederzeit mit

abfragen, wie groß die Komponente tatsächlich ist. Diese Methode liefert ein Objekt vom Typ Dimension, das die beiden Datenfelder width und height enthält (Breite und Höhe in Pixeln).

Zeichenobjekte (Canvas)

Die Klasse Canvas beschreibt eine leere Zeichenfläche der Größe null mal null Pixels. Für konkrete Zeichenobjekte muss man deshalb jeweils eine Subklasse der Klasse Canvas definieren und in ihr die folgenden Methoden überschreiben:

Beispielskizze:

public class Xxxxx extends Canvas {

  public Dimension getMinimumSize() {
    return new Dimension (300, 200);
  }
  public Dimension getPreferredSize() {
    return getMinimumSize();
  }
  public void paint (Graphics g) {
    g.setColor (...);
    g.draw... (...);
    g.fill... (...);
  }
}

Graphiken (Graphics, paint)

Bei fast allen Komponenten ist das Aussehen bereits festgelegt, die paint-Methode soll daher nicht überschrieben und die repaint-Methode nicht aufgerufen werden.

Nur bei Komponenten des Typs Canvas muss explizit mit der paint-Methode angegeben werden, welche graphischen Elemente dargestellt werden sollen. Zu diesem Zweck wird eine Subklasse der Klasse Canvas definiert und in ihr die paint-Methode überschrieben.

Die paint-Methode arbeitet mit einem Objekt des Typs Graphics und hat den folgenden Aufbau:

public void paint (Graphics g) {
  g.setColor (...);
  g.draw... (...);
  g.fill... (...);
}

Die wichtigsten Methoden des Graphics-Objekts sind:

Farbe und Schrift:

Bevor man Linien zeichnet, Flächen füllt oder Strings ausgibt, muss man die Farbe und eventuell auch die Schriftart für die unmittelbar folgenden Graphik-Befehle festlegen.

Die Festlegung der Farbe erfolgt hier also nicht mit setForeground und setBackground sondern Schritt für Schritt mit

setColor ( Color );

Die Festlegung der Schriftart erfolgt wie gewohnt mit

setFont ( Font );

Linien:

drawLine (x1, y1, x2, y2);

zeichnet eine gerade Linie vom ersten Punkt zum zweiten Punkt. Wenn die beiden Punkte identisch sind, wird nur ein Punkt gezeichnet (1 Pixel). Die Linienbreite ist immer 1 Pixel. Breitere Linien kann man erzeugen, indem man mehrere parallele Linien, jeweils um 1 Pixel verschoben, nebeneinander zeichnet, oder mit fillPolygon (siehe unten).

Mit den folgenden Methoden werden Linienzüge oder Kurven gezeichnet. Die Parameterliste gibt meistens zuerst den linken oberen Eckpunkt und dann die Breite und Höhe der Figur bzw. des sie umschreibenden Rechtecks an (Genauer: Das Rechteck hat eine Breite von (width+1) Pixels und eine Höhe von (height+1) Pixels). Kreise erhält man als Spezialfall von Ellipsen mit gleicher Breite und Höhe.

drawRect (x1, y1, width, height);

drawPolygon (x[], y[], n);

drawPolyLine (x[], y[], n);

drawOval (x1, y1, width, height);

drawArc (x1, y1, width, height, startangle, angle);

Flächen:

Mit den folgenden Methoden werden Flächen farbig gefüllt. Die fill-Methoden haben stets die gleichen Parameter wie die entsprechenden draw-Methoden.

fillRect (x1, y1, width, height);

fillPolygon (x[], y[], n);

fillOval (x1, y1, width, height);

fillArc (x1, y1, width, height, startangle, angle);

Texte (String):

drawString (String, x1, y1);

stellt einen einzeiligen Textstring dar. Ein automatischer Zeilenumbruch ist hier nicht möglich.

Bilder (Image)

drawImage (Image, x1, y1, this);

Programmtechnische Details:

Die Koordinaten werden in Bildpunkten (Pixels) angegeben. Der Ursprung (0,0) des Koordinatensystems liegt links oben, die x-Achse geht von links nach rechts, die y-Achse von oben nach unten (also umgekehrt wie in der Mathematik).

Winkelangaben erfolgen hier in Grad (im Gegensatz zu den mathematischen Funktionen in der Klasse Math, wo sie in Radiant erfolgen). Winkel werden von "Osten" aus gegen den Uhrzeigersinn gezählt.

Die Reihenfolge der Aufrufe der einzelnen Methoden für das Graphics-Objekt ist wichtig: Spätere Aufrufe überschreiben die von früheren Aufrufen eventuell bereits gesetzten Bildelemente (Pixels), d.h. die Reihenfolge der Befehle beginnt mit dem Hintergrund und endet mit dem Vordergrund.

Die paint-Methode wird in verschiedenen Situationen aufgerufen:

  1. explizit beim Aufruf der Methode repaint() sowie bei setVisible(true)
  2. automatisch immer dann, wenn sich die Sichtbarkeit des Fensters am Bildschirm des Benutzers ändert, also z.B. wenn der Benutzer das Fenster verschiebt, vergrößert oder verkleinert, oder wenn das Fenster ganz oder teilweise durch ein anderes Fenster verdeckt wird bzw. wieder zum Vorschein kommt.

Da man den zweiten Fall vom Programm aus nicht kontrollieren kann, ist es wichtig, dass die gesamte Graphik-Ausgabe innerhalb der paint-Methode definiert ist und nicht in irgendwelchen anderen Methoden, die nur explizit aufgerufen werden.

Wenn sich die dargestellte Graphik in Abhängigkeit von bestimmten Aktionen verändern soll, dann empfiehlt sich folgende Vorgangsweise: Man legt globale Datenfelder an, die die verschiedenen Situationen beschreiben und die von der paint-Methode abgefragt werden. Wenn sich das ausgegebene Bild ändern soll, dann verändert man diese Datenfelder und ruft repaint() auf, sodass die paint-Methode den neuen Zustand der Ausgabe erzeugt.

Java-2D und 3D (ab JDK 1.2)

Ab JDK 1.2 gibt es das sogenannte Java-2D-API, also eine Gruppe von Packages und Klassen, mit denen 2-dimensionale graphische Objekte genauer und bequemer konstruiert werden: die Klasse Graphics2DObject und eine Reihe von weiteren Klassen in den Packages java.awt.color, java.awt.geom und java.awt.image. Damit können z.B. Linien mit beliebiger Strichstärke gezeichnet sowie geometrische Figuren um beliebige Winkel gedreht werden.

Für die Zukunft wird auch an einer Java-3D-API für die Darstellung von 3-dimensionalen Objekten gearbeitet. Genauere Informationen darüber findet man auf dem Java-Web-Server (siehe die Referenzen).

Zeichnen in einem Container

Die Klasse Container ist eine Unterklasse von Component, man kann daher die Container (wie z.B. Frame oder Applet), wenn man will, wie eine einzelne Komponente behandeln, so wie einen Canvas. Man kann also

aber man sollte diese beiden Möglichkeiten nicht gleichzeitig verwenden, weil sonst die eine die andere stört.

Wenn man Graphiken und Standard-Komponenten in einem Container haben will, ist es am besten, wenn man für die Graphiken eigene Canvas-Komponenten erzeugt, die die entsprechende paint-Methode enthalten, und dann alle diese Komponenten mit add zum Container hinzufügt.

Wenn man doch beides kombinieren will, z.B um mit der paint-Methode den Hintergrund des Containers hinter den Komponenten festzulegen, dann muss man in der paint-Methode nach dem Zeichnen der Hintergrund-Graphik mit
super.paint(g)
für das richtige Zeichnen der Komponenten sorgen.

Mehrzeilige Texte

Mit Label und TextField und mit der Graphics-Methode drawString kann jeweils immer nur eine Textzeile ausgegeben werden. Wenn man will, dass ein längerer Text automatisch je nach dem verfügbaren Platz in mehrere Zeilen umgebrochen wird, kann man eine TextArea ohne horizontalen Scrollbar verwenden. Beispiel:

TextArea t1 = new TextArea( longString, 5, 20,
       TextArea.ScrollbarS_VERTICAL_ONLY );

In Swing bzw. JDK 1.2 gibt es diverse Möglichkeiten für die formatierte Darstellung von Texten, von Methoden wie setLineWrap(true) und setWordWrap(false) bis zur Verwendung von HTML oder RTF. Hier nur eine kurze Beispielskizze:

JEditorPane pane = new JEditorPane();
String somehtml = "<html>
  <head><title>Hello</title></head>
  <body>
  <h1>Hello!</h1>
  <p>Some <b>Java-generated</b> long text
  </body>
  </html>";
pane.setContentPane("text/html");
pane.setText(somehtml);
container.add(pane);

Übung: Canvas einfache Verkehrsampel

Erzeugen Sie ein einfaches Bild einer Verkehrsampel, ein schwarzes Rechteck mit 3 Kreisen (rot, gelb, grün) - entweder als Unterklasse von Frame, oder besser als Unterklasse von Canvas, die Sie in einem Frame darstellen.

In dieser einfachen Version sollen alle 3 Farben gleichzeitig leuchten, ohne Event-Handling (Abbruch mit Ctrl-C im Befehlsfenster). Erweiterungen dieses Beispiels, bei der die richtigen Farben jeweils nach einander eingschaltet werden, folgen in den Kapiteln über Applets. und über Threads.

Event-Handling

Wie kann mein Programm auf die Aktionen des Benutzers reagieren?

Graphische User-Interfaces bieten mehrere Möglichkeiten für Eingaben und sonstige steuernde Aktivitäten des Benutzers: Er kann eine oder mehrere Tasten auf der Tastatur drücken, die Maus bewegen, eine Maustaste drücken oder loslassen oder klicken, und er kann Kombinationen davon ausführen. Das Programm kann nicht vorhersagen, was er wann tun wird, sondern muss darauf warten, welche der möglichen Aktivitäten er in welchen Komponenten des GUI durchführt, und dann jeweils darauf reagieren. Diese Reaktion auf vom Benutzer ausgelöste Ereignisse wird als Event-Handling bezeichnet.

Mit JDK 1.1 wurde eine komplett neue Version des Event-Handling eingeführt. Dazu muss das Package java.awt.event importiert werden. Die alte Version 1.0 des Event-Handling ist weiterhin im Package java.awt verfügbar und wird vom Compiler nur mit der Warnung "deprecated" versehen (kein Fehler).

Hier wird die Version 1.1 beschrieben.

Für die verschiedenen Arten von Ereignissen (Events) sind Interfaces mit Namen der Form XxxxListener vorgesehen. Zu jeder Komponente, die auf solche Events reagieren soll (z.B. Mausklicks auf einen Button oder innerhalb eines Canvas), muss mit addXxxxListener ein Listener-Objekt vereinbart werden, das die Methoden des jeweiligen Interfaces implementiert und damit die gewünschten Aktionen ausführt. Beispiel:

...
Button b = new Button("B");
ActionListener ali = new MyListener();
b.addActionListener(ali);
...

public class MyListener implements ActionListener {
  public void actionPerformed (ActionEvent e) {
    ...
  }
}

Es gibt zwei Arten von Listenern bzw. Events:

Listener-Interfaces

Die folgenden Interfaces stehen zur Verfügung und besitzen die folgenden Methoden, die implementiert werden müssen:

Event-Klassen

Die Methoden haben jeweils einen Parameter vom Typ XxxxEvent (also ActionEvent, ItemEvent, MouseEvent etc., nur beim MouseMotionListener wird ebenfalls der MouseEvent verwendet), und diese Objekte haben Methoden, mit denen man abfragen kann, welcher Button angeklickt wurde, welcher Text in ein Textfeld eingegeben wurde, an welcher Position die Maustaste gedrückt oder losgelassen wurde, welche Taste auf der Tastatur gedrückt wurde etc.

Alle Events haben die folgende Methode:

Die wichtigsten weiteren Methoden in den einzelnen Event-Arten sind:

Wenn diese Methoden "nur" ein Objekt (die Komponente, die das Ereignis ausgelöst hat) liefern, dann kann man in diesem Objekt die genaueren Informationen finden, z.B. welcher Text im Texteingabefeld steht. Für die Details wird auf die Online-Dokumentation (API) verwiesen. Beispiel:

String s = null;
if (e.getSource() instanceof TextField) }
  s = ( (TextField)(e.getSource()) ).getText();
}

Beispiel: typischer Aufbau einer einfachen GUI-Applikation

Bei sehr einfachen GUI-Applikationen kann man alles in einer Klasse definieren, wie hier skizziert. Bei komplexeren GUI-Applikationen ist es aber besser, die Anwendung nach den Grundsätzen der objekt-orientierten Programmierung in mehrere kleinere Klassen aufzuteilen (siehe den nachfolgenden Abschnitt).

import java.awt.* ;
import java.awt.event.* ;

public class TestFrame extends Frame
    implements ActionListener, WindowListener {

  private Button b1;
  // ... more Components ...

  public TestFrame (String s) {
    super(s);
  }

  public void init() {
    setLayout (new FlowLayout() );
    b1 = new Button("Press me! ");
    b1.addActionListener (this);
    add(b1);
    // ... more Components ...
    addWindowListener(this);
    setSize(300,200); // or: pack();
    setVisible(true);
  }

  public static void main (String[] args) {
    TestFrame f = new TestFrame("Test");
    f.init();
  }

  public void actionPerformed (ActionEvent e) {
    System.out.println("Button was pressed.");
  }
  public void windowClosing (WindowEvent e) {
    dispose();
    System.exit(0);
  }
  public void windowClosed (WindowEvent e) { }
  public void windowOpened (WindowEvent e) { }
  public void windowIconified (WindowEvent e) { }
  public void windowDeiconified (WindowEvent e) { }
  public void windowActivated (WindowEvent e) { }
  public void windowDeactivated (WindowEvent e) { }
}

Anmerkungen:

Im Sinn der objekt-orientierten Programmierung erfolgen die wesentlichen Aktionen nicht direkt in der statischen main-Methode, sondern in der nicht-statischen Methode init. Der Name dieser Methode wurde so gewählt, dass eine Umwandlung der Applikation in ein Applet bei Bedarf leicht möglich ist. Der Aufruf dieser init-Methode kann entweder, wie oben, explizit in der main-Methode oder, wie im nächsten Beispiel unten, im Konstruktor erfolgen.

Bei Fenstern (Frames) soll immer ein WindowListener aktiv sein, der das im Fenster laufende Programm beendet, wenn der Benutzer das Fenster schließt - dies gehört zum normalen Verhalten von Fenstern in jedem System.

Weitere Beispiele für typische GUI-Komponenten und Event-Handlung finden Sie im Kapitel über Applets:

Das Event-Handling ist in allen diesen Beispielen der Einfachheit halber mit public Methoden in den public Klassen realisiert. Wenn man den Zugriff auf diese Methoden von fremden Klassen aus verhindern will, kann man stattdessen sogenannte "anonyme innere Klassen" verwenden, Beispiel:

frame.addWindowListener (
   new WindowAdapter() {
      public void windowClosing (WindowEvent e) {
         frame.dispose();
         System.exit(0);
      }
   }
);
Erklärungen dazu finden Sie in den Referenzen.

Beispiel: typischer Aufbau einer komplexen GUI-Applikation

Bei sehr einfachen GUI-Applikationen kann man alles in einer Klasse definieren (siehe oben). Bei komplexeren GUI-Applikationen ist es aber besser, die Anwendung nach den Grundsätzen der objekt-orientierten Programmierung in mehrere kleinere Klassen für die einzelnen Objekte aufzuteilen. In Sinne des MVC-Konzepts (Model, View, Controller) sollte man zumindest die folgenden Klassen bzw. Gruppen von Klassen verwenden:

Von den vielen Möglichkeiten, wie man das gestalten kann, wird im Folgenden eine Möglichkeit skizziert:

Applikation

Die Applikation legt zuerst ein Objekt des Datenmodells an und dann ein Objekt der Ansicht. Die Referenz auf das Datenobjekt wird der Ansicht im Konstruktor übergeben. Die init-Methode der Ansicht wird gleich vom Konstruktor ausgeführt:

public class XxxApplication {
  public static void main (String[] args] {
    XxxModel model = new XxxModel();
    String title = "Title String";
    XxxView view  = new XxxView (model, title);
  }
}

Datenmodell (Model)

Das Datenmodell enthält die Datenfelder, die get- und set-Methoden für alle Datenfelder, sowie Berechnungsmethoden, mit denen die Ergebnisse aus den Eingabewerten berechnet werden:

public class XxxModel {
  private type xxx;
  ...
  public void setXxx (type xxx) {
    this.xxx = xxx;
  }
  public type getXxx() {
    return xxx;
  }
  ...
}

Ansicht (View)

Die Ansicht baut in der init-Methode das graphische User-Interface auf. Das Event-Handling wird jedoch nicht von dieser Klasse sondern von einem Objekt der Steuerungsklasse oder von mehreren solchen Objekten durchgeführt. Dem Steuerungsobjekt werden die Referenzen sowohl auf das Datenobjekt als auch auf die Ansicht (this) als Parameter übergeben.

Im Konstruktor der Ansicht wird nicht nur der Title-String des Frame sondern auch die Referenz auf das Datenobjekt übergeben. Die Datenfelder können dann mit den get-Methoden des Datenmodells abgefragt und in den GUI-Komponenten graphisch dargestellt werden.

Für komplexere Anwendungen ist es eventuell besser, dass sich alle View-Klassen bei der zugehörigen Model-Klasse "registrieren". Eine Skizze dazu finden Sie weiter unten am Ende dieses Beispiels.

Außerdem wird im Konstruktor auch gleich die init-Methode aufgerufen, die den Frame komplett aufbaut.

Die Komponenten werden hier nicht als private sondern als protected oder package-friendly deklariert, damit sie vom Steuerungsobjekt angesprochen werden können. Die im Beans-Konzept verlangten set- und get-Methoden fehlen in dieser Skizze.

import java.awt.* ;
import java.awt.event.* ;

public class XxxView extends Frame {
  private XxxModel model;
  private XxxController controller;
  protected TextField field1;
  protected Label label1;
  ...

  public XxxView (XxxModel m, String s) {
    super(s);
    this.model = m;
    this.init();
  }

  public void init() {
    controller = new XxxController (this, model);
    setLayout ( new FlowLayout() );
    field1 = new TextField (30);
    field1.addActionListener (controller);
    add(field1);
    label1 = new Label ( model.getYyy() );
    add (label1);
    ...
    addWindowListener (controller);
    setSize (300, 200);
    setVisible (true);
  }
}
Wenn das GUI aus mehreren Objekten besteht, werden dafür wiederum eigene Klassen definiert. So werden z.B. für die graphische Darstellung von Datenfeldern Subklassen von Canvas (oder dergleichen) verwendet, denen ebenfalls die Referenz auf das Datenobjekt im Konstruktor übergeben wird.

Steuerung (Controller)

Die Steuerungsklasse implementiert die Listener-Interfaces und führt die entsprechenden Aktionen durch. Ihr werden im Konstruktor die Referenzen sowohl auf das Datenobjekt als auch auf das Ansichtobjekt übergeben. Sie kann dann je nach den Benutzer-Aktionen die Datenfelder mit den set-Methoden des "Model" setzen und die Anzeige der Daten und Ergebnisse mit den set- und repaint-Methoden der GUI-Komponenten im "View" bewirken.

import java.awt.* ;
import java.awt.event.* ;

public class XxxController
    implements YyyListener {
  private XxxModel model;
  private XxxView view;

  public XxxController (XxxView v, XxxModel m) {
    this.view = v;
    this.model = m;
  }

  ... // Methoden der Listener-Interfaces
}
Hier eine Beispielskizze, wie die Aktionen innerhalb der Methoden des Listener-Interface aussehen können:
  public void actionPerformed (ActionEvent e) {
    Object which = e.getSource();
    if (which == view.field1) {
      model.setXxx ( view.field1.getText() );
      view.label1.setText ( model.getYyy() );
    }
    ...
  }
Wenn mehrere, verschiedene Aktionen von verschiedenen GUI-Komponenten ausgelöst werden, dann können dafür jeweils eigene Steuerungsklassen definiert und entsprechend mehrere Steuerungsobjekte angelegt werden.

In dem hier skizzierten Fall muss der Controller "wissen", welche Ansichten (View-Klassen) welche Daten (Model-Klassen) darstellen. Eine bessere Lösung wäre es, dass sich alle View-Klassen bei der Model-Klasse registrieren und dann von der Model-Klasse automatisch benachrichtigt werden, wenn die Daten sich geändert haben und daher von der View-Klasse neu dargestellt werden müssen. Beispielskizze:

import java.util.*;
public class XxxModel extends Observable {
  ...
  // change some data in the model
  setChanged();
  notifyObservers (arg);
  ...
}

import java.util.*;
public class XxxView extends Xxx implements Observer {
  XxxModel m;
  ...
  public XxxView (XxxModel m) {
    this.m = m;
    m.addObserver(this);
    }
  ...
  public void update (Observable o, Object arg) {
    // show the new data in the view
    ...
  }
  ...
}

Übung: einfache GUI-Applikation

Schreiben Sie eine einfache GUI-Applikation, die ein Fenster mit folgendem Inhalt anzeigt:

Führen Sie diese Applikation aus und testen Sie auch, was passiert, wenn Sie das Fenster vergrößern oder verkleinern.

Anmerkung: Diese Übung stellt gleichzeitig eine Vorarbeit für die Übung im Kapitel Applets dar.

Swing-Komponenten (JComponent)

In Swing (Java Foundation Classes JFC) stehen als Alternative zu den AWT-Komponenten die folgenden "light weight" Komponenten zur Verfügung. Sie verwenden keine Peers vom Betriebssystem, sondern sind komplett in Java definiert. Man kann das Aussehen und die Funktionalität (look and feel) für diese Komponenten im Java-Programm genau festlegen:

Swing-Komponenten sollten niemals mit Peer-Komponenten gemischt verwendet werden, weil dann das Zeichnen der Swing-Komponenten durch die Java Virtual Machine und das Zeichnen AWT-Komponenten durch das Window-System nicht richtig koordiniert würden. .

Für Swing muss das Package javax.swing importiert werden. Dieses Package bzw. die Java Foundation Classes JFC müssen bei JDK 1.1 zusätzlich installiert werden und sind ab JDK 1.2 Teil des JDK.

Die Swing-Komponenten werden von den meisten Web-Browsern noch nicht unterstützt.

Komponenten und Datenmodelle

Die wichtigsten Swing-Kompontenten sind:

Beispielskizze

Hier eine kurze Skizze für die Verwendung von einfachen Swing-Komponenten. Die Beschreibung der verwendeten Klassen und Methoden finden Sie in der Online-Dokumentation von Swing (im JDK ab 1.2).

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class SwingTest {
  public static void main (String[] args) {

    JFrame f = new JFrame ("Swing Test");
    Container inhalt = f.getContentPane();
    inhalt.setLayout (new FlowLayout() );

    String labText =
    "<p align=\"center\"><b>Lautst&auml;rke</b><br>(volume)</p>" ;
    JLabel lab = new JLabel ("<html>" + labText + "</html>");
    inhalt.add(lab);

    JSlider slid = new JSlider (0, 100, 50);
    // slid.addChangeListener ( ... );
    slid.setToolTipText("Stellt die Lautstaerke ein.");
    inhalt.add(slid);

    ImageIcon stopIcon = new ImageIcon ("images/stop.gif");
    JButton but = new JButton ( "Stop", stopIcon );
    // but.addActionListener ( ... );
    but.setToolTipText("Stoppt das Abspielen.");
    inhalt.add(but);

    f.setDefaultCloseOperation (JFrame.DISPOSE_ON_CLOSE);
    f.setSize(400,150);
    f.setVisible(true);
  }
}

Model, View, Controller

Die Swing-Klassen unterstützen das Prinzip von Model, View und Controller.

Zu den sichtbaren Komponenten (View) gibt es jeweils geeignete Datenobjekte (Model, siehe die Hinweise in der obigen Klassenliste).

Die Verbindung vom View zum Controller erfolgt wie beim AWT durch das Event-Handling mit den add-Listener-Methoden.

Der Controller muss nur die Daten im Model-Objekt verändern (set-Methoden).

Die Verbindung zwischen Model und View erfolgt automatisch nach dem Konzept von Observer und Observable. Dafür muss man nur im Konstruktor des View-Objekts das entsprechende Model-Objekt angeben. Die Registrierung des View als Observer des Model (addObserver) wird vom Konstruktor erledigt, und das automatische Update der Daten-Darstellung im View (notifyObservers) wird von den set-Methoden des Model erledigt.

Wie die Darstellung der Daten im View aussehen soll, wird mit Renderern festgelegt.

Beispiel:

Analoges gilt für andere Komponenten wie JList, JTable usw.

Für genauere Informationen über die Swing-Klassen für Komponenten, Models, Renderer, Events, LookAndFeels etc. und über die Konstruktoren und Methoden dieser Klassen wird auf die Online-Dokumentation (API) und auf die Referenzen verwiesen.


Applets

Als Java-Applikationen bezeichnet man selbständig laufende Programme.
Als Applets bezeichnet man "unselbständige" Java-Programme, die als Teil einer Web-Page laufen.

Java-Applets werden innerhalb einer Web-Page dargestellt und unter der Kontrolle eines Web-Browsers ausgeführt.

Technisch gesehen zeichnen sich Java-Applets dadurch aus, dass sie Unterklassen der Klasse Applet sind. Die Klasse Applet ist ihrerseits eine Unterklasse von Panel. Applets sind also Panels, die innerhalb des Browser-Fensters (das eine Art von Frame ist) dargestellt werden.

Ausgaben auf System.out oder System.err werden bei Web-Browsern in einem eigenen Bereich, der "Java-Console", ausgegeben, der den Benutzern meist nur auf Umwegen zugänglich ist; beim Appletviewer erscheinen sie in dem Terminal-Fenster, in dem der Appletviewer aufgerufen wird. Dies eignet sich also nur für Test-Meldungen, aber nicht für die normale Interaktion mit dem Benutzer. Diese sollte ausschließlich über die graphische Benutzungsoberfläche (GUI) des Applet erfolgen.

Bei der Deklaration von Applets müssen die Packages java.applet sowie java.awt und java.awt.event importiert werden.

Sicherheit (security, sandbox, signed applets)

Applets werden meist über das Internet von einem Server geladen, und spezielle Sicherungen innerhalb des Web-Browser ("Sandkasten", sandbox) sorgen dafür, dass sie keine unerwünschten Wirkungen auf den Client-Rechner haben können.

So können Applets im Allgemeinen

Bei signierten und zertifizierten Applets (signed Applets) kann der Benutzer, wenn er sie für vertrauenswürdig hält, auch Ausnahmen von den strengen Sicherheitsregeln zulassen. Er kann z.B. vereinbaren, dass ein bestimmtes Applet auf ein bestimmtes File oder ein bestimmtes Subdirectory seiner Harddisk zugreifen darf (nur lesend, oder lesend und schreibend), oder dass ein anderes Applet E-Mail an fremde Internet-Rechner senden darf.

Wie man die Applets bzw. ein Archiv-File, in dem sie gespeichert sind, mit einer verschlüsselten Unterschrift (Signatur) versieht, wie man diesen Schlüssel von einer vertrauenswürdigen Person oder Institution beglaubigen (zertifizieren) lässt, und wie das Applet dann den Benutzer bittet, dass dieser die Erlaubnis für die normalerweise verbotenen Aktionen erteilt, muss leider für jeden Web-Browser anders gemacht werden. Genauere Informationen über die Version von Sun (javakey) findet man in der Online-Dokumentation von Java, die Informationen für die Versionen von Netscape und Microsoft findet man auf den Web-Servern dieser Firmen.

Für die Zertifizierung von Applets innerhalb eines kleinen Benutzerkreises wie z.B. innerhalb des Intranet einer Firma braucht man keine weltweite Zertifizierung, sondern es genügt eine Gruppen- bzw. Firmen-interne Beglaubigung der Signatur durch eine persönlich bekannte vertrauenswürdige Person.

Beim Testen von Applets in einem lokalen HTML-File mit dem Appletviewer sind nicht alle diese Einschränkungen aktiv. Zum Testen der Security-Einschränkungen empfiehlt sich die Angabe eines URL im Appletviewer oder die Verwendung eines Web-Browsers.

HTML-Tags <applet> <object> <embed> <param>

Innerhalb der Web-Page werden Applets mit dem HTML-Tag <applet> eingefügt und aufgerufen. Dies erfolgt nach dem folgenden Schema:

<applet code="Xxxx.class" width="nnn" height="nnn">
<param name="xxx" value="yyy">
  Text
</applet>

Im Applet-Tag muss mit dem Parameter code der Name des Java-Applet angegeben werden (Class-File mit dem plattformunabhängigen Bytecode), und mit width und height die Breite und Höhe des Applet in Pixels.

In diesem Fall muss sich das Class-File im selben Directory am selben Server wie das HTML-File befinden. Wenn dies nicht der Fall ist, muss man mit einem zusätzlichen Parameter codebase den URL des Directory angeben, aus dem das Class-File geladen werden soll. Der URL (Uniform Resource Locator) gibt im Allgemeinen das Protokoll, die Internet-Adresse des Servers und den Namen des Directory innerhalb des Servers an. Dieses Directory ist dann auch das Package, aus dem eventuell weitere Klassen geladen werden, und dieser Server ist derjenige, auf den das Applet über das Internet zugreifen darf. Beispiel:

<applet codebase="http://www.boku.ac.at/javaeinf/"
        code="HelloApp.class" width="300" height="100">

Falls das Class-File kein einzelnes File ist sondern in einem komprimierten Archiv (ZIP- oder JAR-File) enthalten ist, muss man den Namen dieses Archiv-Files mit dem Parameter archive angeben:

<applet codebase="http://hostname/dirname/"
        archive="xxxxx.jar" code="Xxxxx.class"
        width="300" height="100">

Mit einem oder mehreren Param-Tags können Parameter an das Applet übergeben werden, die innerhalb des Applet mit der Methode getParameter abgefragt werden können. Ein Beispiel dafür folgt im Kapitel über Parameter.

Der (abgesehen von den Param-Tags) zwischen <applet> und </applet> stehende Text wird von den Browser-Versionen, die Java nicht unterstützen, an Stelle des Java-Applet dargestellt. Dies kann beliebig formatierter HTML-Text sein, also z.B. auch Bilder enthalten.

In der neuen HTML-Norm HTML 4.0 ist vorgesehen, dass statt dem Tag <applet> der Tag <object> verwendet werden soll. Dies wird aber von dem meisten Browsern noch nicht unterstützt.

Ausführlichere Informationen über Web-Pages und die Hypertext Markup Language HTML finden Sie in der HTML-Einführung und in den Referenzen.

Java Plug-in

Als Alternative zu den in die Web-Browser integrierten Java Virtual Machines, die die neueste Java-Version meist noch nicht oder nur unvollständig unterstützen, bietet die Firma Sun ein "Java Plug-in" an (auch Java Activator genannt), das von den Benutzern kostenlos in den Internet-Explorer oder in den Netscape-Navigator unter MS-Windows oder Sun Solaris oder HP-UX eingebaut werden kann und dann die jeweils neueste Version des JDK voll unterstützt. Bei Netscape Version 6, iPlanet und Mozilla ist dies sogar der Standard.

Dazu genügt allerdings nicht die Installation der entsprechenden Software am Client, sondern es muss auch das HTML-File am Web-Server verändert werden: Je nach der Browser-Version muss der Aufruf des Applet in diesem Fall nicht mit <applet> sondern mit <object> oder <embed> erfolgen. Wenn man als Autor einer Web-Page erreichen will, dass die Web-Page mit allen Web-Browsern funktioniert, muss man dafür eine geeignete Kombination der HTML-Befehle <object>, <embed> und <applet> angeben. Genauere Informationen darüber finden Sie on-line an der Adresse
http://java.sun.com/products/plugin/

Beispiel: einfaches HelloWorld Applet

Um einen ersten Eindruck zu geben, wie ein Applet aussieht, hier das Gegenstück zur primitiven Hello-World-Applikation aus dem ersten Teil dieser Kursunterlage. Eine Erklärung der darin vorkommenden Methoden folgt weiter unten.

Applet (Java-Source)

import java.awt.*;
import java.applet.*;

public class HelloApp extends Applet {

  private String s = "Hello World!";

  public void paint (Graphics g) {
    g.drawString (s, 25, 25);
  }
}
Dieses Java-Programm muss in einem File mit dem Filenamen HelloApp.java stehen.

Web-Page (HTML-File)

Ein einfaches HTML-File, das dieses Applet enthält, könnte so aussehen:
<html>
<head>
<title>Example</title>
</head>
<body>

<h1>Example</h1>

<p align=center>
<applet code="HelloApp.class" width="200" height="100" >
  Hello World!
</applet>
</p>

</body>
</html>
Für Tests mit dem Appletviewer genügt die folgende Minimalversion:
<html>
<applet code="HelloApp.class" width="200" height="100">
</applet>
</html>
Wenn dieses HTML-File den Filenamen hello.html hat, dann erfolgen Compilation und Ausführung des Applet mit den folgenden Befehlen:

javac HelloApp.java

appletviewer hello.html

Wenn Sie sehen wollen, wie das Applet funktioniert, probieren Sie es in Ihrem Browser aus (falls er das kann).

Übung: einfaches HelloWorld Applet

Schreiben Sie das oben angeführte HelloWorld-Applet (oder eine Variante davon mit einem anderen Text) und ein entsprechendes HTML-File, übersetzen Sie das Applet und rufen es mit dem Appletviewer auf.

Falls Sie einen Java-fähigen Web-Browser zur Verfügung haben, probieren Sie auch aus, ob dieser Ihr Applet richtig verarbeiten kann.

Methoden init, start, stop

Analog zur main-Methode von Applikationen werden in Applets die folgenden Methoden automatisch ausgeführt:

Die Methode init wird ausgeführt, wenn die Web-Page und das Applet geladen werden.

Die Methode start wird immer dann ausgeführt, wenn die Web-Page mit dem Applet im Browser sichtbar gemacht wird, also sowohl beim ersten Laden als auch bei jeder Rückkehr in die Seite (z.B. mit dem Back-Button des Browsers, wenn man inzwischen eine andere Web-Page angesehen hat).

Die Methode stop wird immer dann ausgeführt, wenn die Web-Page verlassen wird, also beim Anklicken einer anderen Web-Page oder beim Beenden des Web-Browsers.

Im Allgemeinen gibt man in der init-Methode alles an, was zur Vorbereitung des Applet und zum Aufbau der graphischen Benutzungsoberfläche (GUI) dient. In der start-Methode werden dann eventuelle Aktionen wie z.B. Animationen oder Datenbankverbindungen gestartet. In der stop-Methode müssen alle in der start-Methode begonnenen Aktionen beendet werden.

Da das Laden und Starten des Applet ohnehin immer eine längere Wartezeit mit sich bringt, wird empfohlen, alle während der Interaktion mit dem Benutzer benötigten weiteren Klassen sowie Bilder, Töne, Datenfiles etc. so weit wie möglich und sinnvoll schon bei der Initialisierung des Applet über das Internet zu laden (also in der init-Methode), damit die interaktive Arbeit mit dem Benutzer dann zügig ablaufen kann und nicht durch neuerliche Wartezeiten für das nachträgliche Laden solcher Files verzögert bzw. unterbrochen wird.

Größenangabe

Die Größe des Applet wird nicht innerhalb des Applet mit setSize, getPreferredSize oder dergleichen angegeben, sondern mit den Parametern width und height im Applet-Tag im HTML-File.

In den width- und height-Parametern von <applet> können nicht nur Pixel-Angaben sondern auch Prozent-Angaben stehen, dann kann der Benutzer die Größe des Applet verändern.

Innerhalb des Java-Programms kann man die aktuelle Größe des Applet mit der Methode getSize() abfragen.

Beenden des Applet

Bei Applikationen mit graphischen User-Interfaces ist meist ein Close- oder Exit-Button notwendig, um die Applikation zu beenden.

Bei Applets ist ein Close- oder Exit-Button im Allgemeinen nicht sinnvoll. Das Applet wird vom Benutzer einfach dadurch beendet, dass er die Web-Page verlässt oder den Web-Browser (mit Close oder Exit in seinem File-Menü) beendet.

In Applets soll daher auch kein System.exit ausgeführt werden, d.h. das Applet darf den Web-Browser nicht "abschießen".

Beispiel: typischer Aufbau eines Applet mit Button-Aktionen


import java.applet.*;
import java.awt.*;
import java.awt.event.*;

public class ButtonTest extends Applet
  implements ActionListener {

  private Button b1, b2;
  private Label mess;

  public void init() {
    setLayout (new FlowLayout() );
    b1 = new Button("B1");
    b1.addActionListener (this);
    add(b1);
    b2 = new Button("B2");
    b2.addActionListener (this);
    add(b2);
    mess = new Label("Nothing was pressed.");
    add(mess);
    setVisible(true);
  }

  public void actionPerformed (ActionEvent e) {
    String s = e.getActionCommand();
    if (s.equals("B1")) {
      mess.setText("The first button was pressed.");
    }
    else
    if (s.equals("B2")) {
      mess.setText("The second button was pressed.");
    }
    this.validate();
  }
}
Hier das zugehörige minimale HTML-File, mit der Angabe der Größe des Applet:
<html>
<applet code="ButtonTest.class" width="500" height="100">
</applet>
</html>

Wenn Sie wissen wollen, was dieses Applet tut, probieren Sie es einfach aus (in Ihrem Browser, wenn er das kann, oder Abschreibübung oder Download).

Beispiel: typischer Aufbau eines Applet mit Maus-Aktionen


import java.applet.*;
import java.awt.*;
import java.awt.event.*;

public class MouseTest extends Applet
  implements MouseListener, MouseMotionListener {

  private int mouseX, mouseY;
  private int radius, diameter = 20;
  private Color circleColor = Color.red;

  public void init() {
    Dimension full = getSize();
    mouseX = full.width/2;
    mouseY = full.height/2;
    radius=diameter/2;
    addMouseListener(this);
    addMouseMotionListener(this);
  }

  public void paint (Graphics g) {
    g.setColor (circleColor);
    g.fillOval ((mouseX-radius), (mouseY-radius),
       diameter, diameter);
  }

  public void mousePressed (MouseEvent e) {
    mouseX = e.getX();
    mouseY = e.getY();
    circleColor = Color.green;
    repaint();
  }
  public void mouseReleased (MouseEvent e) {
    mouseX = e.getX();
    mouseY = e.getY();
    circleColor = Color.red;
    repaint();
  }
  public void mouseClicked (MouseEvent e) { }
  public void mouseEntered (MouseEvent e) { }
  public void mouseExited (MouseEvent e) { }

  public void mouseDragged (MouseEvent e) {
    mouseX = e.getX();
    mouseY = e.getY();
    circleColor = Color.green;
    repaint();
  }
  public void mouseMoved (MouseEvent e) { }

}
Wenn Sie wissen wollen, was dieses Applet tut, probieren Sie es einfach aus (in Ihrem Browser, wenn er das kann, oder Abschreibübung oder Download).

Beispiel: typischer Aufbau eines Applet mit Text-Eingaben


import java.applet.*;
import java.awt.*;
import java.awt.event.*;

public class TextTest extends Applet
  implements ActionListener {

  private TextField field1, field2;
  private Label helpText;

  public void init() {
    setLayout (new FlowLayout() );
    field1 = new TextField("", 30);
    field1.addActionListener(this);
    add(field1);
    field2 = new TextField("", 30);
    field2.addActionListener(this);
    add(field2);
    helpText = new Label ("Enter text and hit return.");
    add(helpText);
    setVisible(true);
  }

  public void actionPerformed (ActionEvent e) {
    String s = null;
    Object which = e.getSource();
    if (which==field1) {
      s = field1.getText();
      field2.setText(s);
      field1.setText("");
    }
    else
    if (which==field2) {
      s = field2.getText();
      field1.setText(s);
      field2.setText("");
    }
  }
}
Wenn Sie wissen wollen, was dieses Applet tut, probieren Sie es einfach aus (in Ihrem Browser, wenn er das kann, oder Abschreibübung oder Download).

Übung: einfaches Applet Thermostat

Schreiben Sie ein Applet, das folgende Daten und Steuerelemente darstellt und die folgenden Aktionen ausführt:

Als Erweiterung können Sie die Anzeige farbig gestalten, und zwar je nach dem erreichten Temperaturbereich in verschiedenen Farben.

Wenn Sie sehen wollen, wie das aussehen könnte, probieren Sie es in Ihrem Browser aus (falls er das kann).

Für das Grundgerüst dieses Applet können Sie Teile der Übung aus dem Kapitel über Graphical User-Interfaces verwenden.

Führen Sie dieses Applet mit einem minimalen HTML-File im Appletviewer aus und testen Sie auch, was passiert, wenn Sie das Fenster vergrößern oder verkleinern.

Falls Ihnen dieses Beispiel zu einfach ist und Sie mehr Zeit investieren wollen, können Sie auch eines oder mehrere der folgenden, aufwendigeren Beispiele ausprobieren.

Übung: Applet einfache Verkehrsampel

Schreiben Sie ein Applet, das folgende Elemente darstellt und folgende Aktionen ausführt:

Wenn Sie sehen wollen, wie das aussehen könnte, probieren Sie es in Ihrem Browser aus (falls er das kann).

Führen Sie dieses Applet mit einem minimalen HTML-File im Appletviewer aus und testen Sie auch, was passiert, wenn Sie das Fenster vergrößern oder verkleinern.

Übung: komplexes Applet Sparbuch

Erweitern Sie das einfache Rechenbeispiel Sparbuch aus dem Kapitel Syntax und Statements bzw. dessen Erweiterung aus dem Kapitel Exceptions zu einem Applet mit graphischem User-Interface.

Überlegen Sie zunächst die zu Grunde liegenden Daten (Model): Geldbetrag, Zinssatz, und der Wert in jedem Jahr.

Überlegen Sie dann die graphische Darstellung (View):

Überlegen Sie schließlich die Steuerung (Controller): Wenn der Benutzer einen neuen Geldbetrag oder Zinssatz eingibt, sollen sofort die entsprechenden Werte sichtbar werden (oder eine Fehlermeldung, wenn die Eingabe ungültig war).

Wenn Sie sehen wollen, wie das aussehen könnte, probieren Sie es in Ihrem Browser aus (falls er das kann).

Führen Sie dieses Applet mit einem minimalen HTML-File im Appletviewer aus und testen Sie auch, was passiert, wenn Sie das Fenster vergrößern oder verkleinern.

Übung: komplexes Applet: Bäume im Garten

Überlegen, entwerfen und schreiben Sie ein Applet, das folgende Elemente darstellt und folgende Aktionen ausführt:

Führen Sie dieses Applet mit einem minimalen HTML-File im Appletviewer aus und testen Sie alle vorgesehenen Aktionen.

Wenn Sie sehen wollen, wie das aussehen könnte, probieren Sie es in Ihrem Browser aus (falls er das kann).

Vermeidung von Flimmern oder Flackern (buffered image)

Wenn die paint-Methode eine längere Rechnerzeit benötigt, kann beim Aufruf der repaint-Methode ein störendes Flimmern oder Flackern entstehen. Dies hat folgende Gründe:

Die repaint-Methode ruft intern die update-Methode auf. Diese löscht zunächst den bisherigen Inhalt der Komponente (clear screen), indem sie die Fläche mit der Hintergrundfarbe füllt, und ruft dann die paint-Methode auf, die die Graphik-Ausgabe bewirkt. Wenn der ganze Vorgang länger als etwa 1/20 Sekunde beträgt, wird er als Flimmern oder Flackern wahrgenommen.

Dieses störende Flimmern oder Flackern kann auf die folgende Weise vermieden werden:

  1. Man macht die zeitaufwändige Berechnung des neuen Bildes nicht in der paint-Methode sondern in einer eigenen Methode in einem eigenen Image-Objekt (Pufferbild, buffered image), und definiert die paint-Methode so, dass sie nur das fertig aufbereitete Bild ausgibt.
  2. Man definiert die update-Methode so, dass sie nur die paint-Methode aufruft, ohne vorheriges Clear-Screen, das in diesem Fall auch nicht nötig wäre, weil von der paint-Methode ohnehin die gesamte Fläche neu gezeichnet wird.

Dies erfolgt nach dem folgenden Schema:

In der Klasse (die im Allgemeinen eine Subklasse von Canvas oder Applet ist) werden globale Datenfelder für das Pufferbild und die darin enthaltene Graphik angelegt:

private Image img;
private Graphics g;

In der init-Methode werden die Bild- und Graphik-Objekte mit der richtigen Größe angelegt:

Dimension d = getSize();
img = createImage (d.width, d.height);
g = img.getGraphics();

Das globale Graphik-Objekt kann nun in beliebigen Methoden der Klasse bearbeitet werden, und mit repaint kann dann die Ausgabe bewirkt werden:

g.setColor (...);
g.draw... (...);
g.fill... (...);
repaint();

Die paint-Methode wird so definiert, dass sie nur das globale Pufferbild in ihren Graphik-Parameter ausgibt, und zwar in diesem Fall ohne Image-Observer (null als 4. Parameter):

public void paint (Graphics g) {
  if (img != null)
    g.drawImage (img, 0, 0, null);
}

Die update-Methode wird so definiert, dass sie nur die paint-Methode aufruft:

public void update (Graphics g) {
  paint(g);  // no clear
}

Uniform Resource Locators (URL)

Die Klasse URL im Paket java.net beschreibt Netz-Adressen (Uniform Resource Locator). Applets können im Allgemeinen nur Files und Directories auf dem Server ansprechen, von dem sie geladen wurden. Zu diesem Zweck können die folgenden Methoden verwendet werden:

Wie man ein Daten- oder Text-File vom Web-Server lesen kann, wird im Kapitel über Ein-Ausgabe beschrieben. Hier folgen Hinweise zum Laden von Bildern und Tönen und dergleichen vom Web-Server.

Bilder (Image, MediaTracker)

Bilder können mit getImage vom Web-Server geladen und dann mit drawImage im Applet oder in einem Canvas innerhalb des Applet dargestellt werden. Die allgemein unterstützten Bildformate sind GIF und JPEG.

Methode im Applet-Objekt:

Methoden im Image-Objekt: Methode im Graphics-Objekt:

Beispielskizze:

Image logo;
public void init() {
  logo = getImage( getDocumentBase(), "logo.gif");
}
public void paint (Graphics g) {
  if (logo != null)
    g.drawImage (logo, 25, 25, null);
}

Die Methode getImage startet nur das Laden des Bildes. Der Ladevorgang über das Internet kann längere Zeit dauern. Wenn man abwarten will, bis der Ladevorgang erfolgreich abgeschlossen ist, muss man ein Objekt der Klasse MediaTracker verwenden.

Dies ist zum Beispiel dann notwendig, wenn man mit getWidth und getHeight die Größe des Bildes abfragen will, denn diese Werte sind erst dann verfügbar, wenn das Bild fertig geladen wurde.

Beispiel:

Image img;
int imgWidth, imgHeight;

public void init() {
  MediaTracker mt = new MediaTracker(this);
  img = getImage( getDocumentBase(), "img.jpg");
  mt.addImage (img, 0);
  try {
    mt.waitForID(0);
    imgWidth = img.getWidth(this);
    imgHeight = img.getHeight(this);
    ...
  } catch (InterruptedException e) {
    System.out.println("Image not loaded.");
  }
}

In Applikationen kann man Bilder analog mit
x.getToolkit().getImage(...)
(wobei x irgendeine AWT-Komponenente ist) oder mit
Toolkit.getDefaultToolkit().getImage(...)
laden.

Töne (sound, AudioClip)

Töne (Audio-Clips) können mit getAudioClip vom Web-Server geladen und mit den Methoden play, loop und stop abgespielt werden. Bis JDK 1.1 wird nur das Audio-Clip-Format (File-Extension .au) unterstützt, ab JDK 1.2 auch andere Audio-Formate wie MIDI und WAV und auch Video-Clips (Java Media Framewortk JMF).

Methode im Applet-Objekt:

Methoden im AudioClip-Objekt:

Beispielskizze für ein einmaliges Abspielen beim Starten des Applet:

AudioClip sayHello;
public void init() {
  sayHello = getAudioClip( getDocumentBase(), "hello.au");
  sayHello.play();
}

Beispielskizze für eine ständige Wiederholung bis zum Verlassen des Applet:

AudioClip music;
public void init() {
  music = getAudioClip( getDocumentBase(), "music.au");
}
public void start() {
  music.loop();
}
public void stop() {
  music.stop();
}
Für das Abspielen von AudioClips "im Hintergrund" zugleich mit anderen Aktionen verwendet man eigene Threads.

Parameter

Ähnlich wie die Laufzeit-Parameter beim Starten einer Applikation mit java, können mit dem <param>-Tag in der Web-Page Parameter an das Applet übergeben werden. Jeder Parameter hat einen Namen und einen Wert:

<param name="xxx" value="yyy">

Innerhalb des Applet können die Parameter mit der Methode getParameter abgefragt werden. Die in HTML-Tags angegebenen Werte sind immer Strings. Die Umwandlung von Ziffernfolgen in Zahlen etc. muss innerhalb des Java-Applet erfolgen. Bei Namen in HTML-Tags wird meist nicht zwischen Groß- und Kleinbuchstaben unterschieden, es wird daher empfohlen, die Namen nur mit Kleinbuchstaben zu vereinbaren.

Beispiel:

<html>
<applet code="ParamTest.class" width="300" height="200">
  <param name="firstname" value="Octavian">
  <param name="age" value="17">
</applet>
</html>

import java.applet.*;
public class ParamTest extends Applet {
  String firstname = null;
  int age = 0;
  public void init() {
    try {
      firstname = getParameter ("firstname");
      String ageString = getParameter ("age");
      age = Integer.parseInt(ageString);
    } catch (Exception e) {
    }
    if (age < 18) {
     System.out.println(name + ", you are too young!");
    }
    ... // do something
  }
}

Web-Browser

AppletContext

Der Web-Browser, der das Applet geladen hat und darstellt, kann mit der Methode getAppletContext verfügbar gemacht werden. Dann kann man mit getApplets und getApplet auf die anderen, in der selben Web-Page enthaltenen Applets zugreifen, oder den Web-Browser mit der Methode showDocument bitten, eine andere Web-Page zu laden und darzustellen.

Methode im Applet-Objekt:

Methoden im AppletContext-Objekt:

Web-Pages, Mail, News

Wenn showDocument mit nur einem Parameter aufgerufen wird, wird die aktuelle Web-Page (die das Applet enthält) durch eine neue Web-Page ersetzt.

Wenn showDocument mit zwei Parametern aufgerufen wird, wird die neue Web-Page in dem angegebenen Target-Frame dargestellt. Dies eignet sich z.B. für die Darstellung von Hilfe-Texten zum Applet in einem eigenen Frame (oder Browser-Fenster) mit dem Targetnamen "help".

Wenn der URL nicht mit http: oder ftp: sondern mit mailto: oder news: beginnt, wird das Mail- bzw. News-Fenster des Web-Browsers mit der angegebenen Mail-Adresse bzw. Newsgruppe geöffnet, und der Benutzer kann eine E-Mail absenden bzw. eine Newsgruppe lesen.

In allen Fällen hängt es vom Web-Browser ab, ob und wie er das alles unterstützt.

Beispielskizze:

URL newPage = null;
AppletContext browser = getAppletContext();
try {
  newPage = new URL (getDocumentBase(), "xxx.html");
  browser.showDocument (newPage);
} catch (Exception e {
}

Zugriff auf andere Applets

Wenn die Web-Page mehrere Applets enthält, etwa in der Form

<applet code="Applet1.class" name="a1"
  width="nnn" height="nnn">
</applet>
  ...
<applet code="Applet2.class" name="a2"
  width="nnn" height="nnn">
</applet>
dann kann man innerhalb des Applet a1 nach folgendem Schema auf Felder xxxx und Methoden yyyy des Applets a2 zugreifen:
AppletContext browser = getAppletContext();
Applet2 otherApplet = (Applet2) browser.getApplet("a2");
x = otherApplet.xxxx;
otherApplet.yyyy();
otherApplet.xxxx.yyyy();
In dieser Skizze fehlt noch die Fehlerbehandlung, z.B. ob das andere Applet in der Web-Page gefunden wurde.

Zugriff auf JavaScript

Wenn ein Web-Browser sowohl Java als auch JavaScript unterstützt, dann kann man auch zwischen Java-Applets und JavaScript-Scripts Daten und Informationen austauschen:

Innerhalb von JavaScript kann man public Methoden und Felder von Java-Applets in der Form
document.appletname.feldname
bzw.
document.appletname.methodenaufruf
ansprechen.

Innerhalb von Java-Applets kann man JavaScript-Funktionen aufrufen, wenn man die Klasse JSObject aus dem Package netscape.javascript (von Netscape) verwendet und im <applet>-Tag den Parameter mayscript angibt. Beispielskizze:

import netscape.javascript.*;
...
String[] somehtml = {
  "<h1>Hello!</h1>",
  "<p>Some <b>Java-generated</b> hypertext ..."
  };
JSObject w = JSObject.getWindow(this);
w.eval("w1 = window.open ('','windowname','')");
w.call("w1.document.write", somehtml);
w.eval("w1.document.close()");

Achtung! JavaScript wird nicht von allen Web-Browsern unterstützt, und viele Benutzer haben JavaScript wegen des damit verbundenen Sicherheitsrisikos in ihren Browser-Optionen ausgeschaltet. JavaScript eignet sich daher nicht für allgemein verwendbare Applets über das Internet sondern nur für Spezialfälle innerhalb eines Intranet.

Übung: Zwei Applets

Schreiben Sie eine Web-Page mit zwei Applets:

Applet-Dokumentation

Um den Benutzern die richtige Verwendung eines Applet zu erleichtern, sollten die folgenden Hilfsmittel verwendet werden:

Beispiel:
import java.awt.*;
import java.applet.*;

/**
* an example for an Applet with javadoc documentation,
* Applet info, und parameter info.
* @since JDK 1.0
*/
public class AppletDoc extends Applet {

  String text;
  int fontSize;
  Font f;

  public void init () {
    text = getParameter("text");
    if (text == null)
      text = "Hello World!";
    try {
      fontSize = Integer.parseInt (
        getParameter("fontsize").trim() );
    } catch (Exception e) {
        fontSize = 12;
    }
    f = new Font ("Helvetica", Font.BOLD, fontSize);
  }

  public void paint (Graphics g) {
    g.setFont (f);
    g.drawString (text, 25, 50);
  }

  public String getAppletInfo() {
    String s =
      "AppletDoc = Hello World Applet with parameters";
    return s;
  }

  public String[][] getParameterInfo() {
    String[][] s = {
      { "text",     "String",  "Text to be displayed" },
      { "fontsize", "Integer", "Font size in points" }
    };
    return s;
  }
}

Doppelnutzung als Applet und Applikation

Man kann ein Java-Programm auch so schreiben, dass es sowohl als Applet innerhalb einer Web-Page als auch als lokale Applikation ausgeführt werden kann. Zu diesem Zweck muss das Programm

Die main-Methode wird nur bei Aufruf als Applikation ausgeführt und im Applet ignoriert. Sie muss ein Frame (als Ersatz für das Fenster des Web-Browsers) erzeugen und in diesem ein Objekt des Applet einfügen und dann die init- und start-Methoden des Applet aufrufen, die den Inhalt des Applet darstellen.

Außerdem sollte ein WindowListener implementiert werden, der beim Schließen des Frame die Applikation beendet (analog zum Schließen des Web-Browsers).

Falls das Applet eine stop-Methode enthält, muss diese von der Applikation vor deren Beendigung aufgerufen werden.

Beispiel:

import java.awt.*;
import java.awt.event.*;
import java.applet.*;

public class AppApp extends Applet
    implements WindowListener {

  public void init() {
    setLayout( new FlowLayout() );
    Label hello = new Label("Hello World!");
    add(hello);
    // add more content ...
    setVisible(true);
  }

  public static void main (String[] args) {
    AppApp a = new AppApp();
    Frame f = new Frame("AppApp");

    f.setLayout( new BorderLayout() );
    a.init();
    f.add (a, "Center");
    f.setSize(300,200);
    f.addWindowListener( a );
    f.setVisible(true);
    a.start();
  }

  public void windowClosing (WindowEvent e) {
    this.stop();
    System.exit(0);
  }
  public void windowClosed (WindowEvent e) { }
  public void windowOpened (WindowEvent e) { }
  public void windowIconified (WindowEvent e) { }
  public void windowDeiconified (WindowEvent e) { }
  public void windowActivated (WindowEvent e) { }
  public void windowDeactivated (WindowEvent e) { }
}


Threads

Napoleon konnte angeblich mehrere Arbeiten gleichzeitig erledigen. Kann das mein Computer auch?

Multithreading, Thread, Runnable

Threads ("Fäden") sind Programmteile, die parallel, also unabhängig voneinander, mehr oder weniger gleichzeitig ablaufen.

Es gibt Threads, die von der Java Virtual Machine automatisch erzeugt werden, z.B. für das Event-Handling und den Garbage-Collector. Für die Aufspaltung eigener Programme in mehrere Threads dienen Objekte der folgenden beiden Klassen:

Das Programm, das innerhalb des Thread läuft, muss das Interface Runnable implementieren und eine Methode mit der Signature

enthalten, die dann innerhalb des Thread ausgeführt wird.

Beispiel:

public class MyProgram implements Runnable {
  public void run() {
    System.out.println("Hello World!");
  }
}

Starten eines Thread (start)

Konstruktor:

Der Parameter gibt an, welches Programm innerhalb des Thread laufen soll (siehe oben).

Das Starten des Thread erfolgt mit der Methode

Damit wird die run-Methode des Runnable-Objekts gestartet.

Beispielskizze:

Thread t;
Runnable theProgram = new MyProgram();
t = new Thread (theProgram);
t.start();
Der Zusammenhang zwischen Runnable-Objekt und Thread-Objekt ist ähnlich wie zwischen einem Auto und seinem Lenker:

Beenden oder Abbrechen eines Thread

Der Thread läuft selbständig (also unabhängig von dem Thread, der ihn gestartet hat, siehe auch unten) und endet automatisch, wenn das Ende der run-Methode erreicht wird.

Falls die Aktionen des Thread eine längere Ausführungszeit benötigen oder in einer Schleife wiederholt werden, muss man eine Möglichkeit vorsehen, den Thread "von außen" zu beenden oder abzubrechen.

Dazu verwendet man am besten ein boolean Datenfeld, das von außen gesetzt werden kann und innerhalb des Runnable-Objekts abgefragt wird.

Beispielskizze:

Thread t;
MyProgram myProg = new MyProgram();
t = new Thread (myProg);
t.start();
...
myProg.setRunFlag(false);

public class MyProgram implements Runnable {
  private volatile boolean runFlag;
  public void setRunFlag (boolean runFlag) {
    this.runFlag = runFlag;
  }
  public boolean getRunFlag() {
    return this.runFlag;
  }
  public void run() {
    runFlag=true;
    while (runFlag) {
      // do something
    }
  }
}

Es gibt auch Methoden stop(), interrupt(), suspend() und resume() für das Beenden, Abbrechen oder Unterbrechen eines Thread. Die Verwendung dieser Methoden wird jedoch nicht empfohlen ("deprecated" ab JDK 1.2), weil es dabei entweder zu Deadlock-Situationen oder zu unvollständig ausgeführten Aktionen und damit zu ungültigen Objekt- oder Datenzuständen kommen kann.

Beispiel: Wenn ein Thread, der mehrere zusammengehörige Daten in eine oder mehrere Files oder Datenbanken schreiben will, mit einer dieser Methoden abgebrochen wird, so kann es passieren, dass nur ein Teil der neuen Daten abgespeichert wird und damit ein ungültiger, inkonsistenter Datenzustand entsteht.

Unterbrechungen (sleep)

Man kann nicht davon ausgehen, dass die Rechenzeit "gerecht" auf alle parallel laufenden Threads aufgeteilt wird. Es kann durchaus passieren, dass ein Thread alle Betriebsmittel für sich verbraucht und alle anderen Threads dadurch aufgehalten werden und untätig warten müssen. Dies kann auch durch das Setzen von Prioriäten mit setPriority() oder yield() nicht völlig ausgeschlossen werden.

Deshalb muss man bei parallel laufenden Threads immer dafür sorgen, dass jeder von ihnen Pausen einlegt, in denen die anderen Threads eine Chance bekommen, ihrerseits zu laufen. Dies gilt auch für das Programm, das den Thread mit start() gestartet hat und nun will, dass dieser auch tatsächlich läuft.

Die wichtigste Warte-Methode ist die statische Methode

Beim Aufruf von sleep muss die Fehlersituation InterruptedException abgefangen werden, für den Fall, dass der Thread während der Wartezeit gestoppt oder abgebrochen wird.

Besonders wichtig ist die sleep-Methode, wenn die run-Methode eine Schleife oder Endlos-Schleife ist, die bestimmte Aktionen immer wieder wiederholt (so lange, bis der Thread beendet wird). Beispiel:

public class MyProgram implements Runnable {
  private volatile boolean runFlag;
  public void setRunFlag (boolean runFlag) {
    this.runFlag = runFlag;
  }
  public boolean getRunFlag() {
    return this.runFlag;
  }
  public void run() {
    runFlag=true;
    while (runFlag) {
      // do something
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) { }
    }
  }
}

Synchronisierung (join, wait, notify, synchronized)

Wenn ein Thread die Ergebnisse verwenden will, die ein anderer Thread produziert, muss der eine Thread auf den anderen warten und der andere dem einen mitteilen, wann er mit seiner Aufgabe fertig ist. Dazu dienen die folgenden Methoden:

Methode im Thread-Objekt:

Methoden in jedem Objekt (jeder Subklasse von Object):

Dabei muss darauf geachtet werden, dass keine Deadlock-Situation auftritt (Prozess A wartet, dass Prozess B ein notify() sendet, und Prozess B wartet, dass Prozess A ein notify() sendet).

Wenn zwei oder mehrere parallel laufende Threads das selbe Objekt ansprechen, kann es zu Problemen kommen, wenn beide gleichzeitig das Objekt verändern wollen und dabei auf vom anderen Thread halb ausgeführte Zwischenzustände zugreifen (concurrent update problem). In diesem Fall muss man alle "gefährlichen" Aktionen jeweils in einen synchronized-Block der folgenden Form einfügen:

synchronized (Object) {
  Statements;
}
wobei für Object meistens this anzugeben ist, oder die jeweilige Methode insgesamt als synchronized deklarieren:
public synchronized typ name (parameter) {
  Statements;
}

Dies bewirkt, dass der mit synchronized markierte Statement-Block (bzw. die mit synchronized markierte Methode) nur dann ausgeführt wird, wenn kein anderer, ebenfalls mit synchronized markierter Statement-Block für dieses Objekt läuft; andernfalls wartet der neue Statement-Block, bis der alte Statement-Block fertig ist und sich das Objekt daher wieder in einem gültigen Zustand befindet, und führt dann seine Aktionen ebenfalls komplett und ungestört aus.

Die Methoden wait() und notify() sind nur innerhalb von synchronized Blocks oder Methoden erlaubt.

Ab JDK 1.3 kann man auch sogenannte "Piped Streams" zur Kommunikation von Daten und von Wartezuständen zwischen zwei Threads verwenden (Klassen PipedWriter und PipedReader im Paket java.io): Jedes Lesen aus dem PipedReader in dem einen Thread wartet, bis vom anderen Thread entsprechende Daten in den zugehörigen PipedWriter geschrieben werden.

Beispiel: einfacher HelloWorld-Thread

Wir erweitern unser Hello-World-Programm nun so, dass es das Runnable-Interface implementiert:

public class HelloLoop implements Runnable {

  private String messageText = "Hello World!";

  public void setMessageText(String newText) {
    messageText = newText;
  }

  public String getMessageText() {
    return messageText;
  }

  public void printText() {
    System.out.println (messageText);
  }

  public void run() {
    for (int i=1; i<=5; i=i+1) {
      printText();
      try { Thread.sleep(50); }
      catch (InterruptedException e) {}
    }
  }

}

Zunächst schreiben wir eine main-Methode, die direkt die run-Methoden aufruft:

public class HelloRun {
  public static void main (String[] args) {
    HelloLoop engl = new HelloLoop();
    HelloLoop germ = new HelloLoop();
    germ.setMessageText("Hallo, liebe Leute!");
    HelloLoop cat = new HelloLoop();
    cat.setMessageText("Miau!");

    engl.run();
    germ.run();
    cat.run();
  }
}
Dieses Programm läuft als ein einziger Thread, in dem die Methodenaufrufe nach einander ausgeführt werden, und bewirkt die folgenden Ausgabe:
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hallo, liebe Leute!
Hallo, liebe Leute!
Hallo, liebe Leute!
Hallo, liebe Leute!
Hallo, liebe Leute!
Miau!
Miau!
Miau!
Miau!
Miau!
Nun schreiben wir eine main-Methode, die drei parallel laufende Threads erzeugt:
public class HelloThreads {
  public static void main (String[] args) {
    HelloLoop engl = new HelloLoop();
    HelloLoop germ = new HelloLoop();
    germ.setMessageText("Hallo, liebe Leute!");
    HelloLoop cat = new HelloLoop();
    cat.setMessageText("Miau!");

    Thread te = new Thread (engl);
    Thread tg = new Thread (germ);
    Thread tc = new Thread (cat);
    te.start();
    tg.start();
    tc.start();
  }
}
Dieses Programm bewirkt läuft im Multi-Threading und bewirkt eine Ausgabe wie zum Beispiel:
Hello World!
Hallo, liebe Leute!
Miau!
Hello World!
Miau!
Hallo, liebe Leute!
Hello World!
Miau!
Hallo, liebe Leute!
Hello World!
Miau!
Hallo, liebe Leute!
Hello World!
Hallo, liebe Leute!
Miau!
wobei sich je nach der Prozessorbelastung auch andere Reihenfolgen ergeben können.

Beispiel: eine einfache Animation


import java.awt.* ;
import java.applet.* ;

public class Anima extends Applet
  implements Runnable {

  private volatile boolean runFlag;
  private int x, y, height, width;
  private Image img;
  private Graphics g;
  private Thread t = null;
  private Color nightColor = new Color (0, 0, 102);
  private Color moonColor  = new Color (204, 204, 255);

  public void init() {
    Dimension d = getSize();
    width = d.width;
    height = d.height;
    img = createImage (width, height);
    g=img.getGraphics();
    x = width/2;
    y = height/2;
  }

  public void start() {
    if (t == null)
      {
      t = new Thread (this);
      t.start();
      }
  }
  public void stop() {
    if (t != null)
      {
      runFlag=false; // or: t.stop();
      t=null;
      }
  }

  public void run () {
    runFlag=true;
    while (runFlag) {
      g.setColor (nightColor);
      g.fillRect(0,0,width,height);
      g.setColor (moonColor);
      g.fillArc(x, y-25, 50, 50, 270, 180);
      repaint();
      x = x + 2;
      if (x > width+50 ) {
        x = -50;
      }
      try { Thread.sleep(100); }
      catch (InterruptedException e) {}
    }
  }

  public void update (Graphics g) {
    paint(g);
  }

  public void paint (Graphics g) {
    if (img != null) {
      g.drawImage(img,0,0,null);
    }
  }

}

Wenn Sie wissen wollen, was dieses Applet tut, probieren Sie es einfach aus (in Ihrem Browser, wenn er das kann, oder Abschreibübung oder Download).

Übung: einfaches Applet Blinklicht

Schreiben Sie ein einfaches Applet, das eine gelb blinkende Verkehrsampel zeigt (schwarzes Rechteck mit oder ohne gelbem Kreis).

Wenn Sie sehen wollen, wie das aussehen könnte, probieren Sie es in Ihrem Browser aus (falls er das kann).

Übung: erweitertes Applet Verkehrsampel mit Blinklicht

Erweitern Sie das Applet Verkehrsampel (siehe die Übung im Kapitel Applets) in folgender Weise:

Wenn Sie sehen wollen, wie das aussehen könnte, probieren Sie es in Ihrem Browser aus (falls er das kann).


Copyright Hubert Partl - nächstes Kapitel - Inhaltsverzeichnis