Kurs:Java – ein schneller Einstieg/Anwenderfreundlichkeit

Anwenderfreundlichkeit

Bearbeiten

Anwenderfreundlich ist ein weites Feld der Programmierung. Bereits diese kleine Anwendung wird zeigen, dass dieser Bereich oft mehr Arbeit bedeutet als Berechnungen und ihre Ergebnisse. Es kann nicht oft genug betont werden: "Der Benutzer darf nicht enttäuscht werden!" Ohne Benutzer sind Programme sinnlos, egal ob sie umsonst oder für sehr viel Geld angeboten werden.

Beschriftung von Komponenten

Bearbeiten

Irgenwie muss dem Anwender die Bedeutung der Felder mitgeteilt werden. Am einfachsten dürfte eine Beschriftung vor jedem TextField sein. Beschriftungen werden labels genannt und stehen als Klasse bereit (API-Doku). Also vor jedes TextField einen Label gesetzt.

this.add( new JLabel( "Betrag"),  BorderLayout.NORTH);
this.add( betragFeld,  BorderLayout.NORTH);
this.add( new JLabel( "Rabatt"),  BorderLayout.CENTER);
this.add( rabattFeld,  BorderLayout.CENTER);
this.add( new JLabel( "Endbetrag"),  BorderLayout.SOUTH);
this.add( ausgabeFeld, BorderLayout.SOUTH);

Die Überraschung ist nur am Anfang vorhanden (nach kurzer Zeit der Java-Programmierung weicht sie der Resignation). Nichts, aber auch gar nichts hat sich geändert. Kein Label weit und breit. Woher denn auch. in der ersten Zeile wird dem LayoutManager ein Label überantwortet, das bitte an der nördlichen Grenze residieren soll. In der darauffolgenden Zeile wird verlangt, an derselben Stelle ein TextField. unterzubringen. Natürlich wird der LayoutManager dieser Bitte nachkommen und genau dort ein TextField unterbringen, mit der Konsequenz, dass der Label überschrieben wird. Genau so wird es allen anderen Labels ergehen. Offensichtlich ist das Panel durch den LayoutManager dicht. Aber ein Blick gen Westen läßt Hoffnung keimen, denn dieser Grenzbereich ist frei. Also einen Label im Westen unterbringen, um zu probieren ob sich der Layoutmanager so überreden läßt, die Anwenderfreundlichkeit herzustellen.

this.add( new JLabel( "Betrag"), BorderLayout.WEST);

Diese Zeile vor den Integrationsanweisungen der TextFields eingefügt, zeigt tatsächlich einen Label mit dem Inhalt "Betrag" an. Abgesehen von der fehlerhaften vertikalen Position schon ein Erfolg. Einen weiteren Label im Westen unterzubringen scheitert an den bereits besprochenen Gründen, weshalb Nachdenken erforderlich ist (wie immer, wenn Aufgaben an ein Management delegiert werden).

Es werden eigentlich drei Labels (jeder in einer Zeile) verlangt, aber vielleicht kann ein Label mit drei Zeilen die gleiche Wirkung erzielen. Ändern der Anweisung zu

this.add( new JLabel( "Betrag\nRabatt\nEndbetrag"), BorderLayout.WEST);

führt zu einem traurigen Ergebnis. Labels können also nicht über mehrere Zeilen verteilt werden; jedenfalls nicht bei diesem LayoutManager. Könnten die Labels doch so einfach angeordnet werden wie es bei den TextFields der Fall war. Genau das können sie auch, denn nichts hindert daran, sie in einem Panel dem gleichen LayoutManagement zu unterziehen. Wie aber sollen dieses "LabelPanel" und das "AnzeigePanel" zusammenkommen?

Panels als Komponenten

Bearbeiten

"Auf nach Westen!" ("westward ho!" - Ausruf der Siedler vor dem Weg nach Oregon). So lautet auch hier die Antwort. Das "AnzeigePanel" enthält Components. Es ist also ein Container. Diese sind aber eine Instanz der Klasse Component, und können somit als solche eingesetzt werden. Wenn also die Positionierung einer einzigen Komponente im Westen funktioniert und ein Panel als eben solches angesehen wird, dürfte die Vermutung naheliegen, dass die Lösung bereits in Form der folgenden Sequenz gefunden ist.

JPanel labels = new JPanel();
labels.setLayout( new BorderLayout());
labels.add( new JLabel( "Betrag"), BorderLayout.NORTH);
labels.add( new JLabel( "Rabatt"), BorderLayout.CENTER);
labels.add( new JLabel( "Endbetrag"), BorderLayout.SOUTH);
this.add( labels, BorderLayout.WEST);

