![]() |
|
|||||
Das AWT von Java 1.0 besteht nur aus heavyweight Components. Damit wurde das Aussehen z.B. einer Schaltfläche oder Liste fest an Windows, Motif oder Macintosh gebunden. Bei Swing wird es als Vorteil angesehen, jederzeit auf einer Plattform alle anderen Look-and-Feels emulieren zu können. Swing: awaiting the GigaHertz-World Die Kehrseite ist klar. Swing ist äußerst ressourcenintensiv und selbst auf schnellen CPUs nicht sehr reaktiv. Dies ist der Preis der flexiblen Konzeption, die sich in Hunderten von Klassen mit einem komplexen Beziehungsgeflecht und Mustern kristallisiert. 15.3 Top-Level-Container
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Die Top-Level-Container im »alten« AWT sind Applet, Dialog, Frame und Window, die in Swing JApplet, JDialog, JFrame und JWindow.3
Im AWT waren allerdings gewisse Ungereimtheiten enthalten, die einer einfachen Erweiterung entgegenstanden. Unter anderem wurden Menü-Komponenten nicht von Component abgeleitet.
JComponent:
Basisklasse in Swing
Eine weitere Schwierigkeit besteht darin, dass in Swing alle Komponenten von JComponent abgeleitet werden, um die erweiterten Eigenschaften zu nutzen.
Natürlich bilden die Top-Level-Container von Swing eine Ausnahme, da sie bereits von ihren AWT-Pendants abgeleitet wurden. Also muss zwangsläufig4 ein einzelnes inneres Objekt JRootPane in jedem Root-Container von Swing erzeugt werden, das sich dann – der neuen Swing-Hierarchie folgend – von JComponent ableitet.
Damit sieht die Klassen-Hierarchie nicht mehr ganz so einfach aus (siehe Abb.15.2).
Klassen-Hierarchie der Root-Container
|
Interessant an der Hierarchie ist, dass alle Swing-Komponenten auch Container sind. Aber nur wenige werden tatsächlich als Container benutzt.
JInternalFrame:
virtueller Top-Level-Container
Des Weiteren gibt es noch einen lightweight Container JInternalFrame, der sich wie ein Top-Level-Container verhält, allerdings nicht als äußeres Fenster verwendet werden kann. Da er von keinem AWT-Container abgeleitet ist, ist JInternalFrame Subklasse von JComponent.
Zusammen mit JDesktopPane, einem Container für JInternalFrame, unterstützt somit Swing auch MDI (Multiple Document Interface), d.h., es können komplette Fenster in Fenster eingebettet werden.
Die fünf Root-Container, d.h. die vier Top-Level-Container und JInternalFrame, enthalten genau eine Instanz von JRootPane, die automatisch erzeugt wird.
JRootPane =
glassPane + layeredPane
JRootPane ist ein Container mit einem festen Aufbau. Er setzt sich aus einer Komponente, glassPane, und layeredPane, Instanz der Klasse JLayeredPane, zusammen.
glassPane ist per Default eine Instanz von JPanel, liegt über den anderen Komponenten und fängt die Ereignisse der Maus ab. Allerdings könnte es als beliebige Komponente auch zum Zeichnen eingesetzt werden.
Wie bereits der Name sagt, wird JLayeredPane dazu benutzt, mehrere Komponenten, die sich möglicherweise überlappen, in einer Reihenfolge anzuordnen (siehe 15.3.2).
layeredPane =
menuBar + contentPane
Die layeredPane enthält in JRootPane allerdings nur die beiden Komponenten menuBar, Instanz von JMenuBar, und einen Container contentPane. Der Container contentPane ist per Default ein JPanel und nimmt die eigentlichen Komponenten des Fensters auf (siehe Abb. 15.3).
|
Von allen aufgeführten Komponenten ist nur die Menüleiste optional.
Das Interface RootPaneContainer wird von allen Root-Containern implementiert und enthält Zugriffs-Methoden auf JRootPane und seine internen Komponenten (mit Ausnahme der optionalen Menüleiste).
interface RootPaneContainer { JRootPane getRootPane(); Component getGlassPane(); JLayeredPane getLayeredPane(); Container getContentPane();
void setGlassPane (Component glassPane); void setLayeredPane(JLayeredPane layeredPane); void setContentPane(Container contentPane); }
Da die Ereignisbehandlung erst nachfolgend behandelt wird, reagiert der Top-Level-Container JFrame einzig auf das Ereignis »Schließen des Fensters« mit Beenden der Applikation (siehe Abb. 15.4).
JFrame-Beispiel:
JMenuBar + JLabel
package kap15;
import java.awt.*; import java.awt.event.*; import javax.swing.*;
public class Test { public static void main(String[] args) {
// neues Top-Level-Fenster anlegen JFrame jf= new JFrame();
// Ereignis "Schließen der Frame" beendet Applikation jf.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } });
// eine Menüleiste hinzufügen JMenuBar jbar= new JMenuBar(); jf.getRootPane().setJMenuBar(jbar);
// einen Menüeintrag hinzufügen JMenu jmenu= new JMenu("Menü1"); jbar.add(jmenu);
// zwei Einträge zu Menü1 hinzufügen jmenu.add("1. Eintrag"); jmenu.add("2. Eintrag");
//auch möglich: JPanel jp= (JPanel) jf.getContentPane(); Container jp= jf.getContentPane();
// Eine Text in den contentPane einfügen jp.add(new JLabel("--- Komponente im contentPane ---"));
// Anzeigen des Fensters jf.pack(); jf.setVisible(true); // show() ist deprecated! } }
Alle Container verfügen über eine Methode add(), um andere Komponenten aufzunehmen.
|
GlassPane-Variation: Im folgenden Test wird eine Komponente in glassPane eingefügt. Da glassPane über menuBar und contentPane liegt, werden die Komponenten in glassPane zum Schluss gezeichnet und überdecken eventuell die darunter liegenden (siehe Abb. 15.5).
|
Subklasse von JFrame:
reagiert auf Ereignis »Schließen«
Damit nicht immer wieder der Code für »Schließen des Fensters« hinzugefügt werden muss, wird eine Subklasse EJFrame von JFrame abgeleitet.
class EJFrame extends JFrame { public EJFrame() { addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) { System.exit(0); } }); } }
Komponenten im glassPane überdecken alles
public class Test { public static void main(String[] args) { JFrame jf= new EJFrame(); // Kommentar siehe unten
JPanel jg= (JPanel) jf.getGlassPane(); jg.add(new JLabel("--- Komponente im glassPane ---")); jg.setVisible(true); // sonst unsichtbar Container jp= jf.getContentPane(); jp.add(new JLabel("--- Komponente im contentPane ---")); jf.setSize(250,70); // Fenstergröße manuell setzen jf.setVisible(true); } }
Anordnung mittels Layers und Positionen
Der Container layeredPane von JRootPane ordnet alle Komponenten, die man mittels seiner speziellen add()-Methoden einfügt, in einem Stack an, sofern sie sich überlappen. Hierzu benutzt layeredPane Ebenen bzw. Layer und innerhalb der Ebenen wieder Positionen.
| Layer werden anhand Integer-Objekten geordnet, wobei Layer mit einer größeren Integer-Zahl über denen mit einer kleineren liegen. |
| Komponenten innerhalb derselben Layer werden anhand einer int-Position geordnet, wobei die Komponente mit der Position 0 zuoberst liegt. Komponenten mit einer größeren Position liegen dann unter denen mit einer kleineren. |
In der nächsten Test-Variation werden Schaltflächen zur besseren Sichtbarkeit in verschiedenen Ebenen und Positionen angeordnet. Da JLayeredPane keinen Layout-Manager benutzt, müssen die Komponenten ihre Größe und Pixel-Position selbst setzen.
class MyButton extends JButton { private static int num= 0;
public MyButton (JLayeredPane lp, int layer, int pos) { setBounds(num*30,num*30,150,50); // x,y, Breite, Höhe setForeground(Color.black); // Schrift schwarz setText("Nr. "+ num++ +" Layer "+ layer+", Pos. "+ pos); lp.add(this,new Integer(layer),pos); // anordnen } }
Anordnung von Schaltflächen mittels Layer- und Positionsangabe
public class Test { public static void main(String[] args) { JFrame jf= new EJFrame(); // EJFrame wie oben
JLayeredPane jlp= jf.getLayeredPane();
new MyButton(jlp,3,1); // obere Layer new MyButton(jlp,1,1); // untere Layer, untere Position new MyButton(jlp,1,0); // untere Layer, obere Position new MyButton(jlp,2,1); // mittlere Layer
jf.setSize(300,180); jf.setVisible(true); } }
Die Anordnung entspricht dann der in Abb. 15.6 dargestellten.
Layer und Positionen am Beispiel
|
Die beiden Komponenten contendPane und menuBar werden in einer vordefinierten Ebene JLayeredPane.FRAME_CONTENT_LAYER (der zugehörige Wert ist -30000) eingeordnet, d.h. im Normalfall ganz unten.
Die oberste vordefinierte Ebene JLayeredPane.DRAG_LAYER (Wert: 400) ist dann Drag-Komponenten vorbehalten, die beim Ziehen über die Oberfläche immer über den anderen erscheinen müssen.
JWindow: Fenster ohne Dekoration
JWindow, direkt abgeleitet von Window, ist ein rudimentäres Fenster ohne Dekoration (Fensterleiste, Größenanpassung, Schließen).
Damit sind die Einsatzmöglichkeiten recht gering und in der Regel auf Einsätze wie z.B. Startfenster begrenzt.
Zentrieren des Fensters auf dem Bildschirm
Diese Variation der Test-Klasse verwendet eine ScreenUtil-Klasse zur Zentrierung eines Windows auf dem Bildschirm. Des Weiteren werden der Font und die Größe des Textes manipuliert.
getDefaultToolkit(),
getScreenSize()
final class ScreenUtil { private ScreenUtil() {} public static void center(Window w, int width, int height) { Dimension scr= Toolkit.getDefaultToolkit().getScreenSize(); w.setBounds((scr.width-width)/2,(scr.height-height)/2, width,height); } }
JWindow:
ein ideales Startfenster
public class Test { public static void main(String[] args) { JWindow jw= new JWindow(); JLabel jl= new JLabel();
JLabel, Font:
setText(), setFont()
// Font setzen: Font-Name, Stil, Größe jl.setFont(new Font("Bookman",Font.BOLD+Font.ITALIC,18)); jl.setText("Startfenster");
// ohne Layout-Manager: Größe und Position selbst setzen jw.getContentPane().setLayout(null); jl.setBounds(20,10,180,50);
jw.getContentPane().add(jl);
ScreenUtil.center(jw,150,70); jw.setVisible(true); // ... weitere Aktionen, Window schließen etc. } }
|
Toolkit-Klasse
mit nützlichen Operationen
Die abstrakte Klasse java.awt.Toolkit liefert mit Hilfe der statischen Methode getDefaultToolkit() eine Instanz von sich, die Informationen und Anpassungen an die Plattform/Hardware bereitstellt.
Die Toolkit-Instanz liefert u.a. Bildschirmauflösung und -größe, bietet Cursor-Anpassungen und Font-Informationen.
| Die Klasse Toolkit sollte selten direkt genutzt werden, da sie die Plattformunabhängigkeit nicht unbedingt fördert. |
Die Klasse java.awt.Font erlaubt alle Font-Familiennamen im Konstruktor, die das System bietet. Im zweiten Argument wird der Stil (als Konstanten Font.PLAIN, BOLD, ITALIC oder BOLD+ITALIC) und im dritten die Größe in Punkten übergeben.
JFrame mit plattformspezifischer
Dekoration
Eine Instanz von JFrame oder Frame ist normalerweise das Hauptfenster einer Applikation. Beide Top-Level-Container bieten per Default plattformspezifisch einen Rahmen, Titelleiste mit Minimierungs-, Maximierungs- und Schließ-Schaltflächen.
| Die Superklasse Frame aus dem AWT enthält kein JRootPane, d.h., in Frame werden die Komponenten direkt mit add() eingefügt. |
JFrame enthält dagegen wie alle Top-Level-Container von Swing ein JRootPane und lässt das Einfügen von Komponenten nur in contentPane zu. Der Versuch, Komponenten in JFrame direkt mit add() einzufügen, endet mit einer Ausnahme.
Wie bereits bekannt, reagiert JFrame nicht automatisch auf das Ereignis »Schließen« mit dem Beenden der Applikation, sondern macht nur das Fenster unsichtbar.5
Frame, JFrame:
subtile Unterschiede
Das folgende Code-Fragment öffnet ein AWT- und ein Swing-Fenster. Der Code für beide ist sehr ähnlich, jedoch gibt es subtile Unterschiede:
Frame f= new Frame(); // oder direkt Frame("Titel") f.setSize(100,100); f.setTitle("Titel"); f.setBackground(Color.green); // direkt für die Frame f.add(new Label("hallo")); // direkt in die Frame f.setVisible(true); // Frame lässt sich per Default nicht schließen!
JFrame jf= new JFrame(); // oder direkt JFrame("Titel") jf.setSize(100,100); jf.setTitle("Titel");
JFrame:
setBackground(), add()
// jf.setBackground(Color.green); wäre möglich, // aber nutzlos, da contentPane alles überdeckt. jf.getContentPane().setBackground(Color.green); jf.getContentPane().add(new JLabel("hallo")); jf.setVisible(true); // JFrame wird per Default beim Schließen nur unsichtbar!
Frame reicht
für einfache Java-Apps
| Für einfache Oberflächen und zur Schonung der Ressourcen reicht in der Regel ein Frame. Haben Flexibilität oder die Nutzung neuer Fähigkeiten Priorität, ist JFrame vorzuziehen. |
WindowConstants:
Anpassen des Schließverhaltens
JFrame zusammen mit JDialog und JInternalFrame implementieren das Interface WindowConstants, welches nur drei Konstanten definiert, um das Verhalten beim »Schließen« anzupassen.
public interface WindowConstants { int DO_NOTHING_ON_CLOSE= 0; // wie Frame int HIDE_ON_CLOSE= 1; // default int DISPOSE_ON_CLOSE= 2; // Ressourcen freigeben }
Ein bestimmtes Schließverhalten wird dann wie folgt gesetzt:
jf.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
UIManager:
L&F innerhalb eines Top-Level-Containers
Für die grafische Darstellung der Komponenten in einem Top-Level-Container wie JFrame stehen zumindest zwei bzw. drei verschiedene Oberflächen zur Verfügung.
Im folgenden Code-Fragment werden die verschiedenen L&F angezeigt und die erste gesetzt:
UIManager.LookAndFeelInfo[] lafArr= UIManager.getInstalledLookAndFeels();
for (int i= 0; i<lafArr.length; i++) System.out.print(lafArr[i].getName()+" ");
try { UIManager.setLookAndFeel(lafArr[0].getClassName()); } catch (Exception e) { System.out.println(e); }
JDialog:
spezialisiert auf Dialoge
Die Klasse JDialog ist ein Top-Level-Container, der auf Dialoge spezialisiert ist. Er hat deshalb auch keine Minimierungs- bzw. Maximierungs- Schaltflächen, sondern nur eine Schließ-Schaltfläche.
Des Weiteren gibt es einen Konstruktor, der das Fenster im modalen Modus öffnet, d.h., kein anderes Fenster derselben Applikation kann aktiv sein, bevor dieser Dialog nicht geschlossen wird.
Nachfolgend wird das L&F auf Motif gesetzt, der Dialog im Hauptfenster zentriert und das Schließverhalten auf DISPOSE_ON_CLOSE gesetzt.
try { UIManager.setLookAndFeel( "com.sun.java.swing.plaf.motif.MotifLookAndFeel"); } catch (Exception e) { System.out.println(e); }
JFrame jf= new JFrame("JFrame"); ScreenUtil.center(jf,200,150); // siehe Beispiel in JWindow jf.setVisible(true);
JDialog jd= new JDialog(jf,"JDialog",true); // true= modal jd.setSize(100,50); jd.setLocationRelativeTo(jf); // zentiert in jf
jd.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); jd.show(); // wie setVisible(true)
|
| Die Superklasse Component im AWT enthält eine Methode show(), die als deprecated markiert ist. In der Regel sollte setVisible(true) verwendet werden. |
Allerdings wird in einigen Subklassen wie z.B. Dialog die Methode show() wieder gültig überschrieben und wie in JInternalFrame mit zusätzlicher Funktionalität versehen.
JOptionPane:
nützliche Dialog-Muster
Kurze Mitteilungen oder Abfragen sollten eine einheitliche Struktur aufweisen. Immer wieder den gleichen JDialog-Code zu schreiben, ist lästig und redundant.
Hier kommt die Klasse JOptionPane zur Hilfe. Sie ist direkt von JComponent abgeleitet und definiert bzw. implementiert eine Palette brauchbarer Dialog-Muster mit Hilfe von JDialog oder JInternalFrame (siehe Abb. 15.9).
Visuelles Äußeres von JOptionPane
|
JOptionPane bietet Dialog-Schablonen
JOptionPane stellt für verschieden Aufgaben statische Methoden und Konstanten zur Verfügung, die für die meisten Aufgaben ausreichen. Sollte eine Instanz von JOptionPane wiederverwendet werden, können sie auch mit Hilfe passender Konstruktoren angelegt werden.
Die statischen Methoden decken vier Typen von Dialogen ab:
| Bestätigen/Confirm: Frage, Fehler, Warnung etc. mit diversen Schaltflächen. |
| Eingabe/Input: Zeile, Liste etc. mit zwei Schaltflächen OK und Cancel. |
| Mitteilung/Message: Mitteilung mit der Schaltfläche OK. |
| Option: Beliebige Daten mit Schaltflächen. |
Drei statische Methoden sollen an kleinen Code-Fragmenten demonstriert werden.
Der folgende Dialog hat kein übergeordnetes Fenster (parentComponent ist null), hat einen String als Mitteilungs-Objekt, einen Titel, die Schaltfläche OK und ein Icon, das einen Fehler symbolisieren soll (siehe Abb. 15.10). Ist das übergeordnete Fenster null, wird der Dialog im Bildschirm zentriert.
System.out.println( JOptionPane.showConfirmDialog (null, "Mitteilung mit CLOSED_OPTION","ConfirmDialog", JOptionPane.CLOSED_OPTION, JOptionPane.ERROR_MESSAGE) ); // :: 0
Bestätigung mit JOptionPane im
Motif-L&F
|
Die Methode showInputDialog() akzeptiert ein Array von Objekten als Auswahl, wobei als letztes Argument das per Default selektierte Objekt übergeben werden kann.
Die Darstellung der Objekte erfolgt in diesem Fall in einer JComboBox (siehe Abb. 15.11).
System.out.println( JOptionPane.showInputDialog (null, "Auswahliste","InputDialog", JOptionPane.QUESTION_MESSAGE, null, new String[] { "1.","2.","3.","4.","5.","6."}, "3.") ); // Bei Cancel :: null // Bei OK :: 3.
Auswahlliste mit JOptionPane im
Motif-L&F
|
Im folgenden Beispiel wird ein Hauptfenster erzeugt, in dem das Option-Dialog-Fenster zentriert und modal geöffnet wird.
Die Darstellung der Objekte erfolgt in diesem Fall mit Schaltflächen. Bei einer Auswahl wird als Rückgabewert der Array-Index zurückgegeben, ansonsten der Wert -1 (siehe Abb. 15.12).
try { UIManager.setLookAndFeel( "com.sun.java.swing.plaf.windows.WindowsLookAndFeel"); } catch (Exception e) { System.out.println(e); }
JFrame jf= new JFrame("JFrame"); jf.show();
String[] sarr= new String[] { "1.","2.","3.","4.","5.","6."}; System.out.println( JOptionPane.showOptionDialog(jf, "Optionsauswahl","OptionDialog", JOptionPane.DEFAULT_OPTION, OptionPane.QUESTION_MESSAGE,null,sarr,null) );
Optionen mit JOptionPane in einem Hauptfenster im Windows-L&F
|
Die beiden null-Werte in showOptionDialog() bedeuten »kein eigenes Icon« und »keine Default-Selektion«.
Layout-Manager:
Design- Kompromisse
Container verfügen per Default über einen Layout-Manager, der die Aufgabe hat, die Komponenten, die hinzugefügt werden, im Inneren nach einem Schema anzuordnen.
| Layout-Manager sind immer ein Kompromiss zwischen optimalem Design und Flexibilität. |
Kennt man alle Umgebungs-Variablen wie Fenster-, Schrift- oder Randgrößen, führt eine exakte Positionierung und Größe der Komponenten zu einem optimalen Design der Oberfläche.
Layout-Manager: Anpassungen zur Laufzeit
Da die Oberfläche allerdings meistens auf unterschiedlichen Plattformen bzw. unterschiedlicher Hardware laufen soll, muss man die Arbeit einem Layout-Manager überlassen, der dann zur Laufzeit versucht, die Lage der Komponenten an die Umgebung (sub)optimal anzupassen.
Eigenschaften von Layout-Managern
| können für die meisten Container mit setLayout() frei gewählt werden. |
| können auf null gesetzt werden, was bedeutet, dass man Position und Größe für jede Komponente selbst setzen muss, z.B. mit setBounds().6 |
| können selbst entwickelt werden, d.h., sie müssen das Interface LayoutManager oder das Subinterface LayoutManager2 implementieren. |
| werden vom Container bei allen Änderungen, die das Layout berühren, wie z.B. beim Einfügen und Entfernen von Komponenten aufgerufen. |
Das AWT und Swing definieren bereits einige häufiger benötigte Layout-Schemata (siehe Abb. 15.13).
AWT- und Swing- Layout-Manager: interface-basierende Hierarchie
|
Der LayoutManager2 erweitert also das ursprüngliche Interface um weitere Methoden, um unter anderem Constrains – zusätzliche Beschränkungen – spezifizieren zu können.
Größeneigenschaften von JComponent
Jede Swing-Komponente JComponent hat eine minimale, maximale und bevorzugte Größe. Sie kann – sofern erforderlich – mit setMinimumSize(), setMaximumSize() bzw. setPreferredSize() gesetzt werden.
Die letztgenannte bevorzugte Größe (Typ Dimension) gibt an, wie groß die Komponente sein sollte, um alle internen Elemente optimal darzustellen und ist deshalb für Layout-Manager von besonderer Bedeutung.
Bei einer Schaltfläche JButton berechnet sich die bevorzugte (und minimale) Größe z.B. aus der Textgröße und -länge, dem Textabstand zum Rand und der Randgröße selbst. Sie wird automatisch berechnet, wobei das Maximum praktisch unbegrenzt ist.
| Lassen es die Umgebung und das Layout-Schema zu, wird der Layout-Manager die bevorzugte Größe der Komponente einhalten. |
Versuche, z.B. mittels setSize() oder setBounds(), die Größe der Komponenten selbst zu bestimmen, scheitern immer daran kläglich, dass der Layout-Manager das letzte Wort hat.
Schemata der AWT-Layout-Manager
| Layout-Manager | Layout-Schema |
| FlowLayout | Komponenten werden mit ihrer bevorzugten Größe wie Wörter in Zeilen angeordnet, wobei diese umbrechen können. Wie bei Text kann die Zentrierung mit LEFT, RIGHT oder CENTER angegeben werden. |
| GridLayout | Komponenten werden ohne Rücksicht auf ihre bevorzugte Größe in die Zellen einer Matrix eingeordnet. |
| BorderLayout | Komponenten können in die Kompass-Bereiche NORTH, SOUTH, EAST, WEST oder in das Zentrum CENTER eingefügt werden. Die bevorzugte Größe wird abhängig vom Bereich berücksichtigt. |
| CardLayout | Jede Komponente ist so groß wie der Container und zu einem Zeitpunkt ist immer nur eine Komponente sichtbar. |
| GridBagLayout | Ein GridLayout, in dem Reihen und Spalten unterschiedliche Höhen oder Breiten haben können. Komponenten können eine oder mehrere Zelle(n) einnehmen, wobei die Informationen beim add() an eigene Instanzen der Klasse GridBagConstraints übergeben werden. |
Alle in Swing definierten Layout-Manager sind für spezifische Komponenten definiert, wobei das BoxLayout allerdings auch generell nützlich sein kann.
Schemata der Swing-Layout-Manager
| Layout-Manager | Layout-Schema |
| ScrollPaneLayout | Spezialisiert für den JScrollPane-Container, definiert dieses Layout neun Bereiche, in die ein »Lauf«-Bereich eingeteilt werden kann. Neben dem eigentlichen Laufbereich JViewPort sind dies die vier Ecken, die Zeilen- und Spaltenköpfe und die vertikalen und horizontalen Rollbalken. |
| ViewportLayout | Spezialisiertes Layout für JViewPort (siehe oben). |
| BoxLayout | Dieses Layout, benutzt vom Box-Container, fügt die Komponenten entweder horizontal von links nach rechts oder vertikal von oben nach unten in einer Reihe/Box ein. Dabei beachtet es im Gegensatz zum GridLayout die bevorzugte Größe der Komponenten. |
| OverlayLayout | Überlagert Komponenten anhand eines Ausrichtungspunkts und wird u.a. vom JMenuItem für Text und Icons benutzt. |
Ausgangspunkt für einen optimalen Entwurf ist ein idealer Bildschirm mit exakt positionierten Komponenten idealer Größe, mit anderen Worten ein typisches XY-Design.
Es gibt – bis auf triviale Anordnungen – keinen einzelnen Layout-Manager, den man der contentPane eines Top-Level-Fensters zuweisen könnte, um dieses XY-Design nachzubilden. Deshalb gilt:
Kunst der Komposition von Layout-Managern
| Die Kunst besteht darin, eine geeignete Komposition von passenden Layouts zu finden, die dem idealen Design am nächsten kommt. |
Hierzu muss man sicherlich die Wirkung der o.a. Layouts im Detail kennen, um Container mit passenden Layout-Managern entsprechend zusammenzusetzen.
Im folgenden Code-Fragment wird die Wirkung eines Border-Layouts mit einem eingebetteten Container mit Flow-Layout demonstriert.
Border-Layout mit internem Flow-Layout
JFrame jf= new EJFrame("Border/Flow-Layout"); jf.setSize(200,200); // siehe Erklärung
Container cp= jf.getContentPane();
//cp.setLayout(new BorderLayout()); überflüssig, da default! cp.add(new JButton("Nord"),BorderLayout.NORTH); cp.add(new JButton("Süd"),BorderLayout.SOUTH); cp.add(new JButton("West"),BorderLayout.WEST); cp.add(new JButton("Ost"),BorderLayout.EAST);
Container jp= new JPanel();
//jp.setLayout(new FlowLayout()); überflüssig, da default! for (int i= 0; i<9; i++) jp.add(new JButton("Nr. "+(i+1)));
jp.setSize(100,50); // ohne Wirkung! cp.add(jp,BorderLayout.CENTER);
//jf.pack(); siehe Kommentar unten! jf.setVisible(true);
Reaktionen von Border- und Flow-Layout zur Laufzeit
|
Erklärung: Die Größe des Top-Level-Fensters lässt im Center-Bereich nur eine Schaltfläche pro Zeile des Flow-Layouts zu (Abb. 15.14 oben links).
Sollte jf.pack() in der vorletzten Zeile ausgeführt werden, wird die Größe des Top-Level-Fensters anhand der inneren Komponenten bestimmt, d.h., jp.setSize() in der zweiten Zeile wird wirkungslos (Abb. 15.14 unten).
Eine Größenänderung des Top-Level-Fensters durch den Benutzer führt dann zu diversen anderen Anordnungen (Abb. 15.14 oben rechts).
CardLayout:
Karten nebeneinander angeordnet
Bei Containern mit CardLayout kann mittels add(Component c, Object constraints) jeweils eine Komponente pro Karte eingefügt werden. Das Objekt constraints muss dabei ein String sein, der die Aufgabe hat, die Karte zu identifizieren (kann aber auch "" gesetzt werden).
CardLayout enthält dann fünf Methoden, die Karten zu wechseln:
public void first (Container parent); public void last (Container parent); public void next (Container parent); public void previous(Container parent); public void show (Container parent, String name);
Die contendPane wird nachfolgend auf CardLayout gesetzt und es werden drei Bilder eingefügt. Mittels next() werden dann die Karten gewechselt.
|
JFrame jf= new EJFrame("3 Cards"); Container cp= jf.getContentPane(); CardLayout cl; cp.setLayout(cl= new CardLayout()); JLabel im; for (int i= 0; i<3; i++) { im= new JLabel(); // langweilige Version, siehe GridLayout im.setIcon(new ImageIcon("C:/Scan/bild"+i+".gif")); cp.add(im,"image"+i); } jf.pack(); jf.setVisible(true); while (true) { try { Thread.sleep(2000); } catch (Exception e) {} cl.next(cp); // wechselt am Ende zur ersten Karte }
GridLayout:
grafische Matrix mit einheitlichen Zellen
Wie bei einer Matrix, wird bei der Anlage eines GridLayouts in der Regel die Anzahl der Zeilen und Spalten angegeben.
Da es sich um eine »grafische« Matrix handelt, können die Abstände in Pixel zwischen den Zellen angegeben werden. Die beiden ersten Konstruktoren sind Spezialfälle des letzten unten angeführten, d.h. rufen diesen über this(..) auf.
public GridLayout(); // this(1, 0, 0, 0); public GridLayout(int rows, int cols); // this(rows,cols,0,0) public GridLayout(int rows, int cols, int hgap, int vgap);
Das folgende Code-Fragment ist nur eine einfache Variation des Beispiels in CardLayout.
|
JFrame jf= new EJFrame("GridLayout"); Container cp= jf.getContentPane();
Bildübergabe an
JLabel-Konstruktor
cp.setLayout(new GridLayout(2,2,10,10)); for (int i= 0; i< 3; i++) cp.add(new JLabel(new ImageIcon("C:/Scan/bild"+i+".gif"))); cp.add(new JButton("Schaltfläche")); jf.pack(); jf.setVisible(true);
JLabel kann im Konstruktor sofort ein Bild übergeben werden. Alle Komponenten bekommen die gleiche Größe, und zwar die der größten Komponente.
GridBagLayout:
große Flexibilität vs. Codier-Aufwand
Das GridBagLayout ist aufgrund der GridBagConstraints-Instanz, die bei add() mit jeder Komponente übergeben wird, sehr flexibel.
GridBagConstraints:
Steuerung mittels elf Parametern
| Der Aufwand für das Einfügen einer Komponente mit Hilfe eines GridBagLayout ist in der Regel größer als beim null-Layout. |
Mit Hilfe von elf Parametern(!), gekapselt in einer GridBagConstraints-Instanz, wird das Verhalten jeder einzelnen Komponente definiert.
public GridBagConstraints(); // setzt Default-Werte public GridBagConstraints(int gridx,int gridy, int gridwidth,int gridheight, double weightx,double weighty, int anchor, int fill, Insets insets, int ipadx,int ipady)
Übersicht über GridBagConstraints- Parameter
| Parameter | Bedeutung |
| gridx gridy |
Zeile/Spalte in der Matrix. Die Konstante RELATIVE (der Default-Wert) bedeutet bei x bzw. y rechts bzw. unter der letzten eingefügten Komponente. |
| gridwidth gridheight |
Anzahl der benötigten Zellen in der Zeile/Spalte (Default-Wert: 1,1). RELATIVE zeigt die vorletzte Komponente der Zeile bzw. Spalte an. REMAINDER bedeutet die letzte Komponente, die alle restlichen Zellen der Zeile/Spalte besetzt. |
| weightx weighty |
Steht zusätzlicher Platz in der Zeile oder Spalte zur Verfügung, wird dieser nach der relativen Gewichtung in der entsprechenden Zeile bzw. Spalte aufgeteilt. Je höher der Wert, umso größer die Zuteilung (Default-Wert: 0.0). Bei 0.0 erhält die Komponente nichts. |
| anchor | Richtet die Komponente in der Zelle aus und bestimmt, welche Seite der Komponente zuerst abgeschnitten werden soll. Es können die Kompass-Bereiche NORTH, NORTHEAST, NORTHWEST, SOUTH, SOUTHEAST, SOUTHWEST, EAST, WEST und das Zentrum CENTER (der Default-Wert) angegeben werden. |
| fill | Art, wie der zusätzliche Platz von der Komponente selbst ausgefüllt wird: NONE (der Default-Wert), HORIZONTAL, VERTICAL oder BOTH. |
| insets | Instanz der Klasse Insets, welche die Abstände um die Komponente herum spezifiziert (siehe zweites Beispiel). |
| ipadx ipady |
Anzahl der Pixel, um die die minimale Größe der Komponente selbst vergrößert werden soll. |
Myriaden von Constrains-Kombinationen lassen sich ohnehin nicht darstellen. Zwei Beispiele zeigen deshalb nur die prinzipiellen Möglichkeiten, wobei das zweite ein Layout für einfache Bildschirmformulare nachbildet (und deshalb recht nützlich ist).
Aufgabe: Ein Bild mit einer Schaltfläche gleicher Breite unter dem Bild soll links am Fenster fixiert werden (siehe Abb. 15.17).
|
JFrame jf= new EJFrame("GridBagLayout"); Container cp= jf.getContentPane(); cp.setLayout(new GridBagLayout());
GridBagConstraints c= new GridBagConstraints(); // Default
cp.add(new JLabel(new ImageIcon("C:/Bilder/bild0.gif")),c);
weightx: Unsichtbare Komponente bekommt gesamten zusätzlichen Raum
// c kann wiederverwendet werden, da es im add() kopiert wird c.weightx=1.; cp.add(new JLabel(""),c);
c.weightx=0; // wieder auf Default-Wert c.gridy= 1; // unterhalb anordnen c.ipadx= 75; // Komponentenbreite vergrößern cp.add(new JButton("Nr. 1"),c); // Übergabe des Constraints
jf.pack(); jf.setVisible(true);
Erklärung: Jedem add() wird ein Constrains c übergeben, welches wiederverwendet werden kann.
Mit Hilfe eines positiven weightx für die rechte unsichtbare JLabel-Komponente wird der zweiten Spalte die gesamte zusätzliche Breite zugeordnet.

GridBagLayout: Felder von Eingabeformularen optimal ausrichten
Aufgabe: Erstellen eines Formulars mit untereinander angeordneten Eingabefeldern mit vorangestellten Bezeichnungen (siehe Abb. 15.18).
|
class InputField { private String name; private int len; public InputField(String s, int l) { name= s; len= l; } public String getName() { return name; } public int getLength() { return len; } }
class InputForm extends JPanel { private JTextField[] txtfld;
public InputForm (InputField[] fld) { setLayout(new GridBagLayout());
GridBagConstraints c= new GridBagConstraints(); c.anchor= GridBagConstraints.WEST; c.insets= new Insets(3,3,3,3); ¨ txtfld= new JTextField[fld.length]; // Texteingabefelder for (int i= 0; i<fld.length; i++) {
c.gridy= i; c.gridx=0; // neue Zeile, 1.Spalte add(new JLabel(fld[i].getName()),c); c.gridx= 1; add(txtfld[i]= new JTextField(fld[i].getLength()),c); c.weightx= 1.; add(new JLabel(),c); c.weightx= 0.; ¦ }
c.gridy=fld.length; c.weighty=1.; add(new JLabel(),c); Æ } String getInput(int fpos) { return txtfld[fpos].getText(); } void setInput(int fpos,String s) { txtfld[fpos].setText(s);} }
Zu ¨: Damit die Felder nicht zu dicht aneinander »kleben«, wird mit Hilfe einer Insets-Instanz ein gewisser Abstand gehalten.
Zu ¦ und Æ: In Abb. 15.18 nicht zu erkennen ist die zweite Forderung, dass die Felder in der linken oberen Ecke fixiert werden sollen, d.h. bei Fenstervergrößerungen stabil bleiben (siehe Abb. 15.19).
Dies wird (wie im ersten Beispiel) durch unsichtbare Endkomponenten erreicht, deren weightx oder am Ende weighty größer Null ist.
Reaktion des
Formulars auf Vergrößerung
|
Abschließend die obligatorische Test-Klasse:
public class Test { public static void main(String[] args) { JFrame jf= new EJFrame("Eingabeformular"); Container cp= jf.getContentPane(); cp.setLayout(new BorderLayout());
cp.add(new InputForm( // default: im CENTER new InputField[] { new InputField("Erstes Feld",10), new InputField("2. Feld",2), new InputField("Letztes Feld",20)}) );
JPanel jp= new JPanel(); // default: FlowLayout jp.add(new JButton("Ok")); jp.add(new JButton("Abbruch")); cp.add(jp,BorderLayout.SOUTH);
jf.pack(); jf.setVisible(true); } }
Das BorderLayout wird für die abschließende Schaltleiste benötigt.
BoxLayout:
optimal für eine Zeile bzw. Spalte
In Swing wurde BoxLayout speziell für Toolbars entworfen, d.h. Symbolleisten, die entweder horizontal oder vertikal ausgerichtet sind.
Hierzu wird dann die zugehörige Klasse Box7 verwendet, die über zusätzliche Methoden für Toolbars verfügt.
Unterschiedlich große Komponenten möglich
Aber BoxLayout kann durchaus auch für andere Container verwendet werden und ist dann eine Alternative zu GridLayout, wenn nur eine Zeile oder Spalte mit unterschiedlich großen Komponenten angezeigt werden soll.
Ein Dialog-Fenster mit BoxLayout enthält nachfolgend horizontal, dann vertikal zwei Bilder und eine Schaltfläche (siehe Abb. 15.20).
|
JDialog jf= new JDialog((Frame)null," "); Container cp= jf.getContentPane(); cp.setLayout(new BoxLayout(cp,BoxLayout.X_AXIS));
cp.add(new JLabel(new ImageIcon("C:/Temp/cross.gif"))); cp.add(new JLabel(new ImageIcon("C:/Temp/Kaktus.gif"))); cp.add(new JButton("Ok"));
jf.pack(); jf.setVisible(true);
Umschalten der Darstellung von Zeile auf Spalte
try { Thread.sleep(5000); } catch (Exception e) {} jf.setVisible(false); cp.setLayout(new BoxLayout(cp,BoxLayout.Y_AXIS)); jf.pack(); jf.setVisible(true);
Wie zu erkennen ist, bleiben die bevorzugten Größen der Komponenten erhalten.
Ohne eine Art der Behandlung von äußeren Ereignissen (events) wie Tastatureingabe, Mausbewegung oder -klicken ist die schönste Grafikoberfläche ziemlich autistisch (wie z.B. auch das Eingabeformular in GridBagLayout).
Ein Ereignis-Modell war also von Anfang an Bestandteil von Java, wobei es sich allerdings von Java 1.0 zu 1.1 drastisch geändert hat.
Notgedrungen wurden eine Zeit lang zwei Modelle erklärt. Mit dem Aussterben der alten JVM kann man sich nun auf das neue beschränken.
Betrachtet man die Behandlung von Ereignissen, wird recht schnell klar, dass man ohne ein generelles, erweiterbares Modell nicht klar kommt.
Nach dem ersten Missgriff in Java 1.0 wurde in 1.1 ein brauchbares Verhaltensmuster, das Event-Delegation-Modell8 eingeführt. Dieses Modell entspricht einer speziellen Form des Observer-Patterns, was daher zuerst vorgestellt werden soll.
Observer-Pattern:
Muster für Beobachter und zu Beobachtende
| Das Observer-Pattern lässt die dynamische Registrierung von Beobachtern (Observers) bei zu beobachtenden Objekten (Observable) zu, von deren Zustandsänderung sie per Callback informiert werden wollen. |
Beobachter und zu Beobachtende sind nur Interfaces IObserver und IObservable, da keine weiteren Kenntnisse über die Klassen vorliegen.
Die konkreten Beobachter-Klassen implementieren als IObserver nur Callback-Methoden, allgemein notify() genannt. notify() wird bei Zustandsänderung vom Observable-Objekt aufgerufen und mit entsprechenden Argumenten als Information versehen.
Multicaster für Benachrichtigungen
| Ein Multicaster dient zur Entkopplung der Registrierungs- und Benachrichtigungs-Mechanismen der Observable-Klassen. |
Damit kann ein Multicaster von verschiedenen Observable-Klassen genutzt werden, ohne dass diese die Implementierung der Registrierung bzw. Benachrichtigung wiederholen müssen (siehe Abb. 15.21).
Der Multicaster ist natürlich eine Indirektionsstufe mehr, die sich nur lohnt, wenn es mehrere Observable-Klassen und mehrere Observer-Instanzen für eine Observable-Instanz gibt.
Observer-Pattern mit Interfaces und Multicaster
|
Die Methoden notify(), register() und unregister() im Modell können natürlich abhängig von der Applikation unterschiedlich benannt werden, wie z.B. prozess<X>(), add<X>() und delete<X>().
Bei sehr vielen Zustandsänderungen im Observable-Objekt besteht die Notwendigkeit, die Benachrichtigungen erst in eine Warteschlange (z.B. des Multicasters) einzureihen, um dann ein Batch-notify() – eine Zusammenfassung von mehreren Benachrichtigungen – durchzuführen.
| Der Vorteil der Batch-Verarbeitung liegt einerseits in weniger notify()-Aufrufen und andererseits darin, dass mehrere gleichartige Zustandswechsel zu einem einzigen zusammengefasst werden können. |
Im Package java.util gibt es bereits seit Java 1.0 eine Klasse Observable und ein Interface Observer, die zusammen eine simple Version des Patterns darstellen und auch nicht weiter benutzt werden.
Vereinfachtes Observer-Pattern in java.util
|
Observable vereint Interface, Klasse und Multicaster und kann also nur als Superklasse oder per Delegation genutzt werden.

Ereignis- Delegation:
Notation und Aufbau
Das Ereignis-Delegations-Modell im AWT bzw. in Swing ist eine weit verzweigte Spezialisierung des o.a. Observer-Patterns.
| Die Rolle des IObserver wird Event-Listener genannt. |
| Die Observable-Klassen werden Event-Sources genannt, worunter alle Komponenten des AWTs und Swings fallen.9 |
| Die Event-Listener registrieren sich bei den Event-Sources, an deren Ereignissen sie interessiert sind. |
| Das Auftreten eines Ereignisses in einer Event-Source stellt dann die Zustandsänderung dar, die die Benachrichtigung auslöst. |
| Mit Hilfe der Callback-Methode(n) der Event-Listener-Interfaces wird das Ereignis dem Event-Listener übergeben. Die Ereignisse enthalten eine Referenz auf die Event-Source und ein Ident ihrer Art.10 |
| Die Benachrichtigungs-Methoden in den Event-Listener-Interfaces haben statt notify() Namen, die zu den Ereignissen passen. |
Die meisten Ereignisse haben natürlich ihren Ursprung im Betriebssystem und werden durch einen internen, für die Event-Listener transparenten Verteiler-Thread den Event-Sources zugeordnet.
Aber aus der Sicht der Event-Listener generieren die Event-Sources die Ereignisse.
Event-Dispatch-Thread als Multicaster
Natürlich gibt es auch einen Multicaster in einem Event-Dispatch-Thread, der die Callback-Methoden für die Event-Sources aufruft. Aber auch dieser ist aus der Sicht der Event-Listener transparent.
Nur bei Multi-Threading oder der Entwicklung eigener AWT-Komponenten muss man sich dieser Tatsache bewusst sein.11
Das Ereignis-Modell ist wirklich sehr verzweigt und verteilt sich auf verschiedene Packages, Klassen- und Interface-Hierarchien.
Um den Überblick zu behalten, sind die meisten Klassen und Interfaces in den Packages java.awt.event und javax.swing.event enthalten.
Namenskonvention zur Ereignis-Delegation
Es gibt auch eine Namenskonvention, die Klassen, Interfaces und Zusammengehörigkeiten identifiziert.
| Alle Ereignisse in den Packages leiten sich aus der Basisklasse java.util.EventObject ab und enden auf Event. |
Die Basisklasse enthält u.a. getSource() zur Identifizierung der Event-Source.
| Ist der Name eines Ereignisses <X>Event, ist der des zugehörigen Interfaces <X>Listener. |
| Die Methoden zur (De-)Registrierung in den Event-Sources heißen dann add<X>Listener und remove<X>Listener. |
Die Listener-Interfaces haben nicht nur eine notify()-Methode, sondern abhängig vom Ereignis durchaus mehrere. Jede dieser Methoden hat jedoch nur genau einen <X>Event-Parameter (siehe Abb. 15.23).
Ist ein Observer nur an einer Methode interessiert, kann er auf Adapter-Klassen zurückgreifen, die bereits die Methoden des Listener-Interfaces mit leeren Implementierungen überschrieben haben. Er braucht dann nur die zu überschreiben, an der er interessiert ist.
| Die Adapter, zugehörig zum <X>Listener, heißt <X>Adapter. |
Template zur Struktur des Event-Delegations-Modells
|
Das o.a. Diagramm ist ein Template, da es keine wirklichen Klassen und Interfaces enthält. <X>Listener steht z.B. allein für 15 Listener im AWT. <EventSource> steht z.B. für jede beliebige Komponente, wobei diese durchaus mehr als einen Listener registrieren kann.
Wie fast zu erwarten, steht <X>Event für unterschiedliche Ereignis-Typen in package-abhängigen Unterhierarchien.
Hier unterscheidet man vor allem die Basis-AWT-Ereignisse, abgeleitet von java.awt.AWTEvent, und die zusätzlichen swing-spezifischen Ereignisse, abgeleitet vom AWTEvent oder direkt von EventObject.
Klassen-Hierarchie zu Events und Listeners
|
EventQueue mit Dispatch-Thread
Ein Objekt der Klasse EventQueue enthält in seiner Warteschlange alle Ereignisse, die an die Listener übergeben (dispatched) werden müssen.
Das AWT legt genau ein Objekt von EventQueue an. Es erzeugt automatisch einen zugehörigen Dispatch-Thread, der Ereignisse am Kopf der Warteschlange versendet und am Ende eingefügt.
Überblick über
Events, Listeners und Methoden
| Event /Listener | Ereignis-Generierung | Listener-Methoden |
| ActionEvent ActionListener |
Komponenten-Ereignis (z.B. Anklicken) | actionPerformed() |
| AWTEvent AWTEventListener |
Bei allen AWT-Ereignissen, nützlich für Event-Recoder | eventDispatched() |
| AdjustmentEvent AdjustmentListener |
Bei Anpassaktionen, z.B. bei Roll/Scrolling-Operationen | adjustmentValue- Changed() |
| ComponentEvent ComponentListener |
Bei Bewegung, Änderung der Größe, Sichtbarkeit der Komponente | componentHidden() componentMoved() componentResized() componentShown() |
| ContainerEvent ContainerListener |
Bei Änderung der Komponenten in einem Container | componentAdded() componentRemoved() |
| FocusEvent FocusListener |
Komponente erhält/verliert den Fokus (siehe ) | focusGained() focusLost() |
| HierarchyEvent HierarchyListener |
Bei Änderung der Hierarchie der Komponenten | hierarchyChanged() |
| HierarchyEvent HierarchyBoundsListener | Bei Änderung eines übergeordneten Containers | ancestorMoved() ancestorResized() |
| InputMethodEvent InputMethodListener |
Texteditier-Methode ändert Text oder Textposition | inputMethodTextChanged() caretPositionChanged() |
| ItemEvent ItemListener |
(De-)Selektieren von Elementen in Komponenten | itemStateChanged() |
| KeyEvent KeyListener |
Drücken/Loslassen einer Taste. Bei Eingabe einer Taste | keyPressed() keyReleased() keyTyped() |
| MouseEvent MouseListener |
Bei Drücken/Loslassen oder beidem (Klicken der Maus). Bei Eintritt/Verlassen des Mauszeigers in/aus Komponente | mouseClicked() mouseEntered() mouseExited() mousePressed() mouseReleased() |
| MouseEvent MouseMotionListener |
Bei Mausbewegung | mouseDragged() mouseMoved() |
| TextEvent TextListener |
Bei Änderung eines Texts | textValueChanged() |
| WindowEvent WindowListener |
Bei Fensteränderungen (z.B. Öffnen, Schließen, Verkleinern, Vergrößern zum Icon) | windowActivated() windowClosed() windowClosing() windowDeactivated() windowDeiconified() WindowIconified() WindowOpened() |
Um über interessante Ereignisse informiert zu werden, muss man nach dem Ereignis-Delegations-Modell den passenden Listener implementieren und sich bei der Source mittels add<X>() anmelden.
Innere Klassen zur Ereignisverarbeitung
| Da die Implementierung in der Regel nur aus einer Methode besteht, kann man die eigentliche Ereignisverarbeitung an eine innere Klasse delegieren. |
Listener-Adapter: vereinfachte Implementation
| Sollte es ein Listener mit mehreren Methoden sein und man ist nur an einer oder zwei interessiert, wird die innere Klasse vom Listener-Adapter abgeleitet. |
Im Folgenden wird die Klasse InputForm zum Eingabeformular in Abb. 15.1 (siehe GridBagLayout) benutzt, um vier verschiedene Möglichkeiten zu zeigen, Listener zu implementieren.
Ereignisverarbeitung in einem Eingabeformular
|
Mit den vier Schaltflächen des Formulars »Inventar-Eingabe« (Abb.15.24) sind folgende Aktionen verbunden:
| <Neu>: Vergibt eine zufällige ganze Zahl als Inventar-Nr. und leert das Feld «Bezeichnung”. |
| <Speichern>: Speichert den Inventar-Artikel ab, sofern eine Bezeichnung eingetragen wurde. |
| <Pfeil zurück>: Zeigt – wenn vorhanden – den vorherigen Inventar-Artikel an, sonst den letzten. |
| <Pfeil vor>: Zeigt – wenn vorhanden – den nächsten Inventar-Artikel an, sonst den ersten. |
Wird das Icon »Fenster schließen« angeklickt, wird das Programm (wie bisher) ohne Vorwarnung beendet.
// 1. Alternative: die Frame selbst ist Listener class InventarFrame extends JFrame implements ActionListener {
[A1]
Frame ist selbst Listener (Implementierung am Ende)
InputForm frm; // Implementierung in GridBagLayout Item inv; // zeigt auf aktuellen Eintrag java.util.List iLst; // Liste enthält Inventar-Einträge int pos; // Position in Liste
// Inventar-Eintrag, nur das absolut notwendige private static class Item { int nr; String bez; public Item(int nr, String s) { this.nr= nr; bez= s; }
}
// Darstellung des Eintrags im Formular public void showCurrentItem() { frm.setInput(0,Integer.toString(inv.nr)); frm.setInput(1,inv.bez); }
// Neuer Eintrag im Formular: zufällige Nr., Bez. leer private void newItem() { frm.setInput(0, Integer.toString((int)(Math.random()*1000000.))); frm.setInput(1,""); pos= -1; }
[A2]
Member-Klasse ist Listener
// 2. Alternative: Member-Klasse als Listener private class StoreItem implements ActionListener {
public void actionPerformed (ActionEvent e) { if(frm.getInput(1).equals("")) return; if (pos==-1) { // Lazy Creation: Anlage nur, wenn notwendig if (iLst==null) iLst= new ArrayList(); iLst.add( // RuntimeException wird nicht abgefangen inv= new Item(Integer.parseInt(frm.getInput(0)), frm.getInput(1))); pos= iLst.size()-1; } else if (inv!=null) inv.bez= frm.getInput(1); } }
/* Im Konstruktor wird das Formular mit Aktionen angelegt */
public InventarFrame() { super("Inventar-Eingabe"); // Konstruktor: JFrame
[A3]
Anonyme Klasse als Listener: abgeleitet von WindowAdapter
// 3. Alternative: Anonyme Subklasse eines Adapters // Event-Source ist InventarFrame selbst // Registrierung der anonymen Klasse addWindowListener(new WindowAdapter() { // es wird nur die benötigte Methode überschrieben public void windowClosing(WindowEvent e) { System.exit(0); } });
Container cp= getContentPane(); cp.setLayout(new BorderLayout()); cp.add(frm= new InputForm( // default: im CENTER new InputField[] { new InputField("Inventar-Nr.",6), new InputField("Bezeichnung",20)}) ); JPanel jp= new JPanel();
JButton newB= new JButton("Neu"), storeB= new JButton("Speichern"), prevB= new JButton(new ImageIcon("C:/Scan/prev.gif")), nextB= new JButton(new ImageIcon("C:/Scan/next.gif")); jp.add(newB);jp.add(storeB);jp.add(prevB); jp.add(nextB);
cp.add(jp,BorderLayout.SOUTH);
[A4]
Anonyme Klasse als Listener: abgeleitet vom Interface ActionListener
// verbindet mit der Schaltfläche einen String-Schlüssel revB.setActionCommand("P");
// 4. Alternative: Anonyme Klasse vom Listener-Interface newB.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) {newItem();} });
// zur 2. Alternative: Registrierung bei Schaltfläche storeB.addActionListener( new StoreItem() ); // zur 1. Alternative: Registrierung von InventarFrame prevB.addActionListener(this); nextB.addActionListener(this);
pack(); setVisible(true); }
/* Zum Schluss die Implementierung der 1. Alternative */
public void actionPerformed(ActionEvent e) { if (iLst==null) return; int opos=pos;
// getActionCommand() liefert den String-Schlüssel // zur Unterscheidung der beiden Schaltflächen if(e.getActionCommand().equals("P") || pos==-1) pos= (pos>0 ? pos-1:iLst.size()-1); // round robin else pos= (pos+1<iLst.size() ? pos+1:0); // andere Richtung if (pos!= opos) { inv= (Item) iLst.get(pos); showCurrentItem(); } } }
Der Test besteht dann z.B. im Erzeugen einer InventarFrame-Instanz:
new InventarFrame();
Eine GUI-Oberfläche hat in der Regel diverse Fenster mit Containern und Komponenten. Für eine geregelte Interaktion mit dem Benutzer existiert folgende Regelung:
| Es gibt immer ein aktives Fenster und innerhalb des Fensters eine aktive Komponente. Sie besitzen dann den Fokus. 12 |
Das Fokus-Management innerhalb von Swing übernimmt der DefaultFocusManager, Subklasse der abstrakten Klasse FocusManager.
| Unter einem Fokus-Zyklus versteht man alle Komponenten, die mittels <TAB> bzw. <SHIFT><TAB> angesprungen werden können. |
| Das Default-Sprungverhalten beim Fokus-Zyklus ist von links nach rechts und von oben nach unten. Es umfasst alle Komponenten des Top-Level-Containers. |
Container mit eigenem Fokus-Zyklus
Ein Container kann einen eigenen Fokus-Zyklus festlegen, d.h., mit <TAB> bzw. <SHIFT><TAB> können dann nur seine Komponenten angesprungen werden.
Der Container muss hierzu die Methode isFocusCycleRoot() mit return true überschreiben, da sie per Default false liefert.
Anpassungen des Fokus-Managers
Bestimmte Komponenten wie z.B. JLabel erhalten vom Fokus-Manager per Default nie den Fokus, d.h., isFocusTraversable() liefert false. Sie können dann nicht per <TAB> bzw. <SHIFT><TAB> angesprungen werden. Auch hier kann das Verhalten isFocusTraversable() entsprechend überschrieben werden.
Eine Komponente, die vom Fokus-Manager nie den Fokus erhält, kann dies aber durchaus programmintern mit Hilfe der Methode requestFocus(), sofern die Methode setRequestFocusEnabled() den Wert true liefert. Auch hier kann das Verhalten überschrieben werden.
| Liefern beide Methoden, isFocusTraversable() und setRequestFocusEnabled(), einer Komponente den Wert false, kann sie keinen Fokus mehr erhalten. |
Mit Hilfe der Methode setNextFocusableComponent() kann man entgegen dem Default die Sprungfolge des Fokus-Managers selbst bestimmen.
Die Methode isManagingFocus() muss mit return true; überschrieben werden, sofern eine Komponente die Sprungfolge mit <TAB> bzw. <SHIFT><TAB> selbst steuern will. Hierzu muss sie dann processComponentKeyEvent(KeyEvent e) implementieren.
In Tabelle 15.4 sind noch einmal die o.a. Methoden zusammengefasst.
Methoden zur Manipulation des Fokus
| Fokus-Methoden zu setzen bzw. zu überschreiben |
Beschreibung |
| boolean isFocusCycleRoot() | Zu überschreiben für Container mit eigenem Fokus-Zyklus |
| boolean isFocusTraversable() | Zu überschreiben, wenn nicht im Fokus-Zyklus |
| boolean isManagingFocus() | Zu überschreiben, wenn das Fokus-Management von einer Komponente übernommen werden soll |
| void setNextFocusableComponent (Component aComponent) |
Setzen der Sprungfolge (über Fokus-Zyklen hinweg) |
| void setRequestFocusEnabled (boolean aFlag) |
Erlaubt (nicht) das programmtechnische Setzen des Fokus |
Zwei Instanzen von JPanel mit einem Grid-Layout enthalten vier Textfelder bzw. vier Schaltflächen. Jedes Panel hat einen eigenen Fokus-Zyklus.
Fokus-Management: Beispiel mit zwei Zyklen
Im ersten Panel werden die Textfelder der zweiten Spalte aus dem Zyklus herausgenommen, im zweiten wird die Sprungfolge auf »Spalten zuerst« abgeändert (siehe Abb. 15.26).
|
public class Test { public static void main(String[] args) { JFrame jf= new EJFrame("Fokus-Management"); Container cp= jf.getContentPane(); cp.setLayout(new BoxLayout(cp,BoxLayout.X_AXIS)); JPanel[] jp= new JPanel[2]; for (int i= 0; i<2; i++) { cp.add(new JLabel(i+1+".Zyklus ")); jp[i]= new JPanel() { // anonyme Klasse public boolean isFocusCycleRoot() { return true; } }; jp[i].setLayout(new GridLayout(2,2)); cp.add(jp[i]); } JButton[] jb= new JButton[4]; int[] jmp= new int[] {0,2,1,3}, nxt= new int[] {2,3,1,0}; for (int i= 0; i< 4; i++) { jp[0].add(i%2==0? new JTextField(10):new JTextField() { { super.setColumns(10); } // anonyme Klasse
public boolean isFocusTraversable() { return false; } }); jp[1].add(jb[i]= new JButton("Nr. "+jmp[i])); } for (int i= 0; i< 4; i++)
jb[i].setNextFocusableComponent(jb[nxt[i]]); jf.pack(); jf.setVisible(true); }}
Betrachtet man die Ereignisse, aufgelistet in Tabelle 15.3, kann man sie anhand ihrer Entstehung bzw. Semantik klassifizieren.
Ereignisse werden aufgrund ihres Typus in Ebenen eingeordnet.
| Die meisten Ereignisse haben ihren Ursprung im Betriebssystem und liegen semantisch in der Regel auf einem unteren Niveau. |
Hierzu zählen unter anderem alle MouseEvent-Ereignisse. Die Methoden keyPressed() und keyReleased() gehören ebenfalls zu zwei Low-Level-Ereignissen von KeyEvent, keyTyped() dagegen zu einem höherstufigen.
| Ereignisse, die nicht vom Betriebsystem, sondern von Komponenten generiert werden, liegen auf einer semantisch höheren Ebene. |
Hierzu zählt z.B. das ActionEvent. Die Komponente muss in diesem Fall Betriebssystem-Ereignisse abfangen und gegebenenfalls in ein eigenes Ereignis vom Typ ActionEvent umwandeln.
| Event-Listener sollten – sofern sie die Wahl haben – ein Ereignis möglichst hoher Ebene wählen. |
Eine Komponente vom Typ Component wird über ein Ereignis auf Betriebssystem-Ebene durch Aufruf ihrer Methode processEvent() informiert.
process<X>Event():
spezialisierte Low-Level Events
In processEvent() erfolgt die erste Verteilung (Dispatching) anhand des Event-Typs auf spezialisierte process<X>Event(). Diese Methoden übergeben dann die Ereignisse an die registrierten Listener.
Alle Methoden sind protected. Deshalb können Subklassen von Component wie z.B. JComponent diese – sofern notwendig – überschreiben. Es gilt folgende Regel für die Ereignisbearbeitung:
Ereignisbearbeitung von Low-Level-Events
| Ein Ereignis wird einer Komponente nur zur Behandlung bzw. zur Verteilung übergeben, sofern |
| hierfür mindestens ein Listener registriert ist. |
| sie mittels Aufruf von void enableEvents(long eventsToEnable) die Ereignisbehandlung für sich selbst ermöglicht hat. |
Dabei ist eventsToEnable eine Bitmaske, die mit Hilfe von Konstanten der Form <X>_EVENT_MASK der Klasse AWTEvent zur Identifizierung der Ereignisse gesetzt wird.
In Tabelle 15.5 sind die zugehörigen Ereignisse, Methoden und Masken für Low-Level-Events zusammengestellt, und zwar in der Reihenfolge, in der sie in Component bearbeitet werden.13
Bearbeitungsreihenfolge von Low-Level-Events
| Ereignis | process<X>Events | ...EVENT_MASK |
| FokusEvent | processFocusEvent() | FOCUS_ |
| MouseEvent | processMouseEvent() | MOUSE_ |
| MouseEvent | processMouseMotionEvent() | MOUSE_MOTION_ |
| KeyEvent | processKeyEvent() | KEY_ |
| ComponentEvent | processComponentEvent() | COMPONENT_ |
| InputMethodEvent | processInputMethodEvent() | INPUT_METHOD_ |
| HierarchyEvent | processHierarchyEvent() | HIERARCHY_ |
| HierarchyEvent | processHierarchyBoundsEvent() | HIERARCHY_BOUNDS_ |
AWT vs. Swing: geänderte Low-Level Event-Bearbeitung
AWT vs. Swing: Die spezialisierten Komponenten im AWT erzeugen aufgrund der Low-Level-Ereignisse eigene Ereignisse. Diese können im AWT nach demselben Muster wie in Tabelle 15.5 in den Komponenten verarbeitet werden, wie z.B.:
ActionEvent processActionEvent() ACTION_EVENT_MASK
| Dies ist in Swing nicht mehr in der Form möglich, da Swing eine verteilte Komponenten-Architektur hat. Als Ersatz bietet Swing andere Mechanismen (wie z.B. Tastatur-Aktionen). |
Die Ereignisbehandlung einer Komponente wird normalerweise automatisch durch die Registrierung von Listenern ermöglicht (Regel in Ereignisverteilung bzw. Event-Dispatching).
Soll eine Subklasse einer Komponente die Bearbeitung und/oder Verteilung selbst übernehmen, so gibt es zwei Alternativen:
Die neue Subklasse der Komponente implementiert
| 1. | zu den Ereignissen, die sie bearbeiten will, selbst den entsprechenden Listener. |
| 2. | Ruft sie enableEvents() auf und überschreibt process<X>Event(). |
Reihenfolge der Listener-Aufrufe
Für die Beurteilung der beiden Alternativen ist folgende Regel wichtig:
| Hat eine Komponente mehrere Listener, gibt es keine feste Ordnung, in der die Listener aufgerufen werden. |
Dies bedeutet, dass bei der ersten Alternative die neue Komponente nicht unbedingt zuerst, sondern durchaus auch nach den externen Listenern benachrichtigt werden kann, d.h. natürlich in der Regel zu spät.
Bei der zweiten Alternative ist dagegen gewährleistet, dass zuerst process<X>Event() aufgerufen wird, bevor andere Listener benachrichtigt werden.
Konsumieren vs. Weitergeben von Events
Bei einem Textfeld soll der erste Buchstabe immer groß geschrieben werden. Dies soll nach Verlassen des Felds geprüft werden.
class FUTextField extends JTextField { public FUTextField(int size) { super(size); enableEvents(AWTEvent.FOCUS_EVENT_MASK); }
protected void processFocusEvent(FocusEvent e) { if (FocusEvent.FOCUS_LOST==e.getID()) { String s= getText(); if (s.length()>0) setText( s.substring(0,1).toUpperCase()+s.substring(1)); } //super.processFocusEvent(e); ¨ } }

Probleme beim Konsumieren des Events
Zu ¨: Die Frage bei der Ereignisbearbeitung ist immer »konsumieren vs. nicht konsumieren«. Im oberen Beispiel wird das Fokus-Ereignis konsumiert und nicht mit super weitergereicht. Somit bekommt die Superklasse das FokusEvent nicht mit. Unangenehm, denn:
| Erst bei FOCUS_GAINDED schaltet die Komponente JTextField den Textcursor ein, d.h., erst dann sieht man die Eingabeposition im Feld. |
Das Ereignis-Delegations-Modell in Ereignis-Delegations-Modell (Abb. 15.23) ist die Umsetzung des Observer-Patterns. Dabei wurden die Event-Sources bisher als monolithische Komponenten betrachtet, was für die AWT-Komponenten auch richtig ist.
Swing fügt der Ereignisverarbeitung jedoch noch ein weiteres Muster hinzu, das auf der Model-View-Controller-Architektur, kurz MVC, basiert.
Es folgt einer bereits in Smalltalk eingesetzten Architektur, die eine GUI-Komponente prinzipiell aus drei Elementen mit verteilten Aufgaben zusammensetzt.
| Model: Das Modell ist zuständig für den Zustand der Komponente, d.h., es enthält abhängig davon, was die Komponente darstellt, die dazu passenden Daten. |
| View: Die Sicht übernimmt die Darstellung der Komponente in Abhängigkeit von den Daten. Die Trennung ermöglicht verschiedene Sichten auf Basis derselben Datenstruktur. |
| Controller: Die Kontrollinstanz ist letztendlich für die Interaktion zuständig. Sie bestimmt, ob und wie die Komponente auf Ereignisse reagiert. |
MFC:
weniger ein Pattern, eher Design-Strategie
Offen in der Architektur ist außer der genauen Realisierung auch die Kommunikation zwischen den drei Elementen.
Denn ohne Daten weiß z.B. die View-Instanz sich nicht korrekt darzustellen, und ohne passende Controller-Mitteilung können wiederum keine Änderungen im Modell vorgenommen werden. Des Weiteren muss der Controller mit Ereignissen versorgt werden.
Eine konkrete MVC-Implementierung basiert in der Regel auf einer Variante des Observer-Patterns.
Wie zu erwarten basiert die Kommunikation zwischen den MVC-Komponenten in Swing wieder auf dem Ereignis-Delegations-Modell.
Die abstrakte Basisklasse JComponent enthält genau eine Instanz der abstrakten Klasse ComponentUI, die das View-Element darstellt.
| Das View-Element, die konkrete Implementierung mit allen darin enthaltenen Elementen, wird in Swing UI-Delegate genannt (siehe Abb. 15.27). |
JComponent meldet sich bei dem UI-Delegate mit installUI(this) an. Somit ist UI-Delegate in der Lage, eigene spezialisierte Listener bei JComponent anzumelden, die dann Controller-Aufgaben übernehmen.
JComponent enthält selbst keine Instanz einer Modell-Klasse. Aus der Sicht der Swing-Designer sind die Anforderungen an ein Modell derart abhängig vom Typ der Komponente, dass eine Definition auf der Ebene nicht sinnvoll scheint.
Konkrete Komponenten J<X> enthalten Referenz auf Modell
Erst die konkreten Komponenten-Klassen enthalten eine Referenz auf ein Modell-Interface. Somit kann der Swing-Anwender eine beliebige Klasse als Modell benutzen, sofern es nur das Interface implementiert (siehe Abb. 15.27).
| Jede konkrete Swing-Komponente J<X> enthält zumindest eine Modell-Instanz <X>Model und genau eine UI-Delegate-Instanz.14 |
Template zur Model-Delegate-Variante des MFC
|
In der Anwendung hat man mit dem UI-Delegate eigentlich nichts zu tun. Die Komponenten erzeugen – je nach L&F – automatisch eine passende Instanz.15
Dies gilt aber nicht unbedingt für die Modell-Klassen. Einfachen Komponenten wie JLabel und JButton überlässt man gerne die automatische Anlage einer Instanz einer Default-Modell-Klasse.
Bei komplexen Komponenten wie JTable oder JTree ist man aber häufig aufgrund der Anwendung gezwungen, eine eigene Modell-Klasse einzusetzen.
Nach Swing-Konvention existiert zu jeder Komponente ein(e)
| <X>Model-Interface, das als Event-Source Listener für Modelländerungen registriert. |
Default<X>Model als Default-Klasse
| Default<X>Model-Klasse, die das Interface implementiert und automatisch als Daten-Modell benutzt wird, sofern kein anderes angemeldet wird. |
Damit steht man vor der Wahl, eine eigene Modell-Klasse auf Basis des Interfaces zu entwickeln oder die bereits vorhandene Default-Modell-Klasse als Superklasse zu nutzen.
Scroll-Bar, Slider, Progress-Bar
Ein sehr einfaches Daten-Modell verbindet die grafischen Komponenten Scroll-Bar (Rollbalken), Slider (Schieberegler) und Progress-Bar (Fortschrittsleisten).
Eigenschaften/Zustände: BoundedRangeModel
Die gemeinsamen Zustandswerte im BoundedRange-Modell sind (siehe Abb. 15.28):
| min, max: Grenzen des erlaubten Intervalls |
| value: aktueller Wert im Intervall |
| extent: der aktuelle Wert ist selbst ein Intervall [value,value+extent] |
| isAdjusting: zeigt mit true an, dass der aktuelle Wert permanent angepasst wird |
|
Swing fügt einige High-Level-Events dem AWT hinzu. Eines davon ist das ChangeEvent, das – wie der Name bereits sagt – Änderungen von Werten anzeigt.
ChangeListener zur Kommunikation der Zustandswerte
Ein Daten-Modell muss Änderungen seiner Werte kommunizieren. Objekte, die an diesen Wertänderungen interessiert sind, müssen das ChangeListener-Interface implementieren und sich mit addChangeListener() bzw. removeChangeListener() bei dem Daten-Modell an- und abmelden.
Ein Scroll-Bar und ein Slider sollen über ein gemeinsames Daten-Modell synchronisiert werden. Der aktuelle Wert soll in einem Textfeld angezeigt und verändert werden können (Abb. 15.29).
Scroll-Bar und Slider mit gemeinsamen Zuständen
|
Zur Implementierung wird hier das DefaultBoundedRangeModel als Superklasse verwendet (Abb. 15.30).
Klassen-Diagramm zum BoundedRangeModel-Beispiel
|
Um Veränderungen der Werte zu kommunizieren, müssen MyRangeModel und ScrollValueField gegenseitige Listener sein.
Im Klassen-Diagramm erkennt man, dass eine Referenz des BoundedRangeModel gleichermaßen von drei visuellen Darstellungen genutzt werden kann.
Synchrone Anzeige des Zustands: gemeinsames Daten-Modell
In unserem Beispiel dient eine Instanz von MyRangeModel gleichzeitig als Daten-Modell für eine Scroll-Bar und einen Slider, womit diese dann die Werte von value synchron anzeigen.
In der Test-Klasse werden die Instanzen angelegt und miteinander verbunden:
public class Test { public static void main(String[] args) { JFrame jf= new EJFrame("BoundedRangeModel"); Container cp= jf.getContentPane();
int val= 50; // Default-Wert zum Starten
// Modell mit Intervall [0,100] und extent 0 MyRangeModel rm= new MyRangeModel(val,0,0,100);
// Anlage einer vertikalem Scroll-Bar JScrollBar jsb= new JScrollBar(); jsb.setOrientation(JScrollBar.VERTICAL); jsb.setPreferredSize(new Dimension(15,100));
Ein RangeModel –Object, zwei visuelle Instanzen
// Anlage eines Sliders mit Markierungsstrichen/Werten JSlider jsl= new JSlider(); jsl.setMajorTickSpacing(20); // Striche alle 20 Einheiten jsl.setPaintTicks(true); // Striche zeigen jsl.setPaintLabels(true); // Zahlenwerte zeigen
// Slider und Scroll-Bar auf gleiches Modell setzen jsl.setModel(rm); jsb.setModel(rm);
JTextField jtf= new JTextField(5);
addFocusListener()
addChangeListener()
// gegenseitig Textfeld und Modell als Listener anmelden jtf.addFocusListener(rm); rm.addChangeListener(new ScrollValueField(jtf,val));
cp.add(jtf,BorderLayout.CENTER); cp.add(jsb,BorderLayout.EAST); cp.add(jsl,BorderLayout.SOUTH); jf.pack(); jf.setVisible(true); } }
Nachfolgend werden noch die beiden Klassen ScrollValueField und MyRangeModel implementiert.
Beide Klassen gehen der Einfachheit halber davon aus, dass sie und keine anderen Klassen gegenseitige Listener sind.
Gegenseitige Listener
am Beispiel ScrollValueField und MyRangeModel
class ScrollValueField implements ChangeListener { JTextField jtf;
public ScrollValueField(JTextField tf, int val) { jtf= tf; jtf.setText(Integer.toString(val)); }
public void stateChanged(ChangeEvent e) { // unsauber, aber kurz: geht davon aus, dass der // Event von einem BoundedRangeModel ausgelöst wird jtf.setText(Integer.toString( ((BoundedRangeModel)e.getSource()).getValue())); } }
class MyRangeModel extends DefaultBoundedRangeModel implements FocusListener { public MyRangeModel(int val, int ext, int min, int max) { super(val,ext,min,max); }
public void focusLost (FocusEvent e) { // unsauber, aber kurz: geht davon aus, dass der // Event von einem Textfeld ausgelöst wird setValue(Integer.parseInt( ((JTextField)e.getSource()).getText())); } public void focusGained (FocusEvent e) {} }
Die Interaktion des Benutzers mit dem Programm zeugt von der Robustheit der Implementierung des RangeModels.
Denn bei Eingaben im Textfeld, die außerhalb des erlaubten Intervalls [0,100] liegen, werden diese vom Modell automatisch auf die obere bzw. untere Grenze korrigiert und an das Textfeld zurückgegeben, das prompt 0 oder 100 anzeigt. Natürlich synchronisieren auch die Regler.
Observer- und Event-Delegation: keine dynamischen Modelle
Das Observer- bzw. Event-Delegations-Modell sagt nichts über den Ablauf aus. Es beschreibt nur eine Klassen-Struktur, nicht die Dynamik, d.h. die Verteilung der Arbeit auf Threads. Gerade eine Ereignisverarbeitung muss aber schnell und reaktiv sein.
Swing läuft nicht in dem Hauptthread main(). In Swing wird die Ereignisbearbeitung und die MVC-Kommunikation unter mehreren Threads aufgeteilt, um wichtige Aufgaben asynchron ausführen zu können.
Die AWT/Swing-Ereignisse werden in einer Warteschlange, einer Instanz der Klasse EventQueue, verwaltet.
Event-
Verarbeitung: PostEventThread Dispatch-Thread
Der »PostEventQueue«-Thread ist dafür zuständig, Ereignisse am Ende dieser Warteschlage einzufügen. Mit Erschaffen der EventQueue wird weiterhin ein Dispatch-Thread gestartet, der die Ereignisse vom Kopf der Warteschlange entfernt und verteilt.
Dispatch-Thread ruft Callbacks
| Der Dispatch-Thread ist aus der Sicht des Swing-Anwenders von entscheidender Bedeutung, da aus dem Dispatch-Thread alle Callback-Methoden der Listener aufgerufen werden. |
Damit laufen alle Zugriffe der Listener auf Swing-Komponenten, bzw. das zugehörige Daten-Modell innerhalb des Dispatch-Threads, und sind deshalb auch thread-sicher.
Im Folgenden soll kurz die Frage geklärt werden, inwieweit Zugriffe anderer Threads sicher und wann sie überhaupt notwendig sind.
Unter erlaubten Zugriffen versteht man thread-sichere Zugriffe auf Swing, wobei diese nur sehr restriktiv möglich sind.
Restriktionen für
externe Threads
Für externe Threads gelten folgende Regeln:
| 1. | Nur wenige Methoden, die in Swing als thread-sicher markiert sind, dürfen aufgerufen werden. |
| Hierzu zählen als wichtigste asynchrone Methoden repaint() und revalidate() aus JComponent. |
| 2. | Aus externen Threads können Komponenten erschaffen, gesetzt und eingefügt werden, sofern für die beteiligten Komponenten (einschließlich der Container) nicht bereits pack(), setVisible() oder show() aufgerufen wurde. |
| 3. | Aus externen Threads können Listener an- und abgemeldet werden. |
Da nur der Dispatch-Thread die Methoden der Listener ausführt, können ernsthafte Probleme entstehen. Denn die Callback-Methode
| kann blockieren (z.B. aufgrund von I/O-Operationen). |
| kann für ihre Ausführung einen längeren Zeitraum benötigen. |
Die Arbeit des Dispatchers setzt aus, d.h. es werden keine Ereignisse mehr verteilt und verarbeitet, die GUI-Oberfläche »friert ein«. Man ist also gezwungen, einen neuen Thread zu starten.
Startet man aus den oben genannten Gründen einen externen Thread, hat man nach Erlaubte Zugriffe von Threads auf Swing folgende Regel zu beachten:
| 1. | Vor dem Start der Thread müssen alle Informationen den Komponenten entnommen werden. |
| 2. | Sind Methoden während der Thread-Ausführung aufzurufen, die die Swing-Komponenten betreffen, müssen diese im Dispatch-Thread ausgeführt werden. |
| public static void invokeAndWait(Runnable r) |
| public static void invokeLater(Runnable r) |
Der Unterschied zwischen beiden Methoden ist der, dass die erste synchron abläuft, also den externen Thread mit wait() bis zum Ende suspendiert, wogegen die zweite asynchron ausgeführt wird.
Runnable-
Interface als Kommando-Klasse
Das Runnable-Interface wurde nun nicht etwa genommen, um die Ausführung von run() in einem neuen Thread auszuführen17 , sondern nur um genau eine fest definierte Methode ohne Parameter innerhalb des Dispatch-Threads ausführen zu müssen.
| Die Methode run() ist somit ein Wrapper für die eigentliche Methode. |
Die Klasse Timer im Package javax.swing erlaubt eine thread-sichere einmalige oder periodische Ausführung der actionPerformed()-Methode von Listenern. Es existiert eine weitere Klasse Timer in java.util, die ähnliche Aufgaben wahrnimmt, allerdings außerhalb von Swing.
Dazu verwendet Timer intern ebenfalls die Methode invokeLater(), um die Listener-Methoden im Dispatch-Thread auszuführen.
Im Test wird der Timer dazu benutzt, die abgelaufenen Sekunden seit Anzeige des Fensters in einem Label anzuzeigen.
Simulation:
Eine GUI friert ein!
In der actionPerformed()-Methode der Schaltfläche wird dann mittels sleep() eine lang andauernde Ausführung simuliert, die die GUI auf nichts mehr reagieren lässt (Abb. 15.31).
|
Solange man nicht die Schaltfläche auslöst, werden rechts daneben die abgelaufenen Sekunden angezeigt (Abb. 15.31, Bild links).
Wird aber die Schaltfläche gedrückt, so blockiert sie jegliche weitere Ereignisverarbeitung. Der Dispatch-Thread wartet auf das Ende der Ausführung (in diesem Fall zehn Sekunden).
Ergebnis: Die Zeit bleibt stehen und selbst die grafische Anzeige »gedrückt« wird nicht mehr zurückgesetzt (Abb. 15.31, Bild rechts).
class LongRunner implements ActionListener {
ActionListener
mit sleep(): No Swinging!
// simuliert eine Ausführungszeit von 10 Sekunden public void actionPerformed(ActionEvent e) { try { Thread.sleep(10000); } catch (Exception ex) {} } }
Nachfolgend lässt LongRunner in Test die Zeit stillstehen.
Test-Klasse zur Simulation »Eingefrorene GUI«
public class Test { public static void main(String[] args) { JFrame jf= new EJFrame("Timer"); Container cp= jf.getContentPane();
JPanel jp= new JPanel(); cp.add(jp);
JButton jb= new JButton("GUI friert ein"); jb.addActionListener(new LongRunner()); jp.add(jb);
final JLabel jl= new JLabel("0 sec"); jp.add(jl);
// anonyme Klasse setzt Label jede Sekunde javax.swing.Timer t= new javax.swing.Timer(1000, new ActionListener() { int i= 1; public void actionPerformed(ActionEvent e) { jl.setText(i++ +" sec"); // jl muss final sein } }); jf.setSize(200,70); jf.setVisible(true); t.start(); // startet Timer } }
Simulation:
Mehrere Threads bearbeiten eine Komponente
Im folgenden Beispiel werden aus mehreren Threads in eine Liste Strings eingefügt. Hierzu wird invokeLater() verwendet.
|
Die Klasse JList verwendet im Test das DefaultListModel für Strings.
class WorkerThread implements Runnable { private JList lst; private String[] words;
public WorkerThread(JList lst, String[] words) { this.lst= lst; this.words= words; }
public void run() { for(int i=0; i<words.length; i++) { try { Thread.sleep((int)(2000* Math.random())); } catch (Exception ex) {}
invokeLater() mit anonymer Klasse
final int j= i; // notwendig wegen anonymer Klasse EventQueue.invokeLater( // Wrapper hüllt die eigentliche Methode ein new Runnable() { public void run() {
run():
Callback-Kommando in der Event-Queue
((DefaultListModel)lst.getModel()) .addElement(words[j]); } }); } } }
public class Test { public static void main(String[] args) { JFrame jf= new EJFrame("InvokeLater"); Container cp= jf.getContentPane();
String[] pat= {"Adapter","Decorator","Delegation", "Facade", "Factory","Immutable", "Interface", "Iterator","Marker", "Observer","Singleton", "Proxy"}; JList jl= new JList(new DefaultListModel()); JScrollPane jsp= new JScrollPane(jl); cp.add(jsp); jf.setSize(160,200); jf.setVisible(true); for (int i= 0; i<5; i++) new Thread(new WorkerThread(jl,pat)).start(); } }
Ein Zitat18 vorab: »Als Java aus dem Ei schlüpfte und umherschaute, sah es zuerst einen Browser und dachte von da an, es sei ein Applet«.
Dies bedeutet, dass der Fokus von Java einmal – in grauer Vorzeit – auf kleinen Programmen für Browser lag. Java ist nun erwachsen geworden.
Die Zertifizierung enthält so gut wie keine relevanten Fragen zu Applets, man sollte nur wissen, wie man sie schreibt und startet. Auch dieser Abschnitt beschreibt wirklich nur die Grundlagen und Methoden, da die Entwicklung von Applets nicht zum Inhalt dieses Buchs gehört.
Kurz zu den Eigenschaften, die Applets von normalen Applikationen unterscheiden:
| Ein Applet startet seine Ausführung nicht mit main(), sondern nach dem Laden wird als Erstes die Methode init() ausgeführt. |
| Ein Applet kann somit nicht von der Kommandozeile als eigenes Programm, sondern nur mit einem Appletviewer appletviewer oder in einem Browser gestartet werden. |
| Ein Applet wird in HTML-Code mit Hilfe von Applet-Tags eingebettet, wobei alle Tags ab OBJECT optional sind: |
<HTML> ... <APPLET CODE="AnyApplet.class" WIDTH=300 HEIGHT=400> OBJECT= serializedfilename ARCHIVE= jarfile1, jarfiel2, ... CODEBASE= absoluteOrRelativeURLOfApplet ALT= alternativeTextForNonDisplayingApplet NAME= NameOfApplet ALIGN= left|rigth|top|bottom|... VSPACE= NumberOfPixelAboveApplet HSPACE= NumberOfPixelOnBothSidesOfApplet <PARAM NAME= "parmName1" VALUE="value1"> ... </APPLET>
Sicherheitsrestriktionen für (fremde) Applets
| Alle Applets unterliegen per Default Sicherheitsrestriktionen, sodass ohne explizite Erlaubnis praktisch kein Zugriff auf den Client-Rechner möglich ist. |
Die Sicherheitsrestriktionen können aber browserabhängig für Applets, die lokal vom Client selbst gestartet werden, geringer sein als für die aus dem Netz.
Applet vs. JApplet oder das Browser-Dilemma
Applets werden als Subklassen von Applet bzw. JApplet erstellt. Entscheidet man sich direkt für ein JApplet, stehen einem zwar alle Möglichkeiten von Swing offen, aber leider verstehen Browser mit dem höchsten Marktanteil JApplets nicht ohne Anpassung der HTML-Datei und Plug-In.
Deshalb werden hier – entgegen den vorherigen Abschnitten – nur Applets besprochen, die auf dem normalen AWT basieren. Da JApplet eine Subklasse von Applet ist, gelten ohnehin alle Aussagen auch für Swing-Applets.
Im Folgenden werden die Methoden und ihre Bedeutungen beschrieben, die eine Subklasse der Klasse Applet überschreiben bzw. aufrufen kann.
Statt immer beide Begriffe »Browser« und »Appletviewer« aufzuführen, wird für beide nachstehend »Browser« verwendet.
Ein Browser führt nur den No-Arg-Konstruktor eines Applets beim Laden aus. Normalerweise wird ein Applet mit Hilfe der folgenden Methode init() initialisiert.
Beim Laden des Applets wird einmalig die Methode init() vom Browser aufgerufen. Somit können hier alle Initialisierungen erfolgen, die normalerweise im Konstruktor einer Applikation ausgeführt werden.
Die Methode start() wird immer dann aufgerufen, wenn ein Applet (wieder) sichtbar wird.
Diese Methode wird zusammen mit stop() eingesetzt, um visuelle Ausgaben zu stoppen, wenn das Applet nicht mehr sichtbar ist (obwohl noch aktiv), und diese wieder zu starten, wenn es erneut sichtbar wird.
Bevor die Applet-Ausführung beendet wird, wird vom Browser einmalig destroy() aufgerufen, um eventuell Ressourcen freizugeben.
Sie zeigt eine Mitteilung in der Statuszeile des Browsers an.
Sie holt Informationen zum Applet und kann passend überschrieben werden.
In der HTML-Datei können Parameter mittels Parameter-Tags (siehe Eigenschaften) übergeben werden. Diese werden dann als String[][] zurückgeliefert.
Die Methode getParameter(String paramName) gibt dagegen nur den Wert des benannten Parameters als String zurück.
Wie die Namen besagen, laden diese Methoden entweder ein Bild oder ein Audio-Clip (z.B. eine wav-Datei) von einer URL-Adresse und liefern diese als Objekt zurück.
Die Aufrufe werden allerdings an die zum Applet gehörige AppletContext-Instanz delegiert, die direkt angesprochen werden kann (siehe unten).
Sie liefert die URL-Adresse, von der das Applet geladen wurde.
getAppletContext()
Klasse AppletContext
Diese Methode holt die zum Applet gehörige AppletContext-Instanz, die das Dokument des Applets repräsentiert.
Die Klasse AppletContext definiert zwar nur sieben, dafür aber wichtige Methoden. Neben dem Laden von Bildern und Audio-Clips kann z.B. mit showDocument(URL url) eine neue Web-Seite angezeigt werden.
Abschließend sei angemerkt, dass die Ereignisverarbeitung bei Applets genauso wie für alle AWT- bzw. Swing-Komponenten erfolgt, da das Ereignis-Modell auf dem AWT 1.1 aufbaut.
Das rudimentäre AWT 1.0 hat nur eine beschränkte Anzahl von einfachen visuellen Komponenten, die nicht sehr attraktiv für GUI-Oberflächen sind.
Swing ist angetreten, das alte und das neue 2D-AWT um folgende komfortable Komponenten zu ergänzen:
| Tabellen |
| Visualisierung von Baum-Modellen |
| MDI-Fenster |
| anpassbare, scrollbare Teilbereiche |
| Registerkarten |
| Textverarbeitung |
| Drag&Drop-Operationen |
| Menüs, Symbolleisten, Aktionen und Quickinfos |
Die daraus resultierende Vielfalt an Komponenten zusammen mit ihren Daten-Modellen macht es notwendig, dedizierte Bücher zum Thema »Swing« zu schreiben.
Die Aufgabe dieses Kapitels war dagegen, die für alle gemeinsamen Konzepte und Basismechanismen so darzustellen, dass eine Evaluierung und der Einsatz von Swing leicht fällt.
Swing hat seinen Ursprung in der Idee, dass Java als Plattform in jeglicher Hinsicht unabhängig vom Betriebssystem sein muss, auch oder gerade für 2D-GUIs.
Dieses Swing-Konzept – so lobenswert es sein mag – ist aus mehreren Gründen problematisch. Hier nur drei:
| Zerstörte Oberfläche: Heavyweight-Komponenten zerstören beim Neuzeichnen jede logisch über ihr liegende Lightweight-Komponente. |
Zu den Heavyweight-Komponenten zählen aber alle performanten Anwendungen wie 3D-Grafik, Video etc., die die Hardware und das Betriebssystem optimal nutzen müssen.
Multimedia zusammen mit Swing ist ein Adventure-Game.
| Trägheit: Swing ist träge, da leichtgewichtige Komponenten beim Zeichnen keine Hardware-Ressourcen nutzen, sondern jedes Pixel und jeden Event selbst verwalten. |
| Nicht thread-sicher: Swing ist zwar intern multi-threaded, jedoch keineswegs thread-sicher. Das ist gerade für Grafikapplikationen ein gravierender Nachteil. |
Und was ist mit einer Java-Anwendung, deren L&F sich von allen anderen nativen Applikationen derselben GUI unterscheidet?
Betriebssysteme mit ihren GUIs werden geliebt (Mac), beschworen (Linux) und beschimpft (Windows), aber jeder Anwender ist aufgrund der täglichen Nutzung an seine eigene Oberfläche gewöhnt. So what?
Betriebssysteme und zugehörige GUIs entwickeln sich parallel zu Java sehr schnell fort, haben neue Erscheinungsformen (MAC OS X), verbesserte Funktionalität und – nicht zu vergessen – sie sind nativ, d.h. schnell.
Swing und Betriebssysteme spielen also Hase und Igel.
Swing im Rennen zu halten kostet mehr Ressourcen, als mittels native Peers die Fähigkeiten der OS-Plattformen optimal zu nutzen und nur den unbedingt notwendigen Rest zu ergänzen. Der Anwender fühlt sich Zuhause und der Programmierer hat »lightweighted problems«.
Nicht der Ersatz eines Betriebssystems, sondern die optimale Anpassung von Java macht es so wertvoll.
| Denn der eigentliche Vorteil von Java ist nicht eine überdimensionierte Plattform, sondern – wie Sun selbst sagt – »one size doesn't fit it all« und »one language is all you need«. |
Egal, ob Handys, PDAs, PCs, Workstations, Mainframes, ob Realtime- oder normales OS, ob GH- oder MH-CPU, die Sprache Java ist schon da und sollte sich optimal in ihre Umgebung einpassen, auch wenn es ein »intelligenter« Kühlschrank sein sollte.
1 WFC: Window Foundation Classes von Microsoft.
2 Alternativ könnte ein Top-Level-Container als oberster Root-Container definiert werden, was jedoch den Container JInternalFrame einschließt, der nicht als äußeres Fenster verwendet werden kann.
3 Eine zu AWT äquivalente Komponente in Swing hat denselben Namen,
nur mit Präfix »J«.
4 Die Alternative wäre Mehrfachvererbung oder ein Bruch mit dem alten
AWT gewesen.
5 Was eventuell auch katastrophale Auswirkungen auf die Applikation haben könnte.
6 Die meisten Entwicklungsumgebungen bieten eine komfortablere Alternative zu null an, den so genannten XYLayout-Manager, der eine exakte XY-Positionierung relativ zum Container erlaubt.
7 Merkwürdigerweise heißt es nach Swing-Konvention nicht JBox.
8 Auch hier ist wieder das generelle Konzept der Delegation flexibler als das
der Vererbung.
9 Es gibt kein allgemeines Interface IObservable.
10 Damit wird das Argument IObservable im notify() überflüssig.
11 Für die Entwicklung stellt java.awt eine Klasse AWTEventMulticaster bereit.
12 Unter Windows werden Fenster über die Tastatur mit <ALT><TAB> bzw. <SHIFT> <ALT><TAB> gewechselt.
13 Die Reihenfolge ist deshalb wichtig, weil Events entweder weitergereicht oder konsumiert werden können.
14 Eine Komponente kann auch mehr als ein Modell enthalten. Zum Beispiel enthält JList ein ListModel und ein SelectionModel.
15 Sofern man nicht eine Komponente inklusive L&F komplett neu entwickeln will.
16 Auch in der Klasse SwingUtilities.
17 Damit würde man ja den Teufel (externen Thread) mit dem Belzebub (neuen externen Thread) austreiben.
18 Aufgefangen auf einer Konferenz von einem Sun-Evangelisten, dessen Namen mir leider nicht mehr präsent ist. Es ist so übersetzt, wie ich es in Erinnerung habe.
| << zurück |
| |||||
| |||||
| |||||
| |||||
| |||||
| |||||
| |||||
| |||||
Copyright © Galileo Press GmbH 2001 - 2002
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken und speichern. Ansonsten unterliegt das <openbook> denselben Bestimmungen wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.
Die Veröffentlichung der Inhalte oder Teilen davon bedarf der ausdrücklichen schriftlichen Genehmigung von Galileo Press. Falls Sie Interesse daran haben sollten, die Inhalte auf Ihrer Website oder einer CD anzubieten, melden Sie sich bitte bei: stefan.krumbiegel@galileo-press.de