eine ladbare Anwendung erstellen

Einführung

Der Einstieg in die GUI-Anwendung ist SRStarter, welcher zum Funktionsumfang von SRJRCFrames gehört. Es muss also "nur" der ladbare Teil der Anwendung erstellt werden. Für die Anwendungsentwicklung bedeutet dies, dass man kein eigenes Toplevel-Fenster erstellt, sondern das verwendet, was der Anwendungsrahmen SRStarter bereitstellt. Im Sinne von MVC entspricht die Anwendung dem "C" also dem Kontroller. Das Hauptfenster der Anwendung ist vom Typ MainFrame und es sollten keine Annahmen getroffen werden, von welchem JFC-Typ dieses Fenster ist. Je nachdem, welchen Fensterverwalter der spätere Anwender auswählt, ist dies ein JFrame, ein JInternalFrame, oder einfach nur ein JPanel.

Anwendungs-Klasse erstellen

Für die Zusammenarbeit mit dem Anwendungsrahmen stellt das Framework die Klasse AbstractApplication bereit, die die Schnittstelle Application bedient. Das "M" von MVC steht für Model (die Datenhaltung) und bei der Anwendung sind das die Daten der Konfiguration. Die Konfiguration ist eine Kindklasse von ApplicationConfig und somit kann Position und Größe des Anwendungsfensters automatisch gespeichert werden. Hieraus folgt die Definition:

public class BeispielApp extends AbstractApplication<BeispielCfg> {
} 

Die Konfiguration wird im Kontext angelegt (siehe Konfiguration) und der Anwendungsrahmen SRStarter überschreibt die Vorgaben aus dem Kontext mit den aktuellen Benutzerdaten. Dies geschieht natürlich vor der Übergabe der Instanz an die Anwendungsklasse.

Zutaten für eine Minimalanwendung:

Aber jetzt etwas ausführlicher ...

Anwendungsmenü und Toolbar

Um der Gesamtanwendung ein einheitliches Gesicht zu geben, wird weder das Menü, noch die Toolbar über Actions definiert, sondern über Enum-Einträge, die von einem AbstractActionHandler ausgewertet werden.

private enum Command {
   FILE_NEW, SEP_FILE, FILE_OPEN,
   EDIT_COPY, EDIT_RELEASE, SEP_EDIT, EDIT_DELETE,
   VIEW_REFRESH
} 

Wie die Enum-Einträge vermuten lassen, wird der Text ausgewertet. Dies erfolgt durch den ActionHandler. FILE_NEW führt dabei zu einem Menüeintrag "New" in der Hauptmenü-Rubrik "File", SEP_EDIT zu einer Trennlinie im Untermenü Edit. Um die Anwendungstexte übersetzen zu können, werden alle Menüeinträge als Schlüssel bei der Textersetzung verwendet (Näheres siehe Konfiguration).

Der Anwendungsrahmen SRStarter mischt einige Menüeinträge selbstständig dazu. Hierzu gehören das Beenden der Anwendung, das Umschalten zu einer anderen geladenen Anwendung, ein gemeinsamer Konfigurationsdialog und natürlich die Hilfeseiten.

Funktionen für die Menüeinträge

Die Funktionen, die von einem Menüeintrag ausgeführt werden können müssen keine besonderen Voraussetzungen erfüllen, d.h. es können alle Methoden verwendet werden.

Um durch die Auswahl eines Menüeintrages eine Funktion ausführen zu können, muss die Funktion mit dem Menüeintrag verheiratet werden. Hierzu ist u.a. der AbstractActionHandler zuständig. Er sorgt dafür, dass alle Einträge gleich aussehen, prüft ob der Benutzer die Funktion ausführen darf (z.Zt. nur vorgesehen, aber noch nicht aktiv) und merkt sich alle Einträge, sodass die jederzeit über den ActionHandler abgefragt werde können.

class BeispielActionHandler extends AbstractActionHandler
                            implements ApplicationActionHandler {
   public BeispielActionHandler(String name) {
      super(name);
   }


   @Override
   public void init() {
      setupAction(getName(), Command.FILE_NEW, new AbstractActionCallback() {
         @Override
         public void actionPerformed(ActionEvent ae) {
            doCreateSample(ae);
         }
      }, KeyEvent.VK_N
       , AccessMode.APP_READ);

       ...
   }


   public Enum[] getCommands() {
      return Command.values();
   }


   public Enum[] getToolBarCommands() {
      return Command.values();
   }
} 

Der Konstruktor hat einen Namen-Parameter, sodass der ActionHandler weiß, für wen er zuständig ist.

In der Funktion init erfolgt die Verheiratung von Menüeintrag und aufzurufender Funktion. Im einfachsten Fall kann eine Funktion ohne Argumente aufgerufen werden. Wer den Aufruf aus dem Menü anders behandeln will, als z.B. den Aufruf aus der Toolbar, kann den ActionEvent an die Funktion übergeben.

Die Funktionen getCommands und getToolBarCommands dienen dazu, um z.B. auch unterschiedliche Teilmengen von Command für Menü und Toolbar zu verwenden.

