Java Swing-Codeknacker
Willemers Informatik-Ecke
Codeknacker Java-Kurs Swing

Zur Begleitung des Java-Kurses wird das Spiel Codeknacker implementiert. Zuletzt wurde es in einer Klasse zusammengefasst, die nun benutzt werden kann, um in einem Fenster gespielt zu werden.

Als grafische Oberfläche wird Swing verwendet. Tatsächlich ist Swing schon etwas älter, hat aber gegenüber FX Vorteile:

Erstellung eines Fensters

Die hier vorgestellte Version ist recht einfach gehalten. Der Spieler tippt seinen vierstelligen Versuch ein, klickt auf den Button und erhält eine Bewertung der Treffer. Damit er sich das nicht merken muss, werden diese in einer Liste abgelegt, damit der Spieler aus den bisherigen Versuchen kombinieren kann, welche Geheimzahl sich der Computer wohl ausgedacht hat.

Wir benötigen ein Fenster mit mehreren Kontrollementen.

Das fertige Fenster sähe etwa so aus:

Ein Fenster wird erstellt, indem man die Klasse JFrame erweitert.

package code;

public class SwingCodeKnacker extends JFrame {

}
JFrame wird von Eclipse rot unterkringelt. Klickt man auf den roten Kreis links neben der Zeile, kann man Eclipse motivieren, die richtigen Importe zu setzen ...

package code;

import javax.swing.JFrame;

public class SwingCodeKnacker extends JFrame {

}

... und die module-info.java anzupassen, damit ein Swing-Programm ausgeführt werden kann.

module CodeKnacker {
    requires java.desktop;
}

Erstellen eines Fensterrahmens

Wir erstellen ein Fenster, das zunächst einmal einen Button enthält. Die anderen notwendigen Kontrollelemente werden wir nachreichen.

package code;

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

public class SwingCodeKnacker extends JFrame {
    public SwingCodeKnacker() {
        this.add(new JLabel("CodeKnacker"));
        this.pack();
        this.setVisible(true);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }
    public static void main(String[] args) {
        new SwingCodeKnacker();
    }
}

Layout und Kontrollelemente

Leider kann das Standardlayout eines JFrame nur ein Element per add aufnehmen. Wir benötigen aber ein Eingabefeld, einen Button, ein Label und eine Liste. Die sollen auch nicht nur einfach untereinander angeordnet werden, sondern eine gewisse Struktur haben. Also müssen wir uns etwas mit den Layouts befassen.

Zunächst verpassen wir dem Fenster ein BorderLayout. In die Mitte kommt die Liste, nach oben erst einmal der Label.

public SwingCodeKnacker() {
    this.setLayout(new BorderLayout());
    this.add(BorderLayout.NORTH, new JLabel("CodeKnacker"));
    String[] liste = {"erstens", "zweitens, drittens"};
    this.add(BorderLayout.CENTER, new JList(liste));
    this.pack();
    this.setVisible(true);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}

In die obere Zeile soll neben dem Label auch gern noch das Eingabefeld und der Button. Damit verschiedene Layouts verschachtelt werden können, benötigen wir noch ein Panel (JPanel), das unsichtbar ist, aber Kontrollelemente aufnehmen kan.

package code;

import java.awt.BorderLayout;
import java.awt.FlowLayout;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;

public class SwingCodeKnacker extends JFrame {
    JTextField eingabe = new JTextField(5);
    DefaultListModel<String> model = new DefaultListModel<>();
    JList<String> liste = new JList<>(model);
    JButton button = new JButton("Raten");
    JLabel label = new JLabel("00");
    
    public SwingCodeKnacker() {
        this.setLayout(new BorderLayout());
        JPanel panel = new JPanel();
        panel.setLayout(new FlowLayout());
        panel.add(eingabe);
        panel.add(button);
        panel.add(label);
        this.add(BorderLayout.NORTH, panel);
        JScrollPane scroll = new JScrollPane(liste);
        this.add(BorderLayout.CENTER, scroll);
        this.pack();
        this.setVisible(true);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }
    public static void main(String[] args) {
        new SwingCodeKnacker();
    }
}
Das sieht schon gut aus, tut aber nichts. Man kann eingeben, was man will, kann den Button drücken. Es passiert nichts. Nur das Anklicken des Kreuzes rechts oben führt zum Ende des Programms.

Einbinden des Spiels

Bevor wir definieren, was bei einem Button-Klick passieren soll, muss das eigentliche Spiel eingebunden werden. Dazu wird einfach ein Attribut der Klasse CodeKnacker angelegt. Für die Rateversuche kann man eine Zählvariable definieren. Eigentlich könnte man diese allerdings auch in das Spiel integrieren.

