Ein Teil der Arbeit auf dem Weg zu einer neuen grafischen Anwendung besteht im Anlegen und Ordnen der grafischen Elemente. Ebenso wichtig ist, dass diese aufeinander reagieren. Wie das mit Hilfe des RCP-Framework funktioniert, zeigt der letzte Teil des Eclipse-Tutorials.
Der in den ersten drei Teilen des RCP-Tutorials gewachsenen Beispielanwendung fehlt bislang jegliche Funktionalität. Eine über eine Listbox ausgewählte Bilddatei soll nun im Fenster erscheinen, eine weitere View die zugehörigen Exif-Daten, Jpeg-Kommentare und Detailinformationen anzeigen (siehe Abbildung 1). Das Prinzip des modularen Aufbaus und vor allem den Nutzen einer RCP-Anwendung zeigt die überarbeitete Beispielanwendung.

Abbildung 1: Die Beispielanwendung verknüpft die Views miteinander. Das markierte Bild erscheint im Hauptfenster, weitere Informationen im rechten Bereich.
Als GUI-Toolkit verwendet Eclipse statt des plattformunabhängigen Java-Standards Swing das Standard Widget Toolkit (SWT, [1]). Es beruht auf den nativen Toolkits des jeweiligen Betriebssystems und steigert damit einerseits die Performance, andererseits aber auch die plattformspezifischen Unterschiede. Das Vorgehen aus Programmierersicht bleibt jedoch dasselbe: Er verwendet die von SWT gelieferten Klassen und Methoden, um Buttons, Texte und Felder anzuordnen sowie um auf Ereignisse wie User-Eingaben zu reagieren.
Im Beispielprogramm sollen die einzelnen Views sich aufeinander abstimmen, sodass das Exif-Fenster stets die Informationen zum gerade angezeigten Bild ausgibt. Die klassische Lösung hierzu heißt Model View Controller (MVC). Die Rolle des Model übernimmt hier der Dateiname des aktiven Bilds. Beim Bildbetrachter und beim Exif-Fenster handelt es sich um verschiedene Sichten auf das Model, als Controller fungiert der Java-Code, der das Ganze koordiniert.
Man könnte dazu eine Filename-Bean mit Events und Listenern schreiben und die Views damit verknüpfen. Dies manuell zu programmieren brächte zwar maximale Kontrolle über das Verhalten der Anwendung mit sich, aber auch am meisten Arbeit. Einfacher geht es mit dem von den Eclipse-Views angeboten MVC-Konzept.
Die erste View dient der Bildanzeige. Wie im zweiten Teil dieses Tutorials gezeigt, implementiert jede View das Interface »IViewPart«, meist indem sie die abstrakte Klasse »ViewPart« erweitert. »ViewPart« stellt die grundlegenden Implementationen der Life-Cycle-Methoden bereit. Am wichtigsten ist dabei die Methode »createPartControl()«, die die Controls auf der View platziert.
Code-Recycling
Die Image-View des Beispiels enthält nur ein Control, nämlich ein »ImageCanvas«. Dabei handelt es sich um eine intelligente Subklasse von »org.eclipse.swt.widgets.Canvas«. Sie stammt in der Originalfassung aus dem Eclipse-Plugin-Beispiel [2] – dort heißt sie »SWTImageCanvas«. Während das Original eine Methode »onFileOpen()« besitzt, mit der die Benutzer über einen Datei-Öffnen-Dialog eine Bilddatei angeben, geschieht im vorgestellten Beispiel die Datei-Auswahl über eine andere View.
Um auf die Eingabe des Anwenders zu reagieren, nutzt sie deshalb den so genannten Selection-Service der Workbench. Jede View kann ein Control als Selection-Provider definieren, jede View kann sich auch als Selection-Listener registrieren. Im letzteren Fall muss die View zusätzlich das Interface »ISelectionListener« mit seiner Methode »selectionChanged()« implementieren. Listing 1 zeigt sowohl die Methode »createPartControl()« als auch »selectionChanged()«. In Zeile 38 registriert sich die View als Selection-Listener.
| Listing 1: »ImageView.java« |
|---|
04: package de.bablokb.epm.views;
05:
07: import org.eclipse.jface.viewers.ISelection;
08: import org.eclipse.jface.viewers.IStructuredSelection;
09: import org.eclipse.swt.widgets.Composite;
10: import org.eclipse.ui.INullSelectionListener;
11: import org.eclipse.ui.IWorkbenchPart;
12: import org.eclipse.ui.part.ViewPart;
13:
14: import de.bablokb.epm.utils.ImageCanvas;
15:
20: public class ImageView extends ViewPart implements ISelectionListener {
21: private ImageCanvas iCanvas;
22: private String iFilename;
23: public final static String VIEW_ID="ImageView";
24:
28: public ImageView() {
29: }
30:
35: public void createPartControl(Composite parent) {
36: iCanvas=new ImageCanvas(parent);
38: getViewSite().getPage().addSelectionListener(this);
39: }
40:
45: public void setFocus() {
46: iCanvas.setFocus();
47: }
48:
57: public void selectionChanged(IWorkbenchPart part, ISelection selection) {
58: if (selection instanceof IStructuredSelection) {
59: Object obj = ((IStructuredSelection) selection).getFirstElement();
60: if (obj == null) {
61: iCanvas.setImageData(null);
62: }
63: if (obj instanceof String) {
64: String fileName = (String) obj;
65: if (fileName.equals(iFilename)) {
66: return;
67: }
68: iFilename = fileName;
69: showBusy(true);
70: iCanvas.loadImage(fileName);
71: iCanvas.fitCanvas();
72: iCanvas.syncScrollBars();
73: showBusy(false);
74: }
75: }
76: }
77: }
|
Als Selection-Provider fungiert im Beispiel die »DirListView«. Sie enthält unter anderem eine Open-Directory-Action, über die ein SWT-Standard-Control ein Verzeichnis öffnet. Dabei handelt es sich um einen List-Viewer aus dem Package »org.eclipse.jface.viewers«. Viewer ähneln den Controls, doch sie bringen ihr eigenes Model mit einem Content-Provider für den Inhalt und einem Label-Provider für die Darstellung mit. Entsprechende Viewer-Klassen wie List-, Tree- oder Table-Viewer stehen damit direkt zur Verfügung.
Content-Provider
Für die hier benötigten Funktionen – also Datei-Auswahl und Anzeige – böte sich eigentlich ein Tree-Viewer an. Allerdings gestaltet sich dessen Programmierung ziemlich aufwändig; zu Demonstrationszwecken verwendet das Beispiel deshalb stattdessen einen List-Viewer. Ein Beispiel für einen Tree-Viewer inklusive des Quelltexts zeigt der zweiteilige Artikel unter [3] und [4].
Der Content-Provider des Beispiels heißt »DirListProvider« und implementiert das Interface »IStructuredContentProvider« (Listing 2). Seine wichtigste Methoden sind »inputChanged()« und »getElements()«. Das Programm ruft »inputChanged()« auf, sobald sich der Input des Content-Providers ändert. Beim Input handelt es sich technisch um ein beliebiges Java-Objekt, logisch enthält es die notwendige Information, aus der der Content-Provider seinen Inhalt erfährt, hier den Verzeichnisnamen.
Als Label-Provider nutzt das Beispiel die Klasse »org.eclipse.jface.viewers.LabelProvider«. Auszüge von »DirListView« zeigt Listing 3. Die Methode »createPartControl()« initialisiert zuerst den Viewer (Zeilen 67 bis 71) und registriert ihn anschließend als Selection-Provider (Zeile 72). Der komplette Workspace inklusive des Code für die Actions sowie das Kontextmenü und die Toolbars stehen unter [5] zum Download bereit.
»DirListView« ist die Default-View der Anwendung, sie öffnet sich deshalb beim Programmstart automatisch. Ohne nähere Angaben im Programmcode obliegt es der Workbench, dafür eine Position auszuwählen.
Um den nach Programmstart geöffneten Views einen festen Platz im Fenster zuzuordnen, sind für sie Platzhalter vorzusehen. So genannte Folder sammeln die Views unter Reitern im rechten Fensterbereich, wie in Abbildung 1 zu sehen. Auch für Folder reserviert man den entsprechenden Zielbereich über Platzhalter. All dies findet in der Methode »createInitialLayout()« der schon im ersten Teil des Tutorials definierten Klasse »Perspective« statt (siehe Listing 4).
Die Dateiliste erhält im Beispiel den Platz im oberen Fensterbereich, das Bild selbst erscheint darunter, für die Bildinformationen steht ein reservierter Bereich am rechten Fensterrand zur Verfügung.
| Listing 2: »DirListProvider.java« |
|---|
04: package de.bablokb.epm.utils;
05:
06: import java.io.File;
07: import java.io.FilenameFilter;
08:
09: import org.eclipse.jface.viewers.IStructuredContentProvider;
10: import org.eclipse.jface.viewers.Viewer;
11:
16: public class DirListProvider implements IStructuredContentProvider {
17: private String iDirname = null;
18:
22: public DirListProvider() {
23: super();
24: }
25:
38: public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
39: iDirname = (String) newInput;
40: }
41:
42: public Object[] getElements(Object inputElement) {
43: if (iDirname == null)
44: return null;
45: File dir = new File(iDirname);
46: FilenameFilter filter = new FilenameFilter() {
47: public boolean accept(File directory, String filename) {
48: if (filename.endsWith("jpg"))
49: return true;
50: else
51: return false;
52: }
53: };
54: String[] dirList = null;
55: if (dir.isDirectory()) {
56: dirList = dir.list(filter);
57: for (int i=0; i<dirList.length;++i) {
58: dirList[i] = iDirname + File.separatorChar + dirList[i];
59: }
60:
61: }
62: return dirList;
63: }
65: }
|
| Listing 3: »DirListView.java« |
|---|
004: package de.bablokb.epm.views;
005:
006: import org.eclipse.jface.action.Action;
007: ...
026: import org.eclipse.ui.part.ViewPart;
027:
028: import de.bablokb.epm.utils.DirListProvider;
029:
034: public class DirListView extends ViewPart {
035: private String iDirname = null;
036: private IMemento iMemento = null;
037: private ListViewer iViewer;
038:
039: private IAction iOpenAction;
040:
041: private IAction iClearAction;
042:
043: public final static String VIEW_ID = "ListView";
044:
048: public DirListView() {
049: super();
050: }
051:
054: public void init(IViewSite site, IMemento memento) throws PartInitException {
055: iMemento = memento;
056: super.init(site, memento);
057: }
058:
066: public void createPartControl(Composite parent) {
067: iViewer = new ListViewer(parent);
068: iViewer.setContentProvider(new DirListProvider());
069: iViewer.setLabelProvider(new LabelProvider());
070: restoreState();
071: iViewer.setInput(iDirname);
072: getSite().setSelectionProvider(iViewer);
073: makeActions();
074: hookContextMenu();
075: contributeToActionBars();
076: }
077:
078: private void restoreState() {
079: if (iMemento == null) {
080: if (iDirname == null) {
081: iDirname = System.getProperty("user.home");
082: }
083: return;
084: }
085: IMemento dirname = iMemento.getChild("directory");
086: if (dirname != null) {
087: iDirname = dirname.getID();
088: }
089: }
090:
162: public void saveState(IMemento memento) {
163: memento.createChild("directory",iDirname);
164: super.saveState(memento);
165: }
166: }
|
| Listing 4: »Perspective.java« |
|---|
01: package de.bablokb.epm;
02:
03: import org.eclipse.ui.IFolderLayout;
04: import org.eclipse.ui.IPageLayout;
05: import org.eclipse.ui.IPerspectiveFactory;
06: import org.eclipse.ui.part.IPage;
07:
08: import de.bablokb.epm.views.*;
09:
10: public class Perspective implements IPerspectiveFactory {
11:
12: public void createInitialLayout(IPageLayout layout) {
13: layout.setEditorAreaVisible(false);
14: layout.addView(DirListView.VIEW_ID, IPageLayout.TOP,
15: 0.25f, IPageLayout.ID_EDITOR_AREA);
16: IFolderLayout folderLayout = layout.createFolder("miscFolder",IPageLayout.RIGHT,0.5f,DirListView.VIEW_ID);
17: folderLayout.addPlaceholder(ExifView.VIEW_ID);
18: folderLayout.addPlaceholder(DetailsView.VIEW_ID);
19: folderLayout.addPlaceholder(CommentView.VIEW_ID);
20: layout.addPlaceholder(ImageView.VIEW_ID,IPageLayout.BOTTOM,0.25f,DirListView.VIEW_ID);
21: layout.setFixed(true);
22: }
23: }
|
Status sichern
Die RCP-Plattform erlaubt es dem Anwender, die Layoutvorgaben seinem Geschmack anzupassen. Per Drag&Drop schiebt er die Views an die bevorzugten Positionen. Natürlich soll er die Fensterbereiche beim nächsten Programmstart wieder so vorfinden, wie er sie beim letzten Mal angeordnet hat. Ebenso wäre es hilfreich, wenn die Einstellungen der einzelnen Views zwischen den Programmaufrufen gesichert blieben. Die dafür nötigen Methoden hält die RCP bereit, auch wenn sich der Programmer\’s Guide dazu ausschweigt.
Die Zeilen 162 bis 164 in Listing 3 überschreiben im Beispiel die Methode »saveState(IMemento memento)« in »DirListView«. Ein Memento ist ein baumartiges Objekt, seine Knoten und Blätter sichern Einstellungen. Die gezeigte Methode speichert lediglich das aktuell ausgewählte Verzeichnis.
Eine Methode wie etwa »restoreState(IMemento memento)« gibt es jedoch nicht. Das Memento dient der »init«-Methode als Parameter (Zeilen 54 bis 57). Sie sichert seinen Inhalt zunächst nur in einer Instanzvariablen. Erst »createPartControl()« (Zeile 70) ruft die selbst geschriebene »restoreState()«-Methode (Zeilen 78 bis 89) auf, die den gespeicherten Zustand wiederherstellt.
Allein das Speichern und Laden des Zustands ändern am Verhalten der Anwendung allerdings nichts, denn »saveState()« wird nicht aufgerufen und »init« erhält statt des »IMemento« ein Null-Objekt. Abhilfe schafft die Methode »IWorkbenchConfigurer.setSaveAndRestore(boolean)«.
Eine der Klassen aus dem ersten Teil des Tutorials war eine Subklasse von »WorkbenchAdvisor«, darin gibt es eine »initialize()«-Methode, die einen »IWorkbenchConfigurer« als Argument erhält. Der Programmierer überschreibt diese Methode und setzt das Save-and-Restore-Verhalten. Danach klappt bereits alles wie erhofft: Die Fensterposition, das Layout und das letzte gewählte Verzeichnis bleiben auch nach einem Programmneustart erhalten.
Weitere Views
Zusatzinformationen zum Bild wie etwa die Exif-Daten oder den Jpeg-Kommentar liefern externe Programme. Das Beispiel verwendet dazu »exif«, »rdjpgcom«, und »identify«. Wer diese Standardprogramme aus den Paketen Exif, Jpeg und Imagemagick nicht besitzt, trägt im Code Alternativen ein.
Die Klasse »CmdResultView« ruft die externen Befehle auf und leitet deren Ausgabe in ein Textfenster um. Die einzelnen Views »ExifView«, »DetailsView« und »CommentView« beziehen daraus alle nötigen Informationen. Sie definieren lediglich das jeweilige externe Kommando und reichen es an die Superklasse weiter und sind deshalb trivial (siehe Listing 5). Weitere Views ließen sich nach demselben Schema schreiben und einbinden.
| Listing 5: »ExifView.java« |
|---|
01: package de.bablokb.epm.views;
02:
03: public class ExifView extends CmdResultView {
04:
05: public final static String VIEW_ID="ExifView",
06: CMD_NAME = "exif";
07:
08: public ExifView() {
09: super(CMD_NAME);
10: }
12: }
|
Damit enthält die Anwendung alle Views. Menü-Einträge und die Toolbar erlauben es dem Anwender, die verschiedenen Sichten zu laden. Dabei fällt aber ein unerwartetes Verhalten auf: Öffnet man nach der Auswahl eines Bildes in der Datei-Auswahl eine andere View, bleibt diese zunächst ohne Inhalt. Das liegt daran, dass die Workbench den Fokus dann auf die neue View legt. Doch sobald »DirListView« den Fokus abgibt, verliert sie auch den Status als Selection-Provider.
Erst wenn der Benutzer wieder den Verzeichnisbaum anklickt, erscheint der gewünschte Inhalt in den anderen Views. Das ist zwar logisch, aber in diesem Fall für den Benutzer der Anwendung eher verwirrend. Die einfachste Lösung, um dieses Verhalten zu ändern, besteht im Verzicht auf den Selection-Service der Workbench.
Dadurch geht allerdings die Möglichkeit verloren, beispielsweise auf eine fertige View eines Drittanbieters zuzugreifen, der einen Standard-Auswahlmechanismus in Form eines Selection-Providers implementiert. Hier ist also im Einzelfall abzuwägen, welcher Nachteil schwerer wiegt: der Verzicht auf den Selection-Service oder das für den Benutzer unerwartete Verhalten.
finally{}
Mit relativ geringem Aufwand und wenigen selbst geschriebenen Zeilen ist mit Hilfe der Eclipse-RCP eine Anwendung entstanden. Weitere Funktionen lassen sich in Form weiterer Parts problemlos hinzufügen. Das gilt auch für Editoren, die dieses Tutorial nicht behandelt hat. Denn auch bei ihnen handelt es sich lediglich um Parts, die Konzepte gleichen also den hier vorgestellten.
Aber auch technisch reizen die hier vorgestellten Funktionen die Möglichkeiten von RCP noch lange nicht aus. So bietet RCP genauso wie Eclipse ein Ressourcen-Framework. Ressourcen sind Entitäten wie Dateien, Verzeichnisse, Projekte oder Fotoarchive wie im Beispiel. Für eine integrierte Entwicklungsumgebung sind Ressourcen essenziell, doch ein Vorteil des RCP-Framework liegt gerade darin, dass die Ressourcen nicht zum Kern gehören, denn nicht alle Anwendungen benötigen sie.
Die Dokumentation liefert Informationen zu weiteren Eclipse-Features wie beispielsweise zum Update-Manager oder zur Unterstützung beim Entwickeln von Assistenten. Dieser Workshop konnte natürlich nicht alle Eclipse-Funktionen beschreiben, hat aber hoffentlich Lust auf mehr gemacht. (csc)
| Infos |
|---|
| [1] Zum Standard-Widget-Toolkit siehe Bernhard Bablok, “Aufholjagd”: Linux-Magazin 09/02, S. 107 oder [https://www.linux-magazin.de/Artikel/ausgabe/2002/09/coffee/coffee.html]
[2] Image-Viewer-Plugin: [http://www.eclipse.org/articles/Article-Image-Viewer/Image_viewer.html] [3] Tree-Viewer-Beispiel, Teil 1: [http://www-128.ibm.com/developerworks/opensource/library/os-ecgui1/] [4] Tree-Viewer-Beispiel, Teil 2: [http://www-128.ibm.com/developerworks/opensource/library/os-ecgui2/] [5] Listings: [https://www.linux-magazin.de/static/listings/magazin/2006/02/Coffeeshop/] |
| Der Autor |
|---|
| Bernhard Bablok arbeitet bei der AGIS mbH als Anwendungsentwickler. Wenn er nicht Musik hört, mit dem Radl oder zu Fuß unterwegs ist, beschäftigt er sich mit Themen rund um die Objektorientierung. Er ist unter [coffee-shop@bablokb.de] zu erreichen. |