Um auch Kontextmenüs unterstützen zu können, gibt es eine Variante von init, die das Kontextobjekt als Parameter erhält:

@Override
public void init(Object contextObj) {
   setupAction(getName(),
               Command.FILE_OPEN,
               new AbstractActionContextCallback(contextObj) {
                   @Override
                   public void actionPerformed(ActionEvent ae) {
                      doEditSample(new ActionContextEvent(ae, getContext()));
                   }
               },
               KeyEvent.VK_O);
    ...
} 

Für diesen Fall gibt es auch eine erweiterte callback-Klasse AbstractActionContextCallback. Dieser kann das Kontextobjekt im Konstruktor übergeben und dann aus der Funktion heraus abgefragt werden. Damit der ActionHandler seine Arbeit aufnehmen kann, muss er noch angemeldet werden. Der Konstruktor unserer Anwendungsklasse ist ein guter Platz dafür:

public BeispielApp() {
   super(BeispielApp.class.getSimpleName());
   setActionHandler(new BeispielActionHandler(getName()));
} 

Fensterinhalt

Eine grafische Anwendung ohne sichtbaren Inhalt ist wenig sinnvoll. Also brauchen wir noch etwas Fleisch in der Suppe. Für eine Anwendung mit einer übersicht als Standardansicht könnte dies so aussehen:

@Override
protected JComponent createPane() {
    if (content == null) {
       view = new AddressTableView(getInitialData(),
                                   EventSelectionModel.SINGLE_SELECTION,
                                   null);
       view.setSelectionChangedExecutor(new AbstractAction() {
          private static final long serialVersionUID = 713L;
             
             
          public void actionPerformed(ActionEvent e) {
             selectionChanged(e);
          }
       });
       view.setPopDoubleLeftExecutor(new AbstractAction() {
          private static final long serialVersionUID = 713L;
             
             
          public void actionPerformed(ActionEvent e) {
             popupDoubleLeft(e);
          }
       });
       view.setPopSingleRightExecutor(new AbstractAction() {
          private static final long serialVersionUID = 713L;
             
             
          public void actionPerformed(ActionEvent e) {
             popupSingleRight(e);
          }
       });
       content = new ApplicationPage(getName(), view.getView());
    }
    return content;
} 

Wie man sehen kann, verwendet die Anwendung eine eigene View-Klasse, was dem "V" von MVC entspricht. Damit wäre unsere MVC-Anwendung komplett.

Die Klasse AddressTableView wird auf der Seite Übersicht erklärt. Die Übersicht stellt eine Liste von Adressen dar. Die Funktionen, die auf Benutzeraktion reagieren gehören jedoch zum Kontroller, deshalb sind sie in der Anwendung angesiedelt und werden bei der Übersicht (dem View) lediglich angemeldet.

Statusbar

Die Statusbar ist für einfache Meldungen zuständig, die keine Benutzeraktion erfordern. Die Statusbar wird unterhalb des Anwendungsfensters angezeigt und enthält im einfachsten Falle nur den Meldungsbereich.

Der Meldungsbereich wird für alle Statusbar-Instanzen synchronisiert, d.h. eine Statusmeldung wird von allen Anwendungsfenstern angezeigt. Im einfachsten Falle erfordert eine Statusbar diese Deklaration:

public class BeispielStatusBar extends AbstractStatusBar {
   private static final long serialVersionUID = 1L;
    
    
   @Override
   protected void checkExtends() {}
    
    
   @Override
   protected void updateExtends() {}
} 

mit anderen synchronisieren

Bei SRJRCFrames gehört die Kommunikation zwischen den Anwendungen zum Konzept. Dazu gibt es die Möglichkeit, auf Nachrichten von anderen Anwendungen zu reagieren. Wer sich nicht für die Belange anderer interessiert, kann hier einfach eine leere Funktion verwenden:

@Override
public void onApplicationEvent(ApplicationEvent arg0) {
   // do nothing
} 

Das Gegenstück dazu ist das Versenden von Nachrichten. Dazu wird jetzt ein Dienst vom Anwendungsrahmen angefordert:

if (aePublisher == null) {
   aePublisher = (ApplicationEventPublisher) 
                 ApplicationServiceProvider.getService(ApplicationEventPublisher.class);
} 

Wenn wir den Dienst erhalten haben, können wir die (möglichen) anderen Anwendungen von unserer Existenz unterrichten:

aePublisher.publishEvent(new SampleAppStartedEvent("hello world")); 

Seite zur Bearbeitung der Konfigurationsdaten

Wird im Hauptmenü der Menüeintrag "Optionen" aktiviert, fragt der Anwendungsrahmen SRStarter jede geladene Anwendung nach einer Seite für den Dialog der Einstellungen. Der Dialog selbst, wie einige Standardseiten stammen vom Anwendungsrahmen selbst. Die Darstellung ähnelt einem Notebook, d.h. es kann zwischen den Seiten frei umgeschaltet werden. Die Funktion, mit der die Anwendung eine solche Seite zur Verfügung stellen kann, ist:

public JComponent getConfigPage() {
   if (cfgEdit == null)
      cfgEdit = new ConfigEditor(getAppConfig());

   return cfgEdit;
} 

Wer keine Benutzereinstellungen zur Änderung anbieten will, kann bei dieser Methode einfach null zurückgeben. Ansonsten wird ein Editor erstellt, der die Konfigurationsinstanz der Anwendung bearbeiten kann. Ein solcher Editor wird wie folgt definiert:

class ConfigEditor extends AbstractEditor<BeispielCfg> {
   private static final long serialVersionUID = 1L;


   public ConfigEditor(BeispielCfg instance) {
       super(instance, false);
   }


   @Override
   protected JComponent buildPanel() {
      if (msgSource == null)
         msgSource = ApplicationServiceProvider.getService(MessageSource.class);
      FormLayout layout = new FormLayout("left:max(100dlu;pref), 3dlu, 100dlu:grow");
      DefaultFormBuilder builder = new DefaultFormBuilder(layout);
      String prefix = BeispielCfg.class.getSimpleName() + ".";
      PresentationModel pm = getModel();

      builder.setDefaultDialogBorder();
      builder.append(msgSource.getMessage(prefix + "beispielText.label",
                                          null,
                                          prefix + "beispielText.label",
                                          null),
                     componentFactory.createTextField(pm.getBufferedModel("beispielText")));

       ...

      return builder.getPanel();
   }
} 

In der Funktion buildPanel werden die Eingabeelemente auf JGoodies-Art angelegt, d.h. die Dateninstanz wird über ein PresentationModel angesprochen, die einzelnen Attribute über PresentationModel.getBufferedModel("attribute-Name"). Der Editor muss sich nicht um Aktionen wie "Ok" oder "Abbrechen" kümmern, das macht der Konfigurationsdialog, den der Anwendungsrahmen erstellt.

Zusammenfassung

Die erstellte Beispielanwendung sieht jetzt komplett so aus:

public class BeispielApp extends AbstractApplication<BeispielCfg> {
   private enum Command {
      FILE_NEW, SEP_FILE, FILE_OPEN,
      EDIT_COPY, EDIT_RELEASE, SEP_EDIT, EDIT_DELETE,
      VIEW_REFRESH
   }
   class BeispielActionHandler extends AbstractActionHandler
                               implements ApplicationActionHandler {
      public BeispielActionHandler(String name) {
         super(name);
      }


      @Override
      public void init() {
         setupAction(getName(), Command.FILE_NEW, new AbstractActionCallback() {
             @Override
             public void actionPerformed(ActionEvent ae) {
                 doCreateSample(ae);
             }
         }, KeyEvent.VK_N
         , AccessMode.APP_READ);
         ...
      }


      @Override
      public void init(Object contextObj) {
         setupAction(getName(),
                     Command.FILE_OPEN,
                     new AbstractActionContextCallback(contextObj) {
                         @Override
                         public void actionPerformed(ActionEvent ae) {
                             doEditSample(new ActionContextEvent(ae, getContext()));
                         }
                     },
                     KeyEvent.VK_O);
         ...
      }


      public Enum[] getCommands() {
         return Command.values();
      }


      public Enum[] getToolBarCommands() {
         return Command.values();
      }
   }
   public class BeispielStatusBar extends AbstractStatusBar {
      private static final long serialVersionUID = 1L;


      @Override
      protected void checkExtends() {}


      @Override
      protected void updateExtends() {}
   }


   public BeispielApp() {
      super(BeispielApp.class.getSimpleName());
      setActionHandler(new BeispielActionHandler(getName()));
   }


   @Override
   public void onApplicationEvent(ApplicationEvent arg0) {
      // do nothing
   }


   public void selectionChanged(ActionEvent ae) {
      ...
   }


   public void popupDoubleLeft(ActionEvent ae) {
      ...
   }


   public void popupSingleRight(ActionEvent ae) {
      ...
   }


   @Override
   protected JComponent createPane() {
      if (content == null) {
         view = new AddressTableView(getInitialData(),
                                     EventSelectionModel.SINGLE_SELECTION,
                                     null);
         view.setSelectionChangedExecutor(new AbstractAction() {
            private static final long serialVersionUID = 713L;


            public void actionPerformed(ActionEvent e) {
               selectionChanged(e);
            }
         });
         view.setPopDoubleLeftExecutor(new AbstractAction() {
            private static final long serialVersionUID = 713L;


            public void actionPerformed(ActionEvent e) {
               popupDoubleLeft(e);
            }
         });
         view.setPopSingleRightExecutor(new AbstractAction() {
            private static final long serialVersionUID = 713L;


            public void actionPerformed(ActionEvent e) {
               popupSingleRight(e);
            }
         });
         content = new ApplicationPage(getName(), view.getView());
      }
      return content;
   }


   protected void doCreateSample(ActionEvent ae) {
      ...
   }


   protected void doEditSample(ActionEvent ae) {
      ...
   }
}