AS3-Tutorium: Flash: Calculator 01 integer

aus GlossarWiki, der Glossar-Datenbank der Fachhochschule Augsburg
Version vom 6. März 2014, 12:01 Uhr von Kowa (Diskussion | Beiträge)
(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)

Dieser Artikel erfüllt die GlossarWiki-Qualitätsanforderungen nur teilweise:

Korrektheit: 4
(großteils überprüft)
Umfang: 1
(zu gering)
Quellenangaben: 5
(vollständig vorhanden)
Quellenarten: 5
(ausgezeichnet)
Konformität: 5
(ausgezeichnet)

AS3-Tutorium: Calculator: Flash | Flex

Flash: Übersicht | Teil 1 | Teil 2

Taschenrechner

In diesem Teil des Tutoriums wird beschrieben, wie ein einfacher Taschenrechner implementiert werden kann. Dieser kann nur Interzahlen verarbeiten. Auf Komma- und Exponential-Darstellungen wird ebenso verzichtet, wie auf eine Fehlerbehandlung bei einer Division durch Null.

<swf width="300" height="420">http://glossar.hs-augsburg.de/beispiel/tutorium/flash_cs5/calculator/calculator_01_integer/Calculator01Flash11.swf</swf>

Musterlösung Flash (SVN-Repository)

<swf width="300" height="420">http://glossar.hs-augsburg.de/beispiel/tutorium/flex_4/calculator/calculator_01_integer/Calculator01Flex.swf</swf> Musterlösung Flex (SVN-Repository)


Datenmodell

Im Folgenden wird ein Klassendiagramm beschrieben, dass die Implementierung des obigen Taschenrechners gemäß dem VCLSD-Pattern ermöglicht. Den für dieses Model von StarUML erzeugten Code-Rahmen finden Sie im Ordner staruml_code der Musterlösung. Beachten Sie allerdings, dass die Attribute im Allgemeinen mit Hilfe von Getter- und Setter-Methoden implementiert werden müssen. Insbesondere für die Attribute data, controller, logic und keyInput, die von der Main-Klasse Main initialisiert werden, sollten nur Setter-Methoden definiert werden. Auf diese Attribute wird niemals von außerhalb zugegriffen.

Vier LDVCS-Module

Aus folgenden vier LDVCS-Modulen besteht der Taschenrechner (ein Servicemodul gibt es nicht, da der Taschenrechner weder konfiguriert werden muss noch mit anderen Anwendungen kommuniziert):

ViewCalculator (ViewCalculatorRed)
Die View-Komponente enthält ein Panel bestehnd aus Digitalziffern (ein Vorzeichen-Element und acht Sieben-Segment-Elemente) zum Darstellen des Rechenergebnisses. Außerdem enthält sie 16 Tasten zur Eingabe von Ziffern und Operatoren.
ControllerCalculator
Der Controller bietet eine Methode keypress, die von der View aufgerufen wird, sobald eine Taste gedrückt wird. Dieser Methode wird im Parameter p_key übergeben, welche Taste gedrückt wurde: 0, ..., 9, C, =, +, -, ×, ÷. Der Controller überfügt über zwei Attribute, die bei Programmstart initialisiert werden:
  • stage enthält das Stage-Objekt, um auf Tastatur-Ereignisse direkt zugreifen zu können. Damit kann der Taschenrechner auch über die Tastatur gesteuert werden.
  • logic enthält die Logik-Komponente der Anwendung. Die Aufgabe des Controllers ist es, die Methoden dieser Komponenten abhängig von der Benutzer-Eingabe aufzurufen.
LogicCalculator
Aufgabe der Logik-Komponente ist es, abhängig von der Benutzeraktion ein neues Ergebnis zu berechnen und dieses im Data-Objekt zu speichern.
Es gibt drei mögliche Benutzeraktionen:
  • Drücken der Clear-Taste: actionClear
  • Drücken der Ist-gleich-Taste (oder der Return-Taste): actionCompute
  • Drücken einer Zifferntaste: actionDigit
  • Drücken einer Operatortaste: actionOperator
DataCalculator
Das Datenobjekt enthält diejenigen Daten, die von der View visualisiert werden können. Für den in der Musterlösung vorgestellten Taschenrechner braucht das Datenobjekt nur eine einziges Atribut zu enthalten:
  • value: Die Zahl, die aktuell erfasst wird bzw., das Ergebnis, nachdem eine mathematische Operation ausgeführt wurde.

Es ist aber durchaus sinnvoll, weitere Attribute für die Visualisierung bereitzustellen (siehe oben: Musterlösung Flex):

  • operator: Der Operator, der als nächstes angewendet werden soll (wenn die aktuelle Zahl vollständig eingegeben wurde).
  • valueOld: Die letzte Zahl, die erfasst bzw. gerechnet wurde. Diese wird mit der aktuellen Zahl (mit Hilfe des zuvor eingegebenen Operators) verknüpft, sobald der nächste Operator ("=", "+", "-", "×", "÷") gedrückt wird.
  • operationRedo: Ein Boolescher Wert, der angibt, ob sich der Rechner im Operator-Redo-Modus befindet oder nicht. Im ersten Fall wird der Operator vor dem Panel, das den Wert valueOld darstellt, angezeigt. Im anderen Fall, wird der Operator hinter diesem Panel dargestellt.

Separation of Concerns

Jede der im Diagramm angegebenen Klassen hat genau eine Aufgabe (vgl. ProgrammierprinzipenSeparation of Concernse“ und „Single responsibility principle“):

