3.6.3. Die Schnittstelle IContainer

Ein Container ist eine Komponente, welche eine Liste von Controls enthält. Die Schnittstelle IContainer erweitert IComponent und somit auch IWidget. Die Controls eines Containers werden mit Hilfe eines Layouters angeordnet. Es folgt eine Beschreibung der wichtigsten Methoden.

Hinzufügen von Controls mit Hilfe von BluePrints

Folgende Methoden können verwendet werden, um Controls mit Hilfe eines BluePrints zu einem Container hinzuzufügen:

    <WIDGET_TYPE extends IControl> WIDGET_TYPE add(
        int index,
        IWidgetDescriptor<? extends WIDGET_TYPE> descriptor,
        Object layoutConstraints);
        
    <WIDGET_TYPE extends IControl> WIDGET_TYPE add(
        int index,
        IWidgetDescriptor<? extends WIDGET_TYPE> descriptor);

    <WIDGET_TYPE extends IControl> WIDGET_TYPE add(
        IWidgetDescriptor<? extends WIDGET_TYPE> descriptor, 
        Object layoutConstraints);

    <WIDGET_TYPE extends IControl> WIDGET_TYPE add(
        IWidgetDescriptor<? extends WIDGET_TYPE> descriptor);

Ein Control wird hinzugefügt, indem man ein BluePrint hinzufügt. Jedes BluePrint implementiert die Schnittstelle IWidgetDescriptor für einen konkreten WIDGET_TYPE. Die add() Methode liefert das erzeugte Control als Rückgabewert.

Der Index bestimmt, an welcher Stelle das Control in die interne Liste eingefügt werden soll. Wird kein Index angegeben, wird das Control am Ende hinzugefügt.

Die layoutConstraints werden vom verwendeten Layouter ausgewertet und sind somit layout spezifisch. Da ein Layouter nicht immer Constraints benötigt, können diese auch weggelassen werden. Zudem ist es möglich, die Constraints nachträglich auf dem Control zu setzen.

Es folgt ein Beispiel für die Verwendung der add() Methode:

  1      final String growCC = "growx, w 0::";
  2          
  3      final IInputField<Date> dateField = container.add(BPF.inputFieldDate(), growCC);
  4  
  5      final IComboBoxSelectionBluePrint<Gender> genderCmbBp = BPF.comboBoxSelection(Gender.values());
  6      final IComboBox<Gender> genderCmb = container.add(genderCmbBp, growCC);
  7  
  8      final IButton okButton = container.add(BPF.buttonOk());
  9  
 10      final ICheckBoxBluePrint licenceCbBp = BPF.checkBox().setText("Accept");
 11      final ICheckBox licenceCb = container.add(2, licenceCbBp);

In Zeile 3-8 wird ein Datumsfeld, eine ComboBox für die Enum Gender sowie ein OkButton hinzugefügt. Das Datumsfeld sowie die Combobox haben ein MigLayout Constraint, dass sie den ganzen horizontalen Platz ausfüllen sollen (growx).

In Zeile 10-11 wird dann eine Checkbox an Position zwei (zwischen Combobox und Botton) eingefügt.

Hinzufügen von eigenen (Custom) Controls

Will man selbst Controls erstellen, hat man folgende Möglichkeiten:

  • Man erstellt eine eigene Widget Bibliothek, siehe Erstellung eigener Widget Bibliotheken. Dabei wird auch ein BluePrint für das Control definiert welches man wie weiter oben beschrieben verwenden kann. Dies ist das empfohlene Vorgehen, wenn das Control wiederverwendet werden soll, da ein solch erstelltes Widget alle Vorzüge von jowidgets bietet.

  • Man leitet von einem Basis Widget ab, um ein Widget zu kapseln.

  • Man kapselt ein Widgets mit Hilfe eines ICustomWidgetCreator.

Folgende Methoden können verwendet werden, um Controls mit Hilfe eines ICustomWidgetCreator zu einem Container hinzuzufügen:

    <WIDGET_TYPE extends IControl> WIDGET_TYPE add(
        int index, 
        ICustomWidgetCreator<WIDGET_TYPE> creator, 
        Object layoutConstraints);
        
    <WIDGET_TYPE extends IControl> WIDGET_TYPE add(
        int index, 
        ICustomWidgetCreator<WIDGET_TYPE> creator);

    <WIDGET_TYPE extends IControl> WIDGET_TYPE add(
        ICustomWidgetCreator<WIDGET_TYPE> creator, 
        Object layoutConstraints);

    <WIDGET_TYPE extends IControl> WIDGET_TYPE add(
        ICustomWidgetCreator<WIDGET_TYPE> creator);

