Стандартный калькулятор — Часть 3. Программирование

Автор: Андрей. Категория: Уроки Flex

Flex-приложение "Стандартный калькулятор" Программирование

Доброго всем дня. Продолжаем начатое. Сегодня мы «оживим» наш калькулятор, т.е. сделаем его полноценным программным продуктом. Это урок рассчитан как на программистов с опытом, так и на начинающих программистов. Возможно, для начинающих будет немного сложно понять всё сразу. Но не отчаивайтесь. Время, которое вы потратите на изучение и понимание этого урока стоит того. Можете разделить этот урок на несколько частей.

Начнём, пожалуй с общей схемы. На самом деле до этого урока, я не сталкивался с реализацией калькулятора, и, поверьте, это очень интересная задача. Пока вы не прочитали эту статью, рекомендую самим взять обыкновенный калькулятор и попробовать разобраться с алгоритмом его работы. Поверьте, это не просто сложение или вычитание.

Все проекты начинаются с листка бумаги. Вот и в этом случае, я запустил калькулятор в своём HTC T3333 и начал нажимать кнопки. В результате получилась такая таблица:

Последовательное нажатие кнопок калькулятора
Нажатая кнопка 2 = = = = 3 = 3 + = 4 1 =
Результат на экране 2 2 2 2 2 0 −2 3 −4 3 3 6 4 4 1 3 3 1

Выделенные ячейки указывают нам на то, что в калькуляторе хранятся 2 переменных и данные о последней математической операции. Также я заметил, что повторное нажатие клавиш [=], а также нажатие клавиш простых математических операций приводят к различным результатам. Следовательно, на поведение программы влияют какие-то дополнительные факторы. Я выделил следующие: «последняя нажатая кнопка — это кнопка с цифрой» и «последняя произведённая операция — это операция [=]».

Сразу отмечу, что я не искал исходники калькулятора в интернете, поэтому моя реализация может отличаться от других реализаций. Но тем лучше. Наверное, любую задачу можно решить несколькими способами. Кстати, это можно заметить, если протестировать калькулятор в Windows или какой-нибудь настольный калькулятор. Алгоритмы будут отличаться.

Перейдём к классам. Если вы знакомы с шаблонами проектирования, то наверняка встречались с шаблоном MVC (Model — View — Controller). В нашем проекте мы постараемся реализовать эту модель.

Шаблон проектирования MVC

Блок View взаимодействует только с блоком Controller и не знает о существовании и реализации блока Model. Это так называемая пассивная модель. View — это наш пользовательский интерфейс, Model — это данные калькулятора, Controller — классы для управления данными. Первый шаг к реализации данной модели будет создание соответствующих пакетов. Но создадим их не в корневой папке, а в пакете «ru.flexfactory». Это общепринятое правило по именованию главного пакета, которое также однозначно решает проблему пространства имён, т.к. доменное имя всегда уникально. Уже имеющиеся классы переместим в пакет «view». Так будет выглядеть структура на данном этапе:

Начальная структура пакетов приложения

Чтобы полностью отделить визуальную часть от программной, создадим специальный класс AppMediator, так называемый медиатор приложения, где будут прописаны все действия функциональных клавиш. Все элементы управления в нашем приложении есть его public поля, и доступ к ним можно получить также, как к свойству обычного класса. Для связывания приложения я этим классом создадим в классе один статичный метод для передачи ссылки на объект приложения.

package ru.flexfactory.view
{
    public class AppMediator
    {
        private static var instance:AppMediator;
        private var app:Calculator; 
        
        public function AppMediator(app:Calculator)
        {
            this.app = app; 
        }
        
        public static function startup(application:Calculator):void
        {
            if (instance == null) instance = new AppMediator(application);
        }        
    }
}

Видим, что объект этого класса создаётся в нём же. Ссылка на этот объект хранится в статичной переменной instance. А доступ к главному приложения можно будет получить через переменную app. Осталось вызвать из главного приложения метод startup() и мы сможем прописать в медиаторе все необходимые обработчики событий. Для этого в MXML-файле в описании свойств приложений пропишем такую строчку creationComplete="AppMediator.startup(this)".

Отлично. Пока отложим наш медиатор и приступим к блоку Model. Тут я выделил 2 класса. Первый будет хранить значения x, y и значение, которое выводится на дисплей, а также операцию. Второй класс будет хранить и обрабатывать значение memory. С него и начнём. У него будет одна локальная переменная и 4 метода: прибавление к имеющемуся, вычитание из имеющегося, чтение и очистка. Реализация довольно простая. Наличие конструктора тут необязательно. Создаём новый класс Memory в пакете Model.

