Stilikonen

Zentrales Icon Management in einer Eclipse-4-basierten Applikation

Marco Descher
©Shutterstock/aketlee

In größeren Applikationen ist es gewünscht, Icons, die zur Verwendung kommen, in einem zentralen Ressourcen-Bundle vorrätig zu halten. Das erlaubt es unter anderem, die notwendigen Icons sowie deren Lizenz einfach im Blick zu behalten und diese gegebenenfalls auszutauschen. Im Hinblick auf die unterschiedlichen Anwendungs- bzw. Einbindungsarten (Application Model, Code etc.) einer größeren Applikation, ist das nicht ganz einfach und bedarf einiger Planung.

Eine der Voraussetzungen, um dem Anwender Konsistenz in der Anwendung einer Applikation zu bieten, ist es, sich auf ein bestimmtes Set an Icons festzulegen und diese wiederholt bei Aufgaben gleicher Art zu verwenden. Für die Eclipse-Entwicklung beispielsweise ist diese Richtlinie in den User Interface Guidelines [1] definiert. Verwaltet man die verwendeten Icons zentral, ist es zudem sehr einfach, die Lizenzen im Auge zu behalten und auf Wunsch das komplette Icon-Set auszutauschen. Das kann zum Beispiel gewünscht werden, wenn eine Applikation sowohl in einer Open-Source-, als auch in einer kommerziellen Variante vertrieben wird und die Icon-Sets sich hier unterscheiden sollen. Bevor wir aber eine Lösung erarbeiten, die uns das auch für eine größere Applikation erlaubt, gilt es, sich einen Überblick über die verschiedenen Icon-Typen in einer Eclipse-Applikation sowie deren Einbindungsarten zu verschaffen.

Ikonen im Einsatz

In einer Eclipse-Applikation kommen Icons grundsätzlich in drei verschiedenen Variationen vor:

  • Das einfache Icon mit einer Dimension von 16 x 16 Pixel, das in den Parts, Toolbars, Label Providers, Perspectives usw. zum Einsatz kommt.
  • Overlay Icons mit einer Dimension von 7 x 8 Pixel. Diese dekorieren einfache Icons und stellen so Zusatzinformationen dar.
  • Wizard Banners mit einer ungefähren Dimension von 58 x 75 Pixel. Diese werden neben der Verwendung in Wizards auch in TitleAreaDialogen zum Einsatz gebracht.

Es gilt also in der Problemstellung nicht nur die Icons an sich, sondern auch deren jeweilige Dimensionen zu berücksichtigen.

I can haz Icon?

Wie werden nun die Icons in unsere Applikation eingebettet? Entweder binden wir sie direkt per Code ein oder wir adressieren sie über ihren URI. Betrachten wir hierzu je ein Beispiel.

Wollen wir im Code programmatisch das Icon für ein ToolItem (dieses repräsentiert einen Button in einer Toolbar) setzen, so erfolgt dies unter Angabe von ToolItem.setImage(Image image). Wir benötigen hier also direkt eine Instanz der SWT-Klasse Image. Soll das Icon aber innerhalb des Application Models oder Fragment Editors verwendet werden, so muss ein entsprechender URI in der Form platform:/plugin/resource verwendet werden, also beispielsweise platform:/at.medevit.plugin/icons/sample.png. Die URI-Form verwendet transparent den Typ InputStream, der das entsprechende Image zurückliefern muss. Dazu aber später mehr.

Die Beispielanwendung

Um das abstrakte Konzept einer größeren Applikation darzustellen, müssen wir natürlich eine Beispielanwendung schaffen, die diesem Anspruch zumindest ansatzweise gerecht wird. Abbildung 1 bietet einen Überblick, wie diese Applikation aussehen soll. Zur Vereinfachung werden im weiteren Text die Komponenten anhand des in Abbildung 1 unterstrichenen Teils des Namens referenziert.

Der interessierte Leser kann die fertige Beispielanwendung von GitHub [2] beziehen und aus einem aktuellen Eclipse Juno (4.2) Workspace ausführen. Für den einfachen Start befinden sich im Applikations-Plug-in at.medevit.e4.app Run Configurations, die auf der entsprechenden Plattform direkt mittels Run As ausgeführt werden können.

