Java Getriebe

Java und NetBeans

Farbschemata in JList anzeigen

Für zwischendurch einmal eine kleine Swing-Spielerei. Ich habe in unserer Anwendung die Möglichkeit für verschiedene Darstellungen verschiedene Farbschemata auszuwählen. Meist sind in diese Darstellungen irgendwelche Verläufe darzustellen wie etwa in einer „Surface Map„. Damit der Anwender aus den gegebenen Schemata auswählen kann, werden alle bekannten Farblisten in einer Liste gemeinsam angezeigt, wie hier rechts auch in einem einfachen Beispiel 1 zu sehen ist.

Das Besondere an dieser Darstellung ist der kleine Farbbalken unterhalb des jeweiligen Namens. Normalerweise ist eine JList nicht in der Lage so etwas zu erstellen. Aber es ist auch nicht schwer, einer [javadoc]javax.swing.JList[/javadoc] diese Fähigkeit bei zu bringen. Jede JList (wie auch [javadoc]javax.swing.JTable[/javadoc] und [javadoc]javax.swing.JComboBox[/javadoc]) kann mit einem selbst definierten „Renderer“ ausgestattet werden. Bei den unterschiedlichen Klassen heißen sie manchmal leicht unterschiedlich, erfüllen aber alle den gleichen Zweck: Erzeuge eine Swing-Komponente, die den entsprechenden Eintrag anzeigen kann. Im Falle einer JList ist dies quasi immer ein [javadoc]javax.swing.JLabel[/javadoc]. Nicht ohne Grund ist der [javadoc]javax.swing.DefaultListCellRenderer[/javadoc] direkt von JLabel abgeleitet.

Der eigentliche Trick ist aber nicht dieser Renderer selbst, sondern der dass wir dem darin „versteckten“ JLabel eine selbst geschriebene [javadoc]javax.swing.border.Border[/javadoc]-Implementierung mitgeben. Dieser „Rahmen“ besteht aus der oben gezeigten Farbleiste unterhalb des eigentlichen Labels.

Zunächst einmal das Datenmodell. Vereinfacht gesagt, benötigen wir eine Liste von Farben und einen zugehörigen Anzeigenamen, den wir in der Liste darstellen können. Natürlich kann man die Klasse noch durch diverse weitere Attribute wie „Höhenlinien“ und was weiß ich nicht noch alles ergänzen, aber diese Informationen sind für den Artikel hier nicht von belang. Also die Klasse:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class ColorMap
{
  private String displayName;
  private List<Color> colors;

  public ColorMap(String displayName, List<Color> colors)
  {
    this.displayName = displayName;
    this.colors = colors;
  }

  public ColorMap(String displayName, Color... colors)
  {
    this(displayName, Arrays.asList(colors));
  }

  public List<Color> getColors()
  {
    return colors;
  }

  public String getDisplayName()
  {
    return displayName;
  }

}

Die beiden Konstruktoren sind nur meiner Faulheit geschuldet, da ich an unterschiedlichen Stellen entweder die Farbwerte schon bereits als Liste vorliegen habe oder sie aber auch beim Erstellen des Datenobjekts hardcodiert im Quelltext stehen. Der Rest der Klasse sollte klar sein.

Auch der Renderer ist nicht sonderlich komplex. Um sich einiges an Arbeit zu ersparen sollte man immer den DefaultListCellRenderer ableiten. Auch wenn nur die Methode getListCellRendererComponent aus dem [javadoc]javax.swing.ListCellRenderer[/javadoc] Interface von interesse ist, erledigt die Standardimplementierung einiges in Bezug auf „Selection“- und „Focus“-Darstellung für uns:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ColorMapRenderer extends DefaultListCellRenderer
{
  @Override
  public Component getListCellRendererComponent(JList list,
      Object value,int index,
      boolean isSelected, boolean cellHasFocus)
  {
    Component c = super.getListCellRendererComponent(
        list, value, index, isSelected, cellHasFocus);
    if(c instanceof JLabel && value instanceof ColorMap)
    {
      JLabel l = (JLabel)c;
      ColorMap map = (ColorMap)value;
      l.setText(map.getDisplayName());
      l.setBorder(new ColorBarBorder(map));
    }
    return c;
  }

}

Auch wenn es eigentlich nie passieren sollte, dass c etwas anderes ist als ein JLabel und value im Normalfall immer eine ColorMap sein sollte, ist es hier nie falsch dennoch auf Nummer sicher zu gehen. Und wie schon weiter oben erwähnt, passiert die „größte“ Zauberei in Zeile 15: Hier wird dem JLabel ein selbstgeschriebener Rahmen verpasst.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class ColorBarBorder extends AbstractBorder
{
  private static final int COLOR_BAR_HEIGHT = 4;
  private final ColorMap map;

  public ColorBarBorder(ColorMap map)
  {
    this.map = map;
  }

  @Override
  public void paintBorder(Component c, Graphics g,
      int x, int y, int width, int height)
  {
    super.paintBorder(c, g, x, y, width, height);
    if(map != null)
    {
      int count = map.getColors().size();
      int delta = width / count;
      int firstDelta = width % count;
      int offset = 0;
      Color old = g.getColor();
      for(Color color : map.getColors())
      {
        g.setColor(color);
        g.fillRect(offset, y + height - getBottomHeight(c),
            delta + firstDelta, getBottomHeight(c));
        offset += delta + firstDelta;
        firstDelta = 0;
      }
      g.setColor(old);
    }
  }

  @Override
  public Insets getBorderInsets(Component c)
  {
    return new Insets(0, 0, COLOR_BAR_HEIGHT, 0);
  }

  @Override
  public Insets getBorderInsets(Component c, Insets insets)
  {
    insets.left = insets.top = insets.right = 0;
    insets.bottom = COLOR_BAR_HEIGHT;
    return insets;
  }

}

Auch in dieser Klasse wird bereits einiges von der Standardimplementierung von [javadoc]AbstractBorder[/javadoc] übernommen, was für eine eigene Border Klasse notwendig ist. Wir müssen allerdings die beiden getBorderInsets Methoden implementieren, um dem Laufzeitsystem mitzuteilen, dass Rahmen mehr als 0 Pixel hoch ist. Die Konstante COLOR_BAR_HEIGHT kann theoretisch beliebig angepasst werden. Wer mag kann dies auch dynamisch anhand des Parameter Component c berechnen. In dem Bild oben wurden die 4 Pixel aus diesem Beispiel verwendet.

Erklären sollte ich vielleicht die Zeilen 19 und 20 in dem Beispiel. Die Berechnung von int delta in Zeile 19 ist mit Absicht eine Integer-Division. Der Balken sollte innerhalb der vorgegebenen Breite nach Möglichkeit alle Farben darstellen. Durch die Integer-Division erhalten wir auf jeden Fall die richtige Breite, so dass keiner der Balken „abgeschnitten“ werden muss. Allerdings bleibt ein kleiner Rest, so dass der Balken nicht die volle Breite ausfüllt. Dafür ist die Zeile 21 int firstDelta zuständig. Dieser Wert wird auf die Breite des ersten Balkens „aufgeschlagen“ so dass dieser geringfügig breiter ist. Es ist auch nur der erste Farbwert etwas breiter, da in Zeile 29 firstDelta wieder auf 0 zurück gesetzt wird.

Das schöne an diesem Renderer und seinem Border ist, dass wir ihn genauso für eine JComboBox verwenden können:

Notes:

  1. Der eigentliche Auswahldialog sieht bei uns anders aus. Die JList ist der Einfachheit halber in einem JOptionPane angezeigt

One Response to Farbschemata in JList anzeigen