Статьи

AS3 101: ООП — Представление шаблонов проектирования

После многих месяцев обучения программированию с нуля вы готовы использовать все это: мы собираемся создать простое приложение для рисования. Мы сосредоточимся на методах объектно-ориентированного программирования, в частности, на использовании интерфейсов. Установив несколько правил для нашего программирования, мы значительно упростим расширение набора функций и отладку проекта.


Вы увидите, что наше маленькое приложение для рисования будет довольно упрощенным. Будьте уверены, однако, что внутренняя логика может просто взорвать ваш мозг. Не волнуйся; мы сделаем это один шаг за один раз, как мы всегда делаем. Вот вершина того, над чем мы работаем в этом уроке:

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


Перво-наперво: нам нужен дом для нашего проекта. Начните с создания главной папки для всего проекта. Используйте подход, который работает лучше всего для вас. Это может быть так же просто, как создание папки на вашем компьютере, использование ОС или использование Flash Builder для создания нового проекта для вас.

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

Я буду называть это приложение «Drawr», поэтому я буду называть свою папку соответственно. Имя не имеет большого значения для остальной части учебника, поэтому, если вы чувствуете себя изобретательно, придумайте свое имя.

Папка моего проекта

Откройте Flash CS3 или более позднюю версию и выберите « Файл»> «Создать», а затем выберите параметр ActionScript 3.0 . Это будет наш Flash-файл для проекта.

Окно «Новый документ»

Сохраните его в папке вашего проекта как « Drawr.fla » (или как вы считаете, какое имя лучше).

Наш Flash-файл

Мы организуем наши файлы классов в пакеты, но для дальнейшей организации давайте создадим исходный путь.

Создайте папку в папке вашего проекта с именем « классы ». Все наши классы (и папки с пакетами) войдут сюда.

Папка классов

В документе Flash выберите « Файл»> « Параметры публикации» (на Mac: Option-Shift-F12; в Windows Alt-Shift-F12). Нажмите на вкладку « Flash », а затем кнопку « Настройки … » рядом с «Script: ActionScript 3.0». Убедитесь, что выбрана вкладка « Исходный путь », и нажмите кнопку « + ». В новой записи введите « ./classes ».

Если вы хотите узнать больше об этой процедуре, обратитесь к шагу «Исходные пути» этого руководства по ООП .


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

В выбранном вами текстовом редакторе создайте новый файл и сохраните его как « Drawr.as » в папке классов . На этом этапе я рекомендую вам следовать моим инструкциям по именованию файлов и классов. Легко запутаться, если ваши имена отличаются или если вы скопируете код из учебника, в котором используются имена классов, отличные от используемых вами. Я, очевидно, не могу контролировать это, но считаю это дружеским и полезным предложением.

Наш класс документов

Поскольку этот файл является классом документа, у нас есть еще несколько подзадач. Сначала вставьте некоторый шаблонный код (если вы использовали Flash Builder, почти все это уже будет присутствовать):

1
2
3
4
5
6
7
8
9
package {
    import flash.display.Sprite;
 
    public class Drawr extends Sprite {
        public function Drawr() {
            trace(«Drawr has started.»);
        }
    }
}

Теперь убедитесь, что этот класс документа подключен обратно в файл Flash. Когда ничего не выбрано, откройте панель «Свойства» и введите « Drawr » в поле «Class».

Установка класса документа

Чтобы убедиться, что наши файлы хорошо воспроизводятся, выберите « Управление»> «Тестировать ролик», чтобы запустить фильм. Вы должны увидеть след на панели «Вывод»:

Панель «Вывод»

Если хотите, вы можете просто использовать начальный FLA в пакете загрузки для визуальных ресурсов (в папке «drawr-start»). Или вы можете создать свой собственный интерфейс. Вот что нам нужно:

  1. Три «инструментальные» кнопки, а-ля Flash или кнопки инструментов Photoshop. Маленький размер, с каким-то значком для обозначения инструмента, который будет активирован при нажатии кнопки. Три необходимых инструмента:

    1. Кнопка прямоугольник

      Инструмент Прямоугольник

    2. Овальная кнопка

      Овальный инструмент

    3. Кнопка кисти

      Зубная щетка

    Например, они могут быть расположены во Flash следующим образом:

    Пример кнопок инструментов

    Вы можете получить их прямо из FLA или даже загрузив вышеуказанные изображения, которые являются изображениями PNG-24 с альфа-каналом.

  2. Область «холст», которая должна быть прежде всего областью художественных работ, поверх которой мы будем рисовать. Даже если вы сделаете область прозрачной, убедитесь, что область заполнена пикселями; мы будем использовать событие MOUSE_DOWN в качестве ключевого ингредиента нашей логики рисования, и это событие не MOUSE_DOWN для InteractiveObject если мышь не находится над пикселем, занятым пикселями этого InteractiveObject . Кроме того, в качестве элемента пользовательского интерфейса полезно иметь визуально определенную область, чтобы знать, где может происходить рисование.

    Пример холста

Все эти предметы должны быть MovieClips. Назовите эти клипы так:

  • Кнопка прямоугольника : rectangle_mc
  • Овальная кнопка: oval_mc
  • Кнопка кисти: brush_mc
  • Холст: canvas_mc

Нам понадобится несколько классов, чтобы контролировать даже этот минимальный интерфейс. Как уже упоминалось ранее, мы будем следовать рекомендациям и размещать наши классы в пакетах. Мы создадим пакеты по мере необходимости. В вашей папке классов создайте новую папку с именем панель инструментов . Мы будем держать классы, связанные с панелью инструментов, здесь (очевидно).

Начнем с класса Toolbar . Это ответственность будет заключаться в том, чтобы справиться с интерфейсом с помощью инструментов. Создайте файл с именем Toolbar.as в пакете панели инструментов .

Пакет панели инструментов и класс панели инструментов