Abb. 1: Überblick über die Plug-ins und deren Abhängigkeiten in der Beispielanwendung

e4.app bildet die Kernapplikation. Hierbei handelt es sich um die Eclipse-4-Beispielapplikation, die mit File | New | Other | Eclipse 4 | Eclipse 4 Application Project erstellt werden kann (beim Ausführen muss zusätzlich Create sample content (parts, menu etc.) ausgewählt werden, um an ausgiebigeren Beispielcode zu kommen). Wir beginnen also auf vertrautem Boden.

icons, icons.fugue und icons.tango bilden zusammen das zentrale Icon-Management. icons selbst enthält nur ein einziges Icon, das Fallback-Icon. Dieses wird immer verwendet, wenn irgendwo ein Problem auftaucht. In der Beispielanwendung ist das Fallback-Icon einfach ein transparentes 16 x 16 Image. Man könnte dieses natürlich auch durch ein auffälligeres Icon ersetzen, um „Fehlstellen“ direkter wahrzunehmen.

Der eigentliche Aufbewahrungsort der Icons ist das Fragment icons.fugue, das uns das Icon Set Fugue [3] von Y. Kamiyamane zur Verfügung stellen wird. Diese Icons sind unter der Creative-Commons-Attribution-Lizenz verfügbar und können daher bei Erwähnung des Autors frei verwendet werden.

Um in späterer Folge den Austausch zu demonstrieren, ist in icons.tango noch ein alternativer Icon-Satz verfügbar. Hier handelt es sich um die Tango Icons [4], die Public Domain vorliegen.

alpha und beta sind Plug-ins, die unsere Applikation erweitern. Das Ziel ist nun, dass sämtliche Applikations-Plug-ins keinerlei Icons mit sich bringen, sondern diese transparent über die vorhandenen Strukturen nützen können, ohne im Endeffekt über das verwendete Icon Bescheid zu wissen. Natürlich ist es weiterhin jedem Plug-in freigestellt, selbst Icons mitzuführen, doch sollte dies nur bei Icons, die nur im entsprechenden Plug-in Anwendung finden, getan werden.

Aufbau des zentralen Icon-Managements – das Modell

Um die erwünschte Trennung zwischen verwendetem Icon und Adressierung in der Applikation zu erhalten, müssen wir zuerst eine „abstrakte Zwischenschicht“ einziehen. Dazu führen wir im Plug-in icons die Enumerator-Klasse Icon ein. Hier definieren wir die verwendeten Icons, die wir in unserer Applikation benötigen und in späterer Folge auf reale Image-Dateien abbilden werden.

Es ist von Vorteil, hier eine passende Namensgebung für die Definition zu finden bzw. die Icons mit deren Verwendungsart als Prefix zu kennzeichnen. Das erleichtert das korrekte Adressieren der Icons beim Zuweisen im Code und die Designaufgabe, der jeweiligen Anwendung ein Real-Icon zukommen zu lassen. Tabelle 1 zeigt einen Vorschlag zu einer solchen Namensgebung.

Iconpräfix Verwendung in Beispiel
PART View Part PART_CONTACT_DETAIL
PERSPECTIVE Perspektive PERSPECTIVE_AGENDA
ICON Allgemein (z. B. Label Provider) ICON_TELEPHONE
COMMAND Command oder Action Contributions COMMAND_ADD
DIALOG TitleAreaDialog oder Wizard DIALOG_PROPERTIES
OVERLAY Icon Decorators OVERLAY_ERROR

Tabelle 1: Vorschlag zur Präfixnamensgebung der zu verwendenden Icons

In die Applikation werden Icons generell in Form von drei verschiedenen Objekten integriert: Image, ImageDescriptor und InputStream. Idealerweise sollte die Enumerator-Klasse Icon also imstande sein, uns ein gewünschtes Icon auf jede dieser Arten zur Verfügung zu stellen.

Da SWT Images keine leichtgewichtigen Objekte sind, sondern für die Darstellung jeweils vom Betriebssystem eine Ressource vergeben werden muss, ist eine korrekte Verwaltung notwendig. Hier hilft uns die JFace ImageRegistry weiter, die dieses Management für uns übernimmt. Wir wollen also jedes Icon, bevor es zur Verwendung kommt, aus dem ImageRegistry beziehen und müssen es bei diesem zu Beginn natürlich registrieren.