package ru.flexfactory.model
{
    public class Memory
    {
        private var value:Number = 0;
                
        public function add(value:Number):void
        {
            this.value += value;
        }
        
        public function sub(value:Number):void
        {
            this.value -= value;
        }
        
        public function read():Number
        {
            return value;
        }
        
        public function clear():void
        {
            value = 0;
        }
    }
}

Прежде чем переходить к написанию второго класса, давайте подумаем вот над чем. В каком виде мы будем хранить значение математической операции? Я предлагаю реализовать каждую операцию в виде отдельного класса, в котором будет содержаться всего один метод calculate(). Сделаем мы это, используя одно из понятий ООП — полиморфизм. А там, где есть полиморфизм, не обойтись без понятия interface. Не стоит путать это понятие с понятием «пользовательский интерфейс», поскольку interface — это конструкция языка, которая описывает тип данных. Т.е. в интерфейсе мы пишем наименования public методов, а реализацию самих методов пишем в классах, которые имплементируют этот интерфейс. Создадим в пакете controller новый пакет operations, а в нём наш первый интерфейс: Файл -> Создать -> Интерфейс ActionScript. Наш интерфейс MathOperation будет содержать только один метод:

package ru.flexfactory.controller.operations
{
    public interface MathOperation 
    {
        function calculate(x:Number, y:Number):Number;
    }
}

Все классы, имплементирующие данный интерфейс обязаны реализовать данный метод. Таких классов будет 4 — для каждой простой математической операции: AddOperation, SubOperation, MulOperation, DivOperation.

package ru.flexfactory.controller.operations
{
    public class AddOperation implements MathOperation
    {    
        public function calculate(x:Number, y:Number):Number
        {
            return (x + y);
        }
    }
}
package ru.flexfactory.controller.operations
{
    public class SubOperation implements MathOperation
    {        
        public function calculate(x:Number, y:Number):Number
        {
            return (x - y);
        }
    }
}
package ru.flexfactory.controller.operations
{
    public class MulOperation implements MathOperation
    {        
        public function calculate(x:Number, y:Number):Number
        {
            return (x * y);
        }
    }
}
package ru.flexfactory.controller.operations
{
    import flash.errors.IllegalOperationError;

    public class DivOperation implements MathOperation
    {
        public function calculate(x:Number, y:Number):Number
        {
            if (y == 0) throw new IllegalOperationError("Делить на ноль нельзя");
            return (x / y);
        }
    }
}

Кто не работал с интерфейсами подумает, зачем всё так усложнять? Ведь можно эти операции прописать там, где будет нужно через if...else или switch. Но, поверьте мне, если вы хотите профессионально владеть ООП, то рано или поздно вам придётся столкнуться с полиморфизмом и интерфейсами. А позднее и полюбить. Т.к. эта возможность значительно облегчает жизнь разработчику.

Если вы заметили, в последнем классе написано 2 строчки вместо одной. Первая строчка — это обработка ошибки, вызванной математическим правилом «деления на 0». Правильнее назвать не обработка, а конкретизация. Т.к. в нашем случае мы сообщаем, из-за чего именно произошла ошибка. IllegalOperationError — это класс, наследованный от класса Error, который «пробрасывается» по стеку объектов, пока какой-нибудь объект эту ошибку ни обработает. Позже мы рассмотрим обработку ошибок.

Теперь создадим ещё один класс AppData в пакете model. В нём будут такие переменные и константы:

private const MAX_DIGITS:int = 10;              //максимальное количество знаков
private const ZERO_VALUE:String = "0";          //0 в строковом виде
private const COMMA:String = ".";               //знак разделения целой и дробной части
private const NEGATION_SIGN:String = "-";       //знак минус

private var x:Number;
private var y:Number;
private var operation:MathOperation;            //ссылка на математическую операцию
private var displayValue:String;                //строка, которая выводится на дисплей
private var valueIsDecimal:Boolean;             //флаг, который фиксирует, целое ли число или дробное

Теперь приведу класс полностью. Внимательно читаем комментарии в коде.
Комментарии я написал для учебных целей. В работе я оставляю комментарии только в крайнем случае. Вместо этого, я пытаюсь как можно информативнее составлять имена переменных и функций.