Вставьте в него следующий код:

01
02
03
04
05
06
07
08
09
10
11
12
13
package toolbar {
 
    import flash.events.*;
    import flash.display.*;
 
    public class Toolbar extends EventDispatcher {
        private var _target:Sprite;
        public function Toolbar(target:Sprite) {
            _target = target;
            trace(«Toolbar: » + _target);
        }
    }
}

Прежде чем мы это ToolbarButton , нам нужен еще один класс, ToolbarButton . Мы сделаем это на следующем шаге. Однако сейчас вы можете заметить, что я предпочитаю сочинять Sprite а не расширять его. Это мое личное предпочтение, так как в долгосрочной перспективе я считаю его более гибким. Вы можете чувствовать себя по-другому, но, пожалуйста, посмотрите этот предыдущий учебник в серии ООП для длительного обсуждения композиции. Несмотря на это, я призываю вас следовать моим указаниям в этом, поскольку попытка выполнить то же самое, но с наследованием вместо композиции, несомненно, вызовет проблемы, которые будет трудно решить, сравнивая ваш код с моим.

Давайте сделаем быстрый тест класса Toolbar . Сначала в вашем FLA выберите три кнопки панели инструментов и нажмите F8, чтобы сгруппировать их в символ. Назовите символ как «Панель инструментов», но, что более важно, назовите получившийся экземпляр « toolbar_mc ».

Вернемся к Drawr.as , давайте добавим код для создания Toolbar . Добавленные строки выделены ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
package {
 
    import flash.display.Sprite;
 
    import toolbar.Toolbar;
 
    public class Drawr extends Sprite {
 
        private var _toolbar:Toolbar;
 
        public function Drawr() {
            _toolbar = new Toolbar(toolbar_mc);
        }
    }
}

И идти вперед и проверить фильм; Вы должны увидеть след на панели «Вывод».

Flash AS3 ООП учебник

Этот класс будет очень простым, обрабатывая обязанности для одной кнопки на панели инструментов. Самое главное, он будет иметь при себе идентификатор инструмента, так что щелчок по кнопке в конечном итоге приведет к правильному поведению рисования.

В пакете панели инструментов создайте файл с именем ToolbarButton.as . Поместите в него следующий код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package toolbar {
    import flash.display.Sprite;
    import flash.events.EventDispatcher;
    import flash.events.MouseEvent;
 
    public class ToolbarButton extends EventDispatcher {
        private var _target:Sprite;
        private var _toolType:String;
        public function ToolbarButton(target:Sprite, toolType:String) {
            _target = target;
            _toolType = toolType;
            _target.addEventListener(MouseEvent.ROLL_OVER, onOver);
            _target.addEventListener(MouseEvent.ROLL_OUT, onOut);
            _target.addEventListener(MouseEvent.CLICK, onClick);
        }
 
        private function onOver(e:MouseEvent):void {
            // Perform roll over effect.
        }
        private function onOut(e:MouseEvent):void {
            // Perform roll out effect.
        }
        private function onClick(e:MouseEvent):void {
            dispatchEvent(e);
        }
 
        public function get toolType():String {
            return _toolType;
        }
 
 
    }
}

В общем, довольно просто. Возьмите Sprite и добавьте к нему некоторые события взаимодействия с мышью. Также возьмите инструмент типа String и сохраните его в свойстве, предоставляя доступ только для чтения через геттер.

Я не обсуждаю фактические эффекты ролловера и свертывания для этого урока. Не стесняйтесь помещать свой собственный код в соответствующие методы.

Теперь мы можем создавать кнопки инструментов из класса Toolbar . Но для передачи нам нужен toolType . Мы могли бы просто составить строковые значения, но было бы разумнее создать перечисляемые статические константы (см. Мой Быстрый совет по статическим элементам ). Мы можем сделать это, создав очень простой класс.


Создайте новый файл класса с именем ToolType.as в пакете панели инструментов . Просто нужно несколько статических констант. Введите следующее:

1
2
3
4
5
6
7
8
9
package toolbar {
    public class ToolType {
 
        public static const RECTANGLE:String = «rectangle»;
        public static const OVAL:String = «oval»;
        public static const BRUSH:String = «brush»;
 
    }
}

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

Почему бы просто не поместить эти константы, скажем, в класс Toolbar ? Я беру страницу из книги Adobe об этом. Этот класс несет схожую ответственность с классами, такими как StageScaleMode или BlendMode (оба в пакете flash.display ). Они просто объявляют открытые статические константы, которые имеют значения String действительных значений для свойств Stage.scaleMode и DisplayObject.blendMode соответственно.

Одна из причин рассмотреть этот подход, а не помещать константы в класс Toolbar (или в классы Stage или DisplayObject ), заключается в том, что есть хороший шанс, что мы захотим получить доступ к этим значениям, фактически не заботясь о классе Toolbar . Это позволяет нам использовать класс «type», не беспокоясь о классе с большей функциональностью. Это моделирует перечислимый тип , которого нет в ActionScript, и позволяет классу просто сосредоточиться на перечислении допустимых значений. Это перечисление может быть легко использовано в нескольких местах.


Теперь у нас есть несколько поддерживающих актеров, и мы можем позволить Toolbar занять центральное место. Мы заполним его для создания объектов ToolbarButton , передачи значений типа инструмента и подключения событий. Откройте Toolbar.as и внесите изменения, выделенные ниже (включая удаление строки trace (), которая была там ранее):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package toolbar {
 
    import flash.display.*;
    import flash.events.*;
 
    public class Toolbar extends EventDispatcher {
        private var _target:Sprite;
        public function Toolbar(target:Sprite) {
            _target = target;
 
            var rect:ToolbarButton = new ToolbarButton(_target.getChildByName(«rectangle_mc») as Sprite, ToolType.RECTANGLE);
            var oval:ToolbarButton = new ToolbarButton(_target.getChildByName(«oval_mc») as Sprite, ToolType.OVAL);
            var brush:ToolbarButton = new ToolbarButton(_target.getChildByName(«brush_mc») as Sprite, ToolType.BRUSH);
 
            rect.addEventListener(MouseEvent.CLICK, onToolClick);
            oval.addEventListener(MouseEvent.CLICK, onToolClick);
            brush.addEventListener(MouseEvent.CLICK, onToolClick);
        }
 
        private function onToolClick(e:MouseEvent):void {
            trace(«Tool click: » + e.target.toolType);
        }
    }
}