Zu guter Letzt soll ein allgemeines Icon auf verschiedene Arten zum Einsatz kommen können (vgl. Abschnitt „Ikonen im Einsatz“). Also muss es möglich sein, die Größe des Icons als Parameter anzugeben, um ein bestimmtes Icon zu erhalten. Hier werden wir uns mit einer weiteren Enumerator-Klasse behelfen, die wir IconSize nennen. Abbildung 2 bietet einen Überblick über das erstellte Modell.

Abb. 2: Das Icon-Modell mit den Enumerator-Klassen Icon und IconSize

Aufbau des zentralen Icon-Managements – Das Icon-Set

Nachdem wir die Icons in der Enumerator-Klasse Icon definiert haben, müssen wir die Elemente auf reale Image-Dateien der richtigen Größen abbilden. Das Mapping soll transparent für das icon-Plug-in, jedoch simpel austauschbar sein. Zu diesem Zweck verwenden wir zwei Techniken: Fragment Bundle und Java Properties. Fragment Bundles verbinden sich zur Laufzeit mit ihrem Host-Plug-in und teilen sich dessen Classloader und Classpath. Java Properties werden zumeist für die Internationalisierung verwendet. Unser Anwendungsfall ist in Art und Weise der Internationalisierung ähnlich, nur dass wir statt Sprachdateien Icon-Sets austauschen.

Da wir ein Icon in verschiedenen Größen anbieten wollen, sollte außerdem eine entsprechende Verzeichnisstruktur vorhanden sein, damit wir eine Art „Contract-by-Absolute-Filename“ erfüllen können. Das bedeutet, wir erwarten von einer Datei, von der wir den realen Namen durch Auflösung des Java Properties Mapping erhalten, dass diese durch einen fix definierten Pfad erreichbar ist.

Abb. 3: Das icons.fugue-Fragment enthält die eigentlichen Icons

Abbildung 3 zeigt die Struktur des icons.fugue-Fragments. Man findet die Properties-Datei iconset.properties und die Verzeichnisstruktur „Contract-by-Absolute-Filename“ im Verzeichnis icons und seinen Unterverzeichnissen. Die Properties-Datei selbst hält sich sehr nüchtern in ihrem Aufbau. Sie enthält einzeilige Einträge vom Typ Schlüssel/Wert, wobei der Schlüssel der in der Icon-Enumerator-Klasse definierte Begriff und Wert der Dateiname des Icons ist. Wir finden hier also beispielsweise den Eintrag DIALOG_FUN=banner_smiley.png.

Aufbau des zentralen Icon-Managements – Serving the Application

Jetzt, da wir sowohl das Modell als auch ein Icon-Set zur Verfügung haben, müssen wir die Icons entsprechend unserer Applikation zur Verfügung stellen. Die programmatische Anwendung stellt sich hier relativ trivial dar, wie in den folgenden Anwendungsbeispielen zu sehen ist:

Icon.DIALOG_FUN.getImage(IconSize._75x66_TitleDialogIconSize)
Icon.COMMAND_ADD.getImageDescriptor(IconSize._16x16_DefaultIconSize)

Bei beiden angeführten Methoden kümmert sich transitiv die Icon#addIconImageDescriptor()-Methode (Abb. 2) darum, die korrekten Icons zu finden und im JFace ImageRegistry abzulegen, wo sie dann entsprechend als Image oder ImageDescriptor bezogen werden können.

private static boolean addIconImageDescriptor(String name, IconSize is) {
  try {
    ResourceBundle iconsetProperties = ResourceBundle
      .getBundle("iconset");
    String fileName = iconsetProperties.getString(name);
    URL fileLocation = FileLocator.find(Activator.getDefault()
      .getBundle(),
    new Path("icons/" + is.name + "/" + fileName), null);
    ImageDescriptor id =          ImageDescriptor.createFromURL(fileLocation);
    JFaceResources.getImageRegistry().put(name, id);
  } catch (MissingResourceException | IllegalArgumentException e) {
    return false;
  } 
  return true;
}

