3.2+53.6
Das Eclipse Graphical Editing Framework (GEF)

Das Graphical Editing Framework (GEF) besteht aus mehreren Eclipse Plugins, mit deren Hilfe graphische Editoren erstellt werden können, um damit beliebige Datenmodelle zu visualisieren. Diese Bibliothek ist sehr populär, so dass QF-Test die Aufnahme und Wiedergabe von GEF Elementen bereits seit Version 2.2 unterstützt. Dies ist auch ein gutes Beispiel für die Stärke des ItemResolver Konzepts (siehe Abschnitt 53.4), denn das gef Jython Modul enthält eine Implementierung genau dieser Schnittstelle.

Das gef Modul unterstützt GEF-Editoren in generischer Weise und kann sogar mit mehreren Editoren gleichzeitig umgehen. Zwar werden auch für GMF-Anwendungen brauchbare Namen für Unterelemente vergeben, doch nicht immer sind diese hinreichend gut. Je nachdem, wie die zugrunde liegenden Modell-Klassen aussehen, bleibt noch etwas Arbeit zu tun, nämlich die Implementierung eigener Resolver, die brauchbare Namen und Werte für die Unterelemente liefern.

Aufnahme von GEF Elementen

Die eigentliche GEF Komponente ist der FigureCanvas. Dieser stellt Figures dar, die EditParts repräsentieren. Nimmt man einen Mausklick auf solch ein Element auf, registriert QF-Test nicht bloß ein 'Mausevent' für den Canvas mit entsprechenden (x,y) Werten für die Position, sondern versucht, das Objekt unter dem Mauscursor zu erkennen. Die aufgenommene 'QF-Test ID der Komponente' sieht zum Beispiel so aus:

  canvas@/Diagram/My ship/ShipLargeCargo (wine)
  canvas@Connection-2
  canvas@/Diagram/Rectangle 16329001

wobei "canvas" die 'QF-Test ID' der FigureCanvas Komponente ist, gefolgt vom Element-Index des erkannten EditPart Objekts (siehe Abschnitt 5.9). EditParts sind in einer Baumstruktur organisiert, erkennbar am Pfadtrenner '/'. Die Namen der einzelnen Elemente werden folgendermaßen abgeleitet:

  • Der Elementname ist getModel().toString(), wenn darin kein Hashwert (z. B. NodeImpl@2d862) enthalten ist.
  • QF-Test versucht, im Modell einen Namen für das Element zu finden ("My ship" in den obigen Beispielen).
  • Die Klasse zusammen mit einer Beschreibung wird aufgenommen, etwa "ShipLargeCargo (wine)".
  • Wenn es keine Beschreibung aber mehrere Elemente einer Klasse gibt, wird an diese ein Index angehangen, z. B. "Connection-2" für die dritte Connection.
  • Der Wurzelknoten heißt stets "Diagram".

Man kann sich denken, dass die generierten Namen nicht immer sinnvoll sind. Elemente könnten gelöscht werden und aufgezeichnete Indizes damit ihre Gültigkeit verlieren. Oder der Elementname ist instabil wie bei "Rectangle 16329001" aus dem GEF Shapes Beispiel: Die Zahl ist rein zufällig und wird bei einem Neustart der Anwendung neu ermittelt. Drei Möglichkeiten gibt es, um solche Probleme zu lösen:

  • Anstatt mit einem textuellen Index zu arbeiten, kann man es mit einem numerischen versuchen. Dazu setzt man in den Aufnahmeoptionen das Format für die Unterelemente auf den Wert 'Zahl' (siehe Abschnitt 40.2.4). Allerdings ist diese Lösung nicht sehr befriedigend, denn ein numerischer Index wie /0/1 sagt nichts über das Element aus.
  • Man könnte sich an die Entwickler wenden und diese davon zu überzeugen versuchen, eine brauchbare Implementierung der Methode toString() für das Datenmodell eines Elements zu liefern. Das würde Ihnen das Leben leicht machen, aber eben nur, wenn die Entwickler kooperativ sind.
  • Implementierung der ItemNameResolver2 Schnittstelle. Das ist nicht ganz einfach, doch leider oft unumgänglich. Dieses Thema wird im nächsten Abschnitt behandelt.

