Inselraus: Swings JLayeredPane

Die JLayeredPane nimmt JComponent-Objekte auf und stellt sie in einer geschichteten Reihenfolge (auch Z-Order genannt) dar. Die Layered Pane besteht selbst wieder aus zwei Objekten, einer Menüzeile und der Inhaltsfläche Content-Pane. Container vom Typ JLayeredPane platzieren ihre Kinder in Ebenen (engl. layers). Jedem Kind wird eine Ebene zugewiesen, und beim Zeichnen werden die Kinder von unten nach oben gezeichnet. Damit werden die Komponenten, die unter anderen Komponenten liegen, unter Umständen verdeckt. Standardmäßig hat die JLayeredPane keinen zugewiesenen Layoutmanager, und Objekte müssen mit setBounds(…) positioniert werden.

Wird ein JLayeredPane-Container verwendet, ist die add(…)-Methode so implementiert, dass sie die Komponenten auf einer Standardebene (JLayeredPane.DEFAULT_LAYER) platziert. Um Komponenten auf eine eigene Ebene zu setzen, sodass sie vor oder hinter anderen Komponenten liegen, wird ihnen eine eigene Ebene zugewiesen, und zwar mit einem Wert relativ zu DEFAULT_LAYER. Kleinere Werte bedeuten, dass die Komponenten unten liegen, und hohe bedeuten, dass sie oben liegen. Ein Beispiel:

layeredPane.add( component, new Integer(5000) );

Für einige Ebenen sind Werte als Konstanten deklariert. Dazu zählen zum Beispiel JLayered-Pane.DEFAULT_LAYER (0), PALETTE_LAYER (100), MODAL_LAYER (200), POPUP_LAYER (300) und DRAG_LAYER (400).

Neben der Möglichkeit, die Ebenen festzulegen, lässt sich die Reihenfolge innerhalb der Ebene später durch die Methode moveToFront() oder moveToBack() verändern.

Inselraus: Swings JRootPane und JDesktopPane

Unter den JComponent-Objekten gibt es einige ausgezeichnete, die als Container für andere Kinder fungieren.

Wurzelkomponente der Top-Level-Komponenten (JRootPane)

Die Komponenten JFrame, JDialog, JWindow, JApplet und JInternalFrame enthalten als einziges Kind den leichtgewichtigen Container JRootPane. Die Methode getRootPane() liefert dieses JRootPane-Objekt. Die JRootPane verwaltet eine Layered Pane, die wiederum Content-Pane und Menü aufnimmt, und eine Glass-Pane, die wie eine Glasscheibe über allen anderen Komponenten liegt. Sie kann Ereignisse abfangen oder in einer paint(…)-Methode etwas über alle Komponenten zeichnen.

Beispiel: Weise der Glass-Pane einen wartenden Cursor zu:

Component c = getRootPane().getGlassPane();
 if( c != null )
   c.setCursor( Cursor.getPredefinedCursor( Cursor.WAIT_CURSOR ) );

 JDesktopPane und die Kinder von JInternalFrame

Die JDesktopPane ist eine Unterklasse von JLayeredPane und als Container für interne Fenster – also Objekte vom Typ JInternalFrame – gedacht. Mit internen Fenstern (engl. internal frames) lassen sich MDI-Applikationen implementieren, also GUI-Anwendungen, bei denen nicht das grafische Betriebssystem die Fenster verwaltet, sondern die eigene Anwendung.

Bevor ein JInternalFrame sichtbar wird, muss der Container erzeugt und sichtbar gemacht werden:

JDesktopPane desktop = new JDesktopPane();
 container.add( desktop );

Jetzt können beliebig viele JInternalFrame-Objekte erzeugt und auf der JDesktopPane platziert werden. Der einfachste Konstruktor ist der Standard-Konstruktor, der einen nicht vergrößerbaren, nicht schließbaren, nicht maximierbaren und nicht zum Icon verkleinerbaren JInternal-Frame ohne Titel erzeugt. Der ausführlichste Konstruktor erlaubt eine genaue Parametrisierung:

JInternalFrame iframe = new JInternalFrame( title, resizable, closeable,
   maximizable, iconifiable );

Zwar gibt es nun ein Exemplar, doch wäre es nach dem Aufsetzen auf den Container noch nicht sichtbar:

iframe.setVisible( true );

Bis zur Vollständigkeit fehlen aber noch die Maße:

iframe.setSize( /* width = */ 200, /* height = */ 100 );

Nun kann der iframe dem Container hinzugefügt werden:

desktop.add( iframe );

In einem kompletten Programm kann das so aussehen:

package com.tutego.insel.ui.swing;
 
 import javax.swing.*;
 import static java.lang.Math.random;
 
 public class JInternalFrameDemo {
 
   static void addInternalToDesktop( JDesktopPane desktop )
   {
     JInternalFrame iframe;
     iframe = new JInternalFrame( "Ein internes Fenster",  // title
                                  true,                    // resizable
                                  true,                    // closeable
                                  true,                    // maximizable
                                  true );                  // iconifiable
 
     iframe.setBounds( (int)(random() * 100), (int)(random() * 100),
                       100 + (int)(random() * 400), 100 + (int)(random() * 300) );
     iframe.add( new JScrollPane(new JTextArea()) );
     iframe.setVisible( true );
 
     desktop.add( iframe );
   }
 
   public static void main( String[] args ) {
     JFrame f = new JFrame();
     f.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
     JDesktopPane desktop = new JDesktopPane();
     f.add( desktop );
     f.setSize( 500, 400 );
     addInternalToDesktop( desktop );
     addInternalToDesktop( desktop );
     addInternalToDesktop( desktop );
     f.setVisible( true );
   }
 }

Hinweis: Die Schnittstelle von JInternalFrame erinnert an JFrame, doch ist die Ereignisbehandlung anders. So besitzt JInternalFrame eine Methode addInternalFrameListener(…) an Stelle von addWindowListener(…). Ein JInternalFrame empfängt keine WindowEvents, daher darf es addWindowListener(…) – wie es JFrame von java.awt.Window erbt – auch nicht besitzen.

Inselraus: Die Windows-Optik von Swing mit JGoodies Looks verbessern