Diese Sequenz wird vor oder hinter der Integration der TextFields untergebracht. Ein Test des neuen Programms führt leider nur zu einem Teilerfolg. Zwar sind jetzt alle Labels untereinander, aber der LayoutManager beharrt auf seinen Ansprüchen im Norden und Süden des RabattPanels. Erfüllt das Management seine Aufgaben nicht den Anforderungen entsprechend, ist es überfordert. Eine Neubesetzung zieht sehr viel Verwaltungsaufwand nach sich und sollte als letzte Maßnahme in Betracht kommen. Vielleicht kann der Einsatz einer übergeordneten Instanz das bestehende Management unterstützen. Diese Instanz käme jedoch einem Ersatz des bisherigen Managements gleich. Ein anderer Weg verspricht dagegen mehr Erfolg. Die LayoutManager erledigen ihre Aufgaben prinzipiell gut, sind aber etwas stur, sobald ihnen "ins Handwerk gepfuscht" wird. Einer der beiden Manager muss sich immer unterordnen, was seinem Leistungsprofil total widerspricht. Die Lösung kann also nur in völliger Gleichberechtigung beider LayoutManager bestehen. Eine Art "Outsourcing" muss stattfinden.

Zunächst werden aus dem bestehendem Panel die Komponenten und das Management entfernt und in ein neues Panel integriert. Der Name des neuen Panels sei "fields". Nicht verwechseln mit den Java-Fields, die ja ein Synonym für Variablen sind.

JPanel fields = new JPanel();
 fields.setLayout( new BorderLayout());
 fields.add( betragFeld,  BorderLayout.NORTH);
 fields.add( rabattFeld,  BorderLayout.CENTER);
 fields.add( ausgabeFeld, BorderLayout.SOUTH);
JPanel labels = new JPanel();
 labels.setLayout( new BorderLayout());
 labels.add( new JLabel( "Betrag"), BorderLayout.NORTH);
 labels.add( new JLabel( "Rabatt"), BorderLayout.CENTER);
 labels.add( new JLabel( "Endbetrag"), BorderLayout.SOUTH);

Beide Panels verfügen nun über eigenes LayoutManagement und sind völlig unabhängig voneinander. Nach außen hin können sie sich bekanntlich wie Components verhalten, was den gemeinsamen Anforderungen sehr entgegen kommt. Beide (Panel)Komponenten müssen nur noch nebeneinander in das bestehende RabattPanel positioniert werden. Nun hat dieses (this) Panel keinen LayoutManager mehr (zumindest keinen der explizit eingesetzt wurde), weshalb this Panel einen Neuen benötigt. Aufgrund der gemachten Erfahrungen mit dem BorderLayout stellt sich das Management erneut den Anforderungen. Es wird Ausgewogenheit wahren und daher versuchen, eine Komponente im Westen und eine im Osten unterzubringen.

this.setLayout( new BorderLayout());
this.add( labels, BorderLayout.WEST);
this.add( fields, BorderLayout.EAST);

Das Ergebnis ist, abgesehen von Feinheiten wie fehlenden Doppelpunkten, Farbgebung, Feinjustierung der Positionen, ein voller Erfolg. In der Datei "RabattPanel5.java" ist der bis hier entwickelte Quelltext vorhanden.

Bestimmung des Zeitpunktes der Berechnung

Bearbeiten

Der Anwender ist nun über die Inhalte der einzelnen Felder informiert. Ihm fehlt aber immer noch die Möglichkeit seine eigenen Werte berechnen zu lassen. Bisher können zwar Zeichenfolgen jedweder Art eingegeben werden, aber die Anwendung reagiert nicht. Der Grund wurde zwar bereits angesprochen, soll aber hier noch einmal in Erinnerung gerufen werden. Die gesamte Berechnung wird während der Erzeugung des RabattPanels mit fest vorgegebenen Werten durchgeführt. Änderungen der Feldinhalte werden weder erkannt noch können sie in die einmalige und bereits abgeschlossene Berechnung einfließen. Dem Anwender allein muss die Kontrolle darüber, wann gerechnet wird, obliegen. Es ist demnach die Frage

  • Wie kann der Zeitpunkt der Berechnung bestimmt werden?

zu beantworten. Die Antwort ist zwar einfach, die Realisation dagegen nicht. Ein Ereignis (event) bestimmt den Zeitpunkt. Es muss ein Äquivalent für das event "Betätigen der =-Taste eines Taschenrechners" her, das vom Benutzer entweder gezielt ausgelöst wird oder, aufgrund einer bestimmten Reihenfolge der Eingaben, automatisch vom Programm generiert wird.

