Ein Swing-Layout-Manager – OneRowSameSizeLayoutManager

 Java, Swing  Kommentare deaktiviert für Ein Swing-Layout-Manager – OneRowSameSizeLayoutManager
Feb 262018
 

Jeder Java-Entwickler, der schon mal seriös mit Swing eine Oberfläche gebaut hat, hat schon einmal mit dem Layout-Manager seiner Wahl gekämpft. Die standardmäßig im JDK ausgelieferten Layout-Manager sind eher gewöhnungsbedürftig bis leistungsschwach, mindestens aber umständlich. GroupLayout für komplexe Dinge und BorderLayout für Basislayouts sind gerade noch so verwendbar. Beliebte 3rd-party-Layout-Manager sind sicherlich MigLayout (oder MiG Layout? Man kann sich nicht enscheiden…), FormLayout, TableLayout und DesignGridLayout (Jahre nach dem Ableben von java.net wohl nur noch über Maven Central beziehbar).

Für besondere Bedürfnisse ist es manchmal eine gute Idee, einen einfachen LayoutManager selbst zu bauen. Ich habe das mal beispielhaft getan für den Anwendungsfall “Layout einer typischen Reihe von Buttons unten in einem Dialog”. Im Gegensatz zum merkwürdigen Beispiel im Swing-Tutorial von Oracle namens “DiagonalLayout” hoffe ich jedenfalls, dass mein Beispiel etwas mehr Nützlichkeit ausstrahlt. Der Quellcode ist mehr auf Lesbarkeit und Nachvollziehbarkeit denn auf Eleganz getrimmt. An einem schmissigeren Namen arbeite ich noch…

Irgendwann werde ich noch mal den “Packer”, einen Layout-Manager von Tcl/Tk, für Swing nachbauen. M.E. war der “Packer” in seinem Verhalten und der Vorhersagbarkeit des programmierten Resultats absolut untadelig, etwas was ich von den meisten Swing-LayoutManagern nicht sagen kann. Aber vielleicht ist meine Erinnerung an den Packer auch rosarot verklärt – ist schon über zwei Jahrzehnte her seit meinem ersten und letzten Kontakt.

/*
 * (c) hubersn Software
 * www.hubersn.com
 */
package com.hubersn.playground.swing.layout;

import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.LayoutManager;

import javax.swing.SwingConstants;

/**
 * A layout manager supporting a row of components kept at the same width and height with specifiable alignment and gap size between the
 * components. It has its own insets to avoid having to work with empty borders.
 */
public class OneRowSameSizeLayout implements LayoutManager {

  /** Constant for default left alignment (identical to SwingConstants.LEFT). */
  public static final int ALIGN_LEFT = SwingConstants.LEFT;
  /** Constant for default centre alignment (identical to SwingConstants.CENTER). */
  public static final int ALIGN_CENTER = SwingConstants.CENTER;
  /** Constant for default right alignment (identical to SwingConstants.RIGHT). */
  public static final int ALIGN_RIGHT = SwingConstants.RIGHT;

  // calculated overall minimum/preferred size
  private int minWidth = 0;
  private int minHeight = 0;
  private int preferredWidth = 0;
  private int preferredHeight = 0;
  // calculated maximum component preferred sizes
  private int maxPreferredWidth = 0;
  private int maxPreferredHeight = 0;

  private Insets insets;

  private int componentGap = 5;

  private int alignment;

  /**
   * Creates a new layout with right alignment, 4 pixels gap and 8 pixels insets all around.
   */
  public OneRowSameSizeLayout() {
    this(ALIGN_RIGHT, 4, new Insets(8, 8, 8, 8));
  }

  /**
   * Creates a new layout with given alignment, gap between components and insets.
   * 
   * @param alignment component alignment - ALIGN_LEFT, ALIGN_CENTER or ALIGN_RIGHT.
   * @param componentGap gap in pixels between components.
   * @param insets insets to use additional to possible container border - if null, all insets are 0.
   */
  public OneRowSameSizeLayout(final int alignment, final int componentGap, final Insets insets) {
    setAlignment(alignment);
    this.componentGap = componentGap;
    this.insets = insets;
    if (insets == null) {
      this.insets = new Insets(0, 0, 0, 0);
    }
  }

  /**
   * Sets the component alignment for this layout.
   * 
   * @param alignment component alignment - ALIGN_LEFT, ALIGN_CENTER or ALIGN_RIGHT.
   */
  public void setAlignment(final int alignment) {
    this.alignment = alignment;
  }

  @Override
  public void addLayoutComponent(String name, Component comp) {
    // nothing to do - we are not interested in user-given layout constraints.
  }

  @Override
  public void removeLayoutComponent(Component comp) {
    // nothing to do.
  }

  private void calculateSizes(Container parent) {
    final int numberOfComponents = parent.getComponentCount();

    this.preferredWidth = this.insets.left + this.insets.right;
    this.preferredHeight = this.insets.top + this.insets.bottom;
    this.minWidth = 0;
    this.minHeight = 0;

    final int numberOfVisibleComponents = getNumberOfVisibleComponents(parent);
    for (int i = 0; i < numberOfComponents; i++) {
      final Component c = parent.getComponent(i);
      if (c.isVisible()) {
        final Dimension d = c.getPreferredSize();
        this.maxPreferredWidth = Math.max(this.maxPreferredWidth, d.width);
        this.maxPreferredHeight = Math.max(this.maxPreferredHeight, d.height);
        this.minWidth += c.getMinimumSize().width;
      }
    }

    this.preferredHeight += this.maxPreferredHeight;
    this.minHeight = this.maxPreferredHeight;

    if (numberOfVisibleComponents > 0) {
      this.preferredWidth += (this.maxPreferredWidth * numberOfVisibleComponents);
      this.preferredWidth += this.componentGap * (numberOfVisibleComponents - 1);
    }
  }

  @Override
  public Dimension preferredLayoutSize(Container parent) {
    final Dimension dim = new Dimension(0, 0);

    calculateSizes(parent);

    final Insets parentInsets = parent.getInsets();
    dim.width = this.preferredWidth + parentInsets.left + parentInsets.right;
    dim.height = this.preferredHeight + parentInsets.top + parentInsets.bottom;

    return dim;
  }

  @Override
  public Dimension minimumLayoutSize(Container parent) {
    final Dimension dim = new Dimension(0, 0);

    final Insets parentInsets = parent.getInsets();
    dim.width = this.minWidth + parentInsets.left + parentInsets.right;
    dim.height = this.minHeight + parentInsets.top + parentInsets.bottom;

    return dim;
  }

  @Override
  public void layoutContainer(Container parent) {
    final int numberOfVisibleComponents = getNumberOfVisibleComponents(parent);
    if (numberOfVisibleComponents == 0) {
      return;
    }

    calculateSizes(parent);

    final Insets parentInsets = parent.getInsets();

    // will all fit into parent? Which sizes to reduce?
    // width sizing strategy:
    // first, shrink gap and insets to 0
    // then, shrink button width and height
    // height sizing strategy:
    // shrink insets to 0
    final int parentWidth = parent.getWidth() - (parentInsets.left + parentInsets.right);
    final int parentHeight = parent.getHeight() - (parentInsets.top + parentInsets.bottom);
    final int heightToUse = Math.min(parentHeight, this.maxPreferredHeight);
    // width shrinking - toUse values are used for final layout bounds setting step
    int widthToUse = this.maxPreferredWidth;
    int buttonGapToUse = this.componentGap;
    int leftInsetToUse = this.insets.left;
    int rightInsetToUse = this.insets.right;

    int amountToReduceWidth = this.preferredWidth - parentWidth;
    if (amountToReduceWidth > 0) {
      // we need to save some pixels...
      // check for left/right insets
      if (leftInsetToUse + rightInsetToUse >= amountToReduceWidth) {
        int reduceInsetBy = amountToReduceWidth / 2;
        leftInsetToUse -= reduceInsetBy;
        amountToReduceWidth = 0;
      } else {
        leftInsetToUse = 0;
        amountToReduceWidth -= (this.insets.left + this.insets.right);
      }
      if (amountToReduceWidth > 0) {
        // check for button gaps
        if (numberOfVisibleComponents > 1 && (numberOfVisibleComponents - 1) * buttonGapToUse >= amountToReduceWidth) {
          buttonGapToUse -= amountToReduceWidth / (numberOfVisibleComponents - 1);
          amountToReduceWidth = 0;
        } else {
          buttonGapToUse = 0;
          amountToReduceWidth -= (numberOfVisibleComponents - 1) * this.componentGap;
        }
        // now shrink the button size itself as a last resort
        if (amountToReduceWidth > 0) {
          widthToUse = parentWidth / numberOfVisibleComponents;
        }
      }
    }

    // height shrinking - toUse values are used for final layout bounds setting step
    int topInsetToUse = this.insets.top;
    int amountToReduceHeight = this.preferredHeight - parentHeight;
    if (amountToReduceHeight > 0) {
      // we need to save some pixels...
      // check for top/bottom insets
      if (topInsetToUse + this.insets.bottom >= amountToReduceHeight) {
        int reduceInsetBy = amountToReduceHeight / 2;
        topInsetToUse -= reduceInsetBy;
      } else {
        topInsetToUse = 0;
      }
    }

    // finally size and place the components
    int x = parentInsets.left + leftInsetToUse;
    // alignment is only relevant if we have enough space
    if (parentWidth > this.preferredWidth) {
      if (this.alignment == ALIGN_CENTER) {
        x += (parentWidth - this.preferredWidth) / 2;
      } else if (this.alignment == ALIGN_RIGHT) {
        x += parentWidth - this.preferredWidth;
      }
    }

    final int y = parentInsets.top + topInsetToUse;

    final int numberOfComponents = parent.getComponentCount();
    for (int i = 0; i < numberOfComponents; i++) {
      Component c = parent.getComponent(i);
      if (c.isVisible()) {
        c.setBounds(x, y, widthToUse, heightToUse);
        x += widthToUse + buttonGapToUse;
      }
    }
  }

  private static int getNumberOfVisibleComponents(final Container parent) {
    final int numberOfComponents = parent.getComponentCount();
    int numberOfVisibleComponents = 0;

    for (int i = 0; i < numberOfComponents; i++) {
      Component c = parent.getComponent(i);
      if (c.isVisible()) {
        numberOfVisibleComponents++;
      }
    }

    return numberOfVisibleComponents;
  }
}

Eine Swing-Komponente – JVM-Memory-Indicator

 Java, Swing  Kommentare deaktiviert für Eine Swing-Komponente – JVM-Memory-Indicator
Feb 242018
 

Ich entwickle grafische Oberflächen in Java Swing seit der Zeit, als man Swing noch separat herunterladen musste und es noch im Package com.sun.java.swing wohnte. JDK 1.1. In JDK 1.0.2 hat man sich kurz AWT angeschaut, gelacht und es zu den Akten gelegt. Swing hingegen war ein seriöser Versuch eines leistungsfähigen und trotzdem plattformübergreifenden UI-Toolkits.

Drei Dinge jenseits von Java SE braucht der Swing-Entwickler bis heute: Komponenten, Layout-Manager und ggf. ein Application Framework. Neulich war ich auf der Suche nach einer kleinen schmalen Komponente für eins meiner zahlreichen Swing-basierten Inhouse-Tools, um den Zustand der JVM-Speicherverwaltung zu signalisieren, ähnlich wie Eclipse es standardmäßig anbietet wenn man es aktiviert unter “Window -> Preferences -> Global -> Show heap status”.

Als erfahrener Googler hätte ich erwartet, eher ein Problem mit zuviel als zuwenig Auswahl zu haben. Aber Pustekuchen: letztlich fand ich nur eine Frage auf StackOverflow (witzigerweise wenige Stunden nach meiner Antwort als off-topic geschlossen – und das bei einer Frage von 2009!) nach einer solchen Komponente, die lediglich zwei komplett falsche Antworten der Kategorie “Thema verfehlt” nach sich zog. Also habe ich mir mal eine Stunde Zeit genommen, um mit minimalem Aufwand eine solche Komponente zu bauen. Als Basis habe ich JProgressBar verwendet. Das Ergebnis ist natürlich weit weg von der wünschenswerten Flexibilität einer solchen Komponente, wenn sie in unterschiedlichen Kontexten wiederverwendbar sein soll, aber als ein schönes einfaches Beispiel taugt es dennoch. Die Komponente ist lizenztechnisch frei verwendbar allüberall. Eigene Anpassungen ausdrücklich erwünscht. i18n-Unterstützung, MiB vs. MB, Text und Tooltip über extern vorgebbare Texte mit Platzhaltern realisieren, ein “Trash”-Icon zum Auslösen des GC anstatt der nichtoffensichtlichen Doppelclick-Variante, eine “Mark”-Funktionalität ähnlich Eclipse – die Erweiterungsideen sind beinahe endlos.

/*
 * (c) hubersn Software
 * www.hubersn.com
 * 
 * Use wherever you like, change whatever you want. It's free!
 */
package com.hubersn.playground.swing;

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

import javax.swing.JProgressBar;
import javax.swing.Timer;

/**
 * JVM Memory Indicator based on JProgressBar.
 */
public class ProgressBarBasedJVMMemoryIndicator extends JProgressBar {

  private static final long serialVersionUID = 1L;

  private static final int TIMER_INTERVAL_IN_MS = 1000;

  /**
   * Creates a new instance of ProgressBarBasedJVMMemoryIndicator.
   */
  public ProgressBarBasedJVMMemoryIndicator() {
    super(0, 100);
    setStringPainted(true);
    setString("");
    addMouseListener(new MouseAdapter() {
      @Override
      public void mouseClicked(final MouseEvent mev) {
        if (mev.getClickCount() == 2) {
          System.gc();
          update();
        }
      }
    });
    final Timer t = new Timer(TIMER_INTERVAL_IN_MS, new ActionListener() {
      @Override
      public void actionPerformed(final ActionEvent aev) {
        update();
      }
    });
    t.start();
    update();
  }

  private void update() {
    final Runtime jvmRuntime = Runtime.getRuntime();
    final long totalMemory = jvmRuntime.totalMemory();
    final long maxMemory = jvmRuntime.maxMemory();
    final long usedMemory = totalMemory - jvmRuntime.freeMemory();

    final long MEBIBYTE_FACTOR = 1024 * 1024;
    final long totalMemoryInMebibytes = totalMemory / MEBIBYTE_FACTOR;
    final long maxMemoryInMebibytes = maxMemory / MEBIBYTE_FACTOR;
    final long usedMemoryInMebibytes = usedMemory / MEBIBYTE_FACTOR;
    final int usedPercentage = (int) ((100 * usedMemory) / totalMemory);
    final String textToShow = usedMemoryInMebibytes + "MiB of " + totalMemoryInMebibytes + "MiB";
    final String toolTipToShow = "Heap size: " + usedMemoryInMebibytes + "MiB of total: " + totalMemoryInMebibytes + "MiB max: "
        + maxMemoryInMebibytes + "MiB - double-click to run Garbage Collector";

    setValue(usedPercentage);
    setString(textToShow);
    setToolTipText(toolTipToShow);
  }
}

Aus mir unerfindlichen Gründen habe ich danach angefangen, eine superflexible, i18n-fähige, effiziente, JComponent-basierte Variante davon zu bauen. Hat auch nicht viel länger gedauert. Soll “demnächst” im Rahmen meiner ultimativen Swing-Bibliothek das Licht der Welt erblicken – das soll diese Bibliothek aber schon seit ungefähr 2001…

Swing-Rätsel – JTree-Zeilenhöhe

 Java, Swing  Kommentare deaktiviert für Swing-Rätsel – JTree-Zeilenhöhe
Apr 112016
 

Manchmal hat man Probleme schon vor so langer Zeit gelöst, dass man sich bei erneutem Auftreten nicht mehr dran erinnert. Weder an das Problem, noch an die Lösung.

Es geht um Java Swing. Die Basics. Man nehme einen JTree. Da gibt es eine Methode namens “setRowHeight” wo man die zu rendernde Zeilenhöhe setzen kann. Wie so oft gibt es hier auch eine “magic number”, die besonderes Verhalten aktiviert. Hier ist es die 0 (oder negative Werte) – damit instruiert man den Tree, doch bitte den Renderer zu fragen, wie hoch die Zeile denn nun wirklich werden soll. Man sollte meinen, dass das auch ein wirklich guter Default wäre.

Nun gibt es ja in Swing zwei Varianten, wie so ein Default in Aktion tritt. Entweder simple Hartcodierung, oder durch die UI-Defaults des aktiven Look&Feel. Wer weiß auswendig, wie der hartcodierte Wert ist und welches der Standard-Look&Feels dann welchen Wert setzt? Bonuspunkte für denjenigen, der das auch noch schlüssig erklären kann.

Amüsante Randnotiz (Achtung, Spoiler! Teile der Antwort auf obige Frage inside!): hier ein Bug aus Zeiten von Java 1.4 (gab es das wirklich schon 2002?), der sich ebenfalls mit diesem Problem befasst.

Das Ende des Java-Plugins

 Java, Swing, UI  Kommentare deaktiviert für Das Ende des Java-Plugins
Feb 032016
 

Ich habe mich ja schon zu Anfangszeiten dieses Blogs als Fan der Java Applet-Technologie geoutet. Daran hat sich eigentlich nichts geändert. Nach wie vor halte ich Applets für den elegantesten Weg, vernünftige Benutzeroberflächen in den Browser zu bringen und gleichzeitig problemlos mit derselben Codebasis die Freunde des FatClients zu bedienen. Kluge Menschen haben behauptet, die Idee, GUIs in den Browser zu bringen, hätte die Qualität von Benutzeroberflächen 20 Jahre zurückgeworfen. Inzwischen dürften es wohl eher 25 Jahre sein, denn die Qualität von gängigen Oberflächen der 90er, sei es RISC OS oder MacOS oder die diversen GEM-Varianten, ist bis heute nicht erreicht. Optisch vielleicht schon, aber ergonomisch und performancetechnisch auf keinen Fall.

Nun hat Oracle verkündet (und schon vorher anklingen lassen, siehe etwa hier), man werde das Java-Plugin, das für die Ausführung von Applets im Browser zuständig ist, mit Java 9 als “deprecated” erklären und es in einer späteren Version aus Java entfernen. Als Grund wird genannt, dass mobile Browser ja noch nie Plugins unterstützt hätten und diverse Browser entweder die Plugin-Unterstützung schon entfernt hätten (Chrome), dies vorhätten (Firefox) oder noch nie eine gehabt hätten (MS Edge). Dazu die Security-Problematik.

So weit, so traurig. Insbesondere den Security-Aspekt konnte ich noch nie nachvollziehen – man kann das Java-Plugin ja so konfigurieren, dass es nur Applets mit bestimmter Signatur überhaupt ausführt, insofern ist diese Technologie wohl kaum unsicherer als die Ausführung beliebigen anderen Codes auf der Maschine. Dass es ab und zu in der Sandbox (auch klaffende) Security-Lücken gab, ist ja kaum ein Alleinstellungsmerkmal von Plugins – auch die Browser selbst sind ja wandelnde Sicherheitslöcher, selbst bei so simplen Dingen wie Grafikdecodierung. Das hat man halt davon, wenn man in Pseudo-Hochsprachen wie C entwickelt. Aber ich schweife ab.

Letztlich ist das alles “water under the bridge”, und man sollte nach vorne schauen. Dazu sollte man zunächst herausfinden, in welchem Zeitrahmen man nun Abschied von den Applets nehmen muss.

Entscheidend dafür ist zunächst der verwendete Browser. Bei IE11 kann man sich noch etwas Zeit lassen – Januar 2023 endet der Support von Microsoft für den IE11, zumindest unter Windows 8. Bei Firefox muss man sich schon eher sputen, Ende 2016 dürfte es vorbei sein. Es gibt aber durchaus noch Browser, die die NSAPI für Plugins unterstützen und noch nicht abgekündigt haben – Konqueror, QupZilla oder Midori (allesamt WebKit-basiert). Wie lange das anhält, wird man sehen – nicht so weit verbreitete Browser folgen ja häufig mit kurzer Ankündigungsfrist dem Mainstream und bieten selten lange Support-Garantien.

Wie sieht es auf der Java-Seite aus? September 2017 endet voraussichtlich die Verfügbarkeit öffentlicher Java 8-Updates, basierend auf dem Oracle-Releasezyklus wird das Ende der öffentlichen Java 9-Updates also frühestens September 2019 sein. Danach kann man Oracle etwas Geld in den Rachen werfen, um weiterhin Java 9-Updates zu bekommen. Bis heute unterstützt Oracle ja gegen Einwurf kleiner Münzen sogar Java 5. Wer es also aussitzen will bis zuletzt, muss sich vermutlich bis zum Ende von IE11 (frühestens 2023 laut Microsoft) keine Sorgen machen.

Executive Summary: Don’t Panic.

Java auf dem Desktop? Echt jetzt?

 Java, Swing  Kommentare deaktiviert für Java auf dem Desktop? Echt jetzt?
Jan 142015
 

Wie oft haben wir es gehört – der Desktop ist tot, der Browser ist die Plattform der Zukunft, und Java ist auf dem Client sowieso tot und existiert nur auf dem Server.

JSR 377 heißt die Überraschung offiziell. Mindestens zwei der Beteiligten sind keine Unbekannten in der Java-Welt: Andres Almiray und Hendrik Ebbers. Letzterer hat hier darüber gebloggt.

Ich bin gespannt, ob es gelingt, die Pleiten bei JSR 193 (Client Side Container, anno 2002) und dem Swing Application Framework (JSR 296) vergessen zu machen. Der Umfang scheint mir recht ambitioniert zu sein, und man läuft bei diesem Thema immer Gefahr, vom hundertsten ins tausendste zu kommen. Allein die Zielgruppe zu definieren (kleine und/oder große Anwendungen? Modularisierung selbst oder auf Basis von z.B. OSGi (oder gar Jigsaw)? Nur Entwicklung oder auch Test und Deployment? Für ein spezielles Toolkit oder generisch? Support für Persistency irgendeiner Art und wenn ja welcher Art?) ist eine Kunst. Dann die Abgrenzung gegenüber den Platzhirschen Eclipse RCP und Netbeans Platform.

Ich persönlich denke, dass ein schlankes Framework irgendwo zwischen “Swing/JavaFX von Hand” und Netbeans Platform auf jeden Fall seinen Platz finden würde. Aber das dachte ich auch schon beim (B)SAF. Mal sehen, ob es diesmal klappt.

Die initiale maximale Größe eines JPanels

 Java, Swing  Kommentare deaktiviert für Die initiale maximale Größe eines JPanels
Sep 102014
 

Heute bin ich über ein interessantes Detail bei der Swing-Programmierung gestolpert. Gegeben war eine JLayeredPane, die ein JPanel (leer, nur mit Hintergrundfarbe ausgestattet) und eine Editor-Komponente (basierend auf JTextPane)  beinhaltete. Die Editor-Komponente wurde durch sehr viel Textinhalt nun relativ groß (über 70000 Pixel hoch), und man sah plötzlich, dass das JPanel nicht mehr die komplette Höhe ausfüllte.

Debugging brachte zutage, dass regelmäßig setBounds des JPanels aufgerufen wurde, mit einer Höhe von 32767 Pixeln. Aha, das Maximum eines vorzeichenbehafteten 16bit-Wertes. Aber wo kommt der her?

Des Rätsels Lösung: es ist die maximumSize des JPanels im Initialisierungszustand. Man lernt halt jeden Tag dazu, selbst wenn man seit Swing 1.1 damit Oberflächen baut.

Wer dieses Detail aus dem Stegreif wusste, darf mir gerne mailen.

Besonderer Gruß an THR, mit dem ich zusammen das Problem gedebuggt habe.