Autor: 'Kriz'
Wichtiger Hinweis!!!
Die Grundlagen über die Java-Programmierung ist nicht Bestandteil der Tutorialreihe! Zum Lernen von Java empfehle ich das hervorragende Online-Tutorial von Guido Krüger, welches man sich kostenlos von www.javabuch.de runterladen kann.
In diesem Tutorial geht es um das Erstellen einer benutzerdefinierten Liste, die auf der JList von Swing basiert. Die Standardausführung kann genau wie ihr AWT-Gegenstück nur Strings als Auswahl anzeigen. Mit ein paar Kniffen allerdings kann man eine JList bequem dazu bringen, neben Text auch andere wichtige Dinge wie Icons usw. anzuzeigen. Dieses Feature kennt man beispielsweise bei der Projektauswahl von WinOnCD oder beim Download-Manager von Firefox usw.
Ich zeige euch hier, wie man eine einfache Liste erstellt, die Icons und Text gleichzeitig anzeigen kann.
Eine JList besitzt zur Anzeige der Auswahlzellen (der einzelnen, selektierbaren Bereiche einer Liste) einen sogenannten ListCellRenderer, welches die Komponente an die Liste übergibt und etwaige Initialisierungen zum Zeichnen der Komponente übernimmt. Der ListCellRenderer an sich ist ein Interface, welches von jeder JComponent-Klasse implementiert werden muß, welches als Auswahlzelle agieren möchte. Der Standardrenderer der JList ist ein JLabel, der nur den Text anzeigt.
Möchte man nun eine angepasste JList erstellen, so muß man zwangsläufig auch einen eigenen Zellenrenderer programmieren. Man leitet einfach aus einem der JComponent-Klassen (wie JPanel, JLabel, JButton usw.) eine eigene Klasse ab und implementiert dort das besagte Interface ListCellRenderer und definiert die übernommenen Interfacemethoden aus.
Neben dem Zellenrenderer besitzt jede JLIst auch ein eigenes ListModel, eine Klasse die den Inhalt der Liste organisiert. Von Haus aus ist jeder JList Instanz ein Objekt der Klasse DefaultListModel zugeordnet. Eigene Listenmodelle braucht man für dieses Tutorial nicht, es reicht der Standard vollkommen aus. Ein Listenmodell braucht man immer zwingend dann, wenn man den Inhalt der Liste dynamisch (zur Laufzeit) ändern möchte. Das klingt jetzt etwas merkwürdigt (eine AWT-Liste schafft das ja auch alleine), hat aber Sinn. Vom DefaultListModell erzeugt man eine Instanz, fügt nacheinander dort die anzuzeigenden Komponenten hinzu und übergibt das Objekt dem Konstruktor von JList.
Wir merken uns: Das Listenmodell organisiert den Inhalt, der Zellenrenderer stellt den anzuzeigenden Listeninhalt dar.
So, um nun ein Icon mit Text als Listeninhalt zu definieren, sollte man als Basiskomponente nicht direkt ein JLabel nehmen, sondern ein JPanel, in dem ein JLabel eingebettet wird. Ohne ein umgebendes JPanel besitzt das JLabel keinerlei Innenabstände zum Komponentenrand. Das bedeutet, daß Icon und Text ohne einen einzigen Pixel Abstand in alle vier Himmelsrichtungen direkt in die Liste gequetscht werden, was relativ uncool aussieht. Das JPanel dagegen sorgt dafür, daß es einen kleinen Abstand zum Randbereich gibt. Die ganze Sache wird lesbarer.
Desweiteren wollen wir, daß sich der Hintergrund der Listenzelle farbig ändert, sobald die Zelle den Fokus besitzt bzw. wieder die Hintergrundfarbe der Liste annimmt, wenn die Zelle den Fokus verliert.
Wir fangen zuerst einmal mit unserem Zellenrenderer an. Wie erwähnt dient als Basis dafür ein JPanel, welches als Layout das linksbündige FlowLayout besitzt. Die im Quelltext auftauchende Klasse MyListItem wird weiter unten erklärt ;)
// MyCellRenderer.java /* Eventuell hier erst das Paket definieren, falls erforderlich! */ import java.awt.*; import javax.swing.*; public class MyCellRenderer extends JPanel implements ListCellRenderer { private JLabel label = null; public MyCellRenderer() { // Konstruktor des JPanels mit FlowLayout aufrufen super(new FlowLayout(FlowLayout.LEFT)); // JPanel undurchsichtig machen setOpaque(true); // JLabel instanzieren, durchsichtig machen und hinzufügen label = new JLabel(); label.setOpaque(false); add(label); } public Component getListCellRendererComponent(JList list, // JList Objekt Object value, // anzuzeigende Komponente int index, // Zellenindex boolean iss, // Ist selektiert? boolean chf) // Hat den Fokus? { // JLabel das Icon aus unserem MyListItem zuweisen label.setIcon(((ListItem)value).getIcon()); // JLabel den Text aus unserem MyListItem zuweisen label.setText(((ListItem)value).getText()); // Hintergrundfarbe des JPanels bei Fokuswechseln definieren if(iss) setBackground(Color.lightGray); // Hat den Fokus else setBackground(list.getBackground()); // Hat den Fokus nicht return this; } }
Unser Zellenrenderer ist wie gesagt von JPanel abgeleitet und hat das nötige Interface ListCellRenderer implementiert. Als einzubettende Komponente gibt es das JLabel, welches bekannterweise ja ein Icon und Text gleichzeitig darstellen kann. Im Konstruktor der Klasse wird dem Superkonstruktor von JPanel das Layout FlowLayout mit linksbündiger Ausrichtung übergeben. Danach wird mittels setOpaque() bestimmt, ob der Hintergrund des JPanels durchsichtig sein soll oder nicht. Er muß undurchsichtig sein, damit wir bei Fokuswechseln dem Hintergrund des JPanels eine andere Farbe geben können, daher wird der Wert true übergeben.
Dann wird das JLabel instanziert. Den Hintergrund des JLabels setzen wir ebenfalls mit setOpaque() diesmal auf false, denn damit der Hintergrund des JPanels auch zu sehen ist muß der Hintergrund des JLabels durchsichtig sein, logisch. Anschließend fügen wir das JLabel dem JPanel per add() hinzu.
Die nächste Methode mit dem Namen getListCellRendererComponent() ist eine Methode, die vom Interface ListCellRenderer geerbt ist. Es werden fünf Argumente an die Methode übergeben: Ein JList Objekt, ein Object Objekt, ein int Wert und zwei logische Werte.
Das JList Objekt enthält später beim Aufruf der Methode die JList selber, in der wir unsere Komponenten anzeigen lassen wollen. Kniffliger wird es beim Object Objekt. Es ist mit dem verwirrenden Namen „value“ belegt worden, hat aber mit einem „Wert“ an sich nichts zu tun! Vielmehr enthält dieses Objekt die anzuzeigende Komponente selber. Wie was jetzt? Das anzuzeigende Komponente ist doch die Klasse MyCellRenderer selber, oder nicht? In diesem Falle nicht. Vielmehr handelt es sich bei „value“ um eine Komponente des Listenmodells der JList und nicht des Zellenrenderers! Die Listenmodell-Komponente werden wir als nächstes erzeugen, sie trägt den Namen ListItem und beinhaltet den eigentlichen Inhalt des Zellenrenderers. Wie gesagt: Der Zellenrenderer stellt die Sache rein optisch dar, den Inhalt der Zelle dagegen übernimmt eine Komponente des Listenmodells!
Ok, weiter geht's. Der int Wert beinhaltet den aktuellen Index des Elements in der JList, um den es sich dreht. Er wird hier nicht gebraucht. Die beiden logischen Werte „iss“ und „chf“ enthalten die Stati, ob die Komponente in der Liste selektiert (iss) oder den Fokus (chf) besitzt. Und nun wird es endlich ersichtlich, woher der Zellenrenderer seine Daten bekommt:
Wir übergeben unserem JLabel Objekt durch ein fettes Typecasting nacheinander das anzuzeigende Icon und den Text. Beides holen wir uns aus dem ominösen Object Objekt „value“ durch die Aufrufe getIcon() und getText(). Diese beiden Methoden sind in der Klasse ListItem definiert worden und liefern die besagten Elemente. Gesetzt werdem beide Elemente mit den JLabel-Methoden setIcon() und setText.
Abschließend reagieren wir noch auf den Status der logischen Variable „chf“ („iss“ interessiert uns hier nicht weiter). Falls „chf“ den Wert true hat, dann besitzt unsere Zelle momentan den Fokus. Daher färben wir zur Verdeutlichung den kompletten Hintergrund des JPanels in ein helles Grau ein. Falls die Zelle den Fokus nicht besitzt, so färben wir den Hintergrund mit der derzeitigen Hintergrundfarbe der übergebenen JList ein.
Der zweite Codeteil kommt endlich auf die Klasse ListItem zu sprechen, dem Inhaltsobjekt des Listenmodells. Dabei wird einfach eine Klasse erzeugt, die sowohl das Icon als auch den Text als Elemente besitzt. Das Icon wird zwecks besserer Kommunikation mit dem JLabel aus dem Zellenrenderer durch ein Objekt der Klasse ImageIcon realisiert. Das erspart uns lästige Castings und Bildladereien.
// ListItem.java /* Eventuell hier erst das Paket definieren, falls erforderlich! */ import javax.swing.*; public class ListItem { // Das anzuzeigende Icon private ImageIcon icon = null; // Der Text private String text; public ListItem(String iconpath, String text) { // Erzeugung des ImageIcons durch Angabe des Bild-Quellpfads icon = new ImageIcon(iconpath); // Zuweisung des Textes this.text = text; } // Liefert das Icon public ImageIcon getIcon() { return icon; } // Liefert den Text public String getText() { return text; } }
Simpel, was? Mehr braucht ein Listenmodell nicht als „Inhaltsangabe“ für einen Zellenrenderer. Natürlich muß der Inhalt auch mit der anzuzeigenden Komponente in irgendeiner Form zusammengehören, sonst wird die Sache sinnfrei. Nun denn, wir definieren jeweils ein ImageIcon als Icon und einen String als Text.
Der Konstruktor erwartet zwei Strings als Argumente. Der erste „iconpath“ enthält den Pfad zum Bild, aus dem das Icon erzeugt werden soll. Der zweite „text“ enthält logischerweise den anzuzeigenden Text. Im Konstruktor werden dann das ImageIcon erzeugt und der Text zugewiesen.
Außerdem finden wir noch zwei get-Methoden vor: getIcon(), welches das IconImage liefert und getText(), welche den Text liefert. Mehr braucht unser Inhaltselement nicht.
Aus dieser Klasse ListItem werden wir später mehrere Instanzen erzeugen und dem DefaultListModel Objekt hinzufügen, welches wie gesagt selbst der JList als Konstruktorargument übergeben wird. So weiß die JList dann, welche Inhalte ihre einzelnen Zellen besitzen. Das ist auch der Grund, wieso man für jede dynamische Änderung der Inhalte ein neues ListModel an die JList übergeben muß. Nur so kann man beispielsweise eine List realisieren, wie man sie im Windows-Explorer findet: Die vier bis fünf verschiedenen Ansichten des Explorers wären vergleichsweise vier oder fünf verschiedene Listenmodelle in Java. Müßig zu erläutern, daß man dann natürlich auch vier bis fünf verschiedene Zellenrenderer benötigen würde, nämlich einen Renderer pro Ansicht ;)
Der dritte Codeteil ist unser Testfenster für unsere spezielle JList. Ich gehe mal davon aus, daß wir in dem Verzeichnis mit unseren Javadateien auch 5 verschiedene Bilddateien liegen haben, der Einfachheit halber durchnummeriert von img0 bis img4. Der Dateityp ist dabei irrelevant, vorzugsweise für die Arbeit mit Swing eignen sich natürlich PNG-Grafiken, vor allem 32-Bit PNGs wegen der 8 Bit Transparenzfähigkeit.
// Testfenster.java /* Eventuell hier erst das Paket definieren, falls erforderlich! */ import java.awt.*; import javax.swing.*; public class TestFenster extends JFrame { private Container contentpane = null; private JList list = null; private DefaultListModel listmodel = null; public static void main(String[] args) { TestFenster theApp = new TestFenster(); } public TestFenster() { // Superkonstruktor aufrufen mit Fenstertitel super("Testfenster für JList"); // Contentpane holen contentpane = getContentPane(); // Fenstereigenschaften setzen contentpane.setLayout(new GridLayout(1,1)); setSize(200, 200); setLocationRelativeTo(null); setDefaultCloseOperation(EXIT_ON_CLOSE); // Eigenes Listenmodell erzeugen listmodel = new DefaultListModel(); // ListItems erzeugen und dem Listenmodell hinzufügen ListItem item0 = new ListItem("img0.png", "Element 1"), item1 = new ListItem("img1.png", "Element 2"), item2 = new ListItem("img2.png", "Element 3"), item3 = new ListItem("img3.png", "Element 4"), item4 = new ListItem("img4.png", "Element 5"); // Elemente dem Listenmodell hinzufügen listmodel.addElement(item0); listmodel.addElement(item1); listmodel.addElement(item2); listmodel.addElement(item3); listmodel.addElement(item4); // JList erzeugen und Listenmodell übergeben list = new JList(listmodel); // Unseren Zellenrenderer erzeugen und der Liste übergeben list.setCellRenderer(new MyCellRenderer()); /* Falls es Scrollingprobleme geben sollte, geben wir unsere Liste in die Obhut einer JScrollPane und fügen diese dann anstatt der Liste selbsrt dem Fenster hinzu */ contentpane.add(new JScrollPane(list)); // Contentpane setzen setContentPane(contentpane); // Fenster sichtbar machen setVisible(true); } }
Voilá, es ist vollbracht. Startet man nun die Applikation, so sollte ein Fenster erscheinen, in der eine Liste existiert, welches als Elemente eine Kombination aus Icon und Text besitzt. Klickt man auf eines der Listenelemente, so wird der Hintergrund hellgrau als Zeichen der Fokussierung.
Übrigens, die Methode setLocationRelativeTo() ist eine neue Methode aus dem JDK 1.4.x und setzt das Fenster in relativem Bezug zu einem Vaterfenster zentriert in Position. Gibt man hier aber als Parameter null an, so wird der Desktop als Vaterfenster angesehen. So kann man ganz bequem sein Fenster auf dem Bildschirm zentrieren.
User von älteren JDKs sollten diese Methode austauschen gegen die normale setLocation() Methode. Folgende Formel ermöglicht ebenfalls eine zentrierte Ausgabe (allerdings immer in Relation zum Desktop!):
setLocation((getToolkit().getScreenSize().width - getSize().width) / 2, (getToolkit().getScreenSize().height - getSize().height) / 2);
Der von uns im Zellenrenderer außer Acht gelassene logische Wert „iss“ kann übrigens ganz bequem dazu gebraucht werden, bei Auswahl des Elements ein anderes Icon (oder anderen Text) im Element anzuzeigen. So kann man trotzdem bei Fokusverlust immer sehen, ob ein oder mehrere Elemente ausgewählt worden sind. Der Standardrenderer der JList färbt nämlich nicht bei Fokuserhalt den Hintergrund ein, sondern setzt einen Rahmen. Einfärben tut er nämlich nur bei Selektion, so wie man das von allen Listenanwendungen her kennt.
Wie man nun das Element optisch manipuliert, liegt ganz bei euch. Es gibt viele Möglichkeiten, denn schließlich muß das Element ja kein JLabel sein, sondern kann auch was ganz anderes wie z.B. eine JProgressBar usw. sein.
Dieses Tutorial stammt aus der ehemaligen Sammlung des resourcecode.de und konnte dank der freundlichen Zustimmung des Autors in das thewall-Wiki übertragen werden.