JSpinner zur Datums- und Zeitauswahl

Mit Hilfe eines SpinnerDateModel und eines DateEditor kann ein JSpinner zur Datums- und Zeitauswahl genutzt werden. Damit Anzeige und Ausgabe nach Wunsch gelingen, gilt es allerdings einiges zu beachten.

Der vorliegende Artikel behandelt die Nutzung eines JSpinner zur Datums- und Zeitanzeige. Die allgemeinen Grundlagen zu dessen Aufbau und zur weiteren Nutzung eines JSpinner werden im Artikel Auswahlmenu JSpinner erläutert.

Das DateModel

Da ein JSpinner im einfachsten Fall seine Inhalte intern durch ein NumberModel verwaltet, wird er erst durch Übergabe eines DateModels zur Verwaltung von Datums- und Zeitwerten befähigt. Hierzu stellt Java die Klasse SpinnerDateModel bereit, deren Konstruktoren entweder unparametrisiert oder durch Angabe von vier Argumenten aufgerufen werden können.

public class SpinnerDateModelBsp {

    public SpinnerDateModelBsp() {
        init();
    }

    public void init() { 
        SpinnerDateModel model = new SpinnerDateModel();
        JSpinner spinner = new JSpinner(model);
        spinner.addChangeListener(e -> System.out.println(spinner.getValue()));

        JFrame frame = new JFrame("Date-Model-Beispiel");
        frame.add(spinner);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
    
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> new SpinnerDateModelBsp());
    }
}

Das Beispiel demonstriert die einfachste Form eines solchen Spinners mit unparametrisiertem Model. Dem JSpinner wurde lediglich ein ChangeListener hinzugefügt, um auch die Ausgabe der gespeicherten Werte zu demonstrieren. Man sieht, dass hierbei auf die Methode JSpinner.getValue() zugegriffen wird. Sie ruft intern die entsprechende Modelmethode SpinnerDateModel.getValue() auf, sodass also gerade nicht auf die Darstellung im Textfeld des Spinners, sondern auf den im Model gespeicherten Wert zugegriffen wird. Das Ergebnis sind zwei verschiedene Darstellungsweisen, die bei der Betrachtung des Editors zu berücksichtigen sein werden:

  1. Im Model ist der Wert in einer Form gespeichert, wie er hinsichtlich seines Ausgabeformates, der Zeitzone und der Standard-Gebietsvariablen durch ein Calendar-Objekt repräsentiert wird, z.B.
    Mon Oct 25 10:00:00 CEST 2021.
  2. Die Darstellung im Textfeld des Spinners ist zusätzlich formatiert. Sie basiert auf dem mit dem Spinner assoziierten JSpinner.DateEditor, der das oben bereits erwähnte JFormattedTextField liefert und die Formatierung des Strings dort durch einen DateFormatter regelt.

Sollen beide Formen identisch formatiert sein, so muss entweder die Darstellung durch den JSpinner, durch Setzen eines geeigneten Editors oder der Ausgabewert umformatiert werden.

#

Um die Anzeige eines JSpinner nach oben oder unten zu begrenzen und die maximale Schrittgröße beim "Blättern" festzulegen, kann ein SpinnerDateModel mit vier Parametern erzeugt und an den Spinner übergeben werden.

//...
Calendar cal = new GregorianCalendar();
Date now = cal.getTime();

cal.set(Calendar.HOUR_OF_DAY, 5);
Date start = cal.getTime();

cal.set(Calendar.HOUR_OF_DAY, 20);
Date end = cal.getTime();

SpinnerDateModel model = new SpinnerDateModel(now, start, end, Calendar.HOUR_OF_DAY);
JSpinner spinner = new JSpinner(model);
//...

Wichtig ist, dass der Zeitpunkt des ersten Wertes zwischen start und end liegt. Ist das nicht der Fall, so wird eine IllegalArgumentException mit dem Hinweis

(start <= value <= end) is false

geworfen.

Verhalten des Spinners

Wird das obige Programm ausgeführt, so befindet sich der Cursor am linken Rand des Textfeldes vor der Anzeige. Wurde ein unparametrisiertes Model übergeben oder für start und end eines parametrisierten Models null angegeben, so wird bei Betätigen der Pfeilbuttons der erste angezeigte Wert, hier, bei deutschem Datumsformat, der Tag, erhöht oder erniedrigt. Setzt man den Cursor in den Bereich der Jahreszahl oder markiert sie, so wird diese verändert, usw. Die Änderung bezieht sich immer auf den jeweils markierten Anteil des angezeigten Datum-Zeit-Wertes.

Ist es erforderlich, nur vorher festgelegte Datums- oder Zeitbereiche zu ändern, verwendet man einen entsprechend parametrisierten Model-Konstruktor oder man setzt die Schrittweite durch Aufruf von setCalendarField() z.B. auf Calendar.HOUR_OF_DAY. In diesem Fall wird erst dann eine Aktivität beim Betätigen der Buttons registriert, wenn, nach Markieren eines Teilbereiches, sich die Änderung innerhalb der gesetzten Datums-Zeit-Grenze befindet. Hier zeigt sich der Einfluss des Parameters calendarField: Er bewirkt, dass nur die durch ihn angegebene und kleinere Einheiten geändert werden können. Dies gilt natürlich nur im Rahmen des durch start und end gesetzten Wertebereichs, sofern dieser nicht durch Setzen von null aufgehoben wurde.

Der DateEditor

Der DateEditor ist die für die Anzeige zuständige Komponente. Sie wird durch die innere Klasse JSpinner.DateEditor bereitgestellt und ist von JSpinner.DefaultEditor abgeleitet. Auf Ebene des GUI besteht er aus einem JPanel mit einem JFormattedTextField, auf das auf die folgende Weise zugegriffen werden kann.

JFormattedTextField field = ((JSpinner.DateEditor)spinner.getEditor()).getTextField();

Auf diese Weise kann auch der DateFormatter des Textfeldes ermittelt und bei Bedarf modifiziert werden. Durch setAllowsInvalid() kann z.B. eine Überprüfung des eingegebenen Wertes erzwungen werden.

DateFormatter formatter = (DateFormatter) dateEditor.getTextField().getFormatter();
formatter.setAllowsInvalid(false);

Für die Einrichtung der Anzeigeformatierung ist ein direkter Zugriff allerdings im Allgemeinen nicht nötig. In den meisten Fällen bietet es sich an, einen eigenen Editor zu erzeugen und diesen durch setEditor() an den Spinner zu übergeben. Sein Konstruktor kann als zweites Argument einen Formatierungs-String erhalten, der das Datum-Zeit-Format vorgibt und den Definitionen des SimpleDateFormat folgen muss.
Selbstverständlich muss darauf geachtet werden, dass sich der Wert auch innerhalb des eventuell durch ein eigenes Model festgelegten Bereichs befindet.

JSpinner.DateEditor dateEditor = new JSpinner.DateEditor(spinner, "HH:mm:ss"); // z.B. 20:15:25
spinner.setEditor(dateEditor);

Hier steckt der Teufel allerdings im Detail! Das folgende Beispiel ist in der angegebenen Form so nicht verwendbar:

Calendar cal = new GregorianCalendar();
Date now = cal.getTime();
System.out.println("now: " + now);

cal.set(Calendar.HOUR_OF_DAY, 5);
Date start = cal.getTime();
System.out.println("early: " + start);

cal.set(Calendar.HOUR_OF_DAY, 23);
Date end = cal.getTime();
System.out.println("late: " + end);

SpinnerDateModel model = new SpinnerDateModel(now, start, end, Calendar.HOUR_OF_DAY);
JSpinner spinner = new JSpinner(model);
JSpinner.DateEditor dateEditor = new JSpinner.DateEditor(spinner, "HH:mm:ss"); // nicht spinnbar
spinner.setEditor(dateEditor);
//...

Es wird erwartet, dass zwischen 5 und 23 Uhr eine Anpassung des Spinners im Stunden-, Minuten- und Sekundenbereich möglich ist. Diese Erwartung wird jedoch nicht erfüllt.
Die Ursache dieser Dysfunktionalität liegt in der Art und Weise der internen Verarbeitung der beteiligten Werte: Dem Editor werden Date-Objekte in Form von Millisekunden seit dem 1. Januar 1970 00:00:00 übergeben. Bei der Validierung einer Eingabe, entweder nach Klicken auf einen der Pfeile oder nach manueller Eingabe wird der String in ein Date-Objekt gewandelt. Dies wird anschließend mit den angegebenen Grenzen abgeglichen. Fehlt - wie oben - die Datumsangabe, so wird vom 1.1.1970 ausgegangen, sodass der Bereich des zulässigen Intervalls unterschritten wird. Ein ähnliches Verhalten gilt ggf. auch für andere Teile des Datum-Zeit-Strings.

Mögliche Lösungen bestehen darin, entweder das für start und end verwendete Date-Objekt auf den 1.1.1970 zu setzen

Calendar cal = new GregorianCalendar();
cal.set(Calendar.YEAR, 1970);
cal.set(Calendar.MONTH, 0);
cal.set(Calendar.DAY_OF_MONTH, 1);
Date now = cal.getTime();
//...

oder aber den Spinner nicht zu begrenzen, indem die Grenzwerte auf null gesetzt werden.

Wenn Ihnen javabeginners.de gefällt, freue ich mich über eine Spende an diese gemeinnützigen Organisationen.