Die Alternative der automatisierten event-Erzeugung soll zunächst außer acht gelassen werden, denn zunächst ist die Behandlung eines Ereignisses an sich zu klären. In der Datei "MainFrame.java" wurde bereits ein EventHandler für das schließen des Fensters eingesetzt. Dieser "Zuhörer" (EventListener) ist jedoch auf Fensterereignisse spezialisiert und kann daher nur als Ideenlieferant für das neue Ereignis dienen.

Das event war ein Klick auf eine Schaltfläche. Demnach soll auch jetzt eine solche (JButton) eingesetzt werden. der Knopf soll ganz unten (also im Süden) vorhanden sein, womit einfach die entsprechende Sequenz

JButton button = new JButton( "berechnen");

this.add( button, BorderLayout.SOUTH);

an die bisherigen Zeilen der Creator-Methode angehängt wird. Natürlich bleibt ein Klick auf diese Schaltfläche wirkungslos, denn sie hat niemanden der ihr zuhört. Ein Listener muss her. Aber worauf soll er hören?

Aufgabe:

Bitte vor dem Lesen des nächsten Punktes mit der API-Doc lösen.

  1. Welche Events erzeugt ein Button?
  2. Wie lautet die Bezeichnung des "Zuhöhrers" um den Klick auf einen JButton zu "hören"?
  3. Was sind Actions? Diese Frage kann nur nach Beantwortung der ersten beiden eantwortet werden.

EventListener für Schaltflächen

Bearbeiten

Buttons erzeugen sog. ActionEvents, die in einer Methode namens "actionPerfomed(...)" des Listeners – hier also eines ActionListeners – bearbeitet (performed) werden. Dieser ActionListener kann nun für jede Schaltfläche, völlig individuell, realisiert werden, oder für mehrere ActionEvents die Zuständigkeit übernehmen. Im aktuellen Beispiel des RabattPanels ist zwar nur eine Schaltfläche vorhanden, weshalb sich dieses Problem überhaupt nicht ergibt, aber Kunden sind zu verblüffenden Überlegungen imstande. Kaum ist eine Sache abgeschlossen, kommt der Kunde mit der Idee einer "winzigen" Änderung: "Zwei neue Schaltflächen!" Zweckmäßig ist also der Aufbau eines Listeners für alle ActionEvents. Maximale Flexibilität wird in Java über Klassen und deren Instanzierung erzielt, womit gesagt werden soll, dass eine neue Klasse definiert werden soll.

Externen ActionListener erstellen

Bearbeiten

Eine neue Klasse nur für den ActionListener einer Schaltfläche? Gegenfrage: "Warum in funktionierendem Code 'rumbasteln, um ein lächerliches event abzufangen?" Java gewährt hier alle denkbaren Freiheiten und wegen der einleitend gemachten Aussage zur "beherrschbaren Teilen" wird eine eigene Klasse definiert. In Anlehnung an die zu bewältigende Aufgabe trägt sie den Namen "Actions.java".

Aus der API-Dokumentation geht hervor, dass ActionListener ein Interface ist. Der neuen Klasse sind damit also die zu verwendenden Methoden vorgeschrieben. Glücklicherweise ist im Interface ActionListener nur die Methode actionPerformed(...) vorhanden, was die Aufgabe drastisch erleichtert.

class Actions implements ActionListener {
 public void actionPerformed( ActionEvent ae) {
  System.out.println( "Nicht so laut!");
 }
}

Der Rohbau steht. Beim Einsatz von Listenern ist es sinnvoll, zunächst die Fähigkeit des Zuhörens an sich zu überprüfen. Deshalb wird in der Methode actionPerformed(...) erst einmal eine simple Ausgabeanweisung untergebracht. Der nächste Schritt besteht nun darin, der Schaltfläche diesen geduldigen Zuhörer anzuempfehlen.

Wie vermutet findet sich in der API-Dokumentation eine entsprechende Methode namens "addActionListener(...)". Unmittelbar hinter der Instanzierung der Schaltfläche kommt diese Methode zum Einsatz, allerdings nicht ohne vorher ein Objekt der Klasse Actions instanziert zu haben.

Actions actions = new Actions();
JButton button = new JButton( "berechnen");
  button.addActionListener( actions);
  this.add( button, BorderLayout.SOUTH);

In der Datei "RabattPanel7.java" ist die Erweiterung vorhanden und nachdem nun die Zuhörerschaft vorhanden ist, muss ihre Aufmerksamkeit auf die "Rechenkünste" gelenkt werden. Leider ist diese Manipulation der Zuhörer alles andere als einfach, denn wie sollen sie über die anderen Komponenten informiert werden? Das Problem dabei ist: Zwar wird ein event erkannt und es wird auch reagiert, aber wer war der Auslöser und wer ist der Empfänger der neuen Botschaft, die aufgrund des Zuhörens entsteht?