public class SwingCodeKnacker extends JFrame {

    CodeKnacker spiel = new CodeKnacker();
    int rateVersuche = 0;

Aktion durch den Button

Das starre Fenster muss in Bewegung geraten. Die Eingabe kann bereits erfolgen. Das Auslesen sollte dann passieren, wenn der Button gedrückt wird.

Um eine Aktion zu realisieren, muss das Fenster das Interface ActionListener implementieren.

public class SwingCodeKnacker extends JFrame implements ActionListener {

Nun wird das Wort ActionListener rot. Eclipse bietet an, einen Import durchzuführen. Das bestätigen wir. Daraufhin wird der Klassenname rot. Eclipse schlägt vor, nicht implementierte Methoden zu erzeugen.

@Override
public void actionPerformed(ActionEvent e) {
    // TODO Auto-generated method stub
}
Jetzt muss der ActionListener noch mit dem Button verbunden werden.
button.addActionListener(this);

Aktionen beim Klicken auf den Button

Die Methode actionPerformed übernimmt die zentrale Rolle im Spielablauf.
@Override
public void actionPerformed(ActionEvent e) {
    String txt = eingabe.getText();
    eingabe.setText("");
    Ergebnis ergebnis = spiel.vergleiche(txt);
    String eintrag = txt + ": " +ergebnis.toString();
    model.addElement(eintrag);
    rateVersuche++;
    label.setText(""+rateVersuche+" "+spiel.getGeheim());
}

Die Liste der Versuche

Die Rateversuche und ihre Ergebnisse müssen dem Spieler bei den nächsten Versuchen zur Verfügung stehen. Dazu wird eine JList verwendet.

Die JList wird nicht direkt gefüttert. Stattdessen entnimmt sie die darzustellenden Zeilen einem Datenmodell. Das bedeutet, man muss vor dem Erstellen der Liste ein Datenmodell erzeugen, das dem JList-Konstruktor als Parameter übergeben wird.

Eine JList kann auch andere Typen als String auflisten. Der Typ muss bei Liste und Modell übereinstimmen. Das wird hier über Generics erreicht.

Falls die Liste größer wird, sollte ein Schiebebalken dem Benutzer helfen, durch die Liste durchzugehen. Dies wird realisiert, indem ein JScrollPane erzeugt wird, dessen Konstruktor die JList als Parameter übergeben wird. Sobald die Liste mehr Platz benötigt, als der Platz hergibt, erscheint dann der Schiebebalken. Anschließend muss das JScrollPane statt der JList in das Fenster eingebaut werden, weil dieses Schiebebalken und Liste enthält.

DefaultListModel<String> model = new DefaultListModel<>();
JList<String> liste = new JList<>(model);
JScrollPane scroll = new JScrollPane(liste);
this.add(BorderLayout.CENTER, scroll);
Bei jedem Anklicken des Buttons wird der Rateversuch inklusive des Ergebnisses in einem String zusammengefasst, der an das Ende der Liste kommen soll. Dieser wird allerdings nicht der Liste, sondern dem Modell hinzugefügt.

model.addElement(eintrag);
Auch das Löschen des Listeninhalts bei einem Neustart des Spiels erfolgt über das Modell.
model.removeAllElements();

Ausspähen der Geheimzahl

Die Schummelmethode getGeheim muss allerdings noch in die Klasse CodeKnacker implementiert werden.
public String getGeheim() {
    String txt = "";
    for (int i=0; i<4; i++) {
        txt += geheim[i];
    }
    return txt;
}

Siegerehrung

Das Spiel ist gewonnen, wenn alle vier Ziffern an der richtigen Position sind. Dazu muss die Anzahl der Treffer der Anzahl der Code-Zahlen entsprechen.

if (ergebnis.getTreffer()==CodeKnacker.CODEZAHLEN) {
    JOptionPane.showMessageDialog(null, "Gewonnen!");
    spiel = new CodeKnacker();
    rateVersuche = 0;
    model.removeAllElements();            
}

Eingabe mit Spinnern

Problematisch ist die Eingabe. Der Benutzer kann statt eines vierstelligen Zifferncode zwischen 1 und 6 seinen Namen oder zuwenige Ziffern eintippen. In jedem dieser Fälle stürzt das Programm ab. Natürlich kann man versuchen, die Fehler abzufangen und den Benutzer ermahnen, seine Rolle doch bitte ernster zu nehmen.

Man kann aber auch versuche, die Eingabe so weit einzuschränken, dass der Benutzer gar nicht erst Fehler macht. Statt des freien Eingabefelds mit vielerlei Fehlermöglichkeiten kann man Comboboxen oder Spinner verwenden. Spinner sind Elemente mit kleinen Pfeilen daran, mit denen man den Inhalt verändern kann. Wenn man vier Stück nebeneinander setzt, sieht das aus wie bei einem Zahlenschloss.

Einbau der Spinner

Der Inhalt der Spinner wird durch ein Datenmodell bestimmt. Entsprechend benötigen wir ein Array von vier Spinnern und auch ein Array von vier SpinnerListModels. Wird nur eines verwendet, werden die Spinner immer synchron laufen und man kann keine unterschiedlichen Ziffern auswählen.

Die Spinner und ihre Modelle legen wir als Attribute an.

public class SpinCodeKnacker  extends JFrame implements ActionListener {