Все еще ничего сложного, но давайте вспомним. Сначала мы создаем три кнопки. Существует образец того, как они создаются, поэтому понимание одного означает, что вы понимаете их всех. Если вы помните, класс ToolbarButton принимает два аргумента для своего конструктора. Первый — это Sprite , поэтому мы получаем экземпляры символов кнопки и передаем их. Обратите внимание, что нам нужно привести результат getChildByName() в виде Sprite потому что результатом является DisplayObject но нам нужно передать его в Sprite . Второй аргумент — это String обозначающая тип инструмента, поэтому здесь мы используем наш ToolType перечисления ToolType . Итак, теперь у нас есть три кнопки, каждая из которых связана со Sprite и имеет тип.

Затем мы добавляем события CLICK к каждой из кнопок. Обработчик этого просто (на данный момент) отслеживает некоторую информацию.

Если вы сейчас тестируете свой фильм, вы сможете нажимать на кнопки и получать следы.

Панель «Вывод», показывающая, какие кнопки были нажаты

Пока все хорошо, но мы не можем остановиться здесь. Это событие щелчка в конечном счете должно вернуться к классу Drawr , чтобы он, в свою очередь, мог что-то сделать с этой информацией. Для этого мы создадим пользовательский подкласс Event чтобы указать выбор инструмента. Обычной практикой является помещение классов events проекта в один пакет events , и мы сделаем то же самое. Создайте папку с именем events в вашей папке классов .

Затем создайте новый текстовый файл и сохраните его как ToolbarEvent.as .

Текущая структура проекта

Код будет следующим:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package events {
 
    import flash.events.Event;
 
    public class ToolbarEvent extends Event {
 
        public static const SELECT:String = «select»;
 
        private var _toolType = toolType;
 
        public function ToolbarEvent(type:String, toolType:String, bubbles:Boolean=false, cancelable:Boolean=false) {
            _toolType = toolType;
            super(type, bubbles, cancelable);
        }
 
        public function toString():String {
            return formatToString(«ToolbarEvent», «type», «toolType»);
        }
 
        public function clone():Event {
            return new ToolbarEvent(type, toolType, bubbles, cancelable);
        }
 
        public function get toolType():String {
            return _toolType;
        }
 
    }
}

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

Создав этот класс событий, вернитесь к Toolbar.as и импортируйте класс ToolbarEvent :

