Architektur der Swing-Komponenten

Swing-Komponenten zeichnen sich dadurch aus, dass der JavaBeans-Standard unterstützt wird (zu JavaBeans siehe Kapitel 9). Somit eignen sich Swing-Komponenten auch hervorragend für den Einsatz in Entwicklungsumgebungen (Rapid Application Development, RAD). Swing-Komponenten basieren auf der Model-View-Controller-Architektur. Die Grundidee dieser Architektur besteht darin, die Verarbeitung der eigentlichen Daten von ihrer Darstellung im User Interface zu trennen (siehe Kapitel 2). Die Daten werden hierbei zentral und unabhängig von ihrer grafischen Repräsentation gespeichert, wobei eine derartige Datenbasis als Datenmodell bezeichnet wird. Eine Komponente der Benutzeroberfläche, die View, die diese Daten darstellt, bezieht die Daten dann aus dem Modell und speichert sie nicht mehr selbst. Schließlich sorgt der Controller dafür, dass das Modell den Benutzereingaben entsprechend geändert wird.

Dieser Ansatz weist gegenüber der Speicherung aller relevanten Daten im GUI-Element selbst einige Vorteile auf. Es wird einfacher, Änderungen an der Benutzeroberfläche durchzuführen, ohne tiefer in die Struktur des eigentlichen Programm-Codes einzugreifen zu müssen. Außerdem ist die mehrfache Darstellung einer Datenbasis effizienter, da die eigentlichen Daten nur einmal zentral gespeichert werden. Ferner ermöglicht es diese Art der Trennung, eine klarere Struktur der Anwendung zu erreichen, was sowohl der Erweiterbarkeit als auch der Wiederverwendbarkeit des Codes zuträglich ist.

Die Nachteile des Ansatzes bestehen darin, dass einerseits die Anpassung bestehender Programme an Swing einen höheren Aufwand mit sich bringen kann und dass andererseits die Entwicklung von Anwendungen, bei denen sich der Mehraufwand für eine vollständige Umsetzung des MVC-Konzeptes nicht lohnt, erschwert wird.

Die ersten Versuche von JavaSoft, die MVC-Architektur an das Swing-Komponentenmodell anzupassen, scheiterten, was zu einer Modifikation dieser Architektur führte. Die modifizierte MVC-Architektur wird auch als Model-Delegator-Architektur bezeichnet. Somit ist in Swing die Trennung von Modell und View (Sicht) bei den meisten Komponenten klar, wohingegen der Controller nicht klar abgegrenzt ist. So ist es bei einigen Komponenten möglich, Eingaben des Benutzers in Modelländerungen ohne einen expliziten Controller umzusetzen. Der Controller ist also praktisch in das GUI-Element integriert. Bei anderen Elementen übernimmt zum Teil ein Listener-Objekt diese Rolle.

kap84 

Abb. 8.4: Die modifizierte MVC-Architektur (Model-Delegator)

UI-Delegator und UI-Manager

Der sog. Delegator ist das UI-Objekt, das sowohl für die View als auch für den Controller zuständig ist. Die Klasse UIManager verfügt über die Methode setLookAndFeel(), die dazu verwendet wird, das gewünschte Aussehen (Look and Feel) und das Verhalten der verwendeten Komponente festzulegen. Hierzu werden folgende Methoden verwendet:

  • getSystemLookAndFeelClassName()
    Diese Methode liefert ein Look-and-Feel-Objekt zurück, das das zugrunde liegende System repräsentiert.
  • getCrossPlattformLookAndFeelClassName()
    Diese Methode liefert ein plattformunabhängiges Look-and-Feel-Objekt zurück, das auch als Metal-Look-and-Feel bezeichnet wird. Durch die Methode setLookAndFeel() wird eine Ausnahme generiert, wenn das gewünschte Look-and-Feel nicht setzbar ist. Nachfolgend ist ein Beispiel angeführt, das verwendet wird, um die oben erwähnten Methoden der Klasse UIManager in einem beliebigen Programm zu setzen.