LED
Visualisierung einer LED (Leuchtdiode), die ein- und ausgeschaltet werden kann.
LEDRed, LEDPlusRed, LEDMinusRed
Flash-Symbole, die rote LEDs visualisieren und LED als Basisklasse haben.
(Man könnte auf die Klassen LEDPlusRed und LEDMinusRed auch verzichten, wenn LEDRed rechteckig ist. Dann kann man ein Plussymbol einfach durch zwei LEDs symbolisieren und LEDMinusRed durch eine.)
Sign
Visualisierung eines Vorzeichen-Elements, bestehend aus ein bis drei LEDs.
In der Musterlösung kann das Element drei Werte annehmen: „positiv“, „negativ“ und „ausgeschaltet“. Es würde aber auch reichen, nur ein Minuszeichen als Vorzeichen zu verwenden. In diesem Fall würden sich die Zustände „positiv“ und „ausgeschaltet“ bei der Visualisierung nicht unterscheiden (siehe oben: Musterlösung Flex).
Digit
Visualisierung eines Sieben-Segment-Elements, bestehend aus sieben LEDs.
In der Musterlösung kann ein deratiges Element ide Ziffern 0 bis 9 darstellen sowie ausgeschaltet sein (Digit.OFF). Weitere Werte (wie z.B. ein E für Error) sind denkbar.
Panel
Visualisierung einer Intergerzahl mit einem Vorzeichen und maximal acht Stellen.
Key
Visualisierung eines Taschenrechner-Buttons für die Benutzereingabe.
In der Musterlösung wurde allerdings nicht Button als Basisklasse verwendet, sondern eine selbst definierte Klasse Key, da die Skinning-Möglichkeiten von Flash-Buttons eingeschränkt sind. In der Flex-Musterlösung wurden dagegen Flex-Spark-Buttons verwendet. Deren Aussehen kann problemlos gemäß den eigenen Wünschen verändert werden (Skinning).
ViewCalculator
Verknüpfung der anderen Komponenten mit der View:
  • Das Panel muss den Wert des Attributs value der Daten-Komponente darstellen.
  • Die Keys müssen, sobald sie geklickt werden, ihren Label-Wert an die Methode keyClick der Controller-Komponenten weiterleiten.
EnumKey
Bereitstellung von Konstanten, die im Controller, in der Logikkomponente sowie im Daten-Objekt verwendet werden.
ControllerCalculator
Aufbereitung der Benutzer-Eingaben für die Logik-Komponenten. Dabei werden zwei Arten von Eingabe unterstützt:
  • Eingabe per Maus-Klick (über die View-Komponente)
  • Eingabe per Tastatur (Listener für Keyboard-Events)
LogicCalculator
Aktualisieren der Werte in der Daten-Komponente nach jeder Benutzerkation.
DataCalculator
Verwaltung der Daten, die visualisiert werden sollen:
  • Speichern der Daten
  • Informieren der interessierten Observers über Änderungen
Main
Starten der Anwendung, insbesondere Erzeugen und Initialisieren aller VCLSD-Module.

Anmerkung: Das ProgrammierprinzipSingle responsibility principle“ wurde nicht zu 100% umgesetzt. So könnten z.B. zwei Controller-Komponenten definiert werden, eine für die Verarbeitung von Bildschirmeingaben und eine für die Verarbeitung von Tastatureingaben. Genauso könnte die Klasse ViewCalculator durch zwei Klassen ersetzt werde: Eine zur Initialisierung der Daten-Ausgabe (Panel) und eine zur Initialisierung der Benutzereingabe (Keys).

Ziel des „Single responsibility principle“ ist immer, dass es für keine Klasse mehrere Gründe geben soll, warum sie geändert werden muss. Zum Beispiel muss derzeit die Klasse ControllerCalculator geändert werden, wenn sich die Tastatureingabe ändert (was z.B. bei einem Umstieg von einer deutschen auf eine französische Tastatur notwendig wird, da der KeyboardEvent nur die Character Codes einer englischen Tastatur liefert und daher die Controller-Klasse an jede Tastatur angepasst werden muss). Aber auch wenn die Symbole der Tasten, die per Maus aktiviert werden, geändert werden sollen, muss diese Klasse modifiziert werden, obwohl der eine Änderungswunsch nichts mit dem anderen zu tun hat.

Implementierung

Bei folgenden Beschreibungen der Implementierungen wird immer davon ausgegangen, dass in jeder Klasse alle Konstaten, die im obigen Digaramm angegeben wurden, in der Form

public static const KONSTANTE: datentyp = wert;

enhalten sind (siehe den von StarUML erzeugten Coderahmen der Musterlösung).

Implementieren Sie die Klassen in der angegebenen Reihenfolge. Testen Sie eine Implementierung ausgiebig, bevor Sie mit der nächsten Klasse beginnen.

LED

Visualisierung einer LED (Leuchtdiode)

Diese Klasse enhält ein Attribut value, das die beiden Werte LED.OFF und LED.ON annehmen kann.

Das Attribut wird mit Hilfe eines Getter-/Setter-Paares realisiert:

public function get value(): int {...}
public function set value(p_value: int): void {...}

Die Getter-Methode liefert als Ergebnis den aktuellen Status der LED. Dieser wird aus einer privaten Zustandsvariablen ausgelesen. Auf diese Methode könnte sogar verzichtet werden, da es nicht die Aufgabe einer View ist, Auskunft über seinen Zustand zu geben. Eine View dient lediglich dazu, einen Zustand zu visualisieren. Allerdings ist die Getter-Methode für das Debugging vorteilhaft, um den aktuellen Wert, den die LED visualisieren soll, per trace auszugeben.

Sofern die Getter-Methode implementiert wurde, muss die Setter-Methode den neuen Wert in der zuvor genannten Zustandsvariablen speichern. Ihre eigentliche Aufgabe ist jedoch, den neuen Wert zu visualisieren, indem sie das zugeörige Objekt this (dem die Klasse LED als Basisklasse zugeordnet ist) manipuliert. Dafür gibt es mehrere Möglichkeiten:

  • die Eigenschaft visible des Symbols auf true oder false setzen
  • den Alphawert des Symbols verändern
  • zu einer dem Wert ensprechenden Stelle in der Timeline des Symbols springen


Sign und Digit

Visualisierung eines Vorzeichen-Elements und eines Sieben-Segment-Elements

