Oberfächenentwicklung: Anwendungsbeispiele

4 Anwendungsbeispiele

In diesem Kapitel werden einige Beispiele für typische GUI-Aufgaben vorgestellt. Die Beispiele verwenden zum Teil dieselben Klassen und Business Objekte wie die „echten“ Semiramis Anwendungen, sind jedoch von diesen völlig unabhängig.

4.1 Layout-Manager verwenden

4.1.1 StandardLayout

Das folgende Beispiel soll zeigen, wie sich bei mehrspaltigen Layouts die enthaltenen Elemente (Felder) an gemeinsame Fluchtlinien ausrichten lassen. Basis für solche Layouts ist der „StandardLayout“-Layout-Manager (siehe 3.9.1 StandardLayout). Dieser Layout-Manager richtet seine Elemente an entsprechenden Führungslinien (Guides) aus. Je nach Konstrukur werden diese Führungslinien nur benutzt (d. h. von einem übergeordneten Container „geerbt“) oder auch neu definiert[1]. Wenn man also die Elemente von mehreren Containern an gemeinsamen Linien ausrichten möchte, dann sollten diese Linien nur einmal definiert werden.

  • 2-spaltiges Layout mit Subshelf

Im Beispiel werden bei dem äußeren Container die Führungslinien für die Spalten definiert, das geschieht durch die Verwendung von setGuides(2).

Das Beispiel zeigt außerdem noch:

  • Zuordnen von Elementen zu einer oder mehreren Spalten.
  • Verwenden von Metadaten zum Erzeugen von Feldern.
  • Verwenden von relativen Pfaden für die Metadaten (Basispfad auf Containerebene).
  • Selektionsmodus auf Containerebene für alle Felder wählen.
  • Reihenfolge bezüglich Erzeugung, Hinzufügen und Konfigurieren von Elementen.
  • Verwenden von Subshelfs.
 

 
import com.cisag.app.general.gui.ItemField;
import com.cisag.pgm.gui.BooleanField;
 

import com.cisag.pgm.gui.Shelf;

import com.cisag.pgm.gui.StandardLayout;
 

import com.cisag.pgm.gui.TextField;

import com.cisag.pgm.gui.ValueSetField;
import com.cisag.pgm.gui.View;
 

 

 

public class ExampleApplication extends CisUiApplication {

 

 

  private ItemField number;
  private TextField description;
  private ValueSetField type;
  private ValueSetField materialType;
  private ValueSetField status;
  private TextField ean;
  private BooleanField purchaseItem;
  private BooleanField inventoryItem;
  private BooleanField salesItem;
  private BooleanField productionItem;
 

 

 

  /**
   * Create the ident area.
   */
 

protected void createIdentArea() {

    View pane = getIdentPane();
 

// 2-spaltiges Layout für die gesamte “Ident-Area” festlegen

    pane.setGuides(2);
    pane.setLayout(new StandardLayout());
 
    // Basispfad für Metadaten festlegen
    pane.setPath(“com.cisag.app.general.obj.Item:”);
 
    // Selektionsmodus für alle Felder wählen
 

pane.setSelectionView(true);

 
    // Felder erzeugen

(relative Pfade für Metadaten)

    number      = new ItemField(Guid.AUTOGUID, “.number”, true);
 

description = new TextField(Guid.AUTOGUID, “.description”);

    type         = new ValueSetField(Guid.AUTOGUID, “.type”);
    materialType = new ValueSetField(Guid.AUTOGUID, “.materialType”);
 

status       = new ValueSetField(Guid.AUTOGUID, “.status”);

    ean          = new TextField(Guid.AUTOGUID,     “.ean”);
    purchaseItem = new BooleanField(Guid.AUTOGUID, “.purchaseItem”);
    inventoryItem = new BooleanField(Guid.AUTOGUID,  “.inventoryItem”);
    salesItem    = new BooleanField(Guid.AUTOGUID, “.salesItem”);
    productionItem = new BooleanField(Guid.AUTOGUID,  “.productionItem”);
 
    // Hinzufügen der Felder
    // erste Zeile
    pane.add(number,       “0”);  // Spalte 1
    pane.add(description,  “1”);  // Spalte 2
 
    // zweite Zeile
    pane.add(type,         “0”);

// Spalte 1

    pane.add(materialType, “1”);  // Spalte 2
 
   // (Sub-)Shelf für die folgenden Felder/Zeilen erzeugen
   Shelf subshelf = new Shelf(Guid.AUTOGUID);
   // StandardLayout mit Default-Konstruktor verwenden
   subshelf.setLayout(new StandardLayout());
   // Shelf dem Container hinzufügen (alle Spalten nutzen)
   pane.add(subshelf, StandardLayout.SPAN_ALL_COLUMNS);
 
   // Selektionsmodus auch für alle Felder des Shelfs wählen
   subshelf.setSelectionView(true);
 
   // Festlegen, dass das Shelf beim Start „zugeklappt“ sein soll
   subshelf.setCollapsed(true);
 
  // die folgenden Felder/Zeile

n jeweils dem Shelf hinzufügen

  // dritte Zeile
  subshelf.add(status, “0”);
  subshelf.add(ean,    “1”);
 
  // vierte Zeile
  subshelf.add(purchaseItem,  “0”);
  subshelf.add(inventoryItem, “1”);
 
    // fünfte Zeile

subshelf.add(salesItem,      “0”);

    subshelf.add(productionItem, “1”);
 
  }
}