code 

try{

    UIManager.setLookAndFeel(UIManager.getCrossPlattform LookAndFeelClassName());

} catch (Exception exp) {

    System.out.println("Das gewünschte Look and Feel kann nicht gesetzt werden wegen: "+ exp);

}

Folgende Exceptions können erzeugt werden:

  • ClassNotFoundException
    die Look-and-Feel-Klasse kann nicht gefunden werden.
  • InstantiationException
    eine neue Instanz dieser Klasse kann nicht kreiert werden.
  • IllegalAccessException
    auf die Klasse darf nicht zugegriffen werden.
  • UnsupportedLookAndFeelException
    das Look-and-Feel wird nicht unterstützt.

Modell

Swing unterscheidet zwischen zwei Grundtypen von Modellen. Zum einen gibt es Datenmodelle, die benutzt werden, um für die Anwendung relevante Daten zu speichern. Zum anderen existieren auch GUI-Statusmodelle, die den Zustand einer GUI-Komponente darstellen, und die somit Informationen beinhalten, die spezifisch für Benutzeroberflächen sind. Ein Beispiel dafür ist das CheckBox-Modell, das bspw. die Information enthält, ob eine JCheckBox aktiv ist. Ähnliche Modelle existieren auch für andere Swing-Komponenten, wobei deren Struktur von den möglichen Zuständen und anderen Eigenschaften der Komponente abhängig ist.

Im Folgenden wird auf die Datenmodelle in einer allgemeinen Art und Weise eingegangen, da die hinter beiden Modelltypen stehenden Konzepte sehr ähnlich sind. Allgemein ist das Model-View-Controller-Konzept in Swing folgendermaßen umgesetzt:

  • Es existieren mehrere Typen von Modellen, passend zu den GUI-Komponenten, z.B. ListModel, TreeModel oder BoundedRangeModel.
  • Diese sind durch Interfaces festgelegt, die der Programmierer implementiert, um entsprechende Modellklassen zu erhalten.
  • In Java existieren Standardimplementierungen dieser Interfaces.

Verfügt man über ein Datenmodell, so kann man daran beliebig viele passende GUI-Komponenten als Views anschließen. Dazu übergibt man eine Instanz der Datenmodellklasse als Argument an den Konstruktor der Komponente. Implizit wird dabei die GUI-Komponente beim Modell als Listener angemeldet. Je nach Komponente wird auch umgekehrt das Modell bei der View als Listener angemeldet. In diesem Fall ist kein expliziter Controller mehr nötig. Wenn für eine verwendete Swing-Komponente ein Modell nicht explizit definiert wurde, wird das Standardmodell verwendet. So erzeugt Swing bei der Instantiierung eines JSlider-Objekts bspw. ein Standardmodell-Interface, das die Anwendung benutzen kann, um den aktuellen Wert, das Maximum oder das Minimum eines Sliders zu setzen.

code 

this.model = new DefaultBoundedRangeModel(value, 0, min, max);

Bei der Erzeugung eines Modells verwendet Swing die BoundProperty-Eigenschaft von JavaBeans. Wird ein anderes Modell gewünscht, so muss die Methode setModel() überschrieben werden. Folgendes Beispiel verdeutlicht dies:

code 

JSlider mySlider = new JSlider();
JProgressBar myProgressBar= new JProgressBar();
BoundedRangeModel myModel = new DefaultBoundedRangeModel(){

    public void setValue(int currentValue){

      System.out.println("Der Wert ist :", currentValue);
      super.setValue(currentValue);

    }

}
mySlider.setModel(myModel);
myProgressBar.setModel(myModel);

Die ersten zwei Zeilen erzeugen jeweils eine Instanz der Klasse JSlider bzw. JProgressBar, anschließend wird ein Modell instantiiert, das die Standardimplementierung des BoundedRangeModel-Interfaces darstellt. An dieses Modell werden dann zwei Views angeschlossen, JSlider sowie JProgressBar. Bewegt man den Schieberegler, so passt sich der dazugehörige Balken automatisch an den neuen Wert an, wobei keine Ereignisse explizit verarbeitet werden.