Die Verwendung des index und der layoutConstraints ist analog wie beider Verwendung von BluePrints.

Es folgt ein Beispiel für eine Implementierung der ICustomWidgetCreator Schnittstelle:

  1  public final class ErrorLabel implements ICustomWidgetCreator<ILabel> {
  2  
  3      private final String errorText;
  4  
  5      public ErrorLabel(final String errorText) {
  6          this.errorText = errorText;
  7      }
  8  
  9      @Override
 10      public ILabel create(final ICustomWidgetFactory widgetFactory) {
 11          final ILabelBluePrint labelBp = BPF.label();
 12          labelBp.setText(errorText).setIcon(IconsSmall.ERROR);
 13          final ILabel label = widgetFactory.create(labelBp);
 14          return label;
 15      }
 16  }

In Zeile 13 wird die ICustomWidgetFactory verwendet, um ein ILabel Widget zu erzeugen.

Das ErrorLabel Control kann nun wie folgt einem Container hinzugefügt werden:

  1      final ILabel label = container.add(new ErrorLabel("Ups!!!"));

Das folgende Beispiel Implementiert ein Control, welches den gesamten Platz mit schwarzer Farbe ausfüllt:

  1  public final class BlackBox implements ICustomWidgetCreator<IControl> {
  2  
  3      @Override
  4      public IControl create(final ICustomWidgetFactory widgetFactory) {
  5          final ICanvas canvas = widgetFactory.create(BPF.canvas());
  6  
  7          canvas.addPaintListener(new IPaintListener() {
  8              @Override
  9              public void paint(final IPaintEvent paintEvent) {
 10                  final IGraphicContext gc = paintEvent.getGraphicContext();
 11                  final Rectangle bounds = gc.getBounds();
 12                  gc.setBackgroundColor(Colors.BLACK);
 13                  gc.clearRectangle(
 14                      bounds.getX(), 
 15                      bounds.getY(), 
 16                      bounds.getWidth(), 
 17                      bounds.getHeight());
 18              }
 19          });
 20  
 21          return canvas;
 22      }
 23  }

Hier wird in Zeile 5 die ICustomWidgetFactory verwendet, um ein Canvas zu erzeugen welches im IPaintListener ab Zeile 7 den gesammten Bereich mit schwarzer Farbe ausfüllt.

Das BlackBox Control kann dann die folgt einem Container hinzugefügt werden:

  1      final IControl = frame.add(new BlackBox(), "growx, w 0::");
Entfernen von Controls

Controls können entweder auf dem Container mittels der folgenden Methoden entfernt werden:

    boolean remove(IControl control);
    
    void removeAll();

Oder man ruft auf dem Control selbst die dispose() Methode auf:

  1      final IInputField<Date> dateField = container.add(BPF.inputFieldDate(), growCC);
  2      dateField.dispose();

In Zeile 1 wird ein DateField zum Container hinzugefügt, in Zeile 2 wird es wieder entfernt.

Im folgenden Beispiel wird der gleiche Effekt erzielt, nur dass das Entfernen über den Container stattfindet.

  1      final IInputField<Date> dateField = container.add(BPF.inputFieldDate(), growCC);
  2      container.remove(dateField);

In beiden Fällen wird das DateField disposed und kann anschließend nicht mehr verwendet werden.

Verwendung von Layouts

Folgende Methoden können verwendet werden, um das Layout für den Container festzulegen.

    void setLayout(ILayoutDescriptor layoutDescriptor);

    <LAYOUT_TYPE extends ILayouter> LAYOUT_TYPE setLayout(ILayoutFactory<LAYOUT_TYPE> layoutFactory);

Das Layout kann entweder über ein ILayoutDescriptor oder über eine ILayoutFactory festgelegt werden. Der resultierende ILayouter ist dafür verantwortlich, die Controls eines Containers in der richtigen Größe an die richtige Position zu Zeichnen.

Mittels der folgenden Methoden kann der Layoutprozess manuell angestoßen werden:

    void layoutBegin();

    void layoutEnd();
    
    void layout();

    void layoutLater();