Listing 1 zeigt die Implementierung dieser Methode. Hier sieht man in Zeile 8, wie der „Contract-by-Absolute-Filename“ erfüllt und das Icon im entsprechenden Unterverzeichnis gesucht wird.

Aufbau des zentralen Icon-Managements

Die eigentliche Herausforderung zeigt sich darin, die Images auch dem Eclipse-4-Applikationsmodell zur Verfügung zu stellen. An dieser Stelle haben wir keine Möglichkeit, direkt mit Image- oder ImageDescriptor-Objekten zu arbeiten, sondern müssen die Icons in Form ihrer Icon URI referenzieren. Abbildung 4 zeigt den entsprechenden Eintrag, hier angewendet auf ein ToolItem, das in der Application ToolBar dargestellt werden wird.

Abb. 4: Adressierung eines Icons im Eclipse-4-Applikationsmodell

Hier bringen wir die abstrakte Klasse AbstractURLStreamHandlerService aus dem org.eclipse.osgi-Bundle ins Spiel, die wir als Superklasse für die selbst erstellte IconURLStreamHandlerService-Klasse verwenden, implementiert als Singleton. Der Aufruf der IconURLStreamHandlerService#register()-Methode, der in Listing 2 zu sehen ist, führt dazu, dass beim URLStreamHandlerService ein URI Handler für das „Protokoll“ icon registriert wird. Das ermöglicht es uns, Icons mittels icon://ICON_NAME zu adressieren.

public void register() {
  Bundle bundle = FrameworkUtil
    .getBundle(IconURLStreamHandlerService.class);
  BundleContext bundleContext = bundle.getBundleContext();
  try {
    @SuppressWarnings("rawtypes")
    Hashtable properties = new Hashtable();
    properties.put(URLConstants.URL_HANDLER_PROTOCOL,
      new String[] { "icon" });
    iconUrlHandler = bundleContext.registerService(
      URLStreamHandlerService.class, this, properties);
  } catch (Exception e) {
    LogHelper.logError("Could not register icon URL handler.", e);
  }
    LogHelper.logInfo("Icon URL handler registered.");
}

Erfolgt nun ein entsprechender Aufruf aus dem Applikationsmodell, wird vom URLStreamHandlerService die Methode IconURLStreamHandlerService#openConnection() aufgerufen. Diese wiederum erstellt ein Objekt der Klasse IconURLConnection, die sich von URLConnection ableitet und einen InputStream zurückgeben soll. Die Klassenhierarchie ist der Übersicht halber in Abbildung 5 dargestellt.

Abb. 5: Klassenhierarchie für URLConnection und IconURLStreamHandlerService

Wir haben also die Aufgabe, die Methode IconURLConnection#getInputStream so zu überschreiben, dass sie das geforderte Icon zurückgibt. Das stellt sich nicht sonderlich kompliziert dar, wie in Listing 3 zu sehen ist.

@Override
public InputStream getInputStream() throws IOException {
  try {
    Icon selectedIcon = Icon.valueOf(iconName);
    InputStream is = selectedIcon
    .getImageAsInputStream(IconSize._16x16_DefaultIconSize);
    return is;
  } catch (MissingResourceException | IllegalArgumentException e) {
    System.out.println("[ERROR] " +         IconURLConnection.class.getName() + " " + iconName
      + " not found, replacing with empty icon.");
    return FileLocator.find(Activator.getDefault().getBundle(),
      new Path("icons/empty.png"), null).openStream();
  }
}

Das korrekte Icon ist direkt adressierbar, da im Applikationsmodell die Namen der Enumeratoren verwendet werden. Wir können hier also einfach aufgrund des Namens das entsprechende Icon instanziieren. In weiterer Folge beziehen wir das Image als InputStream und retournieren es. Sollte allerdings die Icon-Datei nicht gefunden werden können, da beispielsweise ein Mapping falsch gesetzt oder das Fragment vergessen wurde, soll das Fallback-Icon retourniert und ein entsprechender Hinweis ausgegeben werden. Dies ist im Catch Handler zu sehen.