4.1.2

4.1.2    BoxLayout

Das folgende Beispiel demonstriert die Verwendung und Arbeitsweise des „BoxLayout“-Layout-Managers.

  • Container mit geschachtelten BoxLayouts

In dem Beispiel werden zwei Instanzen von BoxLayout verwendet. Die erste Instanz ist dem „Content-Pane“ des Dialogs zugeordnet und verteilt dessen Elemente in vertikaler Richtung. Unten in dem „Content-Pane“ liegt ein View, der die beiden Buttons („OK“ und „Abbrechen“) aufnimmt. Diesem View ist ein BoxLayout mit horizontaler Ausrichtung zugeordnet.

Hinweis:
In Semiramis-Anwendungen sollte die Klasse com.cisag.pgm.gui.ButtonBar (siehe 3.7.2 ButtonBar) verwendet werden, wenn in einem Dialog ein oder mehrere Buttons angezeigt werden sollen. Diese Klasse reduziert nicht nur den Aufwand, sondern sorgt auch dafür, das in allen Dialogen ein einheitliches Verfahren für das Layout verwendet wird.

Um zwischen den Elementen etwas Abstand zu bekommen, verwendet das Beispiel „Struts“ (siehe 3.8.6 Strut). In dem Subcontainer mit den beiden Buttons wird zusätzlich ein (horizontales) „Glue“ (siehe 3.8.5 Glue) verwendet, um die Buttons nach rechts „zu drücken“.

Bei dem Label und dem Textfeld (MultiLineTextField) ist es notwendig mit setAlignmentX(VisualELement.CENTER_ ALIGNMENT) die horizontale Ausrichtung zu ändern, da hier die Defaults auf VisualELement.LEFT_ALIGNMENT stehen und das nicht mit den anderen Elementen harmoniert[2].

Mit dem Aufruf von setMaximumWidth(Short.MAX_VALUE) wird für den Label und das Textfeld festgelegt, dass sie beliebig verbreitert werden können. Über die Aufrufe von setMinimumHeight(0) und setMaximumHeight(Short. MAX_VALUE) für das Textfeld außerdem festgelegt, dass dieses Element in vertikaler eine beliebige Größe bekommen darf.