Implementierung eines ItemNameResolver2 für GEF

Wie in Abschnitt 53.1 ausgeführt, ist ItemNameResolver2 die Schnittstelle, um Namen für Elemente zu ändern oder überhaupt erst zu definieren. Ein erster Ansatz zur Implementierung ist das folgende Jython 'SUT-Skript', das unter 'Extrasequenzen' eingefügt werden kann:

def getItemName(canvas, item, name):
    print "name: %s" %name
    print "item: %s" %(item.__class__)
    model = item.getModel()
    print "model: %s" %(model.__class__)

resolvers.addItemNameResolver2("myGefItemNames", getItemName,
    "org.eclipse.draw2d.FigureCanvas")
Beispiel 53.35:  Ein erster GEF ItemNameResolver2

Um die Installation des Resolvers zu vereinfachen, wird das in Abschnitt 53.1 beschriebene resolvers Modul verwendet. Der Resolver wird auf die Klasse FigureCanvas registriert, die EditParts als Unterlemente enthält. Der von QF-Test generierte Name wird der Funktion getItemName() beim Aufruf als drittes Argument übergeben. Wenn man das Skript nun ausführt und dann den Aufnahmeknopf drückt, werden beim Überfahren der Elemente mit der Maus - man sollte zuvor ein paar davon erzeugt haben - Informationen im Terminal von QF-Test ausgegeben, etwa wie folgt:

  name: Rectangle 16329001
  item: org.eclipse.gef.examples.shapes.parts.ShapeEditPart
  model: org.eclipse.gef.examples.shapes.model.RectangularShape

Abgesehen von diesen Ausgaben ist der Resolver ohne Funktion. Die Frage ist nun: Gibt es im Modell irgendeine Eigenschaft oder Methode, die einen vernüftigen Namen für das Element liefert? Für das GEF Shapes Beispiel lautet die Antwort: Nein. Hoffentlich sind Sie mit Ihrer Anwendung in einer besseren Lage. Um das herauszufinden, fügen Sie der Funktion getItemName() die Zeile

  print dir(model)

hinzu und führen das Skript erneut aus. Nun werden beim Bewegen der Maus über die Elemente (im Aufnahmenmodus) auch die Methoden des Modells angezeigt. Mit etwas Glück tauchen hier Methoden wie getId() oder getLabel() auf, so dass man einen Resolver wie den folgenden implementieren kann.

def getItemName(canvas, item, name):
    model = item.getModel()
    return model.getId()

resolvers.addItemNameResolver2("myGefItemNames", getItemName,
    "org.eclipse.draw2d.FigureCanvas")
Beispiel 53.36:  Ein einfacher ItemNameResolver2

Zurück zum GEF Shapes Beispiel, wo es solche Methoden nicht gibt. Hier sind nur Informationen über die Geometrie verfügbar, doch das ist wenig hilfreich. Zumindest lassen sich aber Rechtecke und Ellipsen unterscheiden. Um die Elementnamen eindeutig zu machen, fügen wir einfach den Index der Figur (des Kind-Knotens) an, wie im folgenden Resolver gezeigt:

def getItemName(canvas, item, name):
    name = None
    shapes = "org.eclipse.gef.examples.shapes"
    diagrammEditPart = shapes + ".parts.DiagramEditPart"
    shapeEditPart = shapes + ".parts.ShapeEditPart"
    connectionEditPart = shapes + ".parts.ConnectionEditPart"
    ellipticalShape = shapes + ".model.EllipticalShape"
    rectangularShape = shapes + ".model.RectangularShape"
    if qf.isInstance(item, shapeEditPart):
        siblings = item.getParent().getChildren()
        for i in range(len(siblings)):
            if (item == siblings[i]):
                if qf.isInstance(item.getModel(), ellipticalShape):
                    name = "Ellipse " + str(i)
                elif qf.isInstance(item.getModel(),
                                                 rectangularShape):
                    name = "Rectangle " + str(i)
    elif qf.isInstance(item, connectionEditPart):
        source = item.getSource()
        target = item.getTarget()
        sourceName = getItemName(canvas, source, str(source.getModel()))
        targetName = getItemName(canvas, target, str(target.getModel()))
        name = "Connection " + sourceName + " " + targetName
    elif qf.isInstance(item, diagrammEditPart):
        name = "Diagram"
    return name

resolvers.addItemNameResolver2("shapesItemNames", getItemName,
    "org.eclipse.draw2d.FigureCanvas")
Beispiel 53.37:  Ein ItemNameResolver2 für GEF Shapes

Mit diesem Resolver wird der Element-Index zu sowas wie

  /Diagram/Rectangle 1

wobei die abschließende Zahl der Index des Kind-Knotens ist. Diese Implementierung liefert auch Namen für die Verbindungen, indem getItemName() rekursiv für das Quell- und Zielelement aufgerufen wird. Die Typüberprüfung erfolgt mit der Methode qf.isInstance (siehe Abschnitt 49.6), wodurch einem das Importieren der GEF-Klassen (was nicht ganz einfach ist) erspart bleibt.

Sobald der Resolver funktionstüchtig ist, sollte man das Skript in die 'Vorbereitung' Sequenz packen, direkt hinter den 'Warten auf Client' Knoten. Auf diese Weise wird der Resolver automatisch registriert, wenn man das SUT startet.

Implementierung eines ItemValueResolver2 für GEF

Üblicherweise besteht ein GEF Editor aus zwei Teilen. Bislang hatten wir uns auf den Canvas konzentriert, in den die Figuren gezeichnet werden. Nun werfen wir einen Blick auf die Palette, in der die Art der zu zeichnenden Figur ausgewählt wird (z. B. Rechteck, Ellipse oder Verbindung). Die Einträge sehen zwar aus wie Buttons, doch tatsächlich ist die Palette ebenfalls ein FigureCanvas. Erfreulicherweise funktioniert hier alles, ohne dass besondere Vorkehrungen getroffen werden müssten, d. h. ohne einen ItemNameResolver2 zu implementieren. Wenn man etwa auf den 'Rectangle' Button klickt, erkennt QF-Test ein

  /Palette Root/Palette Container (Shapes)/Palette Entry (Rectangle)

Element. Was wird wohl passieren, wenn man einen 'Object value' Check für diesen Button aufnimmt? Man könnte erwarten, den Text 'Rectangle' zu erhalten, doch tatsächlich ist der Wert des Elements

  Palette Entry (Rectangle)

Der Grund dafür ist, dass Name und Wert eines Elements normalerweise gleich sind. Um dieses Verhalten zu ändern und selbstdefinierte Werte zu erhalten, muss ein ItemValueResolver2 implementiert werden. Diese Schnittstelle ist der ItemNameResolver2 Schnittstelle ganz ähnlich. Für die Palette können wir etwa das Folgende codieren:

def getItemValue(canvas, item, value):
    value = None
    paletteEditPart = \
        "org.eclipse.gef.ui.palette.editparts.PaletteEditPart"
    if qf.isInstance(item, paletteEditPart):
        value = item.getModel().getLabel()
    return value

resolvers.addItemValueResolver2("shapesItemValues", getItemValue,
    "org.eclipse.draw2d.FigureCanvas")
Beispiel 53.38:  Ein ItemValueResolver2 für die GEF Shapes Palette

Die Methode getLabel() liefert den Text des Elements, so wie er in der Palette angezeigt wird.