5.05 Grafische Benutzeroberflächen mit Swing Crashkurs

Vorbemerkungen

Dieser Crashkurs soll sehr kompakt die wichtigsten Grundlagen zu Erstellung einer grafischen Benutzeroberfläche mit Swing vermitteln. Ziel ist dabei vor Allem, interessierte Schülerinnen und Schüler in die Lage zu versetzen, Beispiele aus dem Unterricht mit einer solchen Oberfläche zu erweitern.

Aufgrund der Kürze dieses Crashkurses (und der im Unterricht oft knappen Zeit) werden viele Details und Alternativen außer Acht gelassen. Es wird aber dennoch versucht, die Zusammenhänge zu erläutern und nicht bloß Schablonen vorzugeben, in denen dann Leserinnen und Leser für eigene Beispiele mehr oder weniger blind einfach ein paar Bezeichnungen anpassen.

Ein erstes Beispiel

Möchten wir ein Programmfenster mithilfe von Swing erzeugen, benötigen wir ein Objekt der Klasse JFrame. Hier haben wir im Wesentlichen zwei Möglichkeiten ein solches zu erstellen.

Betrachten wir zunächst dieses Beispiel:

import javax.swing.JFrame;


public class FensterErstellen {

    public static void main(String[] args) {
        JFrame meinFenster = new JFrame();
        meinFenster.setTitle("Erstes Fenster");
        meinFenster.setSize(300, 100);
        meinFenster.setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        meinFenster.setVisible(true);
    }

}

Hier wurde schlicht das Objekt meinFensterder Klasse JFrame erstellt und mithilfe einiger Methoden angepasst. Genauer bewirken diese Methoden Folgendes:

  • Mit setTitle("Erstes Fenster"); wird der Titel des Fensters festgelegt.
  • Mithilfe von setSize(300, 100); legen wir die Größe fest.
  • Die etwas sperrig wirkende Anweisung setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE); legt fest, dass das Programm beendet werden soll, wenn man das Fenster schließen möchte. Deutlich wird das vielleicht erst, wenn man diese Anweisung zur Probe weglässt.
  • Mit meinFenster.setVisible(true); machen wir das Fenster sichtbar.

Fenster erstellen mit Assoziation

Man könnte argumentieren, dass in dem einführenden Beispiel im Grunde Assoziation verwendet wurde. Etwas deutlicher wird dies vielleicht, wenn wir das Beispiel ein wenig umschreiben:

import javax.swing.JFrame;

public class FensterMitAssoziationAlternativ {

    JFrame meinFenster;

    public FensterMitAssoziationAlternativ() {
        meinFenster = new JFrame();
        meinFenster.setTitle("Erstes Fenster");
        meinFenster.setSize(300, 100);
        meinFenster.setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        meinFenster.setVisible(true);
    }
}

Hier fällt uns das Attribut meinFenster deutlich ins Auge. Außerdem sehen wir, dass das Einrichten des Fensters im Konstruktor dieser Klasse stattfindet.

Allerdings sehen wir auch, dass wir diese Klasse nicht starten können, da sie keine main-Methode enthält. Halten wir uns konsequent an den Stil dieses Java-Tutorials, lagern wir diese in eine zweite Klasse aus:

public class FensterMitAssotiationTest {

    public static void main(String[] args) {
        FensterMitAssoziationAlternativ meinFenster = new FensterMitAssoziationAlternativ();
    }

}

Diese Klasse können wir starten.

In der Praxis – und auch den meisten Tutorials – wird die main-Methode jedoch der ersten Klasse direkt hinzugefügt:

import javax.swing.JFrame;

public class FensterMitAssoziation {

    JFrame meinFenster;

    public FensterMitAssoziation() {
        meinFenster = new JFrame();
        meinFenster.setTitle("Erstes Fenster");
        meinFenster.setSize(300, 100);
        meinFenster.setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        meinFenster.setVisible(true);
    }

    public static void main(String[] args) {
        FensterMitAssoziation meinFenster = new FensterMitAssoziation();
    }

}

Das ist für uns ungewohnt. Im Grunde führt dies aber lediglich dazu, dass wir eben diese Klasse doch starten können und keinen Umweg über eine zweite Klasse gehen müssen.

Fenster erstellen mit Vererbung

Häufiger als Assoziation sieht man in Tutorials und Beispielen, dass Vererbung verwendet wird. Sehen wir uns auch dazu ein Beispiel an:

import javax.swing.JFrame;


public class FensterMitVererbung extends JFrame {