Diese Klassen enhalten ein Attribut value, das diverse Werte annehmen kann. Wie zuvor wird es mit einem Getter-/Setter-Paar realisiert, wobei auf die Getter-Methode wieder (aus demselben Grund wie zuvor) verzichtet werden könnte:

public function get value(): int {...}
public function set value(p_value: int): void {...}

Die wichtigste Aufgabe der Setter-Methode ist wiederum die Visualisierung des übergebenen Wertes. Dafür werden in einem zugehörigen Symbol (das heißt, in einem Symbol, dem die Klasse Sign bzw. Digit als Basisklasse zugeordnet wurde) LED-Symbole eingefügt und mit „Instanznamen“ (wie z.B. d_led_0, d_led_1, ...) versehen. Die Aufgabe der Setter-Methode ist es nun, diese LEDs (zum Beispiel mit Hilfe einer Schleife) zu initialisieren:

private static const c_led_prefix:     String = "d_led_";
private static const c_number_of_leds: uint   = 7;  // bzw. 3 in der Klasse Sign

private static const c_led_states: Array 
  = [ [LED.ON, LED.OFF, ...], 
      [LED.OFF, LED.ON, ...], 
      ...
    ];

for (var i: int = 0; i < c_number_of_leds; i++)
  this[c_led_prefix + i].value = c_led_states[p_value+1][i];

Die Konstante c_led_states enthält ein Array von Arrays. Für jeden möglich value-Wert ist im äußeren Array ein Array enthalten, welches für jede LED beschreibt, ob sie ein- oder ausgeschaltet werden muss, wenn dieser Wert visualisiert werden soll.

Ein Sieben-Segment-Element enthält sieben LEDs. Das Vorzeichen-Element enthält ein bis drei LEDs:

  • ein Minus-Symbol (hier besteht kein Unterschied zwischen POSITIVE und OFF)
  • ein Minus- und ein Plus-Symbol
  • ein Minus- und ein Plus-Symbol, wobei letzteres aus zwei LED zusammengesetzt wird

Beim Vorzeichen-Element kann man die Setter-Methode auch einfacher realisieren (ohne Array und For-Schleife). Zum Beispiel könnten zwei LEDs (eine Plus-LED und eine Minus LED) folgendermaßen geschaltet werden:

d_plus.value  = (p_value == POSITIVE) ? LED.ON : LED.OFF;
d_minus.value = (p_value == NEGATIVE) ? LED.ON : LED.OFF;


Panel

Visualisierung einer Intergerzahl

Die Aufgabe eines Panel-Objektes ist es wiederun, einen Wert zu visualisieren. Diesmal soll jedoch nicht nur eine Ziffer, sondern eine ganze Zahl (einschließlich Vorzeichen) dargestellt werden. Dazu werden dem Panel ein Vorzeichenelemnt (d_sign) sowie acht Ziffern-Elemente zugeordnet: d_digit_7 bis d_digit_0; das niederwertigste Element befindet sich ganz rechts.

Wie zuvor gibt es eine Getter- und eine Setter-Methode für den Zugriff auf das Attribut value (wobei die Getter-Methode wiederum nicht unbedingt nötig ist):

public function get value(): int {...}
public function set value(p_value: int): void {...}

Die Implementierung der Setter-Methode ist allerdings etwas aufwändiger als zuvor. Bevor diese Methode implementiert werden kann, sollten ein paar Konstanten definiert werden:

private static const c_digit_prefix: String = "d_digit_";

private static const c_base:             uint = 10;
private static const c_number_of_digits: uint = 8;
private static const c_max_value:        uint = Math.pow(10,8)-1;

Nun kann man mit der Implementierung der Setter-Methode beginnen. Zunächst muss der übergebene Wert gesichert werden (sofern man die Getter-Methode implementiert hat):

v_value = p_value;

Als nächstes muss sichergestellt werden, dass der übergebene Wert nicht zu groß oder zu klein ist, um die Zahl mit acht Ziffern darzustellen. Falls dies nicht der Fall ist, wird der übergebene Wert durch die größte bzw. kleinste darstellbare Zahl ersetzt (wobei c_max_value == Math.pow(10,8)-1 gilt):

if (p_value < -c_max_value)
  p_value = -c_max_value;
else if (p_value > c_max_value)
  p_value = c_max_value;

Nun kann der Wert des Vorzeichens bestimmt werden. Als Seiteneffekt wird p_value durch seinen Absolutbetrag ersetzt, damit für die nachfolgenden Befehle p_value >= 0 garantiert werden kann:

if (p_value > 0)
  d_sign.value = Sign.POSITIVE;
else if (p_value < 0)
{
  d_sign.value = Sign.NEGATIVE;
  p_value = -p_value;
}
else
 d_sign.value = Sign.OFF; // 0 hat kein Vorzeichen!

Als nächstes werden die Ziffern-Elemente (von rechts nach links) auf die korrekten Werte gesetzt, wobei führende Nullen nicht beachtet werden. Die do-while-Schleife garantiert, dass die Zahl Null nicht mit null Ziffern, sondern mit einer Null visualisiert wird.

private static const c_number_of_digits: int = 8; // Das Panel hat 8 Ziffern.
private static const c_base: int = 10;            // Wir rechnen im Zehnersystem.

var i: int = 0;
      
do
{
  this[c_digit_prefix + i].value = p_value % c_base;
  p_value /= c_base;
  i++;
}
while (p_value > 0);

Zu guter Letzt werden die restlichen Ziffern „ausgeschaltet“:

while (i < c_number_of_digits)
{
  this[c_digit_prefix + i].value = Digit.OFF;
  i++;
};


Key

Visualisierung eines Taschenrechner-Buttons

Ein normaler Flash-Button kann als Eingabe-Taste für den Taschenrechner verwendet werden. Die Klasse Key hat dann nur eine Aufgabe. Im Konstruktor Key() können die Buttoneigenschaften (wie z.B. Fontart und -größe des Button-Labels) festgelegt werden (siehe Dokumentation von Adobe → Examples). Button-Layout-Änderungen sind allerdings relativ aufwändig.[1]