package ru.flexfactory.model
{
    import flash.errors.IllegalOperationError;
    import ru.flexfactory.controller.operations.MathOperation;

    public class AppData
    {
        private const MAX_DIGITS:int = 10;
        private const ZERO_VALUE:String = "0"; 
        private const COMMA:String = "."; 
        private const NEGATION_SIGN:String = "-";
        
        private var x:Number;
        private var y:Number;
        private var operation:MathOperation;
        private var displayValue:String;
        private var valueIsDecimal:Boolean;
        
        public function AppData()
        {
            clear();
        }
        
        //установка значений по-умолчанию для всех переменных
        public function clear():void
        {
            x = 0;
            y = 0;
            operation = null;
            clearDisplayValue();
        }
        
        //добавление числа в строковом виде справа к уже составленному числу
        public function addDigit(digit:String):void
        {
            if (displayValue == ZERO_VALUE) displayValue = "";
            if (lenghtIsValid()) displayValue += digit;
        }
        
        //проверка на достижение максимального разряда.
        //в подсчёте не учитываются знаки "." и "-"
        private function lenghtIsValid():Boolean
        {
            var subDigit:int = 0;
            if (displayValue.indexOf(COMMA) >= 0) subDigit++;
            if (displayValue.substr(0, 1) == NEGATION_SIGN) subDigit++;
            
            return ((displayValue.length - subDigit) < MAX_DIGITS) ? true : false;
        }

        //добавление разделителя
        //т.к. это можно сделать только один раз, устанавливаем флаг
        public function addComma():void
        {
            if (!valueIsDecimal)
            {
                displayValue += COMMA;
                valueIsDecimal = true;
            }
        }

        //изменение знака числа        
        public function changeSign():void
        {
            var temp:Number = Number(displayValue);
            temp *= -1;
            displayValue = temp.toString();
        }

        //очистка значения, которое выводится на дисплей        
        public function clearDisplayValue():void
        {
            displayValue = ZERO_VALUE;
            valueIsDecimal = false;
        }

        //записываем число на дисплее в переменную х        
        public function displayToX():void
        {
            x = Number(displayValue);
        }

        //записываем число на дисплее в переменную у
        public function displayToY():void
        {
            y = Number(displayValue);
        }

        //запоминаем операцию, которую будем выполнять
        //заметим, что тут мы используем только интерфейс, ничего не зная о реализованных классах        
        public function setOperation(operation:MathOperation):void
        {
            this.operation = operation;
        }

        //вычисление значения
        //используем конструкцию try...catch, чтобы перехватить ошибку        
        public function calculate():void
        {
            if (operation == null) return;
            
            try
            {
                x = operation.calculate(x, y);
                displayValue = x.toString();
            }
            catch (e:IllegalOperationError)
            {
                displayValue = e.message;
            }
        }

        //вычисление корня значения
        //обрабатываем исключение типа "корень из отрицательного числа"
        public function square():void
        {
            if (Number(displayValue) < 0) {
                displayValue = "Не выполнимо";
                throw new Error(displayValue);
            }

            var temp:Number = Number(displayValue);
            temp = Math.sqrt(temp);
            displayValue = temp.toString();
        }

        //вычисление процента        
        public function percent():void
        {
            var temp:Number = Number(displayValue);
            temp = x / 100 * temp;
            displayValue = temp.toString();
            displayToY();
        }

        //setter и getter для локальной переменной displayValue
        public function set displayed(value:String):void
        {
            displayValue = value;
        }
        
        public function get displayed():String
        {
            return displayValue;
        }
    }
}

Теперь рассмотрим подробнее несколько моментов. Во-первых, в методе calculate() мы обрабатываем возможную ошибку при делении на нуль с помощью конструкции try…catch. Т.е. сначала будет выполняться код в блоке try. Но если возникнет ошибка, то блок try выполняться перестанет, а выполнится код из блока catch. Во-вторых, чем интересен метод setOperation? Тем, что здесь нет указания на то, какую именно мы операцию выполняем с числами. Мы работаем с интерфейсом и этого достаточно. Допустим, в дальнейшем мы захотим добавить в наш калькулятор операцию возведения в степень. Вместо того, чтобы делать изменения в классе AppData, мы напишем новый класс PowOperation и передадим объект этого класса в метод setOperation. Это позволит не нарушать принцип ООП о том, что готовые нельзя редактировать, но можно расширять.