    public FensterMitVererbung() {
        this.setSize(300, 100);
        this.setTitle("Erstes Beispiel mit Swing");
        this.setVisible(true);
        this.setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {
        FensterMitVererbung meinFenster = new FensterMitVererbung();
    }

}

Wie oben bereits gesehen, ist es auch in dieser Variante üblich, die main-Methode bereits hier aufzuführen und nicht auszulagern. Die Anweisungen im Konstruktor würden auch ohne das Schlüsselwort this funktionieren, aber auch hier orientiere ich mich an dem was man in Beispielen häufiger vorfindet.

Grundlagen zum Aufbau eines Fensters

Ein Fenster mit Label

Um eine grobe Vorstellung davon zu bekommen, wie es weitergeht, betrachten wir ein Fenster, in dem ein Label zu sehen ist:

import javax.swing.JFrame;
import javax.swing.JLabel;

public class FensterMitGrundkomponenten extends JFrame {

    final JLabel lblMessage;

    public FensterMitGrundkomponenten() {

        lblMessage = new JLabel("Hallo zusammen!");
        this.add(lblMessage);

        this.setTitle("Erstes Beispiel mit Swing");
        this.setVisible(true);

        this.pack();

        this.setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);

    }

    public static void main(String[] args) {
        FensterMitGrundkomponenten meinFenster = new FensterMitGrundkomponenten();
    }

}

Wie wir sehen, wird hier ein Objekt der Klasse JLabel erzeugt. Mit der Anweisung this.add(lblMessage); wird es dem Fenster hinzugefügt.

Neu ist hier auch, dass mittels this.pack(); die Größe des Fensters automatisch an die erforderliche Größe angepasst wird, wodurch das Fenster relativ klein ist.

Im nächsten Abschnitt lernen wir einige weitere wichtige Klassen für verschiedene Komponenten kennen.

Verwendung eines Containers

Anders als im vorangegangenen Beispiel ist es eigentlich üblich, verschiedene Objekte, die die Komponenten des Fensters darstellen, zunächst in einem Container zu gruppieren und diesen dann dem Fenster hinzuzufügen. Als Container verwenden wir Objekte der Klasse JPanel, wie man in diesem Beispiel sieht:

import java.awt.FlowLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

public class FensterMitLabel extends JFrame {

    final JLabel lblMessage;

    public FensterMitLabel() {

        lblMessage = new JLabel("Hallo zusammen!");

        // Ein Objekt der Klasse JPanel dient als Container:
        final JPanel root = new JPanel();
        // Hier legen wir sein Layout fest:
        root.setLayout(new FlowLayout());

        // Einzelne Komponente hinzufügen:
        root.add(lblMessage);

        this.add(root);
        this.setSize(300, 100);
        this.setTitle("Erstes Beispiel mit Swing");
        this.setResizable(false);
        this.setLocation(100, 100);
        this.setVisible(true);
        this.setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);

    }

    public static void main(String[] args) {
        FensterMitLabel meinFenster = new FensterMitLabel();
    }

}

Hier wird außerdem das Layout unseres Containers als FlowLayout festgelegt. Damit können wir bestimmen, in welche Form die Komponenten darin angeordnet werden sollen. Dieses Layout bewirkt, dass die Komponenten einfach in der Reihenfolge angezeigt werden, in der sie hinzugefügt werden. Außerdem passt sich die Anordnung an die Fensterbreite an. Es gibt auch Alternativen, mit denen man komplexere Fenster entwerfen kann. Dies soll aus Zeitgründen hier aber nicht vertieft werden.

Um die Funktionsweise eines Containers zu verdeutlichen sehen wir uns noch ein Beispiel mit zwei Labeln an:

import java.awt.FlowLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

public class FensterMitZweiLabeln extends JFrame {

    final JLabel lblMessage;
    final JLabel lblZweiteMessage;

    public FensterMitZweiLabeln() {

        lblMessage = new JLabel("Hallo zusammen!");
        lblZweiteMessage = new JLabel("Passt Ihr gut auf?");

        // Erzeugen des Layouts und Platzieren der Komponenten
        final JPanel root = new JPanel();
        root.setLayout(new FlowLayout());

        // Einzelne Komponente hinzufügen:
        root.add(lblMessage);
        root.add(lblZweiteMessage);

        this.add(root);
        this.setSize(300, 100);
        this.setTitle("Erstes Beispiel mit Swing");
        this.setResizable(false);
        this.setLocation(100, 100);
        this.setVisible(true);
        this.pack();
        this.setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);

    }

    public static void main(String[] args) {
        FensterMitZweiLabeln meinFenster = new FensterMitZweiLabeln();
    }

}

Ein paar wichtige Komponenten