Für den Subcontainer wird mit setMaximumHeight(0) festgelegt, dass dieser nur seine bevorzugte Höhe (preferredHeight) bekommen darf. Für die darin liegenden Buttons wird mit setMinimumWidth(10) festgelegt, dass diese bei Bedarf auf bis zu 10 Pixel verkleinert werden dürfen. Für maximumWidth wird jedoch kein Wert gesetzt, damit können die Buttons nicht über ihre bevorzugte Breite hinaus verbreitert werden. Wenn mehr Platz zur Verfügung stehen sollte, dann wird dieser Platz dem (unsichtbaren) Glue zugeteilt, was dann dazu führt, das die Buttons an den rechten Rand geschoben werden.

  dialog = new Dialog(…);
  View contentPane = dialog.getContentPane();
 
  contentPane.setLayout(new BoxLayout(BoxLayout.Y_AXIS));
 
  Label prompt = new Label(Guid.AUTOGUID, …); // “Kommentar:”
  contentPane.add(prompt);
  prompt.setAlignmentX(Label.CENTER_ALIGNMENT);
  prompt.setMinimumWidth(0);
  prompt.setMaximumWidth(Short.MAX_VALUE);
 
  contentPane.add(new Strut(Strut.VERTICAL, 5));
 
  MultiLineTextField text = new MultiLineTextField(Guid.AUTOGUID, …);
  contentPane.add(text);
    text.setLabel(null);
    text.setAlignmentX(MultiLineTextField.CENTER_ALIGNMENT);
    text.setMinimumWidth(0)
    text.setMaximumWidth(Short.MAX_VALUE)
    text.setMinimumHeight(0);
    text.setMaximumHeight(Short.MAX_VALUE);
 
    contentPane.add(new Strut(Strut.VERTICAL, 5));
 
    View buttonPanel = new View(new BoxLayout(BoxLayout.X_AXIS));
    contentPane.add(buttonPanel);
    buttonPanel.setMaximumHeight(0); // -> preferredHeight
 
    Action okAction = new Action(OK, “com.cisag.pgm.gui.ButtonBarOK”, …);
    okAction.setDefaultAction(true);
    Button okButton = new Button(okAction, Button.TEXT_BUTTON);
    buttonPanel.add(okButton);
    okButton.setMinimumWidth(10);
 
    buttonPanel.add(new Strut(Strut.HORIZONTAL, 5));
 
    Action cancelAction = new Action(CANCEL, “com.cisag.pgm.gui.ButtonBarCancel”, …);
    Button cancelButton = new Button(cancelAction, Button.TEXT_BUTTON);
    buttonPanel.add(cancelButton);
    cancelButton.setMinimumWidth(10);
  …

 

4.2 Button

4.2 s verwenden

4.2.1 MenuButton

MenuButtons (siehe 3.4.2.2 MenuButton) werden typischerweise in Symbolleisten (siehe 3.7.1 CoolBar) verwendet. Das folgende Beispiel zeigt wie ein MenuButton über eine Liste von Actions (siehe 3.11.2 ActionList) erzeugt und anschließend in die Standard-Symbolleiste (MainCoolBar) eingefügt wird.

  • MenuButon mit geöffnetem Menü

Das Beispiel erzeugt zunächst eine Instanz von ActionList und fügt dieser dann mit add die entsprechenden (Sub-)Actions hinzu. Das ActionList Objekt ist selbst eine Subklasse von Action und bezieht seine Einstellungen wie alle Actions über die als Pfad angegebenen Metadaten (Entwicklungsobjekt „Action“). Bei dem MenuButton wird daraus dann das Icon (bzw. Text), der Shortcut, der ToolTip und die Direkthilfe für den Hauptbutton ermittelt. Aus den, mit add hinzugefügten Actions werden die Menüeintrage des MenuButtons erzeugt.

Hinweis:
In dem Beispiel wird der MenuButton explizit mit „new“ erzeugt und mit add(VisualElement) der MainCoolBar hinzugefügt. Für die MainCoolBar ist das nicht unbedingt erforderlich: Wenn man der MainCoolBar in der Methode add(Action) eine entsprechend konfigurierte Instanz von ActionList übergibt, dann wird der MenuButton implizit erzeugt. Bei den anderen Symbolleisten (com.cisag.pgm. gui.CoolBar) ist jedoch diese explizite Form notwendig und daher wird sie in diesem Beispiel angewendet.

 