Так. Теперь перейдём к написанию основного класса контроллера. Именно он будет производить действия над классами AppData и Memory. Его мы сделаем на базе класса EventDispatcher. Что это значит? Это реализация ещё одного понятия в ООП — наследование. Можно сказать, что наш класс будет наследовать все методы и свойства класса EventDispatcher для того, чтобы генерировать события. Для новичков немного запутано… Скажу проще. Наш класс будет выполнять действия над значениями калькулятора. Но значения — это просто переменные в оперативной памяти, так ведь? Нужно информировать View, что Model изменилась, чтобы изменения отобразились на экране. Для информирования используется класс Event и все его производные. Т.е. это своего рода радиосигнал, который можно поймать, если настроиться на нужную волну. А чтобы генерировать такой сигнал, нам нужен класс EventDispatcher. Мы берём его за основу и дополняем необходимыми нам методами, т.е. расширяем его, используя ключевое слово extends. Теперь приведу полностью класс, а ниже поговорим о нём

package ru.flexfactory.controller
{
    import flash.events.*;
    import ru.flexfactory.controller.operations.MathOperation;
    import ru.flexfactory.model.*;

    public class AppController extends EventDispatcher
    {
        public static const OPERATOR_CHANGE:String = "Operator change";
        
        private var appData:AppData;                        //объект для оперирования значениями
        private var memory:Memory;                          //объект для оперирования ячейкой памяти
        
        private var lastOperationIsResult:Boolean;          //флаг, была ли последняя операция подсчётом итогового значения
        private var lastButtonIsNumeric:Boolean;            //флаг, была ли последняя нажатая кнопка - цифровой кнопкой 
        
        public function AppController()
        {
            appData = new AppData();
            memory = new Memory();
            
            clear();
        }

        //нажатие цифровой кнопки 
        //после изменения отображаемого значения отправляем событие о том, что значение изменилось
        //отправлять событие мы будем при любом изменении данных либо памяти
        public function pressNumericButton(digit:String):void
        {
            if (!lastButtonIsNumeric) appData.clearDisplayValue();
            appData.addDigit(digit);
            sendEvent();
            
            lastButtonIsNumeric = true;
        }
        
        //нажатие кнопки с точкой
        //т.к. точка является частью числа, то флаг lastButtonIsNumeric устанавливаем в true
        public function pressDotButton():void
        {
            appData.addComma();
            sendEvent();
            
            lastButtonIsNumeric = true;
        }
        
        //нажатие кнопки знака "-"
        public function pressSignButton():void
        {
            appData.changeSign();
            sendEvent();
            
            lastButtonIsNumeric = true;
        }
        
        //нажатие кнопки математической операции
        //тут в зависимости от состояния флагов производим вычисление
        public function pressOperationButton(operation:MathOperation):void
        {
            calcForOperation();
            appData.setOperation(operation);
            
            lastButtonIsNumeric = false;
            lastOperationIsResult = false;
        }
        
        //действие, если нажата кнопка математической операции
        //я вынес этот код в отдельную функцию, чтобы соблюдать правило "Одна функция - одно действие"
        private function calcForOperation():void
        {
            if (lastOperationIsResult) 
            {
                appData.displayToX();
            }
            else
            {
                if (lastButtonIsNumeric)
                {
                    appData.displayToY();
                    appData.calculate();
                    sendEvent();
                }
            }
        }
        
        //нажатие кнопки [=]
        public function pressResultButton():void
        {
            if (!lastOperationIsResult) appData.displayToY();
            appData.calculate();
            sendEvent();
            
            lastOperationIsResult = true;
            lastButtonIsNumeric = false;
        }
        
        //вычисление квадратного корня
        //возможна ошибка, если была попытка вычислить корень из отрицательного числа
        public function pressSquareButton():void
        {
            try 
            {
                appData.square();
                pressResultButton();
            }
            catch (e:Error)
            {
                sendEvent();
            }
        }
        
        //нажатие на кнопку [%]
        public function pressPercentButton():void
        {
            appData.percent();
            pressResultButton();
        }
        
        //нажатие на кнопки MEMORY
        //при добавлении или вычитании значения к значению в памяти, производим вычисление, как будто была нажата кнопка [=]
        public function pressMemoryClear():void
        {
            memory.clear();
            sendEvent();
        }
        
        public function pressMemoryRead():void
        {
            appData.displayed = memory.read().toString();
            sendEvent();
            lastButtonIsNumeric = true;
        }
        
        public function pressMemoryAdd():void
        {
            pressResultButton();
            memory.add(Number(appData.displayed));
            sendEvent();
        }
        
        public function pressMemorySub():void
        {
            pressResultButton();
            memory.sub(Number(appData.displayed));
            sendEvent();
        }
        
        //отправляем событие
        private function sendEvent():void
        {
            var event:Event = new Event(OPERATOR_CHANGE);
            dispatchEvent(event);
        }
        
        //метод для получения значения вывода на экран
        public function get displayValue():String
        {
            return appData.displayed;
        }
        
        //метод для вывода буквы "М", если значение в памяти отличное от нуля
        public function get memoryState():String
        {
            return (memory.read() > 0) ? "M" : "";
        }
        
        //сброс значений
        public function clear():void
        {
            appData.clear();
            lastOperationIsResult = true;
            lastButtonIsNumeric = false;
            sendEvent();
        }
    }
}