Zwar bemüht sich das JDK bestmöglich das Windows-Look-and-Feel zu emulieren, doch das gelingt nicht an allen Stellen. Selbst bei dem aktuellen JDK ist das noch nicht perfekt, wobei die Implementierung unter Windows auf den nativen Windows-Renderer zurückgreift. Und kleine Verbesserungen lassen sich auch nicht so einfach publizieren, da die Release-Zyklen vom JDK lang sind. In diesem Fall hilft JGoodies Looks (http://www.jgoodies.com/freeware/libraries/looks/), ein freies Look-and-Feel unter der BSD-Lizenz. Das Ziel von Looks ist die perfekte Nachbildung des Aussehens für alle Windows-Versionen. Gegenüber dem Windows-Look-and-Feel vom JDK korrigiert es viele Feinheiten, wie passende Insets bei Eingabefeldern, Icons, Farben, Rahmen, den richtigen Font, Menüeigenschaften, Auflösungen von 96 und 120 dpi und vieles mehr. Gesetzt wird Looks wie jedes andere LAF:

try {
   String laf = Options.getSystemLookAndFeelClassName();
   UIManager.setLookAndFeel( laf );
 } catch ( Exception e ) {
   System.err.println( "L&F lässt sich nicht initialisieren: " + e );
 }

Inselraus: Die Zusatzkomponentenbibliothek SwingX

Für ernsthafte grafische Oberflächen mit Swing sind die Standardkomponenten der Java SE etwas mau. Wichtige Komponenten wie ein Datumsauswahldialog fehlen, zum Teil sind die angebotenen Komponenten nicht leistungsfähig genug. So kann beispielsweise die Swing-Textkomponente zwar einfaches HTML darstellen, scheitert aber an komplexeren Dokumenten mit CSS. Da die Java Foundation Classes in den letzten Jahren sehr stabil geblieben sind – es gab keine großen Änderungen oder nennenswerte neue Komponenten –, bleibt Entwicklern nichts anderes übrig, als entweder

  • die benötigten Swing-Komponenten selbst zu entwickeln oder
  • sich im Open-Source-Umfeld umzuschauen bzw.
  • eine kommerzielle Swing-Komponentenbibliothek einzukaufen.

Wie üblich ist die Eigenentwicklung immer nur die letzte Option. Im Laufe der letzten Jahre haben sich einige sehr gute quelloffene Swing-Zusätze etabliert. Hervorzuheben ist SwingX, ein Projekt, von dem lange Zeit gedacht wurde, dass die Komponenten in das javax.swing-Paket aufgenommen würden. Das ist aber bisher nicht passiert, nur eine Komponente hat es – wen auch abgewandelt – in die Standardbibliothek geschafft.

Hinweis: Aktiv wird SwingX nicht mehr weiterentwickelt.

Im Angebot: Erweiterte und neue Swing-Komponenten

Die SwingX-Bibliothek erweitert Swing auf zwei Arten:

  • Einige SwingX-Komponenten erweitern die Standard-Swing-Komponenten und fügen Funktionalität hinzu. So ist JXFrame eine Unterklasse von JFrame und kann zum Beispiel eine Statuszeile haben. Oder JXTable ist eine Unterklasse von JTable mit zusätzlicher Suchfunktionalität und dem Vorteil, dass sich Spalten automatisch ein-/ausblenden lassen.
  • SwingX definiert ganz neue Swing-Komponenten, etwa eine Mischung aus Tabelle und Baum (JXTreeTable), ausfaltbare Container oder einen Login-Dialog.

Um einen ersten Überblick über SwingX zu bekommen, lohnt es sich, einen Blick auf das SwingX-Demo zu werfen. Auf der SwingX-Homepage http://swingx.java.net/ wird auf die Web-Startapplikation verlinkt (http://swinglabs-demos.java.net/demos/swingxset6/swingxset.jnlp).

Überblick über erweiterte Standard-Swing-Klassen

SwingX nimmt einige Klassen als Basisklassen und fügt Funktionalität hinzu. Die Standardkomponenten lassen sich problemlos gegen die neuen Klassen austauschen.

SwingX-Klasse Swing-Basisklasse Aufgabe
JXButton JButton Schaltfläche mit Unterstützung für Painter
JXLabel JLabel Beschriftung mit Unterstützung für Painter
JXTable JTable stark erweiterte Tabelle mit Suche, Sortierung, Filterung, …
JXList JList stark erweiterte Liste mit Suche, Sortierung, Filterung, …
JXTree JTree stark erweiterter Baum mit Suche, Filterung, …
JXFrame JFrame Fenster mit optionaler Statuszeile, Warte-zustand, …
JXDialog JDialog Vereinfacht den Aufbau von Dialogen.
JXPanel JPanel Panel mit Transparenz und Painter
JXEditorPane JEditorPane verbesserte Editor-Funktionalität, Undo/Redo und Suche
JXComboBox JComboBox Verbessert den Edit-Modus beim Einsatz in einer Tabelle.
JXFormattedTextField JFormattedTextField Unterstützt Prompts und Buddies in formatierten Textzeilen.
JXTextArea JTextArea Unterstützt Prompts und Buddies in Text-bereichen.
JXTextField JTextField Unterstützt Prompts und Buddies in Text-zeilen.
JXRootPanel JRootPanel Wurzelkomponente mit Symbolleiste und Statuszeile

Erweiterte Standard-Swing-Klassen

Die Klassen JXTable, JXList, JXTreeTable sind sehr leistungsfähig und daher mit einem kleinen Satz kaum zu beschreiben. Die API-Dokumentation gibt mehr Informationen. Wichtig sind dabei Klassen aus dem Paket org.jdesktop.swingx.decorator, die für JXTable, JXTreeTable, JXTree und JXList Sortierung, Filterung und Hervorhebung unterstützen.

In den Zellen tauchen zudem die Begriffe Painter, Prompt und Buddy auf:

  • Ein Painter ist ein Objekt, das an die SwingX-Komponente gesetzt wird. Zum Zeichnen greift die SwingX-Klasse auf das externe Zeichenobjekt zurück. Der Vorteil ist, dass so keine Unterklassen nötig sind, etwa von JButton oder JLabel, nur wenn in den Zeichenprozess eingegriffen werden soll.
  • Prompts und Buddies zeigen bei JXTextField, JXFormattedTextField und JXTextArea Eingabehinweise oder Fehler.

Neue Swing-Klassen

Zusätzlich zu den Erweiterungen der Standard-Swing-Komponenten bietet SwingX weitere Komponenten. Manche sind dringend nötig, etwa zur Auswahl eines Datums, einige sind sehr speziell, etwa zum Zeichnen eines 2D-Graphen.

SwingX-Klasse Swing-Basisklasse Aufgabe
JXDatePicker JComponent Datumseingabe im Textfeld mit Popup-Menü
JXHyperlink JButton Hyperlink
JXBusyLabel JLabel Animation für Ladeprozesse
JXTreeTable JXTable Verschmilzt Tabelle und Baum.
JXStatusBar JComponent Statuszeile, in der Regel unten am Fenster
JXErrorPane JComponent Darstellung von Fehlermeldungen
JXMultiThumbSlider JComponent Slider mit zwei Knöpfen
JXMonthView JComponent Zeigt Monatskalender und erlaubt Monats-auswahl.
JXSearchField JXTextField Textfeld mit kleinem Such-Icon
JXRadioGroup JPanel Vereinfacht die ButtonGroup.
JXMultiSplitPane JPanel geschachtelte Split-Panels
JXCollapsiblePane JXPanel animierte ausfaltbare Panels
JXTaskPane JXPanel Titel mit Icon zum Ausfalten der Container-elemente
JXTaskPaneContainer JXPanel Container für mehrere JXTaskPanes
JXTipOfTheDay JXPanel Gibt den „Tipp des Tages“ aus.
JXTitledPanel JXPanel Panel mit Überschrift
JXTitledSeparator JXPanel Trennlinie mit Text
JXImageView JXPanel Zeigt ein Bild, ein neues kommt mit Drag & Drop.
JXColorSelectionButton JButton Zeigt bei Aktivierung einen Farbauswahldialog.
JXGradientChooser JXPanel Panel zum Aufbau eines Farbverlaufs (-Gradient)
JXGraph JXPanel Zeichnet 2D-Graphen.

Neue Klassen, die SwingX bietet

Zur Suche gehören noch ein paar andere Komponenten, die nicht aufgeführt sind. Für die JXTable gibt es weiterhin JXTableHeader, was eine Spezialisierung von JTableHeader ist.

Weitere SwingX-Klassen

Des Weiteren gibt es ein paar neue Layoutmanager, etwa HorizontalLayout, VerticalLayout oder StackLayout (setzt alle Komponenten aufeinander). SwingX unterstützt ebenfalls die Tastaturvervollständigung bei Textfeldern und der Combo-Box. Beispiele finden sich im Paket org.jdesktop.swingx.autocomplete.

SwingX-Installation

Wer SwingX einsetzen möchte, der lädt von http://java.net/downloads/swingx/releases/ das Java-Archiv swingx-all-1.6.4.jar (etwa 1,4 MiB) herunter und bindet es in den Klassenpfad ein. Auf den Seiten sind auch die Quellen und die Java-API-Dokumentation verlinkt. Nach dem Einbinden stehen die neuen Klassen unter dem Paket org.jdesktop.swingx zur Verfügung.

GUI-Builder für JavaFX und Swing

Mit einem GUI-Builder lassen sich grafische Oberflächen über ein grafisches Werkzeug einfach aufbauen. In der Regel bietet ein GUI-Bilder eine Zeichenfläche und eine Symbolleiste mit Komponenten, die per Drag and Drop angeordnet werden. Zentral bei dem Ansatz ist das WYSIWYG-Prinzip (What You See Is What You Get), dass nämlich im Designschritt schon abzulesen ist, wie die fertige Oberfläche aussieht.

Ein GUI-Builder erzeugt eine Repräsentation der grafischen Oberfläche, die im Prinzip auch von Hand zu erstellen wäre – allerdings ist der Aufwand sehr groß und für jeden nachvollziehbar, der schon einmal in HMTL eine neue Tabellenspalte hinzugeführt hat. Es gibt immer wieder Diskussion über das Für und Wider doch ist es wie mit allen Tools: richtig eingesetzt kann ein GUI-Builder viel Arbeit sparen.

GUI-Builder für JavaFX

Der JavaFX Scene Builder ist ein Werkzeug von Oracle und nennt sich selbst „A Visual Layout Tool for JavaFX Applications“. Er ist kein Teil vom JDK, sondern muss unter http://www.oracle.com/technetwork/java/javafx/tools/index.html bezogen und installiert werden. Danach stehen komfortable Werkzeuge zum Entwurf von Oberflächen und deren Verschönerung mit CSS nichts im Wege.

NetBeans bietet von Haus aus Unterstützung im Entwurf grafischer Oberflächen, denn ein GUI-Bilder ist integriert, und eine Zusatzinstallation ist nicht nötig. Das gibt uns direkte Möglichkeiten, Swing und auch JavaFX spielerisch zu erfahren.[1]

Für Eclipse gibt es keinen speziellen GUI-Builder, das ist auch eigentlich gar nicht nötig, denn der Scene Builder kann in Eclipse integriert werden.[2] Zwar kein direkter WYSIWYG-Editor, aber immerhin ein Werkzeug im Umgang mit XML ist das quelloffenes Eclipse-Plugin e(fx)clipse unter http://efxclipse.org/.

GUI-Builder für Swing

Während NetBeans für Swing gute Unterstützung mitbringt, ist bei Eclipse standardmäßig kein GUI-Builder integriert. Es gilt also, ein Plugin nachzuinstallieren. In den letzten Jahren kamen und gingen verschiedene GUI-Builder, aber letztendlich hat sich der WindowsBuilder (https://developers.google.com/java-dev-tools/wbpro/) von Google als De-facto-Standard etabliert. Über den Update-Mechanismus von Eclipse wird er installiert. Eine Installationsanleitung findet sich auf der Webseite. Neben Swing nimmt der WindowsBuilder gleich noch GWT, SWT und XWT (Eclipse XML Window Toolkit) mit.


[1] Didaktiker nennen das »exploratives Lernen«.

[2] http://docs.oracle.com/javafx/scenebuilder/1/use_java_ides/sb-with-eclipse.htm

Swing-Beschriftungen eine andere Sprache geben

Die Swing-Komponenten sind von Haus aus lokalisiert. So begegnet dem Anwender ein Dateiauswahldialog unter dem deutschen System auch mit deutschen Beschriftungen. Die Sprache lässt sie über das passende Locale-Objekt ändern. So setzt für neue Komponenten folgendes die Sprache auf Arabisch:

JComponent.setDefaultLocale( new Locale("ar") );

Mit der Änderung auf eine Sprache, die von rechts nach links schreibt, ist automatisch eine Umsortierung der Komponenten verbunden, wenn diese zum Beispiel in einem Container mit FlowLayout liegen.

Ändern der Zeichenfolgen

Die Zeichenketten selbst werden aus einer Ressourcen-Datei gelesen und sind im UIManager präsent.

Beispiel: Um die deutsche Beschriftung »Suchen in:« im Dateiauswahldialog zu ersetzen, ist die Eigenschaft FileChooser.lookInLabelText zu setzen:

UIManager.put( "FileChooser.lookInLabelText" ,"Worin'e suchen tust:" );
new JFileChooser().showOpenDialog( null );
System.exit( 0 );

Eine Liste aller zu setzenden Properties ist etwa auf der Webseitehttp://www.rgagnon.com/javadetails/JavaUIDefaults.txt aufgelistet.

Um für alle Programme die Änderungen gültig zu machen, sollten für das verwendete Look and Feel.properties-Dateien angelegt werden. Die Dateien müssen dann im Programmpfad stehen. So sehen die Dateipfade für Windows und Metal folgendermaßen aus:

  • com/sun/java/swing/plaf/windows/resources/windows_de.properties
  • javax/swing/plaf/metal/resources/metal_de.properties

Die Dateien sind die bekannten Ressourcen-Dateien mit den Schlüsseln, die in JavaUIDefaults.txtgenannt sind.

Beispiel: Um unter dem Windows-Look and Feel global einen Bezeichner für den Dateiauswahldialog zu ändern, setzen wir in windows_de.properties:

FileChooser.lookInLabelText=Worin'e suchen tust:

JMapViewer, einfache Komponenten für OpenStreepMap Karte

Siehe http://wiki.openstreetmap.org/wiki/JMapViewer.

  • Provides integrated zoom controls (slider and buttons) in the left upper corner (can be hidden)
  • Switch between different tile sources: Mapnik, Tiles@Home, Cyclemap, … (other tiled maps can be used, too)
  • Configurable in-memory and file-based caching of loaded map tiles
  • A list of map markers (the yellow circles in the screenshot) can be added. Map markers of different shape can be easily added by implementing the MapMarker interface.
  • Configurable/Extentable/Replaceable controller (code part that manages mouse interaction and how the map reacts to it)
  • Requirement: Java 1.6
  • License: GPL

 

JMapViewer demo app screenshot

Neues Release von JFreeChart 1.0.15

http://www.jroller.com/dgilbert/entry/jfreechart_1_0_151. Zu den Updates:

This release contains support for non-visible series in XYBarRenderer, minor gridlines in PolarPlot, new legend item ordering options, chart editor enhancements, updates to StandardDialScale, localisation files for Japanese, refactored parameter checks and a fix for a minor security flaw in the DisplayChart class, detected and reported by OSI Security: http://www.osisecurity.com.au/advisories/jfreechart-path-disclosure.

 

Inselraus: JScrollPane Viewport und Scrollable

Der Viewport

Den sichtbaren Ausschnitt der Fläche bestimmt ein JViewport-Objekt, das mit zusätzlichen Listenern etwa für die Änderungen des sichtbaren Bereichs ausgestattet werden kann. Die Methode getViewport() liefert das JViewport-Objekt. Die Methoden scrollRectToVisible(Rectangle) und setViewPosition(Point) des JViewport-Objekts ermöglichen die Ansteuerung des sichtbaren Bereichs.

Beispiel: Zeige den sichtbaren Bereich auf dem Bildschirm an:

System.out.println( scrollPane.getViewport().getVisibleRect() );

Die JViewport-Methode getVisibleRect() stammt aus der direkten Oberklasse JComponent. Sie liefert ein Rectangle-Objekt, und getLocation() liefert den java.awt.Point vom Rechteck oben links.

Jeweils auf die gegenüberliegende Seite der Rollbalken lassen sich Zeilen- und Spaltenleisten legen, genauso wie in alle vier Ecken Komponenten. Die Leisten liegen selbst wiederum in einem JViewport, um zum Beispiel im Fall einer Linealbeschriftung mitzuwandern. setRowHeaderView(), setColumnHeaderView() und setCorner() setzen bei der JScrollPane die Ecken und Leisten.

Die Schnittstelle Scrollable

Die komplexen Komponenten, wie etwa Textanzeigefelder, Bäume oder Tabellen, implementieren eine Verschiebefähigkeit nicht selbst, sondern müssen dazu in einer JScrollPane Platz nehmen. Damit JScrollPane jedoch weiß, wie zum Beispiel nach einem Klick auf den Bildlauf der Ausschnitt zu verändern ist, implementieren die Komponenten die Schnittstelle Scrollable. Die zentralen Klassen JList, JTable, JTextComponent und JTree implementieren die Schnittstelle und teilen auf diese Weise Maße der Komponente und Anzahl der Pixel bei einer Verschiebung mit, wenn etwa der Anwender den Rollbalken um eine Position versetzt.

Feature: Eine sinnvolle Eigenschaft ist der automatische Bildlauf. Bei diesem Verfahren wird der Bildlauf auch dann fortgesetzt, wenn der Mauszeiger die Komponente schon verlassen hat. Klickt der Benutzer etwa auf ein Element in einer Liste, und bewegt dann den Mauszeiger mit gedrückter Maustaste aus der Liste heraus, so scrollt die Liste mit eingeschaltetem automatischen Bildlauf selbstständig weiter. Die Eigenschaft wird in Swing über die JComponent-Methode setAutoScroll(boolean) gesteuert.

Inselraus: Bilder auf AbstractButton-basierten Swing-Schaltflächen je nach Zustand ändern

Die Integration mit den Icon-Objekten liegt in der AbstractButton-Klasse. Geben wir im Konstruktor das Icon nicht an, so lässt sich dies immer noch über setIcon(…) nachträglich setzen und ändern. Wenn die Schaltfläche angeklickt wird, kann ein anderes Bild erscheinen. Dieses Icon setzt setPressedIcon(…). Bewegen wir uns über die Schaltfläche, lässt sich auch ein anderes Icon setzen. Dazu dient die Methode setRolloverIcon(…). Die Fähigkeit muss aber erst mit setRolloverEnabled(true) eingeschaltet werden. Beide Eigenschaften lassen sich auch zu einem Icon kombinieren, das erscheint, wenn die Maus über dem Bild ist und eine Selektion gemacht wird. Dazu dient setRolloverSelectedIcon(…). Für JToggleButton-Objekte ist eine weitere Methode wichtig, denn ein JToggleButton hat zwei Zustände: einen selektierten und einen nicht selektierten. Auch hier können zwei Icon-Objekte zugeordnet werden, und das Icon der Selektion lässt sich mit setSelectedIcon(…) setzen. Ist die Schaltfläche ausgegraut, ist auch hier ein gesondertes Icon möglich. Es wird mit setDisabledIcon(…) gesetzt. Dazu passt setDisabledSelectedIcon(…).

Inselraus: Die Schnittstelle Icon und eigene Icons zeichnen

Bei einer genauen Betrachtung fällt auf, dass ImageIcon eine Implementierung der Schnittstelle Icon ist und dass die JLabel-Klasse ein Icon-Objekt erwartet und nicht speziell ein Argument vom Typ ImageIcon. Das heißt aber: Wir können auch eigene Icon-Objekte zeichnen. Dazu müssen wir nur drei spezielle Methoden von Icon implementieren: die Methode paintIcon(…) und ferner zwei Methoden, die die Dimensionen angeben.

interface interface javax.swing.Icon

  • int getIconWidth()
    Liefert die feste Breite eines Icons.
  • int getIconHeight()
    Liefert die feste Höhe eines Icons.
  • void paintIcon(Component c, Graphics g, int x, int y)
    Zeichnet das Icon an die angegebene Position. Der Parameter Component wird häufig nicht benutzt. Er kann jedoch eingesetzt werden, wenn weitere Informationen beim Zeichnen bekannt sein müssen, wie etwa die Vorder- und Hintergrundfarbe oder der Zeichensatz.

Die folgende Klasse zeigt die Verwendung der Icon-Schnittstelle. Das eigene Icon soll einen einfachen roten Kreis mit den Ausmaßen 20 × 20 Pixel besitzen:

class CircleIcon implements Icon {

  @Override public void paintIcon( Component c, Graphics g, int x, int y )  {
    g.setColor( Color.red );
    g.fillOval( x, y, getIconWidth(), getIconHeight() );
  }

  @Override public int getIconWidth() {
    return 20;
  }

  @Override public int getIconHeight() {
    return 20;
  }
}

Wir überschreiben die drei erforderlichen Methoden, sodass ein Icon-Objekt der Größe 20 × 20 Pixel entsteht. Als Grafik erzeugen wir einen gefüllten roten Kreis. Dieser kann als Stopp-Schaltfläche verwendet werden, ohne dass wir eine spezielle Grafik verwenden müssen. Für die Grafik stehen uns demnach 400 Pixel zur Verfügung – genau getIconWidth() mal getIconHeight() –, und alle nicht gefüllten Punkte liegen transparent auf dem Hintergrund. Dies ist auch typisch für leichtgewichtige Komponenten. Über das Component-Objekt können wir weitere Informationen herausholen, wie etwa den aktuellen Zeichensatz oder die darstellende Vaterkomponente.

Um das eigene Icon auf ein JLabel zu setzen, lässt sich die Icon-Implementierung entweder im Konstruktor übergeben oder später über setIcon(Icon):

JLabel circle = new JLabel( new CircleIcon() );

Was die Typen Icon und Image verbindet

Vielleicht wird der eine oder andere sich schon überlegt haben, ob nun ImageIcon eine ganz eigene Implementierung neben der Image-Klasse ist oder ob beide miteinander verwandt sind. Das Geheimnis ist, dass ImageIcon die Icon-Schnittstelle implementiert, aber auch ImageIcon intern die Image-Klasse nutzt. Sehen wir uns das einmal im Detail an: Ein ImageIcon ist serialisierbar. Also implementiert es die Schnittstelle Serializable. Im Konstruktor kann ein URL-Objekt oder ein String mit einer URL stehen. Hier wird einfach getImage(…) vom Toolkit aufgerufen, um sich eine Referenz auf das Image-Objekt zu holen. Eine geschützte Methode loadImage(Image) wartet nun mithilfe eines MediaTrackers auf das Bild. Nachdem dieser auf das Bild gewartet hat, setzt er die Höhe und Breite, die sich dann über die Icon-Methoden abfragen lassen. Doch ein richtiges Icon muss auch paintIcon(…) implementieren. Hier verbirgt sich nur die drawImage(…)-Methode.

Kommen wir noch einmal auf die Serialisierbarkeit der ImageIcon-Objekte zurück. Die Klasse implementiert dazu die privaten Methoden readObject(…) und writeObject(…). Der Dateiaufbau ist sehr einfach. Breite und Höhe befinden sich im Datenstrom, und anschließend existiert ein Integer-Feld mit den Pixelwerten. In readObject(…) liest s.defaultReadObject() – wobei s der aktuelle ObjectInputStream ist – das Feld wieder ein, und über die Toolkit-Methode createImage(…) wird die Klasse MemoryImageSource genutzt, um das Feld wieder zu einem Image-Objekt zu konvertieren. Umgekehrt ist es genauso einfach. writeObject(…) schreibt die Breite und Höhe und anschließend das Ganzzahl-Feld mit den Farbinformationen, das es über einen PixelGrabber bekommen hat.

Inselraus: Zeichensatz eines Swing-Labels ändern

Der gesetzte Text wird im zugewiesenen Zeichensatz des Swing-Look-and-Feels angezeigt. Um diesen zu ändern, müssen wir ein neues Font-Objekt erzeugen. Auf zwei Arten lässt sich dieser Font setzen: global für alle JLabel-Elemente oder lokal nur für dieses eine. Die erste Lösung arbeitet über das UIDefaults-Objekt, das die Einstellungen wie Zeichensätze und Farben für alle Swing-Elemente verwaltet:

UIDefaults uiDefaults = UIManager.getDefaults();
uiDefaults.put( "Label.font",
                ((Font)uiDefaults.get("Label.font")).deriveFont(30f) );

Unter dem Schlüssel »Label.font« legen wir ein neues Font-Objekt ab und überschreiben die alte Definition. Den neuen Font mit der Größe 30 leiten wir mit deriveFont(float) vom alten ab, sodass wir den Zeichensatz »erben«.

Die zweite Lösung kann darin bestehen, den Font direkt mit der setFont(Font)-Methode zu setzen:

JLabel l = new JLabel( "Lebe immer First-Class, sonst tun es deine Erben!" );
l.setFont( new Font("Serif", Font.BOLD, 30) );

Einen speziellen Konstruktor, der ein Font-Objekt als Argument annimmt und dieses verwendet, gibt es nicht.

Inselraus: Dynamisches Layout während einer Größenänderung

Wird ein Fenster vergrößert, dann kann während der Größenänderung der Inhalt sofort neu ausgerichtet und gezeichnet werden oder auch nicht. Wird er nicht dynamisch angepasst, dann sieht der Benutzer diese Anpassung erst nach dem Loslassen der Maus, wenn die Größenänderung abgeschlossen wurde. Dieses dynamische Vergrößern lässt sich im Toolkit-Objekt einstellen, und zwar über Toolkit.getDefaultToolkit().setDynamicLayout(true). Nicht jedes Toolkit unterstützt allerdings diese Fähigkeit! Ob es das tut, verrät Toolkit.getDefaultToolkit().getDesktopProperty("awt.dynamicLayoutSupported").

Inselraus: Selektionen einer Tabelle

In einer JTable können auf unterschiedliche Art und Weise Zellen selektiert werden: zum einen nur in einer Zeile oder Spalte, zum anderen kann auch ein ganzer Block oder können auch beliebige Zellen selektiert werden. Die Art der Selektion bestimmen Konstanten in ListSelectionModel. So wird SINGLE_SELECTION nur die Selektion einer einzigen Zelle zulassen.

Beispiel: In einer JTable soll entweder ein ununterbrochener Block Zeilen oder Spalten ausgewählt werden dürfen:

table.setSelectionMode( ListSelectionModel.SINGLE_INTERVAL_SELECTION );

Mit Methoden lassen sich im Programm alle Elemente einer Spalte oder Zeile selektieren. Die Selektier-Erlaubnis geben zunächst zwei Methoden:

  • table.setColumnSelectionAllowed( boolean )
  • table.setRowSelectionAllowed( boolean )

Die automatische Selektion von Spalten gelingt mit der JTable-Methode setColumnSelectionInterval(int, int), weitere Bereiche lassen sich mit addColumnSelectionInterval(int, int) hinzufügen und mit removeColumnSelectionInterval(int, int) entfernen. Das Gleiche gilt für die Methoden, die »Row« im Methodennamen tragen.

Beispiel: Selektiere in einer JTable table die Spalte 0 komplett:

table.setSelectionMode( ListSelectionModel.MULTIPLE_INTERVAL_SELECTION );
table.setColumnSelectionAllowed( true );
table.setRowSelectionAllowed( false );
table.setColumnSelectionInterval( 0, 0 );

Selektiere in einer Tabelle nur die Zelle 38, 5:

table.setSelectionMode( ListSelectionModel.SINGLE_SELECTION  );
table.setColumnSelectionAllowed( true );
table.setRowSelectionAllowed( true );
table.changeSelection( 38, 5, false, false );

Als Selektionsmodus reicht SINGLE_SELECTION aus, MULTIPLE_INTERVAL_SELECTION wäre aber auch in Ordnung. Beide Selektionen sind zusammen in der Form nicht möglich. Bei einer Einzelselektion wird die Zelle nur umrandet, aber nicht wie beim Standard-Metal-Look-and-Feel blau ausgefüllt.

Die Methode selectAll() selektiert alle Elemente, und clearSelection() löscht alle Selektionen.

Inselraus: Tabellenkopf von Swing-Tabellen

Diese Verschiebung von Spalten kann über das Programm erfolgen (mit moveColumn(int column, int targetColumn) von JTable) oder über den Benutzer per Drag & Drop. JTable delegiert diese Methoden an ein spezielles TableColumnModel und implementiert den Code nicht selbst.

Der Kopf (engl. header) einer JTable ist ein JTableHeader-Objekt, das von der JTable mit getTableHeader() erfragt werden kann. Dieses JTableHeader-Objekt ist für die Anordnung und Verschiebung der Spalten verantwortlich.

Beispiel: In der JTable table sollen die Spalten nicht mehr vom Benutzer verschoben werden können. Er soll auch die Breite nicht mehr ändern dürfen:

table.getTableHeader().setReorderingAllowed( false );
table.getTableHeader().setResizingAllowed( false );

Hier wird deutlich, dass ein JTableHeader die Steuerung der Ausgabe und der Benutzerinteraktion übernimmt, dass aber die Informationen selbst in TableColumn liegen.

Inselraus: Spalteninformationen von JTable-Tabellen

Alle Zelleninformationen der Tabelle stecken im Model einer JTable. Informationen über die Spalten stehen allerdings nicht im TableModel, sondern in Objekten vom Typ TableColumn. Jede Spalte bekommt ein eigenes TableColumn-Objekt, und eine Sammlung der Objekte bildet das TableColumnModel, das wie das TableModel ein Datencontainer der JTable ist.

Beispiel: Zähle alle TableColumn-Objekte einer JTable table auf.

for ( Enumeration<?> e = table.getColumnModel().getColumns(); e.hasMoreElements(); )
  System.out.println( (TableColumn)e.nextElement() );

Vom Spaltenmodell der Tabelle bezieht getColumns()eine Enumeration von TableColumn-Objekten. Soll ein ganz bestimmtes TableColumn-Objekt untersucht werden, kann auch die Methode getColumn(Object identifier) genutzt werden.

Liegt ein TableColumn-Objekt vor, lässt sich von diesem die aktuelle minimale und maximale Breite setzen.

Beispiel: Ändere die Breite der ersten Spalte auf 100 Pixel:

table.getColumnModel().getColumn( 0 ).setPreferredWidth( 100 );

AUTO_RESIZE

Verändert der Anwender die Breite einer Spalte, ändert er entweder die Gesamtbreite einer Tabelle, oder er ändert automatisch die Breite der anderen Spalten, um die Gesamtbreite nicht zu verändern. Hier gibt es für die JTable unterschiedliche Möglichkeiten, die eine Methode setAutoResizeMode(int mode) bestimmt. Erlaubte Modi sind Konstanten aus JTable und AUTO_RESIZE_OFF, AUTO_RESIZE_NEXT_COLUMN, AUTO_RESIZE_SUBSEQUENT_COLUMNS, AUTO_RESIZE_ LAST_COLUMN, AUTO_RESIZE_ALL_COLUMNS. Sinnvoll sind drei von ihnen:

  • AUTO_RESIZE_SUBSEQUENT_COLUMNS: Der Standard. Verändert gleichmäßig die Breiten aller rechts liegenden Spalten.
  • AUTO_RESIZE_NEXT_COLUMN: Ändert nur die Breite der nachfolgenden Spalte.
  • AUTO_RESIZE_OFF: Ändert die Größe der gesamten Tabelle. Ist nur sinnvoll, wenn die JTable in einer JScrollPane liegt.

Inselraus: Größe und Umrandung von JTable-Zellen

Jede Zelle hat eine bestimmte Größe, die durch den Zellinhalt vorgegeben ist. Zusätzlich liegt zwischen zwei Zellen immer etwas Freiraum. Dieser lässt sich mit getIntercellSpacing() erfragen und mit setIntercellSpacing(…) setzen:

 table.setIntercellSpacing( new Dimension(gapWidth, gapHeight) ); 

Soll die Zelle rechts und links zum Beispiel 2 Pixel Freiraum bekommen, ist gapWidth auf 4 zu setzen, denn das Dimension-Objekt gibt immer den gesamten vertikalen und horizontalen Abstand zwischen den Zellen an.

Die Gesamtgröße einer Zelle ist dann die der Margin-Zeile + Zellhöhe beziehungsweise Margin-Spalte + Zellbreite. Da jedoch setIntercellSpacing(…) die Höhe einer Zeile nicht automatisch anpasst, muss sie ausdrücklich gesetzt werden:

 table.setRowHeight( table.getRowHeight() + gapHeight ); 

Zusätzlich zur Margin erhöht eine Linie den Abstand zwischen den Zellen. Auch dieses Raster (engl. grid) lässt sich modifizieren:

Code

Funktion

table.setShowGrid( false );

Schaltet die Umrandung aus.

table.setShowGrid( false );
table.setShowVerticalLines( true );

Zeigt nur vertikale Linien.

table.setGridColor( Color.GRAY );

Die Umrandung wird grau.

Rastermodifizierung

Inselraus: Swing/Auswahlmenü mit Separator

Standardmäßig unterstützt die JList und auch JComboBox keine Separatoren, doch die Unterteilung in Segmente ist in der Praxis sehr nützlich. Microsoft Word und PowerPoint benutzen sie zum Beispiel, um die zuletzt vom Benutzer ausgewählten Zeichensätze prominent oben in der Liste zu haben (Excel dagegen tut dies nicht).

Abbildung

Abbildung 9.42: Separator in Words Zeichensatzauswahlliste

Wir wollen diese Möglichkeit nachbilden und dabei noch einiges über Modelle und Renderer lernen.

Abbildung

Abbildung 9.43: Eine eigene Variante mit JSeparator in JComboBox

Bei der Umsetzung gibt es unterschiedliche Varianten, die sich außer in der technischen Implementierung darin unterscheiden, ob das Modell eine Markierung für den Separator enthält oder nicht. Wir stellen beide Ansätze vor und beginnen mit der ersten Variante, also damit, einem Zellen-Renderer Positionen mitzugeben, die sagen, wo ein Trennstrich zu zeichnen ist.

package com.tutego.insel.ui.list;

import javax.swing.*;

public class JComboBoxWithSeparator1 {
  public static void main( String[] args ) throws Exception {
    UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
    JFrame frame = new JFrame();
    frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
    String[] items = { "Cambria", "Arial", "Verdana", "Times" };
    JComboBox<String> comboBox = new JComboBox<>( items );
    ListCellRenderer<String> renderer = new SeparatorAwareListCellRenderer1<>(
                                          comboBox.getRenderer(), 0 );
    comboBox.setRenderer( renderer );
    frame.add( comboBox );
    frame.pack();
    frame.setVisible( true );
  }
}

Die eigene Klasse SeparatorAwareListCellRenderer1 ist ein ListCellRenderer, den die JComboBox zur Darstellung der Komponenten nutzt. Im Konstruktor des Renderers geben wir den Original-Renderer mit – es kann ein bestimmter Renderer schon vorinstalliert sein, den wollen wir dekorieren – sowie eine variable Argumentliste von Positionen. Das Beispiel übergibt nur 0, da nach dem ersten Element (Index = 0) ein Trennzeichen zu setzen sein soll, sodass Cambria und Arial abgetrennt sind.

package com.tutego.insel.ui.list;

import java.awt.*;
import java.util.Arrays;

import javax.swing.*;

public class SeparatorAwareListCellRenderer1<E> implements ListCellRenderer<E>
{
  private final ListCellRenderer<? super E> delegate;
  private final int[] indexes;
  private final JPanel panel = new JPanel( new BorderLayout() );

  public SeparatorAwareListCellRenderer1( ListCellRenderer<? super E> delegate,
                                          int... indexes )
  {
    Arrays.sort( indexes );
    this.delegate = delegate;
    this.indexes  = indexes;
  }

  @Override
  public Component getListCellRendererComponent( JList<? extends E> list, E value,
                                                 int index, boolean isSelected,
                                                 boolean cellHasFocus )
  {
    panel.removeAll();
    panel.add( delegate.getListCellRendererComponent( list, value, index,
                                                      isSelected, cellHasFocus ) );
    if ( Arrays.binarySearch( indexes, index ) >= 0 )
      panel.add( new JSeparator(), BorderLayout.PAGE_END );

    return panel;
  }
}

Die Implementierung basiert auf der Idee, jede Komponente in einen Container (JPanel) zu setzen und in diesem Container dann je nach Bedarf ein JSeparatorunten an diesem Container anzufügen. Statt JPanel mit einem JSeparator auszustatten, kann ebenfalls auch ein Border unten gezeichnet werden. Die AnweisungArrays.binarySearch(indexes, index) >= 0 ist als »contains« zu verstehen, also als ein Test, ob der Index im Feld ist – leider gibt es so eine Methode nicht in der Java API. Wenn der Index im Feld ist, soll der Separator unter der Komponente erscheinen. Dass sich eine Trennlinie auch am Anfang befinden kann, berücksichtigt die Lösung nicht; diese Variante bleibt als Übungsaufgabe für die Leser.

Diese Lösung ist einfach und funktioniert gut, denn vorhandene Renderer werden weiterverwendet, was sehr wichtig ist, denn größere Swing-Anwendungen nutzen viele eigene Renderer, etwa um Icons und Text zusammenzufassen. Ein Nachteil ist, dass der Separator zu einen Element gehört, und wenn das Element etwa in der Liste ausgewählt wird, steht der Separator mit in der Selektion und ist nicht abgetrennt (das ist aber bei Word genauso).

Während die vorgestellte Variante in der Praxis gut funktioniert, wollen wir uns noch mit einer alternativen Umsetzung beschäftigen. Sie ist deutlich komplexer und auch nicht so flexibel. Die Lösung basiert auf der Idee, dass die Modelldaten eine Markierung für den Separator enthalten – die folgende Lösung nutzt null dafür. DaJList oder JComboBox eine null problemlos verträgt, ist die Basis schnell umgesetzt:

JFrame frame = new JFrame();
frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
String[] items = { "Cambria", null, "Arial", "Cambria", "Verdana", "Times" };
JComboBox<String> comboBox = new JComboBox<String>( items );
frame.add( comboBox );
frame.pack();
frame.setVisible( true );

Wenn wir das Programm starten, zeigt es dort, wo das Model eine null liefert, einfach nichts an. Die Selektion dieses null-Elements ist auch möglich und führt zu keiner Ausnahme.

Damit Swing das null-Element nicht als Leereintrag anzeigt, verpassen wir unserer JComboBox einen Zellen-Renderer. Der soll immer dann, wenn das Element nullist, ein Exemplar von JSeparator zeichnen. Vom Design ist es das beste, wenn der Zell-Renderer selbst die null-Elemente darstellt, aber das Zeichnen der Nicht-null-Elemente an einen anderen Zell-Renderer abgibt, anstatt diese etwa durch einen BasicComboBoxRenderer (ein JLabel, das ListCellRendererimplementiert) selbst zu renderen – das würde die Flexibilität der Lösung massiv einschränken.

package com.tutego.insel.ui.list;

import java.awt.Component;
import javax.swing.*;

public class SeparatorAwareListCellRenderer2<E> implements ListCellRenderer<E>
{
  private final ListCellRenderer<? super E> delegate;

  public SeparatorAwareListCellRenderer2( ListCellRenderer<? super E> delegate )
  {
    this.delegate = delegate;
  }

  @Override
  public Component getListCellRendererComponent( JList<? extends E> list, E value,
                                                 int index, boolean isSelected,
                                                 boolean cellHasFocus )
  {
    if ( value == null )
      return new JSeparator();

    return delegate.getListCellRendererComponent( list, value, index, isSelected, cellHasFocus );
  }
}

Die eigene Klasse SeparatorAwareListCellRenderer2 bekommt einen anderen ListCellRenderer übergeben und liefert die Komponenten dieses Delegate-Renderers immer dann, wenn das Element ungleich null ist. Ist es dagegen gleich null, liefert getListCellRendererComponent() ein Exemplar des JSeparators.

Im Beispielprogramm muss der Renderer nun angemeldet werden, und dazu ist Folgendes zu ergänzen:

comboBox.setRenderer(
  new SeparatorAwareListCellRenderer2<String>(comboBox.getRenderer()) );

Nach dem Start des Programms ist das Ergebnis schon viel besser: Dort, wo vorher die JComboBox eine leere Zeile darstellte, ist nun ein Strich.

Die Lösung ist jedoch nur ein Etappensieg, denn die Navigation mit der Tastatur durch die Liste zeigt eine Schwachstelle: Das null-Element lässt sich auswählen und erscheint auch als Linie im Editor/Textfeld. Beheben lässt sich das Problem nicht mit dem Renderer, denn der ist nur mit der Darstellung in der Liste beauftragt. Hier muss in die Interna der Swing-Komponente eingegriffen werden. Jede Swing-Komponente hat ein korrespondierendes UI-Delegate, das für das Verhalten und die Darstellung der Komponente verantwortlich ist. Für die JComboBox sind das Unterklassen von ComboBoxUI, und zwei Methoden sind besonders interessant:selectNextPossibleValue() und selectPreviousPossibleValue().

Da jede UI-Implementierung ihr eigenes Look and Feel (LAF) mitbringt, müssen wir hier eigentlich einen Dekorator bauen und jede Methode bis auf die beiden genannten an das Original weiterleiten, doch das ist jetzt zu viel Arbeit, und so nehmen wir die Basisklasse WindowsComboBoxUI als Basisklasse, denn unser Beispiel nutzt das Windows-LAF. In der Unterklasse implementieren wir eigene Versionen der Methoden selectNextPossibleValue() undselectPreviousPossibleValue(), die so lange die Liste nach oben/unten laufen müssen, bis sie ein Element ungleich null finden.

package com.tutego.insel.ui.list;

import com.sun.java.swing.plaf.windows.WindowsComboBoxUI;

public class SeparatorAwareComboBoxUI extends WindowsComboBoxUI
{
  @Override
  protected void selectNextPossibleValue()
  {
    for ( int index = comboBox.getSelectedIndex() + 1;
          index < comboBox.getItemCount();
          index++ )
      if ( comboBox.getItemAt( index ) != null )
      {
        comboBox.setSelectedIndex( index );
        break;
      }
  }

  @Override
  protected void selectPreviousPossibleValue()
  {
    for ( int index = comboBox.getSelectedIndex() - 1;
          index >= 0;
          index-- )
      if ( comboBox.getItemAt( index ) != null )
      {
        comboBox.setSelectedIndex( index );
        break;
      }
  }
}

Das UI-Objekt muss ebenfalls im main()-Programm angemeldet werden:

comboBox.setUI( new SeparatorAwareComboBoxUI() );

Enthält die Liste null-Elemente, überspringt die Tastennavigation über die Cursor-Taste diese. Doch auch mit diesem Teilstück fehlt ein weiteres Detail: Mit einem Klick lässt sich die Linie doch noch auswählen. Das ist keine Frage des Renderers und auch keine Frage der Tastaturnavigation – es muss untersagt werden, dass bei der Aktivierung ein null-Element zum Editor kommen kann. Hier ist die Methode setSelectedItem() von JComboBox entscheidend. Denn jedes Element, das selektiert wird – und dadurch auch in das Textfeld kommt –, geht durch die Methode hindurch. Wenn wir die Methode überschreiben und bei null-Elementen einfach nichts tun, wird auch das null-Element nicht selektiert, und im Textfeld bleibt das letzte Element stehen.

Damit auch spezielle Implementierungen von JComboBox von diesem Verhalten profitieren können, müssen wir wieder einen Dekorator schreiben, doch das kostet zu viel Mühe, und so überschreibt eine einfache Unterklasse die setSelectedItem()-Methode. (Im Prinzip wäre auch eine überschriebene Methode vonsetSelectedIndex() sinnvoll, denn das könnte eine programmierte Aktivierung von null vermeiden.)

package com.tutego.insel.ui.list;

import javax.swing.*;

public class SeparatorAwareJComboBox<E> extends JComboBox<E>
{
  @SafeVarargs
  public SeparatorAwareJComboBox( E... items )
  {
    super( items );
  }

  @Override
  public void setSelectedItem( Object anObject )
  {
    if ( anObject != null )
      super.setSelectedItem( anObject );
  }
}

Im Hauptprogramm muss nur diese spezielle Klasse zum Einsatz kommen und so folgt:

package com.tutego.insel.ui.list;

import javax.swing.*;

public class JComboBoxWithSeparator2
{
  public static void main( String[] args ) throws Exception
  {
    UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
//    UIManager.setLookAndFeel( new NimbusLookAndFeel() );

    JFrame frame = new JFrame();
    frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
    String[] items = { "Cambria", null, "Arial", "Verdana", "Times" };
    JComboBox<String> comboBox = new SeparatorAwareJComboBox<>( items );
    comboBox.setRenderer( new SeparatorAwareListCellRenderer2<>(comboBox.getRenderer()) );
    comboBox.setUI( new SeparatorAwareComboBoxUI() );
    comboBox.setSelectedIndex( 1 );
    frame.add( comboBox );
    frame.pack();
    frame.setVisible( true );
  }
}