kap85 

Abb. 8.5: Das Swing1-Beispiel: Ein Datenmodell mit zwei Views

Hierbei tritt der Fall ein, dass eine View – in diesem Fall die JSlider-Komponente – implizit auch die Rolle des Controllers übernimmt, also Eingaben des Benutzers, die durch Bewegungen des Schiebereglers eingegeben wurden, in eine passende Änderung der Daten im Modell umsetzt.

code 

//* Beispiel Swing1.java fuer das Buch OpenJava
//Java Application, in der zwei Views
//fuer ein Datenmodell erstellt werden.
//Starten mit: java Swing1

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class Swing1 extends JFrame {

    BoundedRangeModel myModel = new DefaultBoundedRangeModel(30,1,0,100);

    // Konstruktor
    public Swing1() {

      super("Swing 1");

Hierbei wird ein einfaches Datenmodell angenommen, natürliche Zahlen zwischen 0 und 100, wobei der Anfangswert des Sliders auf 30 gesetzt wird.

Zur Anzeige des Datenmodells werden zwei Swing-Komponenten gewählt: Ein JSlider-Objekt und ein JProgressBar-Objekt. JSlider ändert den Wert des Datenmodells, wobei JProgressBar den neuen Wert anzeigt.

code 

    JSlider mySlider = new JSlider(myModel);
    JProgressBar myProgressBar = new JProgressBar(myModel);
    JPanel myJPanel = new JPanel(false);
    mySlider.setPaintTicks(true);
    mySlider.setPaintLabels(true);
    mySlider.setMajorTickSpacing(20);
    mySlider.setMinorTickSpacing(5);

Die letzten vier Zeilen betreffen die Darstellung der verschiedenen JSlider-Eigenschaften, darunter die Kontrollzeichen, die sog. Labels. Weiterhin werden der maximale und der minimale Abstand der Kontrollzeichen angegeben. Im Folgenden wird das Layout des Panels auf BoxLayout gesetzt. In einem Box-Layout werden die enthaltenen Komponenten in vertikaler Richtung angeordnet.

code 

      myJPanel.setLayout(new BoxLayout(myJPanel, BoxLayout.Y_AXIS));
      myJPanel.setBorder(BorderFactory.createEmptyBord er(10,10 ,10,10));
      myJPanel.add(mySlider);
      myJPanel.add(myProgressBar);
      setContentPane(myJPanel);

    }
    // Main Funktion ohne Argumente
    public static void main(String[] args) {

      JFrame frame = new Swing1();
      // Innere Adapterklasse
      WindowListener l = new WindowAdapter() {

        public void windowClosing(WindowEvent e) { System.exit(0); }

      };

      frame.addWindowListener(l);
      frame.pack();
      frame.setVisible(true);

    }

}

Möchte man dieses einfache Beispiel um eine Komponente erweitern, in diesem Fall um ein JTextField-Objekt, das den aktuellen Wert der Zahl in myModel oder das Ergebnis einer Berechnung mit dieser Zahl als Argument anzeigt, so hat man das Problem, dass ein Objekt vom Typ JTextField nicht ohne weiteres als View für ein Bounded-RangeModel benutzt werden kann. Ein Objekt vom Typ JTextField verwendet normalerweise eine Instanz einer Klasse als Datenmodell, die das Interface Document implementiert. Eine einfache und zugleich elegante Lösung besteht hier in einer Adapterklasse. Eine typische Adapterklasse könnte im Wesentlichen folgendermaßen aussehen:

code 

class IntegerToTextAdapter implements ChangeListener {

    BoundedRangeModel myModel;
    JTextField myTextField;
    public IntegerToTextAdapter(BoundedRangeModel theModel, JTextField theText) {

      myModel= theModel;
      myTextField= theText;
      myModel.addChangeListener(this);
      //...

    }