Остановимся на методе sendEvent(). Сначала мы создаём объект event, где в качестве параметра указываем значение нашей константы. Именно по этой константе мы будем ловить это событие. Затем мы отправляем это событие в стек объектов. Чтобы поймать это событие, нужно добавить слушатель ИМЕННО ЭТОМУ объекту. Это очень важно. В начале изучение ООП и AS3 я не до конца понимал этот механизм, поэтому новичкам рекомендую очень тщательно изучить этот момент.
С остальными методами несложно разобраться по комментариям в коде.

Теперь возвращаемся к нашему классу AppMediator. Наша задача теперь каждой кнопке назначить слушателя, в котором прописать действия контроллера. Для этого создадим ещё одну локальную переменную controller, которая будет ссылаться на экземпляр класса AppController. Не забудем этому объекту назначить слушателя события AppController.OPERATOR_CHANGE (обращение к публичной статичной константе класса), чтобы обновлять данные на дисплее.

package ru.flexfactory.view
{
    import flash.events.*;
    
    import ru.flexfactory.controller.operations.*;
    import ru.flexfactory.controller.AppController;
    
    import spark.components.Button;

    public class AppMediator
    {
        private static var instance:AppMediator;
        
        private var app:Calculator;
        private var controller:AppController;

        public function AppMediator(app:Calculator)
        {
            this.app = app;
            controller = new AppController();
            
            setHandlers();
        }
        
        public static function startup(application:Calculator):void
        {
            if (instance == null) instance = new AppMediator(application);
        }
        
        private function setHandlers():void
        {
            //создаём массив объектов цифровых кнопок
            var buttonArray:Array = [app.b0, app.b1, app.b2, app.b3, app.b4, 
                                     app.b5, app.b6, app.b7, app.b8, app.b9];

            //теперь перебором массива присваиваем каждой кнопке слушателя
            for each (var button:Button in buttonArray)
            {
                button.addEventListener(MouseEvent.CLICK, clickNumberHandler);
            }

            //остальных прописываем отдельно
            app.bDot.addEventListener(MouseEvent.CLICK, dotHandler);
            
            app.bC.addEventListener(MouseEvent.CLICK, clearHandler);
            app.bResult.addEventListener(MouseEvent.CLICK, resultHandler);
            
            app.bAdd.addEventListener(MouseEvent.CLICK, addHandler);
            app.bSub.addEventListener(MouseEvent.CLICK,subHandler);
            app.bMul.addEventListener(MouseEvent.CLICK, mulHandler);
            app.bDiv.addEventListener(MouseEvent.CLICK, divHandler);
            
            app.bNSign.addEventListener(MouseEvent.CLICK, nsignHandler);
            app.bSquare.addEventListener(MouseEvent.CLICK, squareHandler);
            app.bPersent.addEventListener(MouseEvent.CLICK, percentHandler);
            
            app.bMC.addEventListener(MouseEvent.CLICK, memoryClearHandler);
            app.bMR.addEventListener(MouseEvent.CLICK, memoryReadHandler);
            app.bMAdd.addEventListener(MouseEvent.CLICK, memoryAddHandler);
            app.bMSub.addEventListener(MouseEvent.CLICK, memorySubHandler);
            
            controller.addEventListener(AppController.OPERATOR_CHANGE, operatorChangeHandler);
        }
        
        protected function clickNumberHandler(event:MouseEvent):void
        {
            controller.pressNumericButton((event.currentTarget as Button).label);
        }
        
        protected function dotHandler(event:MouseEvent):void
        {
            controller.pressDotButton();
        }
        
        protected function clearHandler(event:MouseEvent):void
        {
            controller.clear();
        }
        
        protected function resultHandler(event:MouseEvent):void
        {
            controller.pressResultButton();
        }
        
        protected function addHandler(event:MouseEvent):void
        {
            controller.pressOperationButton(new AddOperation());
        }
        
        protected function subHandler(event:MouseEvent):void
        {
            controller.pressOperationButton(new SubOperation());
        }
        
        protected function mulHandler(event:MouseEvent):void
        {
            controller.pressOperationButton(new MulOperation());
        }
        
        protected function divHandler(event:MouseEvent):void
        {
            controller.pressOperationButton(new DivOperation());            
        }
        
        protected function nsignHandler(event:Event):void
        {
            controller.pressSignButton();            
        }
        
        protected function squareHandler(event:MouseEvent):void
        {
            controller.pressSquareButton();    
        }
        
        protected function percentHandler(event:MouseEvent):void
        {
            controller.pressPercentButton();            
        }
        
        protected function memoryClearHandler(event:MouseEvent):void
        {
            controller.pressMemoryClear();            
        }
        
        protected function memoryReadHandler(event:MouseEvent):void
        {
            controller.pressMemoryRead();            
        }
        
        protected function memoryAddHandler(event:MouseEvent):void
        {
            controller.pressMemoryAdd();            
        }
        
        protected function memorySubHandler(event:MouseEvent):void
        {
            controller.pressMemorySub();            
        }
        
        protected function operatorChangeHandler(event:Event):void
        {
            app.out.text = controller.displayValue;
            app.mem.text = controller.memoryState;
        }
    }
}

