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 benutzt sie zum Beispiel, um die zuletzt vom Benutzer ausgewählten Zeichensätze prominent oben in der Liste zu haben (Excel dagegen nicht).
Wir wollen diese Möglichkeit nachbilden, und dabei noch einiges über Modelle und Renderer lernen.
Bei der Umsetzung gibt es unterschiedliche Varianten, die sich neben 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, einem Zellen-Renderer Positionen mitzugeben, die sagen, wo ein Trennstrich zu zeichnen ist.
JFrame frame = new JFrame();
frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
String[] items = { "Cambria", "Arial", "Verdana", "Times" };
JComboBox<String> comboBox = new JComboBox<String>( items );
ListCellRenderer<String> renderer = new SeparatorAwareListCellRenderer1<String>(
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 Renderes geben wir den Original-Renderer mit – es kann ein bestimmter Renderer schon vorinstalliert sein, den wollen wir dekorieren – und ein 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;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
public Component getListCellRendererComponent( JList 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 Komponenten in einen Container (JPanel) zu setzen und in diesem Container dann je nach Bedarf ein JSeparator mit unten dran zu setzen. Statt dem JPanel mit einem JSeparator auszustatten kann ebenfalls auch ein Border unten gezeichnet werden. Die Anweisung Arrays.binarySearch(indexes, index) >= 0 ist als „contains“ zu verstehen, also 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, und 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ößeren 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 deutlicher 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. Da JList 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 );
Das Programm zum Start gebracht zeigt dort, wie 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 JCombBox einen Zellen-Renderer. Der soll immer dann, wenn das Element null ist, 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, satt diese etwas durch einen BasicComboBoxRenderer (ein JLabel was ListCellRenderer implementiert) 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;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
public Component getListCellRendererComponent( JList 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-Renders 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 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 für die Darstellung in der Liste beauftragt. Hier muss in die Interna der Swing-Komponente eingegriffen werden. Jede Swing-Komponente hat ein korrespondierende UI-Delegate, das für das Verhalten und 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 ihre eines Look & Feel mitbringt, müssen wir hier eigentlichen 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 unter Beispiel nutzt das Windows LaF. In der Unterklasse implementieren wir eigene Versionen der Methoden selectNextPossibleValue() und selectPreviousPossibleValue(), die so lange die Liste nach oben/unten laufen müssen, bis sie ein Element ungleich null finden.
Listing 1.34: com/tutego/insel/ui/list/SeparatorAwareComboBoxUI.java
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 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 weitere Detail: Mit feinem Klick lässt sich die Linie doch noch auswählen. Das ist keine Frage des Renderes und auch keine Frage der Tastaturnavigation – es muss untersagt werden, dass bei der Aktivierung ein null-Element in zum Editor kommen kann. Hier ist Methode setSelectedItem() von JComboBox entscheidend. Denn jedes Element was selektiert wird – und dadurch auch in der Textfeld kommt – geht durch die Methode durch. Wenn wir die Methode überschreiben, und bei null-Elemente einfach nichts tun, wird auch das null-Element nicht selektiert und im Textfeld bleibt das letzte Element.
Damit auch spezielle Implementierungen von JComboBox von diesem Verhalten profitieren könnten, 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 überschiebene Methoden von setSelectedIndex() sinnvoll, denn das könnte eine programmierte Aktivierung der null vermeiden.)
package com.tutego.insel.ui.list;
import javax.swing.*;
public class SeparatorAwareJComboBox<E> extends JComboBox<E>
{
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 zu Einsatz kommen:
JComboBox<String> comboBox = new SeparatorAwareJComboBox<String>( items );
Nutzt man das MS Windows SystemLookAndFeel, dann wird bei deinem Separator Beispiel der weiße Panel Hintergrund auch im nicht ausgeklappten Zustand gezeichnet. Dadurch fehlt der graue (bei MouseHover blaue) Farbverlauf. Durch ein
panel.setOpaque(false)
im Konstruktor wird’s wieder richtig gemalt.
Es gibt noch einige weitere Probleme in Verbindung mit der JGoodies Looks Bibliothek… die korrigiert da an den ComboBox(-Renderern) nämlich schon was und erfordert noch einen weiteren Workaround mit
((JLabel) listCellRendererComponent).setForeground( super.getForeground() );
((JLabel) listCellRendererComponent).setBackground( super.getBackground() );
((JLabel) listCellRendererComponent).setBorder( super.getBorder() );
((JLabel) listCellRendererComponent).setOpaque( isSelected );