Die Methode layout() berechnet das Layout für den Container neu.

Die Methode layoutBegin() kann verwendet werden, um anzuzeigen, dass mehrere Änderungen des Containers folgen, welche das Layout beeinflussen. Dadurch wird verhindert, dass der Container neu gezeichnet wird, bis die Methode layoutEnd() aufgerufen wird. Diese kann verwendet werden, um Flackereffekte zu vermeiden.

Die Methode layoutLater() führt ein Layout in einem späteren UI Event durch. Mehrere Aufrufe der Methode layoutLater() im aktuellen Event bewirken dabei nur einen späteren Aufruf der Methode layout().

Damit lässt sich ein in der Praxis gelegentlich auftretendes Problem lösen, welches bewirken kann, dass viele unabhängige Änderungen eines Containers in einem Ui Event ein sofortiges layout() nach sich ziehen. Dann wird in einem Event mehrfach ein layout() durchgeführt, obwohl das Ergebnis nicht mehr als ein Mal pro UI Event sichtbar werden kann. De facto wird also nur der letzte layout() Aufruf für den Nutzer sichtbar, die vielen anderen layout() Aufrufe haben dabei unnötiger Weise den UI Thread blockiert. Durch den mehrfachen Aufruf der Methode layoutLater() anstatt layout() tritt das Problem dann nicht mehr auf, weil nur ein mal zu einem späteren Zeitpunkt ein layout() durchgeführt wird.

Um einen eigenen Layouter zu implementieren, sind die folgenden Methoden relevant:

    Rectangle getClientArea();

    Dimension computeDecoratedSize(Dimension clientAreaSize);

Die Methode getClientArea() gibt genau den Bereich zurück, welcher für das Zeichnen der Controls zur Verfügung steht.

Die Methode computeDecoratedSize() berechnet für eine gewünschte clientAreaSize die notwendige Größe des gesamten Containers.

Für weitere Details zu diesem Thema sei auf den Abschnitt Layouting verwiesen.

Tab Order

Um die Reihenfolge festzulegen, in welcher durch die Controls durch Verwendung der Tabulator Taste navigiert wird, können die folgenden Methoden verwendet werden:

    void setTabOrder(Collection<? extends IControl> tabOrder);

    void setTabOrder(IControl... controls);

Wird null übergeben, wird die default Reihenfolge verwendet

Zugriff auf die Controls eines Containers

Mit Hilfe der folgenden Methode erhält man alle derzeit vorhanden Kind Controls eines Containers:

    List<IControl> getChildren();

Dabei handelt es sich um eine nicht modifizierbare (unmodifieable) Kopie der aktuellen Liste der Kind Controls. Dadurch ist zum Beispiel das folgende möglich, ohne eine ConcurrentModificationException zu bekommen:

  1      container.layoutBegin();
  2      for (final IControl childControl : container.getChildren()) {
  3          if (!filter.accept(childControl)) {
  4              container.remove(childControl);
  5          }
  6      }
  7      container.layoutEnd();

Um sich darüber informieren zu lassen, wenn Kinder hinzugefügt oder entfernt werden, kann ein IContainerListener verwendet werden:

    void addContainerListener(IContainerListener listener);

    void removeContainerListener(IContainerListener listener);

Dieser hat die folgenden Methoden:

    void afterAdded(IControl control);

    void beforeRemove(IControl control);

Die Methode afterAdded() wird aufgerufen, nachdem ein Control zum Container hinzugefügt wurde. Die Methode beforeRemove() wird aufgerufen, bevor ein Control vom Container entfernt wird. Dabei spielt es keine Rolle, ob das Control mittels dispose() oder mittels remove() entfernt wird. Das Control ist zum Zeitpunkt des Aufrufs der Methode beforeRemove() noch nicht disposed.

Rekursiver Zugriff auf die Controls eines Containers

Ein Container kann Controls enthalten, welche selbst wiederum Container sein können. Dies ist zum Beispiel mit Hilfe eines Composite möglich, welches ein IContainer und ein IControl zugleich ist.

In der Praxis kann es vorkommen, dass zu einem eigenen Control Controls hinzugefügt werden, deren interner Aufbau einem über die Zeit (Controls können kommen und gehen) verborgen ist. Dennoch hätte man eventuell gerne Kenntnis über alle Kinder eines Containers (auch rekursiv), zum Beispiel um einen Listener hinzuzufügen, welcher ein PopupMenu öffnet.