Auslöser des Ereignisses ermitteln

Bearbeiten

Aufschluss gibt eine Methode aus der Klasse EventObject namens getSource(). Weil ActionEvent eine Instanz von EventObject ist, kann diese Methode zumindest Aufschluss über die Frage nach dem Auslöser geben. Ein entsprechender Aufruf wird nun in der Methode actionPerformed(..) in der Form

System.out.println( "Ereignisquelle:\n" + ae.getSource());

untergebracht und liefert so aufschlussreiche Informationen wie:

Ereignisquelle:
javax.swing.JButton[,0,63,135x27,
Layout=javax.swing.OverlayLayout,
alignmentX=0.0,alignmentY=0.5,
Boarder=javax.swing.plaf.BorderUIResource$CompoundBorderUIResource@492535,
flats=1200,
maximumSize=,
minimumSize=,
preferredSize=,
defaultIcon=,
disabledIcon=,
disabledSelectedIcon=,
maigrün=javax.swing.plaf.InsetsUIResource[top=2,lest=14,boomt=2,right=14],
paintBorder=true,
paintFocus=true,
pressedIcon=,
rolloverEnabled=Fasel,
rolloverIcon=,
rolloverSelectedIcon=,
selectedIcon=,
Text=berechnen,
defaultCapable=true]

Diese führen aber im Augenblick nicht wirklich weiter. Es besteht natürlich die Möglichkeit, über die angeklickte Schaltfläche an die TextFields für die Berechnung zu gelangen. Sinnvoll ist ein derartiges Vorgehen jedoch nur in seltenen Fällen. Die Entscheidung für eine separate Klasse eines ActionListeners war falsch.

Internen ActionListener erstellen

Bearbeiten

Wie kann der Listener in das RabattPanel integriert werden, ohne eine eigenständige Klasse zu beanspruchen? Durch einfachesVerfassen der Methode actionPerformed(...), jedoch nicht ohne die Klassendefinition des RabattPanels vorher zu erweitern. Dem Compiler muss nur mitgeteilt werden, dass diese Klasse ihren eigenen ActionListener implementiert. Die Einleitung der Klassendefinition wird erweitert zu

class RabattPanel8 extends JPanel implements ActionListener {

Die Methode actionPerformed(...) wird aus der Datei Actions.java kopiert und in die Datei "RabattPanel8.java" übernommen. Der Ort spielt innerhalb des Klassifizierungsblocks eigentlich keine Rolle, aber um chronologisch fortzufahren, soll die Methode unmittelbar vor dem Constructor liegen. Jetzt wird noch das Objekt actions entfernt und ... Was ist mit der Zeile

button.addActionListener( actions);

Welches Objekt der Klasse ActionListener soll hier als Argument dienen? Wo soll die Instanz sein? Nun, die Sache ist ganz einfach – dieses (this) Objekt ist die Instanz, denn this object implementiert den ActionListener. Damit kann der Schaltfläche auch getrost dieses Objekt als Argument für den Listener übergeben werden.

button.addActionListener( this);

Ein Testlauf ergibt denn auch ein entsprechendes Ergebnis. Jetzt ist es ein Leichtes, die Referenzen der TextFields zu erhalten, denn sie befinden sich ja in dieser Klassendefinition. Die jetzt vorzunehmenden Änderungen sind:

  1. Entfernen der Vorbelegung von Betrag und Rabatt.
  2. Entfernen des Methodenaufrufs zur Berechnung.
  3. Methoden zur Berechnung innerhalb der Methode actionPerformed(...) aufrufen.

Die Datei "RabattPanel9.java" enthält diese Änderungen. Eine Überprüfung ergibt denn auch zunächst leere TextFields nach erfolgtem Start des Programms. Die Eingabe der ursprünglichen Werte (1234.56 und 7) in die ersten beiden Felder ergibt nach Klick auf die Schaltfläche auch noch das korrekte Ergebnis.

Verhalten bei fehlerhaften Eingaben

Bearbeiten

Um das jetzt vielleicht aufkommende Gefühl der Zufriedenheit im Keim zu ersticken, soll einmal folgende Eingabe ausprobiert werden:

Betrag = XYZ und Rabatt = 0

Die Ausgabe ist sehr "umfangreich". Leider hat sie nicht das Geringste mit der Berechnung eines Rabatts zu tun.

Aufgabe:

Spontan entscheiden.

  1. Bei Rabatt = 0 erfolgt eine Division durch NULL.
  2. Eine textuelle Eingabe bei Betrag führt zu chaotischen Ergebnissen bei valueOf.
  3. Die beiden genannten Punkte treffen zu und erst beide zusammen führen zu der Meldung.
  4. Der Fehler liegt ganz woanders.