    public void stateChanged(ChangeEvent e) {

      String derWert= (new Integer(myModel.getValue())).toString();
      myTextField.setText(derWert);

    }

}

Im Konstruktor der Adapterklasse wird das Objekt dann beim Modell als Listener angemeldet. Tritt ein ChangeEvent-Event auf, so liest der Adapter den aktuellen Wert aus dem Modell, führt daraufhin die gewünschten Berechnungen aus und schreibt das Ergebnis schließlich in das entsprechende JTextField-Objekt (myTextField).

Durch die Programmzeile

code 

    IntegerToTextAdapter myAdapter = new IntegerToTextAdapter(myModel,myTextField);

wird eine Verbindung zwischen dem Modell und der neu hinzugekommenen JTextField-Komponente durch ein Adapterobjekt hergestellt. Eine erweiterte Version des ersten Beispiels könnte dann folgendermaßen aussehen:

code 

//* Beispiel Swing2.java für das Buch OpenJava
//Java Application, in der drei Views
//fuer ein Datenmodell erstellt werden.
//Starten mit: java Swing2

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

// Adapterklasse: Ermoeglicht die Nutzung eines JTextField als
//dritte View für ein BoundedRangeModel.

class IntegerToTextAdapter implements ChangeListener {

    BoundedRangeModel myModel;
    JTextField myTextField;
    public IntegerToTextAdapter(BoundedRangeModel theModel, JTextField theText) {

      myModel=theModel;
      myTextField=theText;

      // Adapter als Listener beim Model anmelden
      myModel.addChangeListener(this);
      myTextField.setText((new Integer(myModel.getValue())).toString());

    }

    public void stateChanged(ChangeEvent e) {

      // Model hat sich geaendert: Text soll angepasst werden
      myTextField.setText((new Integer(myModel.getValue())).toString());

    }

}

public class Swing2 extends JFrame {

    // Einfaches Datenmodell: Natuerliche Zahlen zwischen 0 und
    //100, Anfangswert 30.

    BoundedRangeModel myModel = new DefaultBoundedRangeModel(30,1,0,100);

    // Konstruktor
    public Swing2() {

      super("Swing 2");
      // Ein JSlider und ein JProgressBar werden instantiiert
      // JSlider aendert den Wert der Zahl, JProgressBar zeigt
      // diesen an.
      // Beide Swing Komponenten haben ein gemeinsames
      // Datenmodell
      JSlider mySlider = new JSlider(myModel);
      JProgressBar myProgressBar = new JProgressBar(myModel);

      JTextField myTextField = new JTextField();
      JPanel myJPanel = new JPanel(false);

      // Layout
      mySlider.setPaintTicks(true);
      mySlider.setPaintLabels(true);
      mySlider.setMajorTickSpacing(20);
      mySlider.setMinorTickSpacing(5);
      myJPanel.setLayout(new BoxLayout(myJPanel, BoxLayout.Y_AXIS));

      myJPanel.setBorder(BorderFactory.createEmptyBord er(10,10 ,10,10));

      // Ein JTextField kann normalerweise nicht zusammen mit
      // einem BoundedRangeModel verwendet werden.
      // Deshalb: Adapter zwischen Model und
      //JTextField.

      IntegerToTextAdapter myAdapter = new IntegerToTextAdapter(myModel,myTextField);

      myTextField.setEditable(false);
      myJPanel.add(myTextField);
      myJPanel.add(mySlider);
      myJPanel.add(myProgressBar);
      setContentPane(myJPanel);

    }

    // Main Funktion ohne Argumente

    public static void main(String[] args) {

      JFrame frame = new Swing2();
      // Innere Klasse

      WindowListener l = new WindowAdapter() {

        public void windowClosing(WindowEvent e) { System.exit(0); }

      };

      frame.addWindowListener(l);
      frame.pack();
      frame.setVisible(true);

    }

}


SPNavRight SPNavRight SPNavRight
BuiltByNOF