AS3-Tutorium: Flash: Calculator 01 integer
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> |
<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 Parameterp_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
- Drücken der Clear-Taste:
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 WertvalueOld
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. Programmierprinzipen „Separation 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
undLEDMinusRed
auch verzichten, wennLEDRed
rechteckig ist. Dann kann man ein Plussymbol einfach durch zwei LEDs symbolisieren undLEDMinusRed
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 KlasseKey
, 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.
- Das Panel muss den Wert des Attributs
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 Programmierprinzip „Single 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 auftrue
oderfalse
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
undOFF
) - 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
undv_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 KlasseEnumKey
!) 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 Variablenv_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_down und evtl. weitere wie z.B. lb_up
, 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 Eigenschaften
→ Komponentenparameter
(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.