Zu diesem Zweck kann eine IContainerRegistry verwendet werden, welche dem Container hinzugefügt wird:

    void addContainerRegistry(IContainerRegistry registry);

    void removeContainerRegistry(IContainerRegistry registry);

Die Schnittstelle IContainerRegistry hat die folgenden Methoden:

    void register(IControl control);

    void unregister(IControl control);

Die Methode register() wird für alle bereits vorhandenen (zum Zeitpunkt des Aufrufes von addContainerRegistry()) Controls sowie für alle in der Zukunft hinzugefügten Controls aufgerufen. Das ganze wird rekursiv durchgeführt, wodurch auch alle Kind Controls sowie deren Kinder usw. erfasst werden.

Werden Controls (oder Kind Controls, usw.) entfernt, wird die Methode unregister() aufgerufen. Kind Controls welche bereits vor dem Aufruf der Methode addContainerRegistry() entfernt wurden, werden dabei nicht berücksichtigt.

Zum rekursiven hinzufügen von Listenern zu einem Container und dessen Kindern stehen folgende Methoden zur Verfügung:

    void addComponentListenerRecursive(IListenerFactory<IComponentListener> listenerFactory);

    void removeComponentListenerRecursive(IListenerFactory<IComponentListener> listenerFactory);

    void addFocusListenerRecursive(IListenerFactory<IFocusListener> listenerFactory);

    void removeFocusListenerRecursive(IListenerFactory<IFocusListener> listenerFactory);

    void addKeyListenerRecursive(IListenerFactory<IKeyListener> listenerFactory);

    void removeKeyListenerRecursive(IListenerFactory<IKeyListener> listenerFactory);

    void addMouseListenerRecursive(IListenerFactory<IMouseListener> listenerFactory);

    void removeMouseListenerRecursive(IListenerFactory<IMouseListener> listenerFactory);

    void addPopupDetectionListenerRecursive(IListenerFactory<IPopupDetectionListener> listenerFactory);

    void removePopupDetectionListenerRecursive(IListenerFactory<IPopupDetectionListener> listenerFactory);

Die Schnittstelle IListenerFactory sieht wie folgt aus:

public interface IListenerFactory<LISTENER_TYPE> {

    LISTENER_TYPE create(IComponent component);
}

Durch das Hinzufügen eines rekursiven Listeners wird für den Container selbst und für jedes aktuelle und in der Zukunft hinzugefügte Control die create() Methode aufgerufen. Wird durch den Implementierer der IListenerFactory Schnittstelle ein Listener erzeugt, wird dieser der Component hinzugefügt, und vor dem dispose der Component wieder entfernt. Die create() Methode kann auch null zurückgeben, wodurch für diese Component kein Listener hinzugefügt wird.

Das folgende Beispiel soll dies verdeutlichen:

  1      final IPopupMenu labelMenu = container.createPopupMenu();
  2      labelMenu.setModel(labelMenuModel);
  3  
  4      IListenerFactory<IPopupDetectionListener> listenerFactory;
  5      listenerFactory = new IListenerFactory<IPopupDetectionListener>() {
  6          @Override
  7          public IPopupDetectionListener create(final IComponent component) {
  8              if (component instanceof ILabel) {
  9                  final ILabel label = (ILabel) component;
 10                  return new IPopupDetectionListener() {
 11                      @Override
 12                      public void popupDetected(final Position position) {
 13                          labelMenuModel.setLabel(label);
 14                          labelMenu.show(label.toComponent(position, container));
 15                      }
 16                  };
 17              }
 18              return null;
 19          }
 20      };
 21          
 22      container.addPopupDetectionListenerRecursive(listenerFactory);

Für alle Labels eines Containers wird ein Kontextmenü (labelMenu) hinzugefügt. Bevor das Menü angezeigt wird, wird das Label auf dem zugehörigen Model (Zeile 13) gesetzt, um etwaige Aktionen bezüglich des ausgewählten Labels durchführen zu können.

Das Kontextmenü könnte zum Beispiel Aktionen enthalten, welche den Inhalt des Labels in die Zwischenablage kopieren oder dessen Farbe ändern, etc.


Siehe auch PDF Version dieses Dokuments, Jowidgets API Spezifikation