1
2
3
package toolbar {
 
    import events.ToolbarEvent;

И измените метод onToolClick чтобы удалить трассировку и отправить ToolbarEvent :

1
2
3
private function onToolClick(e:MouseEvent):void {
    dispatchEvent(new ToolbarEvent(ToolbarEvent.SELECT, e.target.toolType));
}

Последняя задача на этом этапе — прослушать это событие из класса Drawr . В этом классе импортируйте класс ToolbarEvent и добавьте прослушиватель событий в объект Toolbar :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package {
 
    import events.ToolbarEvent;
 
    import flash.display.Sprite;
 
    import toolbar.Toolbar;
 
    public class Drawr extends Sprite {
 
        private var _toolbar:Toolbar;
 
        public function Drawr() {
            _toolbar = new Toolbar(toolbar_mc);
            _toolbar.addEventListener(ToolbarEvent.SELECT, onToolbarSelect);
        }
 
        private function onToolbarSelect(e:ToolbarEvent):void {
            trace(«Toolbar select: » + e.toolType);
        }
 
    }
}

Это только немного изменит поведение на данный момент; но если вы все еще получаете трассировку при тестировании фильма, то вы успешно отправляете объект из Toolbar в класс документа. Мы вернемся к этому чуть позже.


Мы перейдем к другой ключевой части пользовательского интерфейса, области, где мы будем рисовать иллюстрации. Создайте еще один пакет (папку) с именем canvas в папке классов .

Затем создайте новый файл ActionScript и сохраните его как Canvas.as во вновь созданной папке «canvas».

Flash AS3 ООП учебник

Введите следующий код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package canvas {
 
    import flash.display.*;
    import flash.events.*;
    import flash.geom.*;
 
    public class Canvas {
 
        private var _target:Sprite;
 
        public function Canvas(target:Sprite) {
            _target = target;
            _target.addEventListener(MouseEvent.MOUSE_DOWN, onCanvasDown);
        }
 
        private function onCanvasDown(me:MouseEvent):void {
            trace(«onCanvasDown»);
            _target.stage.addEventListener(MouseEvent.MOUSE_MOVE, onCanvasMove);
            _target.stage.addEventListener(MouseEvent.MOUSE_UP, onCanvasUp);
        }
 
        private function onCanvasMove(me:MouseEvent):void {
            var newX:Number = _target.mouseX;
            var newY:Number = _target.mouseY;
            trace(«onCanvasMove: » + newX + «, » + newY);
        }
 
        private function onCanvasUp(me:MouseEvent):void {
            trace(«onCanvasUp»);
            _target.stage.removeEventListener(MouseEvent.MOUSE_MOVE, onCanvasMove);
            _target.stage.removeEventListener(MouseEvent.MOUSE_UP, onCanvasUp);
        }
    }
}

В этом уроке мы немного поработаем над этим классом, но сейчас идея состоит в том, чтобы реализовать некоторые ключевые функции. Логика рисования будет происходить на холсте, но только после того, как пользователь щелкнет мышью вниз на холсте. Итак, первое, что мы настроили, это прослушиватель MOUSE_DOWN на целевом MOUSE_DOWN canvas (обратите внимание, что мы снова используем композицию, а не наследование. У этого объекта будет Sprite).

В этот слушатель MOUSE_DOWN мы добавляем еще два события: MOUSE_MOVE и MOUSE_UP . Мы не хотим, чтобы эти события запускались в любое старое время, иначе мы могли бы рисовать только потому, что мышь двигалась, независимо от того, произошел щелчок на холсте или нет. Поэтому мы добавляем эти события после того, MOUSE_DOWN произошло сообщение MOUSE_DOWN .

В слушателе MOUSE_MOVE сейчас мы просто получаем положение мыши относительно холста Sprite и отслеживаем его. Мы будем делать намного больше здесь в ближайшее время.

Наконец, в обработчике MOUSE_UP важно сделать, учитывая, что на этом этапе наше рисование завершено, — удалить тех слушателей MOUSE_MOVE и MOUSE_UP , которые оставляют только слушателя MOUSE_DOWN , который может начать процесс заново.

Это хорошая вещь, но пока нечего проверять. Сначала нам нужно создать объект Canvas . Это следующее.


Вернитесь к классу Drawr и добавьте выделенные строки в следующем коде:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package {
 
    import canvas.Canvas;
 
    import events.ToolbarEvent;
 
    import flash.display.Sprite;
 
    import toolbar.Toolbar;
 
    public class Drawr extends Sprite {
 
        private var _canvas:Canvas;
        private var _toolbar:Toolbar;
 
        public function Drawr() {
            _canvas = new Canvas(canvas_mc);
            _toolbar = new Toolbar(toolbar_mc);
            _toolbar.addEventListener(ToolbarEvent.SELECT, onToolbarSelect);
        }
 
        private function onToolbarSelect(e:ToolbarEvent):void {
            trace(«Toolbar select: » + e.toolType);
        }
 
    }
}

Мы просто импортируем класс, создаем свойство и создаем экземпляр объекта Canvas в этом свойстве; должно быть довольно простым до сих пор.

Идите и протестируйте фильм. После запуска нажмите и перетащите в область холста. Вы должны получить целую кучу следов.

Flash AS3 ООП учебник

У нас есть наш основной холст, теперь, как мы в него втягиваемся? Давайте подумаем об общей механике рисования, скажем, прямоугольника.

Давайте разделим фазы рисования прямоугольника на три отдельные фазы. Во-первых, это начальная фаза. Только когда эта фаза случится, мы действительно начнем рисовать (можно сказать, что перед начальной фазой есть фаза, называемая фазой not doin ‘nuthin ). Второй этап — этап рисования ; здесь мы перемещаем мышь, чтобы нарисовать временные прямоугольники, когда мы находимся на прямоугольнике, который мы на самом деле хотим. Наконец, у нас есть фаза фиксации . Здесь мы наконец выбираем нужный нам прямоугольник.

Проницательный среди вас заметит, что эти три фазы хорошо совпадают с тремя событиями, которые мы настроили в классе Canvas : событие MOUSE_DOWN совпадает с фазой запуска , и в этом обработчике событий нам нужно выполнить любую настройку, которую мы нужно начать рисовать. Фаза рисования соответствует событию MOUSE_MOVE , где мы перемещаем мышь и пытаемся получить правильный размер прямоугольника. И событие MOUSE_UP соответствует фазе фиксации ; как только кнопка мыши поднимется, мы закончим рисование и у нас будет наш прямоугольник.

В общих чертах, вот что нам нужно сделать на каждом из этих этапов / событий.

  1. начало : так как один угол прямоугольника всегда привязан к положению мыши на этом этапе, нам нужно зафиксировать положение мыши в событии MOUSE_DOWN и MOUSE_DOWN его в переменной для последующего использования.
  2. рисование : противоположный угол прямоугольника следует за мышью вокруг во время этой фазы. Нам необходимо постоянно обновлять нарисованный прямоугольник на основе постоянно обновляемого местоположения. Важно отметить, что это означает, что мы на самом деле рисуем фигуры на этом этапе. Для этого мы будем использовать API рисования ActionScript.
  3. commit : Здесь на самом деле не так уж много, кроме очистки наших обработчиков событий (как уже упоминалось в нескольких шагах назад относительно класса Canvas ). Форма уже нарисована на этапе рисования , нам просто нужно остановить рисование и оставить форму. Однако вы можете заметить, что инструменты рисования Flash имеют «облегченный» контур, нарисованный на этапе рисования , который затем корректно отображается с заливками и штрихами после фиксации . Таким образом, вполне возможно, что вы захотите выполнить простую процедуру рисования на этапе рисования и нарисовать ее «по-настоящему» на этапе фиксации . Мы не будем этого делать, но я подумал, что упомяну о возможности.

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


… в обработчике MOUSE_MOVE и рисовать различные фигуры соответственно? Конечно, мы могли бы . Но это не очень объектно-ориентированный подход к проблеме.

Что если бы у нас были разные объекты для каждого инструмента? Класс RectangleTool класс OvalTool и т. Д.? Затем мы можем инкапсулировать логику рисования определенного инструмента в одном классе и отделить логику, связанную с Canvas . Объект Canvas может сохранять свойство «текущего объекта инструмента» и переносить логику рисования на этот объект. Текущий объект может обновляться всякий раз, когда это уместно, например, когда вы нажимаете тот или иной инструмент. Эта стратегия звучит привлекательно (по крайней мере, она подходит мне, и я надеюсь, что к настоящему моменту она подходит и вам).

Но это создает другую проблему. Если в объекте Canvas есть единственное свойство «текущий объект инструмента», как мы можем поместить в него различные типы данных; то есть, как нам ввести свойство, чтобы оно могло получать как объекты RectangleTool и объекты OvalTool качестве значений? Не упоминать другие инструменты, которые могут в конечном счете быть построены?

Ответ — интерфейсы, и, несмотря на мое драматическое построение, это не должно удивлять, учитывая тему этого урока. Если мы можем определить интерфейс, который более широко объявляет методы, которые могут быть вызваны классом Canvas , то мы можем использовать интерфейс в качестве типа данных для «текущего объекта инструмента» в Canvas и убедиться, что каждый фактический объект инструмента реализует интерфейс.

Звучит неплохо? Конечно, это так (если это не так, попробуйте перечитать этот шаг. Если после этого он все еще не имеет смысла, попробуйте сделать прыжок веры и продвинуться вперед с учебником; это, вероятно, будет иметь больше смысла, поскольку мы строим это по крупицам).


Создайте еще один пакет в проекте, на этот раз называемый инструментами . Концептуально это будет отличаться от классов в пакете toolbar toolbar : классы toolbar являются элементами пользовательского интерфейса; tools классов будут включать логику, окружающую рисование вещей на холсте.

Создайте новый текстовый файл и сохраните его в папке инструментов как ITool.as .

Flash AS3 ООП учебник

Это будет наш интерфейсный файл, и он не будет слишком длинным:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
package tools {
 
    import flash.display.DisplayObject;
 
    public interface ITool {
 
        function mouseDown(x:Number, y:Number, fillColor:uint):void;
        function mouseMove(x:Number, y:Number):void;
        function mouseUp(x:Number, y:Number):void;
 
        function get art():DisplayObject;
 
    }
}

Мы будем использовать эти три фазы в первых трех методах: mouseDown , mouseMove и mouseUp . Каждый будет ожидать позиции мыши по x и y. Метод mouseDown также будет передан цвет заливки для использования.

Здесь есть дополнительный метод, который мы еще не обсуждали, но я добавляю его сюда, чтобы избежать необходимости добавлять его позже. Мы дойдем до этого на мгновение, но краткое описание этого состоит в том, что инструмент фактически создает иллюстрацию, и Canvas будет извлекать эту иллюстрацию как «слой» для ее отображения.


Давайте применим этот интерфейс к работе. Создайте новый класс с именем RectangleTool в пакете инструментов (то есть создайте файл «Rectangle.as» в папке «инструменты»).

Мы можем заглушки в базовом классе. Поскольку мы будем внедрять ITool , мы можем быть уверены, что у нас есть необходимые методы, даже если они пусты.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package tools {
 
    import flash.display.*;
 
    public class RectangleTool implements ITool {
 
        public function RectangleTool() {
        }
 
        public function mouseDown(x:Number, y:Number, fillColor:uint):void {
            trace(«mouseDown: » + x + «, » + y + «, » + fillColor.toString(16));
        }
 
        public function mouseMove(x:Number, y:Number):void {
            trace(«mouseMove: » + x + «, » + y);
        }
 
        public function mouseUp(x:Number, y:Number):void{
            trace(«mouseUp: » + x + «, » + y);
        }
 
        public function get art():DisplayObject {
            return new Shape();
        }
    }
}

Хотя мы ничего не делаем в этом классе, мы по крайней мере выполнили контракт с ITool и создали необходимые методы. Мы вскоре перейдем к более практичной реализации, но нашей следующей задачей будет использование этого RectangleTool .


Поскольку Canvas получает события мыши, именно этот объект должен сообщить RectangleTool об этих трех фазах. И поскольку объект Drawr — это тот, который получает события от объекта Toolbar при Drawr инструмента, мы можем использовать класс Drawr чтобы сообщить Canvas какой инструмент использовать.

Для этого Canvas требуется открытое свойство (или частное свойство с общедоступными установщиками и получателями), которое содержит ссылку на RectangleTool . Затем он должен быть установлен Drawr когда инструмент выбран.

В Canvas.as добавьте следующие выделенные строки. Обратите внимание, что я также удалил три следа, которые были в этом файле ранее.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package canvas {
 
    import flash.display.*;
    import flash.events.*;
    import flash.geom.*;
 
    import tools.ITool;
 
    public class Canvas {
 
        private var _target:Sprite;
        private var _currentTool:ITool
 
        public function Canvas(target:Sprite) {
            _target = target;
            _target.addEventListener(MouseEvent.MOUSE_DOWN, onCanvasDown);
        }
 
        private function onCanvasDown(me:MouseEvent):void {
            if (!_currentTool) return;
            _target.stage.addEventListener(MouseEvent.MOUSE_MOVE, onCanvasMove);
            _target.stage.addEventListener(MouseEvent.MOUSE_UP, onCanvasUp);
            _currentTool.mouseDown(_target.mouseX, _target.mouseY, 0xFF0000);
        }
 
        private function onCanvasMove(me:MouseEvent):void {
            var newX:Number = _target.mouseX;
            var newY:Number = _target.mouseY;
            _currentTool.mouseMove(newX, newY);
        }
 
        private function onCanvasUp(me:MouseEvent):void {
            _target.stage.removeEventListener(MouseEvent.MOUSE_MOVE, onCanvasMove);
            _target.stage.removeEventListener(MouseEvent.MOUSE_UP, onCanvasUp);
            _currentTool.mouseUp(_target.mouseX, _target.mouseY);
        }
 
        public function get currentTool():ITool {
            return _currentTool;
        }
 
        public function set currentTool(value:ITool):void {
            _currentTool = value;
        }
 
 
    }
}

Все эти изменения _currentTool вокруг добавления нового свойства _currentTool . Импорт, объявление свойства, а также методы установки и получения не должны требовать каких-либо объяснений. Другие строки связаны с использованием свойства _currentTool . И то, что здесь происходит, не должно быть большим сюрпризом; мы просто вызываем три «фазовых» метода в подходящее время в зависимости от взаимодействия с пользователем. Как примечание, мы также проверяем — в строке 20 — на наличие объекта _currentTool прежде чем продолжить со всем этим. Если он не существует, не добавляйте прослушиватели событий и не вызывайте какие-либо методы в _currentTool , потому что в противном случае мы получим ошибки.

Но мы пока не можем проверить; нам нужно _currentTool свойство _currentTool прежде чем мы увидим что-то новое. Итак, в Drawr.as добавьте этот бит кода в метод onToolbarSelect :

1
2
3
4
private function onToolbarSelect(e:ToolbarEvent):void {
    trace(«Toolbar select: » + e.toolType);
    _canvas.currentTool = new RectangleTool();
}

И вы захотите импортировать не только класс RectangleTool , но и почти все в пакете tools .

1
import tools.*;

А теперь иди и протестируй фильм. Нажмите кнопку прямоугольника (или любой из инструментов, если честно), а затем «нарисуйте» в области холста. Вы не увидите никаких прямоугольников, но увидите следы от класса RectangleTool .

Следы от RectangleTool

Чтобы вспомнить, что происходит, мы поставляем объект Canvas с объектом ITool . Это сейчас RectangleTool . Поэтому, когда происходят события мыши, и Canvas отвечает на них, он также просит объект ITool выполнить свою ITool , вызывая методы для них. Это станет более понятным по мере того, как мы продвигаемся, поэтому, если это не имеет смысла прямо сейчас, попробуйте проследить последовательность событий через строки кода в различных классах. Если это имеет смысл, но кажется, что слишком много работы для слишком маленького результата, просто держитесь там. Грандиозная схема еще не раскрыта.


Снова откройте RectangleTool.as . Удалите следы и замените их следующим кодом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package tools {
 
    import flash.display.*;
    import flash.geom.Point;
 
    public class RectangleTool implements ITool {
 
        private var _art:Shape;
        private var _fillColor:uint;
 
        public function RectangleTool() {
        }
 
        public function mouseDown(x:Number, y:Number, fillColor:uint):void {
            _art = new Shape();
            _art.x = x;
            _art.y = y;
            _fillColor = fillColor;
        }
 
        public function mouseMove(x:Number, y:Number):void {
            _art.graphics.clear();
            _art.graphics.beginFill(_fillColor, 1);
            _art.graphics.drawRect(0, 0, x-_art.x, y-_art.y);
        }
 
        public function mouseUp(x:Number, y:Number):void{
        }
 
        public function get art():DisplayObject {
            return _art;
        }
    }
}

И есть еще одна задача, прежде чем мы увидим что-то. Нам нужно вытащить art объект из RectangleTool в Canvas . В Canvas.as добавьте одну строку кода в метод onCanvasDown :

1
2
3
4
5
6
7
private function onCanvasDown(me:MouseEvent):void {
    if (!_currentTool) return;
    _target.stage.addEventListener(MouseEvent.MOUSE_MOVE, onCanvasMove);
    _target.stage.addEventListener(MouseEvent.MOUSE_UP, onCanvasUp);
    _currentTool.mouseDown(_target.mouseX, _target.mouseY, 0xFF0000);
    _target.addChild(_currentTool.art);
}

В текущем состоянии нашего приложения после того, как мы установили Canvas_currentTool в объект RectangleTool , это последовательность событий:

  1. Ничего не происходит, пока MOUSE_DOWN на Canvas .
  2. На этом этапе мы говорим RectangleTool выполнить его метод mouseDown .
  3. Затем RectangleTool создает объект Shape , устанавливает его положение, а также сохраняет fillColor для дальнейшего использования.
  4. Объект Canvas все еще выполняет обработчик MOUSE_DOWN ; Затем он запрашивает art объект из RectangleTool
    объект и добавляет его в свой список отображения.
  5. Мы закончили пока, пока не MOUSE_MOVE событие MOUSE_MOVE .
  6. В обработчике MOUSE_MOVE Canvas направляет текущие координаты мыши вдоль RectangleTool .
  7. Затем RectangleTool использует это новое местоположение вместе с исходным местоположением мыши в MOUSE_DOWN (хранится в
    _art.x и _art.y ), чтобы нарисовать прямоугольник.
  8. Событие MOUSE_UP конце концов срабатывает, после чего мы прекращаем рисовать и оставляем нарисованный прямоугольник как есть.
  9. Если затем щелкнуть снова, событие MOUSE_DOWN снова запускается, повторяя шаги 2–8, в результате MOUSE_DOWN еще один прямоугольник.

И мы можем рисовать прямоугольники!

Flash AS3 ООП учебник

Мы до сих пор не увидели преимущества интерфейса ITool , но скоро доберемся до него.


Давайте перейдем на следующий уровень и представим второй инструмент для рисования: Овал.

В пакете инструментов создайте новый класс с именем OvalTool и добавьте следующий код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package tools {
 
    import flash.display.*;
    import flash.geom.Point;
 
    public class OvalTool implements ITool {
 
        private var _art:Shape;
        private var _fillColor:uint;
 
        public function OvalTool() {
        }
 
        public function mouseDown(x:Number, y:Number, fillColor:uint):void {
            _art = new Shape();
            _art.x = x;
            _art.y = y;
            _fillColor = fillColor;
        }
 
        public function mouseMove(x:Number, y:Number):void {
            _art.graphics.clear();
            _art.graphics.beginFill(_fillColor, 1);
            _art.graphics.drawEllipse(0, 0, x-_art.x, y-_art.y);
        }
 
        public function mouseUp(x:Number, y:Number):void{
        }
 
        public function get art():DisplayObject {
            return _art;
        }
    }
}

Вы можете заметить, что этот класс сильно напоминает класс RectangleTool . Логика почти идентична; кроме необходимости переименовывать класс и конструктор в OvalTool , единственное отличие заключается в mouseMove , где вместо вызова _art.graphics.drawRect мы вызываем _art.graphics.drawEllipse .

Если вы хотите проверить это прямо сейчас , вы можете перейти к классу Drawr и в методе onToolbarSelect установить для _canvas.currentTool new OvalTool() вместо new RectangleTool() . Вы также можете подождать, пока не пройдете следующий шаг, потому что мы сосредоточимся на правильной настройке currentTool следующим.


Конечно, то, что мы имеем, далеко от идеала. У нас есть три кнопки инструментов, нам нужно три инструмента. Итак, у нас сейчас есть два инструмента, поэтому пришло время поработать над тем, как правильно выбрать эти инструменты.

Drawr будет заключаться в переводе между событием выбора инструмента и фактическим ITool нам нужен. Откройте Drawr.as и найдите метод onToolbarSelect . Удалите код, который есть в данный момент, и замените его следующим:

01
02
03
04
05
06
07
08
09
10
11
12
private function onToolbarSelect(e:ToolbarEvent):void {
    switch (e.toolType) {
        case ToolType.RECTANGLE:
            _canvas.currentTool = new RectangleTool();
            break;
        case ToolType.OVAL:
            _canvas.currentTool = new OvalTool();
            break;
        default:
            _canvas.currentTool = null;
    }
}

Нам также нужно импортировать класс ToolType . Добавьте следующую строку импорта в начало файла вместе с остальной частью импорта:

1
import toolbar.ToolType;

А теперь для захватывающего теста. Идите вперед и запустите фильм. Нажмите инструмент Прямоугольник и нарисуйте. Вы должны получить прямоугольник. Затем кликните на овальном инструменте и снова нарисуйте. Это должен быть овал. Отлично сработано!

Flash AS3 ООП учебник

Существует еще один инструмент для создания (по крайней мере, для целей этого урока). Этот будет совсем другим. Если вам OvalTool классы RectangleTool и OvalTool были настолько похожи, что, возможно, мы все OvalTool неправильно, то наш следующий класс инструментов будет достаточно большим, чтобы убедить вас, что отдельные классы для каждого инструмента — это путь.

Однако, хотя основная логика будет радикально отличаться, у нас все еще будет такой же трехфазный подход к задаче рисования. Рисование MOUSE_DOWN кисти все еще не начинается до MOUSE_DOWN , а затем получает обновления с каждым MOUSE_MOVE и заканчивается MOUSE_UP . То, что мы на самом деле делаем на этих трех этапах, будет другим.

Это идеально подходит для нашего интерфейса. У нас есть интерфейс, который определяет эти фазы, но реализации, которые делают уникальные вещи с фазами.

Имея это в виду, создайте файл BrushTool.as в папке инструментов и добавьте следующий код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package tools {
    import flash.display.*;
    import flash.geom.*;
 
    public class BrushTool implements ITool {
 
        private var _bitmap:Bitmap;
        private var _bmd:BitmapData;
        private var _brushStroke:Shape;
 
        public function BrushTool() {
            _bmd = new BitmapData(500, 500, true, 0x00000000);
            _bitmap = new Bitmap(_bmd);
        }
 
        public function mouseDown(x:Number, y:Number, fillColor:uint):void {
            _brushStroke = new Shape();
            var gradBox:Matrix = new Matrix();
            gradBox.createGradientBox(20, 20, 0, 0, 0);
            _brushStroke.graphics.beginGradientFill(GradientType.RADIAL, [fillColor, fillColor], [1, 0], [127,255], gradBox)
            _brushStroke.graphics.drawCircle(10, 10, 10);
 
            var m:Matrix = new Matrix();
            m.translate(x-10, y-10)
            _bmd.draw(_brushStroke, m);
        }
 
        public function mouseMove(x:Number, y:Number):void {
            var m:Matrix = new Matrix();
            m.translate(x-10, y-10)
            _bmd.draw(_brushStroke, m);
        }
 
        public function mouseUp(x:Number, y:Number):void {
        }
 
        public function get art():DisplayObject {
            return _bitmap;
        }
    }
}

Как видите, этот код совсем другой. Я не буду тратить много времени на объяснение этого, поскольку речь BitmapDataне идет о работе над этим учебником (хотя я планирую введение BitmapDataв ближайшее время, поэтому, если вы найдете это интригующим, следите за обновлениями). Общая предпосылка, однако, состоит в том, что у нас есть Bitmapобъект как наш artобъект. Вместо того, чтобы рисовать с помощью API рисования, как мы делали с геометрическими прямоугольниками и овалами, мы будем работать с пикселями для кисти.

Логика растрового изображения включает клонирование пикселей «кончика кисти» (называемого _brushStroke) в BitmapDataобъект. Это _brushStrokeпросто круг с градиентной заливкой, сплошной по центру и переходящий в полностью прозрачный по краям, как кисть с перьями в Photoshop. Копируя это искусство снова и снова в пиксели растрового изображения, мы получаем впечатление непрерывной линии.

Конечно, иллюзия не так хороша, как у фотошопа; Если вы двигаете мышь быстро, вы увидите отдельные мазки. Эту проблему можно решить с помощью большего количества логики и математики, но, чтобы не дать вещам выйти из-под контроля, мы будем придерживаться этой более элементарной реализации.

Теперь, чтобы сделать эту работу, нам нужно расширить область действия onToolbarSelectметода в Drawr. Добавьте выделенные строки:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
private function onToolbarSelect(e:ToolbarEvent):void {
    switch (e.toolType) {
        case ToolType.RECTANGLE:
            _canvas.currentTool = new RectangleTool();
            break;
        case ToolType.OVAL:
            _canvas.currentTool = new OvalTool();
            break;
        case ToolType.BRUSH:
            _canvas.currentTool = new BrushTool();
            break;
        default:
            _canvas.currentTool = null;
    }
}

Проверьте это; у вас должен быть инструмент, похожий на тот, который вы знаете из Photoshop.

Инструмент кисть a-brushing

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

В вашем FLA откройте панель компонентов, выбрав пункт меню Window> Components (или нажав Command / Control-F7 ).

Панель компонентов

Найдите компонент ColorPicker и перетащите его на сцену под панелью инструментов (местоположение не слишком важно, если оно не находится сверху области холста).

Размещение компонента выбора цвета на сцене

Дайте ColorPicker имя экземпляра fillColorPicker .

Откройте Canvasкласс и добавьте _fillColorсвойство и соответствующие публичные set/ getфункции.

1
2
3
4
5
6
7
8
9
private var _fillColor:uint;
 
[firstline="48"]
public function set fillColor(color:uint):void {
    _fillColor = color;
}
public function get fillColor():uint {
    return _fillColor;
}

Измените жестко закодированный, 0xFF0000чтобы использовать _fillColorсвойство вместо:

1
_currentTool.mouseDown(_target.mouseX, _target.mouseY, _fillColor);

И, наконец, добавьте следующий код в Drawrкласс:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package {
 
    import canvas.Canvas;
 
    import events.ToolbarEvent;
 
    import flash.display.Sprite;
    import flash.events.Event;
 
    import toolbar.Toolbar;
    import toolbar.ToolType;
 
    import tools.*;
 
    public class Drawr extends Sprite {
 
        private var _canvas:Canvas;
        private var _toolbar:Toolbar;
 
        public function Drawr() {
            _canvas = new Canvas(canvas_mc);
            _toolbar = new Toolbar(toolbar_mc);
            _toolbar.addEventListener(ToolbarEvent.SELECT, onToolbarSelect);
            fillColorPicker.addEventListener(Event.CHANGE, onFillColorChange);
        }
 
        private function onToolbarSelect(e:ToolbarEvent):void {
            switch (e.toolType) {
                case ToolType.RECTANGLE:
                    _canvas.currentTool = new RectangleTool();
                    break;
                case ToolType.OVAL:
                    _canvas.currentTool = new OvalTool();
                    break;
                case ToolType.BRUSH:
                    _canvas.currentTool = new BrushTool();
                    break;
                default:
                    _canvas.currentTool = null;
            }
        }
 
        private function onFillColorChange(e:Event):void {
            _canvas.fillColor = fillColorPicker.selectedColor;
        }
 
    }
}

Если вы сейчас тестируете фильм, вы сможете изменить цвет заливки текущего инструмента с помощью компонента.

Удивительный Техниколор

Оглядываясь назад, система, вероятно, выглядит довольно гладко. Подумайте, что нужно, чтобы добавить новый инструмент. Конечно, есть некоторые аспекты пользовательского интерфейса: вам понадобится новая кнопка, вам нужно будет обновить ее, Toolbarчтобы приспособиться к ней, и вам нужно будет добавить новое перечисление ToolType.

Однако с точки зрения реализации инструмента у вас есть одна четкая задача: внедрить ITool. Если вы сделали это добросовестно, вам следует идти. Да, вам нужно обновить, onToolbarSelectчтобы создать новый инструмент в нужное время. Возможно более хитрая логика реального рисования будет заключена в свой собственный класс. Вам не нужно беспокоиться о подключении событий мыши; «фазовые» методы вызываются автоматически. Вам просто нужно взять данную информацию и нарисовать ее.

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

Использование интерфейсов не обязательно требует меньше работы или меньшего количества строк кода, чем нет, но фокус работы более острый, то есть вы можете выполнять работу более уверенно за меньшее время. У нас также есть четко определенные обязанности для каждого объекта, что облегчает поиск ошибок.


Мало того, что у нас есть преимущества, описанные на предыдущем шаге, вы теперь неукоснительно познакомились с шаблонами проектирования . Это ужасная вещь, которую нужно затронуть на последнем этапе урока, потому что этот предмет удивительно (и полезен) глубоко. Чтобы получить представление о шаблонах проектирования, они являются решениями общих проблем в программировании, особенно с использованием объектно-ориентированных методов. Они, как правило, рассматриваются как «следующая вещь», которую нужно освоить, когда вы освоитесь с ООП на более общем уровне.

Например, распространенной проблемой является необходимость использования разных алгоритмов. Шаблон « Стратегия» — это общий подход к решению. В нашем приложении для рисования нам нужны разные алгоритмы рисования, основанные на выбранном инструменте. Паттерн Стратегия говорит нам, что мы можем рассматривать каждый отдельный алгоритм как его собственный класс, и все классы алгоритмов должны реализовывать один и тот же интерфейс — так же, как мы делали с классами трех инструментов и IToolинтерфейсом. Класс «Контекст» (потребитель или клиент алгоритмов) затем использует тот или иной класс алгоритма, чтобы фактически выполнить свою собственную задачу.

Однако прелесть паттерна «Стратегия» в том, что алгоритм можно заменить другим, как показано на примере выбора инструментов в нашем приложении для рисования.

Применения этого конкретного шаблона бесконечны, и это только один шаблон. Есть много других шаблонов дизайна, которые так же полезны. Этот предмет, естественно, стоит нескольких уроков, и объяснение всего в нескольких параграфах действительно несправедливо. Я просто хотел упомянуть об этом, потому что довольно преднамеренно разработал приложение для рисования, основанное не только на использовании интерфейсов, но и на шаблоне «Стратегия». Я надеюсь, что тема достаточно интригующая, чтобы самостоятельно провести дальнейшие исследования. Я просто хотел посадить семя.


И это завершает наше обсуждение интерфейсов! Они являются одним из наиболее тупых понятий в объектно-ориентированном программировании, и никто не будет обвинять вас в том, что вы не освоили их сразу. Но я надеюсь, что с помощью этого более практичного приложения вы поймете ценность интерфейсов, хотя бы чуть-чуть. Спасибо, что выложили его и прочитали этот урок.