Die meisten Anwendungen bestehen natürlich nicht nur aus Labeln zur Anzeige von Informationen. Wir betrachten hier ein paar weitere Klassen mit denen man bereits brauchbare Programme entwickeln könnte:

import java.awt.FlowLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;

public class FensterMitGrundkomponenten extends JFrame {

    final JLabel lblMessage;
    final JButton btnKlick;
    final JTextField txfEingabe;
    final JCheckBox chkHaken;

    public FensterMitGrundkomponenten() {

        this.setLayout(new FlowLayout());

        lblMessage = new JLabel("Hallo zusammen!");
        this.add(lblMessage);

        btnKlick = new JButton("Klick mich!");
        this.add(btnKlick);

        txfEingabe = new JTextField("Ihre Eingabe...");
        this.add(txfEingabe);

        chkHaken = new JCheckBox("Ja oder nein?");
        this.add(chkHaken);

        this.setTitle("Nützliche Grundkomponenten in Swing");
        this.setVisible(true);

        this.pack();

        this.setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);

    }

    public static void main(String[] args) {
        FensterMitGrundkomponenten meinFenster = new FensterMitGrundkomponenten();
    }

}

In diesem Beispiel finden wir Objekte folgender Klassen:

Klasse Beschreibung
JLabel Labels zur Anzeige von Text
JButton Schalflächen mit Beschriftung
JTextField Felder zur Texteingabe des Benutzers
JCheckBox Kontrollkästchen zum Aktivieren der Deaktivieren

Wir stellen allerings fest, dass diese noch keine Funktion haben.

Ereignisbehandlung

Damit unsere Komponenten Funktionen erhalten, müssen wir angeben, wie auf Ereignisse bzw. im Englischen Event reagiert werden soll. Die Grundidee dabei ist wie folgt:

  1. Eine Ereignisquelle erzeugt aufgrund einer Zustandsänderung ein Ereignisobjekt.
  2. Ein Lauscher registiert dieses Objekt und bestimmt, wie darauf reagiert wird.

Sehen wir uns ein Beispiel an:

import java.awt.FlowLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class FensterMitEreignisbehandlung extends JFrame {

    final JLabel lblMessage;
    final JButton btnKlick;
    final JTextField txfEingabe;
    final JCheckBox chkHaken;

    public FensterMitEreignisbehandlung() {

        this.setLayout(new FlowLayout());

        lblMessage = new JLabel("Hallo zusammen!");
        this.add(lblMessage);

        btnKlick = new JButton("Klick mich!");
        this.add(btnKlick);

        btnKlick.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent meinEvent){
                System.out.println("Klick!");
            }
        });

        txfEingabe = new JTextField("Ihre Eingabe...");
        this.add(txfEingabe);

        chkHaken = new JCheckBox("Ja oder nein?");
        this.add(chkHaken);

        this.setTitle("Nützliche Grundkomponenten in Swing");
        this.setVisible(true);

        this.pack();

        this.setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);

    }

    public static void main(String[] args) {
        FensterMitEreignisbehandlung meinFenster = new FensterMitEreignisbehandlung();
    }

}

In diesem Beispiel wird die Schaltfläche mittel der Methode addActionListener mit einem Lauscher verbunden. Diese Methode verlangt als Parameter ein Objekt der Klasse ActionListener. Für uns etwas ungewohnt ist vielleicht, dass dieses Objekt, wie man am Schlüsselwort new erkennt, dort an Ort und Stelle auch direkt erzeugt wird. Registriert dieser zugeordnete Lauscher ein Ereignis, führt er die Methode actionPerformed aus. Daher müssen wir angeben, was diese Methode leisten soll. Auch diese Angabe machen direkt an dieser Stelle. In unserem Beispiel schreibt die Methode lediglich einen kurzen Text in die Konsole.

Es soll nicht verheimlicht werden, dass dies nur einer von verschiedenen möglichen Wegen ist, einen Lauscher einzubinden. Außerdem wird dieser Weg hier etwas holzschnittartig eingeführt. Da dies aber nur ein recht kompakter Crashkurs sein soll, möchte ich es dabei belassen und auch auf weitere Typen von Lauschern (z. B. KeyListener oder MouseListener) verzichten.

Sehen wir uns eine Variation des obigen Beispiels an:

import java.awt.FlowLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class FensterMitEreignisbehandlung extends JFrame {

    final JLabel lblMessage;
    final JButton btnKlick;
    final JTextField txfEingabe;
    final JCheckBox chkHaken;

    public FensterMitEreignisbehandlung() {

        this.setLayout(new FlowLayout());

        lblMessage = new JLabel("Hallo zusammen!");
        this.add(lblMessage);

        btnKlick = new JButton("Klick mich!");
        this.add(btnKlick);

        btnKlick.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent meinEvent) {
                if (chkHaken.isSelected()) {
                    lblMessage.setText(txfEingabe.getText());
                } else {
                    System.out.println(txfEingabe.getText());

                }
            }
        });

        txfEingabe = new JTextField("Ihre Eingabe...");
        this.add(txfEingabe);

        txfEingabe.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent meinEvent) {
                System.out.println(txfEingabe.getText());
            }
        });

        chkHaken = new JCheckBox("Ja oder nein?");
        this.add(chkHaken);

        this.setTitle("Nützliche Grundkomponenten in Swing");
        this.setVisible(true);

        this.pack();

        this.setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);

    }

    public static void main(String[] args) {
        FensterMitEreignisbehandlung meinFenster = new FensterMitEreignisbehandlung();
    }

}

Bei Klicken des Buttons wird nun geprüft, ob der Haken im Kontrollkästchen gesetzt ist. Abhängig davon wird der Inhalt des Textfeldes entweder im Label angezeigt oder in der Konsole ausgegeben. Wird bei Eingabe im Textfeld ENTER gedrückt, wird der Text des ebenfalls in der Konsole ausgegeben.

Verknüpfung mit unserem Kiosk

Nun wollen wir sehen, wie wir ein bereits vorliegendes Beispiel mit einer grafischen Benutzeroberfläche erweitern können. Dazu soll das Übungsprojekt Kiosk dienen. Wer diese noch nicht kennt, sollte es sich zunächst ansehen.

Die Klasse Kiosk wurde bereits so angelegt, dass sie sich gut in eine Benutzeröberfläche einbinden lässt, indem auf Ausgaben mittels System.out.print verzichtet wurde sondern stattdessen die aktuellsten Informationen im Attribut message zu finden sind und durch getMessage() abgefragt werden können:


import java.awt.FlowLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class Main extends JFrame {

    Kiosk unserKiosk;

    final JLabel lblMessage;
    final JButton btnAnstellen;
    final JButton btnBedienen;
    final JButton btnGong;
    final JTextField txfEingabe;
    final JCheckBox chkHaken;

    public Main() {

        unserKiosk = new Kiosk();

        this.setLayout(new FlowLayout());

        lblMessage = new JLabel(unserKiosk.getMessage());
        this.add(lblMessage);

        btnAnstellen = new JButton("anstellen");
        this.add(btnAnstellen);

        btnAnstellen.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent meinEvent) {
                    unserKiosk.stelleAn(new Schueler(txfEingabe.getText(), chkHaken.isSelected()));
                    lblMessage.setText(unserKiosk.getMessage());
            }
        });

        btnBedienen = new JButton("bedienen");
        this.add(btnBedienen);

        btnBedienen.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent meinEvent) {
                unserKiosk.bedieneErsten();
                lblMessage.setText(unserKiosk.getMessage());
            }
        });

        btnGong = new JButton("Gong!");
        this.add(btnGong);

        btnGong.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent meinEvent) {
                unserKiosk.pausengong();
                lblMessage.setText(unserKiosk.getMessage());
            }
        });

        txfEingabe = new JTextField("Bitte Namen eingeben!");
        this.add(txfEingabe);

        txfEingabe.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent meinEvent) {
                unserKiosk.stelleAn(new Schueler(txfEingabe.getText(), chkHaken.isSelected()));
                lblMessage.setText(unserKiosk.getMessage());
            }
        });

        chkHaken = new JCheckBox("Oberstufe");
        this.add(chkHaken);

        this.setTitle("Unser Kiosk mit Swing");
        this.setVisible(true);

        this.pack();

        this.setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);

    }

    public static void main(String[] args) {
        Main meinFenster = new Main();
    }

}

Im Wesentlichen besteht die Arbeit also darin, für die verschiedenen Methoden jeweils Schaltflächen einzufügen. Außerdem wird durch lblMessage.setText(unserKiosk.getMessage()); im Label jeweils die aktuelle Meldung angezeigt. Hätten wir stattdessen im Kiosk mit System.out.print, hätten wir dies nicht so einfach umsetzen können.

In der Zeile unserKiosk.stelleAn(new Schueler(txfEingabe.getText(), chkHaken.isSelected())); wurde noch ein kleiner Trick verwendet: Da die Methode isSelected() einen Wahrheitswert liefert, kann sie unmittelbar an dieser Stelle verwendet werden, um anzugeben, ob der neue Schüler in der Oberstufe ist.