public class ExampleApplication extends CisUiApplication {
  // Action IDs
  private static final int CONFIRM             = 50;
  private static final int PICK                = 51;
  private static final int DELIVER             = 52;
  private static final int INVOICE             = 53;
  private static final int CASH_INVOICE        = 54;
  private static final int PROFORMA_INVOICE    = 55;
  private static final int ONHAND_DISTRIBUTION = 56;
  private static final int RELEASE             = 60;
  private static final int HOLD                = 61;
  // Action Instances
  private ActionList executeList;
  private Action confirm;
  private Action proformaInvoice;
  private Action onhandDistribution;
  private Action pick;
  private Action deliver;
  private Action invoice;
  private Action cashInvoice;
  private Action release;
  private Action hold;
 
 
  protected void initCoolBar() {
    MainCoolBar cb = getMainCoolBar();
    …
    // ActionList erzeugen
    executeList = new ActionList(0, “com.cisag.app.sales.order.ui.OrderMaintenanceExecute”);
    // Actions (Menüeinträge) erzeugen
    confirm = new Action(CONFIRM, “com.cisag.app.sales.order.ui.OrderMaintenanceGenerateConfirmation”, this);
    proformaInvoice = new Action(PROFORMA_INVOICE,              “com.cisag.app.sales.order.ui.OrderMaintenanceGenerateProformaInvoice”, this);
    onhandDistribution = new Action(ONHAND_DISTRIBUTION,              “com.cisag.app.sales.order.ui.OrderMaintenanceGenerateOnhandDistribution”, this);
    pick = new Action(PICK, “com.cisag.app.sales.order.ui.OrderMaintenanceGeneratePickingOrder”, this);
    deliver = new Action(DELIVER,              “com.cisag.app.sales.order.ui.OrderMaintenanceGenerateShippingOrder”, this);
    invoice = new Action(INVOICE,              “com.cisag.app.sales.order.ui.OrderMaintenanceGenerateInvoice”, this);
    cashInvoice = new Action(CASH_INVOICE,              “com.cisag.app.sales.order.ui.OrderMaintenanceGenerateCashInvoice”, this);
    release = new Action(RELEASE,              “com.cisag.app.general.order.ui.ReleaseOrder”, this);
    hold = new Action(HOLD,              “com.cisag.app.general.order.ui.HoldOrder”, this);
    // Actions der ActionList hinzufügen
    executeList.add(release);
    executeList.add(confirm);
    executeList.add(proformaInvoice);
    executeList.add(onhandDistribution);
    executeList.add(pick);
    executeList.add(deliver);
    executeList.add(invoice);
    executeList.add(cashInvoice);
    executeList.add(hold);
    // letzte Auswahl nicht speichern (Standard für MenuButtons)
    executeList.setMemoryActive(false);
 
    // MenuButton für MainCoolBar (mit großen Icons) erzeugen
    MenuButton mb = new MenuButton(Guid.AUTOGUID, executeList, true);
 
    // MenuButton dem (Main)CoolBar hinzufügen
    cb.add(mb);
 
  …
  }
 
 
  public void performAction(Action action) {
    int actionId = action.getId();
    switch (actionId) {
       case CONFIRM:
          /** @todo Implementierung/Aufruf von “confirm” */
          break;
       case PROFORMA_INVOICE:
          /** @todo Implementierung/Aufruf von “proforma_invoice” */
          break;
       …
    }
  }
}

 

4.3 Dialoge verwenden

In dem folgenden Beispiel wird ein modaler Dialog dazu verwendet, um die Daten eines Feldes zu bearbeiten. Zum Bearbeiten des Textes, d. h. zum Öffnen des Bearbeitungsdialogs wird im Beispiel der so genannte „Dialog-Button“ benutzt (siehe Dialog-Button). Das Öffnen eines Dialogs kann natürlich auch durch jede andere Benutzeraktion (Action) angestoßen werden.

  • Feld mit „Dialog-Button“ zum Öffnen des Beipieldialogs

Bei dem Beispiel handelt es sich um einen einfachen (mehrzeiligen) Text, für den kein eigener „Editor“ benötigt würde. In der Praxis sollte man die Auslagerung der Bearbeitung in einen extra Dialog nur dann verwenden, wenn es auch einen Gewinn für den Benutzer darstellt.

Hinweis:
Für das Anzeigen von Meldungen bzw. Bestätigungsdialogen sollte die Klasse com.cisag.pgm.gui.Confirmation Dialog verwendet werden.

  • geöffneter Bearbeitungsdialog

In dem Beispiel wird der Code für den Bearbeitungsdialog in eine separate Klasse (TextEditor) ausgelagert. In der Anwendung verbleibt damit nur noch der Code, der diesen Dialog mit der übrigen Anwendung verbindet.