Я написал минимум комментариев, потому что хоть функций довольно много, они все очень прозрачны. Простыми словами, назначаем каждой кнопке определённое действие. Это мы могли прописать и в MXML-файле и код был бы значительно меньше. Но наша задача разделить код и пользовательский интерфейс. И эту задачу мы успешно выполнили.

Остался маленький нюанс. В техническом задании мы указали, что целая часть значения будет разделена по 3 разряда. Для этого нам понадобиться класс <s:NumberFormatter>, который мы пропишем в блок <fx:Declarations> нашего приложения, и изменим свойство text у объекта out. Получилось следующее

...
    <fx:Declarations>
        <s:NumberFormatter id="numberFormatter" groupingSeparator="'" fractionalDigits="9" trailingZeros="false" useGrouping="true"/>
    </fx:Declarations>
    
    <s:Panel left="10" top="10" width="320" height="325" title="Калькулятор">
        <s:VGroup left="10" top="10">
            <s:HGroup width="296">
                <s:TextInput id="mem" width="40" height="40" enabled="false" textAlign="center"/>
                <s:TextInput id="out" width="251" height="40" enabled="false" text="{numberFormatter.format(out.text)}"/>    
            </s:HGroup>
...

Очередной урок можно считать завершённым. Приложение готово. Знания получены. А вам я оставляю в качестве домашнего задания:

  • привязать к нашему калькулятору события клавиатуры
  • реализовать вывод результата в 10 разрядном виде, т.к. пока ограничение стоит только на ввод
  • найти способ отображения сообщений об ошибке, т.к. использование NumberFormatter не позволяет выводить сообщения на дисплей

Можете опробовать клавиши NUMPAD на прикреплённом приложении. Спасибо тем, кто дочитал до конца этот урок.

Результат

Архив с готовым проектом


Тэги: , , , ,

Ссылка для вашего сайта.



Комментарии (6)

  • Julia Rietveld

    |

    Андрей, спасибо за такой хороший урок. Как ни странно, в интернете не так уж много информации о OOP AS3 и в особенности работы в классах и в Flash Builder. Жду продолжения!
    Юля
    Мальмо, Швеция

    Ответить

    • Андрей

      |

      Спасибо, Юля. Рад, что Вам понравилось. )

      Ответить

  • Юля

    |

    Андрей, будте любезны, свяжитесь со мной через мой майл. Я хочу обсудить с Вами возможность частного урока Экшен Скрипт через Скайп. Вы так классно рассказываете! Заранее спасибо за ответ.

    Юля

    Ответить

    • Андрей

      |

      Юля, написал Вам. Буду рад помочь.

      Ответить

  • Сергей

    |

    У Вас крутой ресурс, развивайте

    Ответить

  • Стас

    |

    Не выходит добавить события клавиатуры для клавиш 0-9
    Помогите пожалуйста.

    Ответить

Добавить комментарий

Дополнительно