Die Klasse Key, die der Button-Komponente als Basisklasse zugewiesen wird, könnte folgendermaßen definiert werden, um z.B. eine quadratische Buttonform mit einer gut lesbaren Beschriftung (Größe in Pixeln = 70% der Buttongröße) zu erzwingen:

package component.key 
{
  import fl.controls.Button;
  
  import flash.text.TextFormat;
  import flash.text.TextFormatAlign;

  public class Key extends Button 
  {
    /////////////////////////////////////////////////////////////////////////////
    // Methods
    /////////////////////////////////////////////////////////////////////////////
    
    public function Key() 
    {
      super();
      this.height = this.width;
      
      var l_text_format: TextFormat = new TextFormat();
      l_text_format.size  = this.width*.7;
      l_text_format.bold  = true;
      l_text_format.align = TextFormatAlign.CENTER;
      
      this.setStyle("textFormat", l_text_format);
    }
    
    /////////////////////////////////////////////////////////////////////////////
    // End of class
    /////////////////////////////////////////////////////////////////////////////
  }
}

Allerdings sollten noch weitere Layoutanpassungen vorgenommen werden:

  • Anpassung der Hintergrundfarbe, wenn der Button gedrückt wird
  • Entfernen des Fokusrahmens
  • etc.

Anmerkung: Am Ende des Tutoriums wird eine alternative Implementierungs-Möglichkeit für die Klasse Key vorgestellt, die ohne Flash-Buttons auskommt.

EnumKey

Bereitstellung von Konstanten

Nachdem nun die Klassen zur Visualisierung von Integerahlen und zur Eingabe von Rechenoperationen existieren, ist es an der Zeit, die eigentlichen DCLCV-Klassen (ViewCalculator, ControllerCalculator, LogicCalculator, DataCalculator) zu implementieren. Diese Klassen verarbeiten Ziffern und mathematische Operatoren, die vom Benutzer per Tastendruck übergeben werden.

Die Klasse EnumKey simuliert einen Enumeration-Datentyp, der alle erlaubten Tasten-Werte enthält, indem sie für jeden Tastenwert eine statische Konstante definiert. Im Prinzip kann die von StarUML erzeugte Implementierung dieser Klasse ohne besondere Änderungen übernommen werden:

package calculator.enum 
{
  public class EnumKey
  {
    public static const DIGIT: int = 0;
    public static const OPERATOR: int = 1;
    public static const CLEAR: String = "C";
    public static const COMPUTE: String = "=";
    public static const ADD: String = "+";
    public static const SUBTRACT: String = "-";
    public static const MULTIPLY: String = "×";
    public static const DIVIDE: String = "÷";
  }
}

Sauberer wäre es, einen echten Enumeration-Typ zu erzeugen, der garantiert, dass eine Variable vom Typ EnumType nur die gewünschten Werte annehmen kann. Dies wird im zweiten Teil des Tutoriums realisiert.

Im StarUML-Diagramm ist allerdings neben den oben genannten Konstanten noch ein Hash-Array angegeben: OPERATIONS. Dieses kann und sollte verwendet werden, um alle Operationen zu speichern, die der Taschenrechner ausführen kann:

package calculator.enum 
{
  public class EnumKey 
  {
    /////////////////////////////////////////////////////////////////////////////
    // Private static members
    /////////////////////////////////////////////////////////////////////////////
    
    private static function add     (a: int, b: int): int { return a+b; }
    private static function subtract(a: int, b: int): int { return a-b; }
    private static function multiply(a: int, b: int): int { return a*b; }
    private static function divide  (a: int, b: int): int { return a/b; }
    private static function id2     (a: int, b: int): int { return b; }
    
    /////////////////////////////////////////////////////////////////////////////
    // Constants
    /////////////////////////////////////////////////////////////////////////////

    public static const DIGIT:    String =  "digit";
    public static const OPERATOR: String =  "operator";

    public static const CLEAR:    String = "clear";
    public static const COMPUTE:  String = "=";

    public static const ADD:      String = "+";
    public static const SUBTRACT: String = "−";
    public static const MULTIPLY: String = "×";
    public static const DIVIDE:   String = "÷";

    public static const OPERATIONS: Object = {};
    OPERATIONS[EnumKey.ADD]      = add;
    OPERATIONS[EnumKey.SUBTRACT] = subtract;
    OPERATIONS[EnumKey.MULTIPLY] = multiply;
    OPERATIONS[EnumKey.DIVIDE]   = divide;
    OPERATIONS[EnumKey.COMPUTE]  = id2;
    
    /////////////////////////////////////////////////////////////////////////////
    // End of class
    /////////////////////////////////////////////////////////////////////////////
  }
}

Das Hash-Array OPERATIONS wird mit folgenden Operationen gefüllt:

  • Addition (add)
  • Subtraktion (subtract)
  • Multiplikation (multiply)
  • Division (divide)
  • Identität (id2)

Die Identität (genauer die Identität bezüglich des zweiten Arguments) gibt einfach das zweite Argument als Ergebnis zurück. Dies wird benötigt, um = auch als Operation behandeln zu können. Jede der obigen fünf Funktionen wird später verwendet, um zwei Werte (z.B. valueOld und value) zu verknüpfen und eine Zahl als Ergebnis zurückzugeben. Die Funktion id2 liefert einfach das zweite Argument, also im eben genannten Beispiel value, als Ergebnis. Das heißt, diese Funktion lässt die letzte Zahl, die eingegeben wurde, unverändert.

In der Logik-Komponente kann das Hash-Array verwendet werden, um zwei Zahlen mit einem belibiegen Operator zu verknüpfen, ohne dass eine Fall-Unterscheidung notwendig wäre. Zum Beispiel schreibt man:

v_value_old = EnumKey.OPERATIONS[v_operator_old](v_value_old, v_data.value);

an Stelle von:

switch (v_operator_old)
{
  case EnumKey.ADD:
    v_value_old = v_value_old + v_data.value;
    break;
  case EnumKey.SUBTRACT:
    v_value_old = v_value_old - v_data.value;
    break;
  case EnumKey.MULTIPLY:
    v_value_old = v_value_old * v_data.value;
    break;
  case EnumKey.DIVIDE:
    v_value_old = v_value_old / v_data.value;
    break;
  case EnumKey.COMPUTE:
    v_value_old =              v_data.value;
    break;
};

Das Hash-Array OPERATIONS bietet noch einen weitern Vorteil: Die Zahl der Operationen, die der Taschenrechner verarbeiten kann, kann jederzeit problemlos erhöht werden, ohne dass die Logik-Komponenten angepasst werden müsste. Mann muss nur weitere Operator-Konstanten und die zugehörigen (zweistelligen) Operationen in die Klasse EnumKey einfügen (wie z.B. ^). Anschließend fügt man in die View eine Taste mit der entsprechenden Operation ein und sorgt dafür, dass der Controller diese Taste auch verarbeitet — und schon stellt der Taschenrechner eine neue Operation zur Verfügung.


DataCalculator

Verwaltung der Daten, die visualisiert werden sollen

Die Aufgabe der Daten-Komponente ist es, diejeingen Daten zur Verfügung zu stellen, die von der View visualisiert werden sollen. Die Logik-Komponente schreibt und liest diese Werte.

Folgende Implementierung wird von StarUML erzeugt (plus einem Default-Konstruktor).

  public class DataCalculator extends EventDispatcher
  {
    public var value: int = 0;
  }

Allerdings ist diese Implementierung unvollständig, da eine Änderung des Wertes der View mit Hilfe eines Signals (Events) angezeigt werden muss. Das heißt, man muss das Attribut mit Hilfe einer privaten Variablen (z.B. v_value) und eines Getter-/Setter-Paars implementieren.

Die Getter-Methode liefert den Wert der privaten Variablen als Ergebnis. Die Setter-Methode speichert den übergebenen Wert in der lokalen Variablen und siganlisiert dann die Änderung dieser Variablen mit einem Event:

this.dispatchEvent(new Event(Event.CHANGE));

Weitere Attribute, die visualisiert werden sollen (wie operator, valueOld und operationRedo), werden einfach auf dieselbe Art in diese Klasse als öffentliche Attribute eingefügt. Das heißt, auch hier muss bei einer Änderung eines Wertes, diese per Event signalisiert werden.

Ganz sauber wäre es, wenn nur die Logik-Komponente auf die Setter-Methode Zugriff hätte. Die könnte man mit Hilfe von User define namspaces[2][3] realisieren, soll hier aber nicht weiter verfolgt werden.


ControllerCalculator

Aufbereitung der Benutzer-Eingaben für die Logik-Komponenten

Die Aufgabe des Controllers (genauer: der Methode keyPress) ist es, abhängig von der vom Benutzer gedrückten Taste (p_key) die entsprechende Mehode in der Logik-Komponente aufzurufen und dieser den Tastenwert zu übergeben.

Allerdings ist es (gerade bei der Eingabe über die Tastatur) denkbar, dass der Benutzer unterschiedliche Tasten drücken kann und dennoch dasselbe Ergebis erzielen will. Zum Beispiel kann er auf der Tastatur die Enter-Taste oder das das Ist-gleich-Zeichen drücken, jeweils mit dem Ziel, die Aktion EnumKey.COMPUTE durchzuführen. Der Controller muss also den von der Benutzer-Aktion übermittelten Tastencode in durch den zugehöregen Code der Klasse EnumKey ersetzen, bevor er eine Methode der Logik-Komponente aufruft. Dies kann mit Hilfe von Switch-Anweisungen oder — sauberer — mit Hilfe eines Hash-Arrays erfolgen.

Damit der Controller auf die Logik-Komponente zugreifen kann, wurde das Add-only-Attribut logic eingeführt. Dies wird folgendermaßen implementiert:

private var v_logic: LogicCalculator;
    
public function set logic (p_logic: LogicCalculator): void
{
  v_logic = p_logic;
}

Das heißt, es gibt eine (private) Zustandsvariable, in der die Logik-Komponente gespeichert wird, sowie eine Setter-Methode zum Initialisieren dieser Variablen. Die Initialisierung wird von dem Main-Objekt (der Klasse Main) beim Programmstart vorgenommen.

Falls nicht nur Eingaben per Maus-Klick (über die View) möglich sein sollen, sondern auch Tastatur-Eingaben, wird ein zweites Add-only-Attribut benötigt, das von Main beim Programmstart initialisiert wird: stage.

Hier ist es gar nicht notwendig, das Stage-Objekt in einer Zustandsvariablen zu speichern. Es reicht aus, dem Stage-Objekt einen Event-Listener zuzuordnen, der dafür sorgt, dass bei jedem Tastenklick eine interen Methode aufgerufen wird, die diesen verarbeitet:

public function set stage(p_stage: Stage): void
{
  p_stage.addEventListener(KeyboardEvent.KEY_DOWN, o_key_input);
}

Die Methode o_key_input muss wiederum Tastenklicks in Methodenaufrufe der Logik-Komponente umsetzen. Dabei ist allerdings eine große Unsauberkeit von ActionScript zu beachten: Die Methode charCode der Klasse KeyboardEvent liefert stets das Zeichen, dass auf einer englischen Tastatur an dieser Stelle zu finden wäre. Wenn man auf einer deutschen Tastatur beispielsweise die Ist-gleich-Taste ("=") drückt, liefert String.fromCharCode(p_event.charCode) die geschlossene Klammer (")") als Ergebnis.

Wenn man also die Methode charCode verwendet, muss man theoretisch für jede denkbare Tastatur einen eigenen Tastatur-Contoller schreiben.