Die Klasse TextEditor erzeugt intern eine Instanz von com.cisag.pgm.gui.Dialog und füllt diesen mit entsprechenden UI-Elementen. Dabei wird für das Bestätigen bzw. Abbrechen der Bearbeitung eine Instanz von com.cisag.pgm.gui.ButtonBar (siehe 3.7.2 ButtonBar) benutzt. Die Kommunikation mit der Klasse erfolgt nach folgendem Muster:

  • Erzeugen einer Instanz von TextEditor. Dabei ist als „Owner“ das Element anzugeben zu dem der Dialog gehört bzw. über das der Dialog geöffnet wurde. Über dieses Element wird einerseits das übergeordnete Fenster bestimmt und anderseits dient es dem Dialog als Hinweis für seine Position.
  • Mit setClosedAction() kann eine Action registriert werden, die ausgelöst wird, wenn der Dialog geschlossen wurde (durch den Benutzer oder programmintern).
  • Mit setValue(String) kann dem TextEditor der zu bearbeitene Text gesetzt werden.
  • Nach Abschluss der Konfiguration kann der Dialog mit show() sichtbar gemacht werden.
  • Nachdem der Benutzer den Dialog mit „OK“, „Abbrechen“ oder „x“ wieder geschlossen hat, wird (falls mit setClosedAction() gesetzt) die „Closed-Action“ ausgelöst.
  • Mit isApproved() kann abgefragt werden, ob der Dialog mit „OK“ geschlossen oder abgebrochen wurde.
  • Im Fall von „OK“ (approved) kann mit getValue() der aktuelle Wert ausgelesen werden.

Die Instanz von TextEditor kann beliebig oft wieder verwendet werden, in diesem Fall brauchen die ersten beiden Schritte nicht wiederholt werden.

Hinweis:
Anders, als z. B. bei java.awt.Dialog, kehrt die Methode „show“ (bzw. setVisible(true)) auch bei modalen Dialogen sofort zum aufrufenden Programm zurück, d. h. es wird nicht gewartet, bis der Dialog geschlossen wurde[3]. Als Ersatz steht die „Closed-Action“ zur Verfügung, die ausgelöst wird, wenn der Dialog (egal wie) geschlossen wurde.

 
public class TextEditor {
 
  private static final int DIALOG_CLOSED = 100;
 
  private Dialog dialog;
  private boolean approved;
 
  private MultiLineTextField textField;
 
  private Action closedAction;
 
  public TextEditor(VisualElement owner) {
    dialog = new Dialog(Dialog.MODAL, owner);
    dialog.setTitleFrom(…); // “Demo Dialog”
 
    DialogActionListener listener = new DialogActionListener();
    dialog.setClosedAction(new Action(DIALOG_CLOSED, listener));
 
    View contentPane = dialog.getContentPane();
    contentPane.setLayout(new BoxLayout(BoxLayout.Y_AXIS));
 
    Label prompt = new Label(Guid.AUTOGUID, …); // “Beschreibung:”
    contentPane.add(prompt);
    prompt.setAlignmentX(Label.CENTER_ALIGNMENT);
    prompt.setMaximumWidth(Short.MAX_VALUE);
 
    contentPane.add(new Strut(Strut.VERTICAL, 5));
 
    textField = new MultiLineTextField(Guid.AUTOGUID, “”);
    contentPane.add(textField);
    textField.setLabel(null);
    textField.setAlignmentX(MultiLineTextField.CENTER_ALIGNMENT);
    textField.setMinimumHeight(0);
    textField.setMaximumWidth(Short.MAX_VALUE);
    textField.setMaximumHeight(Short.MAX_VALUE);
 
    contentPane.add(new Strut(Strut.VERTICAL, 5));
 
    ButtonBar buttonBar = new ButtonBar(ButtonBar.OK|ButtonBar.CANCEL, listener);
    contentPane.add(buttonBar);
    buttonBar.setMaximumHeight(0);
  }
 
  public String getValue()
  {
    return textField.getStringValue();
  }
 
  public void setValue(String text)
  {
    textField.setStringValue(text);
  }
 
  public void setClosedAction(Action action)
  {
    closedAction = action;
  }
 