Da wir es im Applikationsmodell nur mit „regulären“ Icons zu tun haben, ist die Icon-Größe fix auf 16 x 16 gesetzt. Dieser Parameter wird also bei der Verwendung im Applikationsmodell nicht benötigt.

Aufbau des zentralen Icon-Managements – URI-Handler-Registrierung

Um den icon-URI während der gesamten Laufzeit der Applikation und somit auch schon beim Parsen des Applikationsmodells verwenden zu können, muss sie natürlich frühzeitig registriert werden. Das erreichen wir mithilfe eines Applikations-Lifecycle-Hooks, der es uns erlaubt, in den Lebenszyklus der Applikation einzugreifen. Eine kurze Dokumentation hierzu ist im Kasten „Application Lifecycle Hooks“ zu finden. Für eine detailliertere Aufarbeitung sei hier auf das Eclipse 4 RCP Tutorial [5] von Lars Vogel verwiesen. In der Beispielapplikation findet sich der entsprechende Code im e4.app-Plug-in in der at.medevit.e4.app.ApplicationLifecycle-Klasse.

Application Lifecycle Hooks
In Eclipse 3 konnte auf den Lebenszyklus der Applikation über den WorkbenchAdvisor zugegriffen werden. Dieser ist in Eclipse 4 nicht mehr verfügbar, und Application Lifecycle Hooks werden derzeit wie folgt registriert:
Im Applikations-plugin.xml muss beim Extension Point org.eclipse.core.runtime.products für das verwendete Produkt eine Property mit Namen lifeCycleURI hinzugefügt werden. Der entsprechende Wert in value zeigt dann auf eine zu erstellende Klasse, die annotierte Methoden für die verschiedenen Lebenszyklen anbieten kann. Also beispielsweise @ProcessAdditions, wie in der Beispielanwendung verwendet.
Die Notwendigkeit zum Registrieren über den Extension Point könnte sich in naher Zukunft ändern. Ein entsprechender Änderungsantrag wurde von Tom Schindl in Eclipse Bug #392903 [6] eingebracht.

Facelifting

Nun, da wir die Icon-Verwaltung zentralisiert haben, ist es ein Leichtes, diese zu verwenden und nach Belieben auszutauschen. Dazu muss in der Run Configuration des Produkts nur das gewünschte Fragment ausgewählt werden. Abbildung 6 zeigt unser Produkt ohne und mit selektiertem icons.fuge- und icons.tango-Fragment. Das defensive Verhalten des Icon-Management-Plug-ins führt also dazu, dass auch im Fehlerfall die Applikation lauffähig bleibt.

Abb. 6: Die Beispielapplikation ohne und mit icons.fugue- und icons.tango-Icon-Fragment

The missing bits

Natürlich wurden nicht alle Anwendungsformen innerhalb dieses Artikels detailliert präsentiert. Beispielsweise sind Decorator Icons zwar erwähnt, aber nicht als Beispiel angeführt. Wer sich jedoch an dieser Stelle bereits den Quellcode von GitHub gezogen hat, wird dazu im Plug-in alpha in der Klasse ListElementDecorator ein Beispiel finden.

Seit Eclipse 4.3 Milestone 3 existiert zudem die Plug-in Image Browser View, die eine Übersicht über sämtliche Icons anbietet. Der Autor empfiehlt an dieser Stelle zusätzlich die Installation der PDE Tools von Jeeeyul Lee [7]. Die durch dieses Plug-in gebotene Icon Preview zeigt im Package Explorer direkt die Icons an – bei der schnellen Suche nach dem passenden äußerst nützlich.

Aufmacherbild: vector sun and moon kissing von Shutterstock / Urheberrecht: aketlee

Geschrieben von
Marco Descher
Marco Descher
Marco Descher, DI (FH), MSc ist wissenschaftlicher Mitarbeiter an der Fachhochschule Vorarlberg (FHV), Österreich, und Geschäftsführer der MEDEVIT OG. Er ist mit Kernentwickler der Open-Source-EMR-Software Elexis [8] und unterrichtet wissbegierige Studenten in Eclipse RCP. Zu erreichen ist er unter marco@descher.at, und manchmal bloggt er auch unter http://vu.descher.at.
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.