Als Workaround kann man ein unsichtbares Textfeld (alpha == 0) über die gesamte Bühne legen, diesem den Eingabefokus geben und alle Tastaturereignisse von diesem Feld abfangen. Dies funktioniert, da bei der Eingabe von Zeichen in ein Textfeld das von Benutzer im Betriebssystem eingestellte Tastatur-Layout beachtet wird. Allerdings ist diese Art der Implementierung aufwändiger und soll hier nicht weiter verfolgt werden.


ViewCalculator

Verknüpfung der anderen Komponenten mit der View

Die Aufgaben der Komponente ViewCalculator sind:

  • Visualisierung von Daten, die in der Daten-Komponente enthalten sind
  • Weiterleiten von Benutzer-Aktionen an die Controller-Komponenten

Damit die View-Komponente Zugriff auf die Daten-Komponente und die Controller-Komponente hat, muss sie zwei Add-only-Attribute data und controller definieren, die ebenfalls vom Main-Objekt beim Programmstart initialisert werden.

Die Klasse ViewCalculator ist einem Flash-Symbol als Basis-Klasse zugeordnet, das ein Panel-Objekte der Art PanelRed (namens d_panel) sowie sechzehn Tasten der Art KeyGray enthält. Den Tasten brauchen keine Namen zugeordnet zu werden. Es reicht, jeder Taste einen eigenen Label-Wert zuzuordnen (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, C, =, +, , ×, ÷).

In der Klasse ViewCalculator müssen nur noch geeignete Event-Listener und -Handler definiert werden.

Jedesmal, wenn sich der Wert value der Datenkomponente ändert, wird dieser Wert ausgelesen und dem Attribut value des Panels zugewiesen:

private var v_data: DataCalculator;

public function set data(p_data: DataCalculator): void
{
  v_data = p_data;
  v_data.addEventListener(Event.CHANGE, o_data_change);
}

private function o_data_change(p_event: Event): void
{
  d_panel.value = v_data.value;
}

Darüber hinaus wird jeder Taste ein Key-Click-Listener zugeordnet, desses Aufgabe es ist, den Inhalt des Button-Labels an die Methode keyPress des Controllers weiterzuleiten:

private var v_controller: ControllerCalculator;

public function set controller(p_controller: ControllerCalculator): void
{
  v_controller = p_controller;
      
  for (var i: int = 0; i < this.numChildren; i++)
  {
    var l_child: Object = this.getChildAt(i);
    if (l_child is Key)
      l_child.addEventListener(MouseEvent.MOUSE_DOWN, o_key_click);
  };
}

private function o_key_click(p_event: MouseEvent): void
{
  v_controller.keyPress((p_event.currentTarget as Key).label);
}


LogicCalculator: Test-Version

Aktualisieren der Werte in der Daten-Komponente

Um die Anwendung testen zu können, bevor man die komplexeste Klasse LogicCalculator vollständig implementiert, kann man zunächste folgende Trivialimplementierung verwenden, die in der Datenkomponente lediglich diejenige Integerzahl speichert, die sich durch eine Folge von Ziffern-Eingaben ergibt. Die Eingabe von Operatoren wird (mit Ausnahme der Clear-Taste) noch nicht beachtet.

package calculator.logic 
{
  import calculator.data.DataCalculator;

  public class LogicCalculator
  {
    private var v_data: DataCalculator;
    
    public function set data (p_data: DataCalculator): void
    {
      v_data = p_data;
    }

    public function actionClear(): void
    {
       v_data.value = 0;
    }

    public function actionCompute(): void
    {
    }
    
    public function actionDigit(p_digit: int): void
    { 
      v_data.value = v_data.value * 10 + p_digit;
    }
    
    public function actionOperator(p_operator: String): void
    {
    }    
  }
}

Man bachte, dass es auch hier ein Add-only-Attribut gibt (data), das vom Main-Objekt initialisiert werden muss, bevor die Komponente verwendet werden kann.

Main

Starten der Anwendung, insbesondere Erzeugen und Initialisieren aller VCLSD-Module

Nun kann man den Taschenrechner testen.

Zunächst platziert man ein Objekt des Symbols ViewCalculator auf der Bühne und gibt diesem den Namen d_view. Damit hat man die erste der vier VCLSD-Module bereits erzeugt.

Als nächstes ordnet man dem Hauptfilm die Klasse Main als Main-Klasse zu.

Diese muss die übrigen drei der benötigten vier VCLSD-Module erzeugen und die Add-only-Attribute aller vier Komponenten initialisieren.

Achtung: In Flash CS5 wird das Stage-Objekt erst nach Ausführung des Konstruktor aber vor Eintritt in das erste Frame (ENTER_FRAME) initialisiert, wenn man die neuartigen TFL-Text in seinem Movie einsetzt. In diesem Fall kann das Attribut stage der Klasse ControllerCalculator nicht direkt im Konstruktor der Klasse Main initialisiert werden. Hier muss mit einen Event-Listern der Eintritt in das erste Frame abgefangen werden.

  public class Main extends MovieClip 
  {
    /////////////////////////////////////////////////////////////////////////////
    // VCLSD modules
    /////////////////////////////////////////////////////////////////////////////
    
    private var v_data:       DataCalculator;
    private var v_logic:      ILogicCalculator;
    private var v_controller: ControllerCalculator;
    
    public  var d_view:       ViewCalculator;
    
    /////////////////////////////////////////////////////////////////////////////
    // Constructor
    /////////////////////////////////////////////////////////////////////////////
    
    public function Calculator01Flash11()
    {
      super();
      this.addEventListener(Event.ENTER_FRAME, o_init);
    }
    
    private function o_init(p_event: Event): void
    {
      this.removeEventListener(Event.ENTER_FRAME, o_init);

      v_data = new DataCalculator();
      
      v_logic = new LogicCalculatorAutomatonMultiCompute();
      v_logic.data = v_data;
      
      v_controller = new ControllerCalculator();
      v_controller.logic = v_logic;
      v_controller.stage = stage;
      
      d_view.data       = v_data;
      d_view.controller = v_controller;
    }
    
    /////////////////////////////////////////////////////////////////////////////
    // End of class
    /////////////////////////////////////////////////////////////////////////////
  }
}

