Heute soll es um meinen speziellen Freund, den „TableViewer“ gehen, speziell darum, wie man einen TableViewer implementiert, der unter Umständen etwas komplexer ist (mit Comboboxen etc…).
Witzigerweise ist es nämlich wie mit beinahe allem, es gibt hierzu keine brauchbaren Tutorials, also muss man sich selbst helfen und die try-and-error-Variante fahren.
Da ich mich eine Zeitlang wirklich enorm über dieses Thema geärgert habe, habe ich beschlossen, dieses Thema in einem kleinen Eintrag zu erläutern (nicht zuletzt für mich selbst, damit ich beim nächsten Mal weiß wo ich nachzuschauen habe).
Als Beispiel implementieren wir einfach mal einen TableViewer, der einer Liste einer bestimmten Person aus einer Liste eine bestimmte Rolle zuordnen kann.
Hierzu bauen wir uns zuerst wieder das typische Personen-Modell zusammen:
Klasse Person:
public class Person {
private String name;
private Rolle rolle;
public Person(String name, Rolle rolle) {
super();
this.name = name;
this.rolle = rolle;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Rolle getRolle() {
return rolle;
}
public void setRolle(Rolle rolle) {
this.rolle = rolle;
}
public String toString() {
return getName() + getRolle().toString();
}}
Enum Rolle:
public enum Rolle {
ADMIN, CUSTOMER, USER
}
Klasse PersonService:
public class PersonService implements IPersonService {
private static PersonService service = new PersonService();
private IObservableList persons = new WritableList();
public PersonService() {
persons.add(new Person(„Hans“, Rolle.ADMIN));
persons.add(new Person(„Fritz“, Rolle.CUSTOMER));
persons.add(new Person(„Gerhard“, Rolle.USER));
persons.add(new Person(„Harald“, Rolle.ADMIN));
}
@SuppressWarnings(„unchecked“)
public List<Person> getPersons() {
return (List<Person>) Collections.checkedList(persons, Person.class);
}
public IObservableList observePersons() {
return persons;
}
public void addPerson(Person person) {
persons.add(person);
}
public static PersonService getInstance() {
return service;
}
public List<Rolle> getRollen() {
return Arrays.asList(Rolle.values());
}
}
Zusätzlich bauen wir einen View, der eine Tabelle bereitstellt, die:
- Alle User anzeigt
- Die Möglichkeit bietet, die Rolle eines Users zu ändern
- Die Möglichkeit bietet, neue User anzulegen
- Mit Databinding arbeitet
Zunächst mal bearbeiten wir die createPartControl-Methode des Views, und legen einen TAbleViewer an:
final TableViewer viewer = new TableViewer(container);
viewer.getTable().setHeaderVisible(true);
viewer.getTable().setLinesVisible(true);
Der TableViewer soll 2 Columns haben (Person und Rolle):
TableViewerColumn person = new TableViewerColumn(viewer, SWT.BORDER);
person.getColumn().setText(„Person“);
person.getColumn().setWidth(100);
TableViewerColumn rolle = new TableViewerColumn(viewer, SWT.BORDER);
rolle.getColumn().setText(„Rolle“);
rolle.getColumn().setWidth(100);
Nun setzen wir den Label- und ContentProvider. Da wir mit DataBinding arbeiten, bietet sich der ObservableListContenentProvider an (der übrigens ausgezeichnet funktioniert!!)
Damit das jedoch richtig funktioniert, passen wir den PersonService folgendermaßen an:
public class PersonService implements IPersonService {
private IObservableList persons = new WritableList();
public PersonService() {
persons.add(new Person(„Hans“, Rolle.ADMIN));
persons.add(new Person(„Fritz“, Rolle.CUSTOMER));
persons.add(new Person(„Gerhard“, Rolle.USER));
persons.add(new Person(„Harald“, Rolle.ADMIN));
}
@SuppressWarnings(„unchecked“)
public List<Person> getPersons() {
return (List<Person>)Collections.checkedList(persons, Person.class);
}
public IObservableList observePersons(){
return persons;
}
public void addPerson(Person person) {
persons.add(person);
}
public static PersonService getInstance() {
return new PersonService();
}
}
Jetzt können wir die ObservableList aus dem Service direkt als Input in den TableViewer verwenden, und alle Änderungen an der PersonenListe werden sofort in der Tabelle widergespiegelt.
viewer.setLabelProvider(new LabelProvider());
viewer.setContentProvider(new ObservableListContentProvider());
viewer.setInput(PersonService.getInstance().observePersons());
Startet man die Anwendung jetzt, sieht das ungefähr so aus:

Zumindest ist es eine Tabelle…
Ok, wir müssen natürlich definieren, in welche Spalte welche Werte angezeigt werden sollen, das erledigt der LabelProvider, also ändern wir das folgendermaßen ab:
viewer.setLabelProvider(new ITableLabelProvider() {
@Override
public Image getColumnImage(Object element, int columnIndex) {
return null;
}
@Override
public String getColumnText(Object element, int columnIndex) {
switch (columnIndex) {
case 0:
return element.toString();
case 1:
return ((Person) element).getRolle().toString();
default:
return „“;
}
}
@Override
public void addListener(ILabelProviderListener listener) {
// TODO Auto-generated method stub
}
@Override
public void dispose() {
// TODO Auto-generated method stub
}
@Override
public boolean isLabelProperty(Object element, String property) {
// TODO Auto-generated method stub
return false;
}
@Override
public void removeListener(ILabelProviderListener listener) {
// TODO Auto-generated method stub
}
});
Noch eleganter wäre, nicht dem TableViewer an sich, sondern jeder Column einen eigenen LabelProvider zur Verfügung zu stellen.
column.setLabelProvider(new ColumnLabelProvider(){
…
}
Wie dem auch sei, Neuer Versuch:

Ok, was wir jetzt aber wollen, ist zum Einen, dass man den Namen der Person editieren kann (durch einfache Eingabe) und zum Anderen wollen wir die Rolle über eine ComboBox auswählen.
Jetzt wirds spannend, weil hier die Dokumentation im Web mehr als spärlich wird.
Eclipse bietet hierzu die Klasse „EditingSupport“, der JavaDoc Kommentar ist hierzu mehr als sprechend:
„EditingSupport is the abstract superclass of the support for cell editing.“
Spitze! und so zieht sich das durch, man findet kein!! ich wiederhole : KEIN wirklich brauchbares Beispiel im Web, bis heute!!
Ok, zurück zum Thema:
Was das Editieren von Columns in einer Tabelle ermöglicht sind sogenannte CellEditoren. Diese Editoren können Text-, COmbo-,Checkbox etc.. sein.
Was uns hier interessiert ist zum Einen der Texteditor zum Eingeben des Personennamens und ein Comboeditor zum Eingeben der Rolle.
Zunächst die Namenseingabe:
person
.setEditingSupport(new ObservableValueEditingSupport(viewer,
ctx) {
private TextCellEditor textEditor;
@Override
protected IObservableValue doCreateCellEditorObservable(
CellEditor cellEditor) {
return SWTObservables.observeText((Text) cellEditor
.getControl(), SWT.Modify);
}
@Override
protected IObservableValue doCreateElementObservable(
Object element, ViewerCell cell) {
return BeansObservables.observeValue(element, „name“);
}
@Override
protected CellEditor getCellEditor(Object element) {
if (textEditor == null) {
textEditor = new TextCellEditor((Composite) viewer
.getControl());
}
return textEditor;
}
});
Man sieht, im Prinzip muss man nur 2 Methoden implementieren, die jeweils ein ObservableValue für den CellEditor (also entweder ein Text oder eine Combo) bereitstellt, und eine Methode, die ein ObservableValue für das beabachtete Objekt (in diesem Fall ein Person-Objekt bereitstellt).
Klickt man jetzt auf ein Feld in der Tabelle ändert sich dieses in ein Text-Feld und man kann den Wert editieren!

Problem ist, drückt man jetzt auf Enter wird zwar der Wert im Modell geändert, nicht jedoch automatisch der Viewer refreshed, d.h. dass weiterhin der alte Wert angezeigt wird, wenn man sich nicht im Edit-Modus befindet.
Hierzu kann man folgenden Listener registrieren, der das bewerkstelligt.
viewer.getColumnViewerEditor().addEditorActivationListener(
new ColumnViewerEditorActivationListener() {
@Override
public void afterEditorActivated(
ColumnViewerEditorActivationEvent event) {
// TODO Auto-generated method stub
}
@Override
public void afterEditorDeactivated(
ColumnViewerEditorDeactivationEvent event) {
viewer.refresh();
}
@Override
public void beforeEditorActivated(
ColumnViewerEditorActivationEvent event) {
// TODO Auto-generated method stub
}
@Override
public void beforeEditorDeactivated(
ColumnViewerEditorDeactivationEvent event) {
// TODO Auto-generated method stub
}
});
So, weiter gehts mit der Rollen-Column, hier haben wir im Prinzip nochmals das Gleiche Prozedere, nur dass wir hier einen ComboViewer in einem TableViewer verbauen wollen.
rolle.setEditingSupport(new ObservableValueEditingSupport(viewer, ctx) {
private ComboBoxViewerCellEditor cellEditor;
@Override
protected IObservableValue doCreateCellEditorObservable(
CellEditor cellEditor) {
return ViewersObservables
.observeSingleSelection(((ComboBoxViewerCellEditor) cellEditor)
.getViewer());
}
@Override
protected IObservableValue doCreateElementObservable(
Object element, ViewerCell cell) {
return BeansObservables.observeValue(element, „rolle“);
}
@Override
protected CellEditor getCellEditor(Object element) {
if (cellEditor == null) {
cellEditor = new ComboBoxViewerCellEditor(
(Composite) viewer.getControl());
cellEditor.setContenProvider(new ArrayContentProvider());
cellEditor.setLabelProvider(new LabelProvider());
cellEditor.setInput(Rolle.values());
}
return cellEditor;
}
});

Ok, zuletzt brauchen wir noch die FUnktionalität, eine neue Person anzulegen.
Hierzu hätte ich gerne ein Contextmenü. DAs ist ganz einfach realisiert:
private void initContextMenu() {
MenuManager manager = new MenuManager();
viewer.getControl().setMenu(
manager.createContextMenu(viewer.getControl()));
manager.add(new Action(„Person hinzufügen“) {
@Override
public void run() {
PersonService.getInstance().addPerson(
new Person(„“, Rolle.USER));
viewer.refresh();
}
});
}

By the way, es hindert uns hier auch nichts daran, dieses Context Menü über das Menüframework zu erweitern.
Hierzu müssen wir lediglich das Menü über die ViewSite registrieren, damit wir global darauf zugreifen können.
getSite().registerContextMenu(mgr,null);
Zu beachten ist hier lediglich, dass innerhalb eines Parts jedes ContextMenu unter der ID des Parts registriert wird.
Um das Context Menü deklarativ zu bevölkern braucht man jetzt nur noch etwas wie:
<menuContribution
locationURI=“popup:de.md.commands.menuview“>
<command
commandId=“de.md.commands.helloworld“
label=“Hallo Popup“
style=“push“>
</command>
</menuContribution>
Mit einem Viewer, der den EditorSupport verwendeet, hat man das Problem, dass dieser voraussetzt, dass
der TableViewer mit dem Flag „SWT.FULL_SELECTION“ erzeugt wurde.
Oft ist es aber nicht erwünscht, dass man die ganze Zeile selektiert, sondern oft möchte man nur eine einzelne
Zelle selektieren (am besten noch so wenig wie möglich mit der Maus sondern per Tastatur).
Hier stösst man schnell an die Grenzen des mit dem normalen Tableviewer machbaren.
Doch wie so oft gibt es hier einige mehr als nützliche Klassen, die ich im folgenden mal kurz dokumentieren möchte.
final TableViewerFocusCellManager focusCellMgr = new TableViewerFocusCellManager(
viewer, new FocusCellOwnerDrawHighlighter(viewer));
Der TableViewerFocusCellManager bietet die Möglichkeit, das Highlighting der Zellen anzupassen, damit nicht immer
die gesamte Zeile markiert ist, sondern nur diejenige, die wir gerade auch wirklich selektiert haben. Das macht der
FocusCellOwnerDrawHighlighter für uns. Die genauere Funktionsweise dahinter braucht uns zunächst nicht zu interessieren,
da diese Zeile schon alles macht, was wir brauchen.
Weiter gehts:
ColumnViewerEditorActivationStrategy actStrategy = new ColumnViewerEditorActivationStrategy(
viewer) {
protected boolean isEditorActivationEvent(
ColumnViewerEditorActivationEvent event) {
return event.eventType == ColumnViewerEditorActivationEvent.TRAVERSAL
|| event.eventType == ColumnViewerEditorActivationEvent.MOUSE_DOUBLE_CLICK_SELECTION
|| event.eventType == ColumnViewerEditorActivationEvent.MOUSE_CLICK_SELECTION
|| (event.eventType == ColumnViewerEditorActivationEvent.KEY_PRESSED);
}
};
Hier wirds dann interessant, denn oft hat man von Fachbereichen die Anforderung, ich möchte aber, dass wenn ich hier auf Enter drücke (oder hoch, oder runter, oder was ganz anderes), dass dann die Combobox aufgeht….
Hierfür gibts so etwas wie eine ColumnViewerEditorActivationStrategy.
Die hat eine CallbackMethode isEditorActivationEvent, die im Prinzip bei jeder User-Interaktion überprüft, ist das Event gewünscht um den Viewer
zu triggern? Wenn ja, CellEditor aktivieren, wenn nein… weiter.
Im obigen Beispiel verwenden wir das Durchlaufen mit Tab (Traversal), MouseEvents und KeyEvents als Activation Events für den Celleditor.
Das ganze binden wir jetzt noch an unseren TableViewer
TableViewerEditor.create(viewer, focusCellMgr, actStrategy,
ColumnViewerEditor.TABBING_HORIZONTAL
| ColumnViewerEditorActivationEvent.TRAVERSAL
| ColumnViewerEditor.KEYBOARD_ACTIVATION);
Hier hat man zusätzlich noch die Möglichkeit, über Flags zu konfigurieren, wie die einzelnen Zellen durchlaufen werden sollen.
ColumnViewerEditor.TABBING_HORIZONTAL
Bei durchlaufen mit Tab soll immer eine Zelle horizontal weitergesprungen werden.
ColumnViewerEditor.KEYBOARD_ACTIVATION
KeyboardActivation einschalten
ColumnViewerEditorActivationEvent.TRAVERSAL
CellAktivierung beim Durchlaufen einschalten.
Das ist so einfach!! und funktioniert wunderbar, nur ein Problem gibts damit. Wenn man das Horizontale Tabbing einschaltet, dann kommt man
mit Tab nie wieder aus dem TableEditor heraus. Wenn jemand hierfür eine brauchbare Lösung hat, bitte immer her damit!!