    SpinnerListModel[] spmod = new SpinnerListModel[CodeKnacker.CODEZAHLEN];
    JSpinner[] spinner = new JSpinner[CodeKnacker.CODEZAHLEN];

Die Initialisierung erfolgt im Konstruktor, da wir eine Schleife bauen müssen. Die Spinnermodelle können durch String-Arrays initialisiert werden. Davon genügt allerdings eines.

JPanel panel = new JPanel();
panel.setLayout(new FlowLayout());
String[] spintxt = {"1","2","3","4","5","6"};
for (int i=0; i<CodeKnacker.CODEZAHLEN; i++) {
    spmod[i] = new SpinnerListModel(spintxt);
    spinner[i] = new JSpinner(spmod[i]);
    panel.add(spinner[i]);
}

Abfrage der Spinner

Nach dem Drücken auf den Button müssen nun alle Spinner abgefragt werden und deren Inhalt zu einem String zusammengebaut werden.

@Override
public void actionPerformed(ActionEvent e) {
    String txt = "";
    for (int i=0; i<CodeKnacker.CODEZAHLEN; i++) {
        txt += spinner[i].getValue();
    }
    Ergebnis ergebnis = spiel.vergleiche(txt);
Im Gegensatz zu der Methode current liefert die Methode get den String, der in der Selektion steht. Wenn man alle vier aneinander hängt, erhält man den Ratestring, so wie man ihn an die vergleich-Methode der Klasse CodeKnacker senden kann.

Und nun alles zusammen

Hier setzen wir alles zusammen.
package code;

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

import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.SpinnerListModel;

public class SpinCodeKnacker  extends JFrame implements ActionListener {
    
    CodeKnacker spiel = new CodeKnacker();
    int rateVersuche = 0;
    
    SpinnerListModel[] spmod = new SpinnerListModel[CodeKnacker.CODEZAHLEN];
    JSpinner[] spinner = new JSpinner[CodeKnacker.CODEZAHLEN];
    
    DefaultListModel<String> model = new DefaultListModel<>();
    JList<String> liste = new JList<>(model);
    JButton button = new JButton("Raten");
    JLabel label = new JLabel("00");
    
    public SpinCodeKnacker() {
        this.setLayout(new BorderLayout());
        button.addActionListener(this);
        JPanel panel = new JPanel();
        panel.setLayout(new FlowLayout());
        String[] spintxt = {"1","2","3","4","5","6"};
        for (int i=0; i<CodeKnacker.CODEZAHLEN; i++) {
            spmod[i] = new SpinnerListModel(spintxt);
            spinner[i] = new JSpinner(spmod[i]);
            panel.add(spinner[i]);
        }
        panel.add(button);
        panel.add(label);
        this.add(BorderLayout.NORTH, panel);
        JScrollPane scroll = new JScrollPane(liste);
        this.add(BorderLayout.CENTER, scroll);
        this.pack();
        this.setVisible(true);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }
    public static void main(String[] args) {
        new SpinCodeKnacker();
    }
    @Override
    public void actionPerformed(ActionEvent e) {
        String txt = "";
        for (int i=0; i<CodeKnacker.CODEZAHLEN; i++) {
            txt += spinner[i].getValue();
        }
        Ergebnis ergebnis = spiel.vergleiche(txt);
        String eintrag = txt + ": " +ergebnis.toString();
        model.addElement(eintrag);
        rateVersuche++;
        label.setText(""+rateVersuche+" "+spiel.getGeheim());
        if (ergebnis.getTreffer()==CodeKnacker.CODEZAHLEN) {
            JOptionPane.showMessageDialog(null, "Gewonnen!");
            spiel = new CodeKnacker();
            rateVersuche = 0;
            model.removeAllElements();            
        }
        label.setText(""+rateVersuche+" "+spiel.getGeheim());
    }
}

Und so sieht das Spiel auf dem Bildschirm aus: