2013-07-20

Ultimate Swing, Teil 26

Wenn Sie Notes and Tasks schon einmal selbst ausprobiert haben, ist Ihnen sicherlich aufgefallen, dass Größen- und Positionsänderungen des Anwendungsfensters verloren gehen, wenn Sie das Programm beenden und neu starten. Ich habe kürzlich eine aktualisierte Version in das Repository eingecheckt, die zeigt, wie leicht Java und Swing Ihnen die Speicherung (und Abfrage) von solchen Einstellungen machen.

Java enthält mit java.util.Preferences eine Klasse, die eine hierarchische (baumartige) Datenstruktur ähnlich der Windows Registrierungsdatenbank implementiert. Wo und wie Java die Preferences speichert, hängt vom Wirtssystem ab. Unter Windows wird (selbstverständlich) die Registrierungsdatenbank verwendet. Unter Mac OS X landen Einträge in XML-Dateien. Es gibt zwei separate Zweige: einen für Systemeinstellungen und einen für benutzerspezifische Daten. Üblicherweise liegen Farb- und Schrifteinstellungen sowie Positionsangaben von Fenstern im benutzerspezifischen Zweig der Preferences. Installationspfade würden Sie hingegen im Systemzweig speichern. Seine hierarchische Struktur erhält die Preferences-Datenbank durch Knoten, die sehr an Verzeichnisse erinnern. Knoten können wiederum Knoten enthalten sowie Blätter als die eigentlichen Informationsträger. Auch die Adressierung erfolgt analog zu Pfaden eines Dateisystems.

Das folgende Quelltextfragment zeigt den Einstieg in den benutzerspezifischen Teil der Preferences (hierzu wird die statische Methode userRoot() aufgerufen) sowie den Zugriff auf einen Knoten. Sein Name ist etwas länglich - er besteht aus einem voll qualifizierten Klassennamen sowie der Bildschirmbreite und Höhe.

 private Preferences getBaseNode() {  
  return Preferences.userRoot().node(callback.getClass().getName()   
                    + "_"   
           + Integer.toString(screenSize.width)   
           + "_"   
           + Integer.toString(screenSize.height));  
 }  

Nun zeige ich Ihnen, wie Sie Informationen schreiben.

 private void storeWindowState() {  
  Preferences p = getBaseNode();  
  int state = getExtendedState();  
  p.putInt(KEY_EXTENDED_STATE, state);  
  if ((state & Frame.MAXIMIZED_BOTH) == 0) {  
   Rectangle r = getBounds();  
   p.put(KEY_WINDOW_BOUNDS, convertToString(r));  
  }  
 }  

Nach dem Ermitteln das Basisknotens (die Methode getBaseNode() haben Sie schon kennen gelernt) werden mit p.putInt() und p.put() Name-Wert-Paare abgelegt.

Das Lesen funktioniert analog:

 ...  
 Preferences p = getBaseNode();  
 Rectangle r = getBounds();  
 String s = p.get(KEY_WINDOW_BOUNDS, convertToString(r));  
 r = convertToRectangle(s);  
 setBounds(r);  
 int state = p.getInt(KEY_EXTENDED_STATE, Frame.NORMAL);  
 setExtendedState(state);  
 ...  

Nach dem Ermitteln des Basisknotens greifen p.get() und p.getInt() lesend auf die Preferences zu.

Lassen Sie uns nun einen Blick auf meine Hilfsmethoden convertToString() und convertToRectangle() werden.

 private static String convertToString(Rectangle r) {  
  return Integer.toString(r.x)   
      + "," + Integer.toString(r.y)   
      + "," + Integer.toString(r.width)   
      + "," + Integer.toString(r.height);  
 }  

convertToString() wandelt ein Objekt des Typs Rectangle in eine Komma-getrennte Zeichenkette um. convertToRectangle() tut genau das Gegenteil.

 private static Rectangle convertToRectangle(String s) {  
  String[] args = s.split(",");  
  return new Rectangle(Integer.parseInt(args[0]),   
             Integer.parseInt(args[1]),   
             Integer.parseInt(args[2]),   
             Integer.parseInt(args[3]));  
 }  

Hinweis: Ich habe darauf verzichtet, ungültige Werte abzufangen; werden die Einträge in den Preferences manipuliert, würde sehr wahrscheinlich eine Ausnahme geworfen. Es steht Ihnen frei, eigene Implementierungen entsprechend zu härten.

Unter AWT und Swing werden Position und Größe eines Fensters in Objekten des Typs Rectangle gespeichert. Da die Preferences diesen Datentyp nicht (so ohne weiteres) kennen, wandeln wir ihn vor dem Speichern in einen String um. Sollen nach einem weiteren Programmstart die Position und Größe des Fensters wiederhergestellt werden, wird die Zeichenkette wieder zu einem Rectangle-Objekt umgewandelt.

Ist ihnen beim Lesen und Schreiben der extended window state aufgefallen? Mann könnte ja annehmen, dass ein Fenster genau dann seine Maximalgröße hat, wenn Breite und Höhe der Bildschirmgröße in Pixel entsprechen. In Java werden maximierte Fenster durch zwei spezielle Flags (horizontal und vertikal) gekennzeichnet. Wenn beim Beenden des Programms das Anwendungsfenster maximiert war, genügt es deshalb nicht, die Koordinaten entsprechend zu setzen. Beim nächsten Start muss der Fensterstatus explizit auf maximiert gesetzt werden. Und es gibt noch eine Besonderheit: ist ein Fenster maximiert, dürfen seine Größe und Position nicht gespeichert werden. Beim Zurückversetzen in den normalen Zustand würden nämlich nicht mehr die Koordinaten vor dem Maximieren zur Verfügung stehen. Sie wären ja mit den Maximalwerten überschrieben worden. Deshalb persistiere ich bei Veränderungen der Fenstergröße oder -position die Daten in die Preferences nur, sofern das Fenster nicht maximiert ist.

Hausaufgabe: warum enthält der Name meines Preferences-Schlüssels die Bildschirmgröße? Schreiben Sie mir…

2 comments:

  1. Es ist schon eine Weile her, dass ich das benutzt habe, kann mich aber noch erinnern, dass es zum Nachgucken / Debugging und für die Eindeutigkeit sinnvoll sein kann, den Anwendungsnamen als ersten Knoten zu verwenden und alles weitere darunter abzulegen. Ich nehme mal an, dass der callback.getClass().getName() im Namen den gleichen Zweck erfüllt und gleichzeitig für Eindeutigkeit sorge trägt.

    Die Codierung der Screensize hat die Auswirkung, dass bei Änderung selbiger die Preference nicht mehr zugegriffen werden kann. Möchtest Du vermeiden, dass bei verkleinerter Bildschirmgröße das Anwendungsfenster zu groß wird?

    ReplyDelete
  2. > dass der callback.getClass().getName() im Namen den gleichen Zweck erfüllt
    > und gleichzeitig für Eindeutigkeit sorge trägt.

    Stimmt. getName() enthält (im Gegensatz zu getSimpleName()) den Paketnamen, ist also voll qualifiziert.

    > Die Codierung der Screensize hat die Auswirkung, dass bei Änderung selbiger
    > die Preference nicht mehr zugegriffen werden kann. Möchtest Du vermeiden,
    > dass bei verkleinerter Bildschirmgröße das Anwendungsfenster zu groß wird?

    Zum einen die Größe, zum anderen die Position der linken oberen Ecke.

    *schmunzel* Hausaufgabe prima gelöst ;-)

    ReplyDelete