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 );
  }
}

Über Christian Ullenboom

Ich bin Christian Ullenboom und Autor der Bücher ›Java ist auch eine Insel. Einführung, Ausbildung, Praxis‹ und ›Java SE 8 Standard-Bibliothek. Das Handbuch für Java-Entwickler‹. Seit 1997 berate ich Unternehmen im Einsatz von Java. Sun ernannte mich 2005 zum ›Java-Champion‹.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.