LogicCalculator

Aktualisieren der Werte in der Daten-Komponente

Um den Taschenrechner voll funktionsfähig zu machen, muss noch die Trivial-Implementierung der Klasse LogicCalculator, die zuvor für Testzwecke eingeführt worden war, deutlich erweitert werden.

Die Aufgabe der Logik-Komponente ist die Umwandlung der Benutzereingaben in Integerzahlen und mathematische Operationen. Die Reaktion auf eine Benutzereingabe hängt nicht nur von der Taste, die der Benutzer gedrückt hat ab, sondern auch von Aktion, die als letztes ausgeführt wurde.

Dementsprechend benötig man eine Zustandsvariable v_state, die zwei verschieden Zustände c_input_digit und c_input_operator annehmen kann:

private static const c_input_digit:    int = 0;
private static const c_input_operator: int = 1;

private var v_state: int = c_input_operator;

Darüber hinaus muss man sich die Zahl sowie den Operator merken, die vor der aktuellen Integerzahl eingegeben wurde.

private var v_value_old:    int    = 0;
private var v_operator_old: String = EnumKey.COMPUTE;

Der Benutzer ruft durch seine Aktionen (= Tastenklicks) jeweils eine von vier Methoden auf: actionClear, actionCompute, actionDigit oder actionOperator.


Implementierung der Methode actionClear

Die Methode actionClear muss lediglich alle Variablen auf die Initialwerte zurücksetzen:

Der Attribut, das durch die View angezeigt wird
v_data.value
Die Zustandsvariablen
v_state, v_value_old und v_operator_old


Implementierung der Methode actionCompute

Falls die Ist-gleich-Taste nicht besonders behandelt wird, kann man die Behandlung des Compute-Operators an die Methode actionOperator weiterleiten:

public function actionCompute(): void
{
  actionOperator(EnumKey.COMPUTE);
}


Implementierung der Methode addDigit

Die Aktion ist abhängig vom Zustand.

v_state == c_input_operator
Der aktuelle Wert muss auf die übergebene Ziffer gesetzt werden:
v_data.value = p_digit;
Der Zustand (v_state) muss aktualisiert werden.
v_state == c_input_digit
Der aktuelle Wert muss um die übergebene Ziffer erweitert werden:
v_data.value = v_data.value * 10 + p_digit


Implementierung der Methode addOperator

Die Aktion ist abhängig vom Zustand.

v_state == c_input_operator
Der gerade zuvor eingegebene Operator muss durch den neuen Operator ersetzt werden.
v_operator_old = p_operator;
v_state == c_input_digit
Sobald der Benutzer nach einer Integerzahl einen neuen Operator eingibt, wird zunächst der Zustand (v_state) aktualisiert und dann werden die alte Integerzahl (v_value_old) und die aktuelle Integerzahl (v_data.value) mit dem alten Operator (v_operator_old) verknüpft (Switch-Anweisung oder Hash-Map mit Funktionsobjekten; siehe Klasse EnumKey!) und das Ergebnis wird sowohl als alter (= v_value_old), als auch als aktueller Wert (= v_data.value) gespeichert. Abschließend wird der neue Operator in der Variablen v_operator_old gespeichert.


Mealy-Automat

Eine alternative Implementierung der Klasse LogicCalculator verwendet einen Mealy-Automat, um die Zustandsänderungen und die notwendigen Fallunterscheidungen zu beschreiben.

Das Ergebnis sind mehr Methoden, aber weniger Fallunterscheidungen. Jede Methode ist nur noch für einen Fall zuständig (Single responsibility principle). Der folgende Mealy-Automat definert zwei Zustände, zwei Aktionen (addDigit und addOperator) und vier Reaktionen (o_add_first_digit, o_add_digit, o_add_operator und o_add_another_operator).

Die Aufgabe der Methode clear ist nun, sowohl alle Variablen als auch den Automaten zurückzusetzen.

Die beiden Aktions-Methoden, addDigit und addOperator (die vom Controller aufgerufen werden), speichern die übergebenen Werte in zwei Zustandsvariablen (v_digit bzw. v_operator) und erzwingen dann einen Zustandsübergang des Automaten (v_automaton.doAction("addDigit") bzw. v_automaton.doAction("addOperator")).

Der Automat reagiert, indem er ein entsprechendes Signal schickt. Jedes dieser Signale sollte von einem gleichnamigen Observer (EventListener) verarbeitet werden. Dabei stimmen die Aufgaben der einzelnen Observer mit den zuvor beschriebenen Aufgaben überein:

o_add_first_digit
siehe addDigit, v_state == c_input_operator
o_add_digit
siehe addDigit, v_state == c_input_digit
o_add_another_operator
siehe addOperator, v_state == c_input_operator
o_add_operator
siehe addOperator, v_state == c_input_digit

Viel aufwändiger wird die Implementierung der Logik-Komponente, wenn man die Ist-gleich-Taste mit zusätzlicher Funktionalität versieht. Nach Drücken dieser Taste ist die aktuelle Rechnung abgeschlossen. Normalerweise könnte man nun das Ergebnis mit der C-Taste löschen und mit einer neuen Rechnung beginnen. Allerdings kann man für alle anderen Tastenklicks, die nach dem Drücken der Ist-gleich-Taste erfolgen, sinnvolle Aktionen definieren:

Die Ist-gleich-Taste wird nochmals gedrückt:
Führe die letzte Operation nochmals aus.
Bevor die Ist-Gleich-Taste nochmals gedrückt wird, wird eine Operator-Taste gedrückt:
Wende diese Operation mit dem zuletzt eingegebenen Wert auf den aktuellen Wert an.
Bevor die Ist-Gleich-Taste nochmals gedrückt wird, wird eine Operatortaste gefolgt von einer Zahl gedrückt:
Wende die eingegebene Operation mit dem eingegebenen Wert auf den aktuellen Wert an.
Bevor die Ist-Gleich-Taste nochmals gedrückt wird, wird eine Zahl eingegeben:
Wende die letzte Operation mit dem neu eingegebenen Wert auf den aktuellen Wert an.
Im Anschluss an die Ist-gleich-Taste wird eine Zahl eingegeben und eine Operator-Taste gedrückt:
Starte mit einer neuen Rechnung (so als ob nach der Ist-gleich-Taste die Clear-Taste gedrückt worden wäre).

Folgender Automat ist geeignet, diese ganzen Spezialfälle zu verarbeiten. Man beachte, dass es neue Ereignisse, wie z.B. o_start_new_operation gibt, bei denen speziell reagiert werden muss. Auf eine detailiertere Diskusion aller Reaktionsmethoden wird hier allerdings verzichtet.

Dieser Automat liegt beiden obigen Beispiels-Taschenrechnern zu Grunde. Probieren Sie es aus!

Hier haben wir ein Beispiel, bei dem eine Fallunterscheidung durch If-Anweisungen schnell so unübersichtlich wird, dass eine stabile Programmierung sehr schwierig zu realisieren ist. Automaten, die die Fallunterscheidung und die Reaktionsmethoden voneinander trennen, sind schon in diesem Beispiel einfachen If-Then-Else-Anweisungen deutlich überlegen.

Key: Eine alternative Realisierung

Visualisierung eines Taschenrechner-Buttons

Wie im Anschnitt Key angekündigt wurde, ist es relativ einfach möglich, die Eingabe-Tasten des Taschenrechners (Klasse Key) auch ohne Zuhilfenahme von Flash-Buttons zu realisieren.

Im Prinzip funktionert dies, wie im Tutorium Butterfly 07a character beschrieben. Man erzeugt ein Symbol, in dessen Zeitleiste die verschiedenen Zustände des Buttons in verschiedenen Frame-Blöcken visualisiert werden. Jeder dieser Blöcke beginnt mit einem Label (lb_up, lb_down und evtl. weitere wie z.B. lb_over für Mouse-Over-Effekte) und endet mit einem stop()-Befehl.

In einer eigenen Ebene wird blockübergreifend ein Text-Feld eingefügt, das den Namen d_label erhält.

Diesem Symbol wird die Klasse Key als Basis-Klasse zugewiesen:

package calculator.view.key 
{
  import fl.text.TLFTextField; // Nur Flash CS5!
  
  import flash.events.Event;
  import flash.events.MouseEvent;
  import flash.display.MovieClip;

  public class Key extends MovieClip
  {
    /////////////////////////////////////////////////////////////////////////////
    // Constants
    /////////////////////////////////////////////////////////////////////////////

    private static const c_lb_up:   String = "lb_up";
    private static const c_lb_down: String = "lb_down";
    
    /////////////////////////////////////////////////////////////////////////////
    // Instance variables
    /////////////////////////////////////////////////////////////////////////////

    public var d_label: TLFTextField; // Nur Flash CS5, sonst fl.controls.Label

    /////////////////////////////////////////////////////////////////////////////
    // Attributes
    /////////////////////////////////////////////////////////////////////////////

    [Inspectable(type="String", defaultValue="0", 
                 enumeration="0,1,2,3,4,5,6,7,8,9,C,=,+,−,×,÷"
                )
    ]
    public function get label(): String
    {
      return d_label.text;
    }
    
    public function set label(p_label: String):void
    {
      d_label.text = p_label;
    }

    /////////////////////////////////////////////////////////////////////////////
    // Constructor
    /////////////////////////////////////////////////////////////////////////////

    public function Key()
    {
      super();
      this.addEventListener(MouseEvent.MOUSE_UP,   o_mouse_up);
      this.addEventListener(MouseEvent.MOUSE_DOWN, o_mouse_down);
    }
    
    /////////////////////////////////////////////////////////////////////////////
    // Observers
    /////////////////////////////////////////////////////////////////////////////
    
    private function o_mouse_up(p_event: Event): void
    {
      this.gotoAndPlay(c_lb_up);
    }

    private function o_mouse_down(p_event: Event): void
    {
      this.gotoAndPlay(c_lb_down);
    }
    
    /////////////////////////////////////////////////////////////////////////////
    // End of class
    /////////////////////////////////////////////////////////////////////////////
  }
}

Man beachte die Meta-Anweisung Inspectable. Damit wird Flash mitgeteilt, dass der Label-Wert eines Key-Objektes auf der Bühne über den Flash-Eigenschaften-Inspektor festgelegt werden kann. Um dies zu erreichen, muss die Klasse calculator.view.key.Key nicht nur als Basisklasse im Eigenschaften-Fenster des Symbols eingetragen werden, sondern zusätzlich auch als Klasse unter Komponentendefinition des Symbols (Klick mit rechter Maustaste auf das Icon vor dem Symbol).

Sollte nach erneutem Öffnen des Komponentendefinition-Inspektors der Parameter label in der Parameterliste nicht erscheinen (dieses Problem besteht bei Flash CS5, aber nicht bei Flash CS4), muss dieser von Hand eingetragen werden:

Name: label, Variable: label, Wert: 0, Typ: String

Nun kann jedem Key-Symbol, das auf die Bühne gelegt wird, ein Label-Wert über das Fenster EigenschaftenKomponentenparameter (bzw. bei Flash CS4: Komponenteninspektor) zugewiesen werden. Allerdings wird dieser Wert nicht auf der Bühne angezeigt, sondern nur bei Start der zugehörigen SWF-Datei.

Quellen

SVN-Repository-Verweise


Dieser Artikel ist GlossarWiki-konform.