  public void show()
  {
    if ( isVisible() ) {
      throw new java.lang.IllegalStateException(“already shown”);
    }
    approved = false;
    dialog.setVisible(true);
  }
 
  public void approve()
  {
    if ( !isVisible() ) {
      throw new java.lang.IllegalStateException(“dialog not shown”);
    }
    approved = true;
    dialog.setVisible(false);
  }
 
  public void cancel()
  {
    if ( !isVisible() ) {
      throw new java.lang.IllegalStateException(“dialog not shown”);
    }
    approved = false;
    dialog.setVisible(false);
  }
 
  public boolean isVisible()
  {
    return dialog.isVisible();
  }
 
  public boolean isApproved()
  {
    return approved;
  }
 
  private class DialogActionListener implements ActionListener
  {
    public void performAction(Action action)
    {
      int actionId = action.getId();
      System.out.println(“actionId = ” + actionId);
      if ( actionId == ButtonBar.OK ) {
        approve();
      }
      else if ( actionId == ButtonBar.CANCEL ) {
        cancel();
      }
      else if ( actionId == DIALOG_CLOSED ) {
        if ( closedAction != null ) {
          closedAction.setState(new Boolean(isApproved()));
          closedAction.fireAction();
        }
      }
    }
  }
}
 

 

Das Anwendungsprogramm verwendet zwei Actions, um mit dem TextEditor zu kommunizieren:

  • Die erste Action wird bei dem Textfeld registriert, und stößt die Erzeugung bzw. das Öffnen des Dialogs an.
  • Die zweite Action wird bei dem TextEditor registriert und stößt die Wertübernahme an.

Die Instanz von TextEditor wird erst erzeugt, wenn der Benutzer das erste Mal auf den Dialog-Button klickt. Die selbe Instanz wird dann auch für die folgenden Male verwendet.

Hinweis:
Solange der Dialog noch nicht geschlossen wurde, muss die Anwendung eine (Java-)Referenz auf den Dialog halten, damit der Garbage-Collector den Dialog nicht vorzeitig zerstören kann.

 
public class ExampleApplication extends CisUiApplication {
 
  …
 
  private static final int OPEN_DIALOG_ACTION   = 200;
  private static final int DIALOG_CLOSED_ACTION = 201;
 
  …
 
  private MultiLineTextField description;
 
  private TextEditor editor;
 
  private Action openDialogAction;
  private Action dialogClosedAction;
 
  …
 
  protected void createWorkArea() {
    …
    description = new MultiLineTextField(Guid.AUTOGUID, …); // Beschreibung
    pane.add(description, “0”);
    openDialogAction = new Action(OPEN_DIALOG_ACTION, this);
    description.setLabelPosition(MultiLineTextField.LABEL_LEFT);
    description.setEditable(false);
    description.setDialogAction(openDialogAction);
    …
  }
 
  …
 
  public void performAction(Action action) {
    …
    switch (action.getId()) {
      …
      case OPEN_DIALOG_ACTION:
        if ( editor == null ) {
          editor = new TextEditor(description);
          editor.setClosedAction(new Action(DIALOG_CLOSED_ACTION, this));
        }
        if ( !editor.isVisible() ) {
          editor.setValue(description.getStringValue());
          editor.show();
        }
        break;
 
      case DIALOG_CLOSED_ACTION:
        if ( editor.isApproved() ) {
          description.setStringValue(editor.getValue());
        }
        else {
          // ignored;
        }
        break;
 
      …
    }
    …
  }
 
  …
}
 

 

 

[1]Die nicht-leeren Konstruktoren des StandardLayouts sollten nicht länger zur Definition von Führungslinien (Spalten) verwendet werden. Stattdessen sollte eine der VisualElementContainer#setGuides Methoden genutzt werden.

[2]Wenn sowohl LEFT_ALIGNMENT als auch CENTER_ALIGNMENT verwendet wird, dann wird der linke Rand der Element mit LEFT_ALIGNMENT an der Mitte der Elemente mit CENTER_ALIGNMENT ausgerichtet.

 

[3]Für den Benutzer ändert es aber nichts am modalen Verhalten des Dialogs.

 

Czy ten artykuł był pomocny?