Дорога из желтого кирпича: строим пользовательские интерфейсы вместе с Flash 9 & ASWing. Часть 3

Сегодня мы продолжим знакомство с библиотекой визуальных элементов управления ASWing3. Мы научимся работать со сложными элементами — такими, как падающие списки, таблицы. Также мы на примерах разберем концепцию MVC, с использованием которой и построена данная библиотека.

Что такое MVC? Вкратце это технология проектирования/программирования, продолжающая идею "Разделяй и властвуй". Сделав части программы максимально независимыми друг от друга и реализовав четко спланированные механизмы коммуникации между этими частями, мы избегаем побочных связей, дубляжа информации (а значит, потенциальной противоречивости). Почти всегда информация, которую мы видим на экране, является не точным подобием информации "истинной", а некоторой ее адаптированной версией. Например, если у вас есть база данных, содержащая список людей с примерно такими полями: ID_ЧЕЛОВЕКА, ФИО_ЧЕЛОВЕКА, ДАТА_РОЖДЕНИЯ, ПОЛ. Поле ID_ЧЕЛОВЕКА служит для уникальной идентификации записи в таблице. Так, каждый человек имеет некоторый фиктивный номер (в идеале этот номер никогда за время всей эксплуатации программы не должен измениться). ДАТА_РОЖДЕНИЯ — значение этого поля очевидно, но момент, формат, в котором это поле хранится, — "yyyy-mm-dd". Так устроена некоторая "third-party" СУБД, которую мы используем (вы же не собираетесь сами реализовывать нечто, по качеству похожее на mysql или oracle?). ПОЛ хранит информацию в виде букв m,f. Но — внимание! — когда вы формируете некоторый интерфейс для ввода сведений об этих людях (этого хочет заказчик — не я, а значит, сделать это придется), то в таблице должны отображаться поля: ФИО_ЧЕЛОВЕКА, ДАТА_РОЖДЕНИЯ (но формат уже привычный для нас "dd-mm-yyyy"). Поле ПОЛ должно быть реализовано в виде списка с вариантами "мужской" и "женский". Стоп, это еще не все: теперь по клику на заголовке таблицы данные должны сортироваться. Ладно, предположим, что все это мы сделали. Так, мы создали таблицу из трех колонок, в цикле прочитали информацию из базы данных и закачали ее внутрь таблицы. Мы даже реализовали сортировку методом "пузырька". А теперь подумаем, что нам нужно сделать для сохранения изменений, когда пользователь предварительно отсортировал таблицу по дате рождения, затем переименовал пользователя и изменил дату его рождения. Как вы собираетесь найти в базе ту старую запись, в которой следует изменить старые значения полей на новые? Возможно, вы хотели использовать поля ФИО и ДАТА_РОЖДЕНИЯ — они единственные могли претендовать на уникальность, а, следовательно, выступать как критерий поиска в базе старой записи. Увы, эти поля были изменены. Для небольших баз данных вполне возможно использовать как критерий уникальности сотрудника его ФИО или дату рождения, хотя у вас остается отличный шанс рассказать директору о том, почему он не может принять на работу сотрудника X. Поле ПОЛ не поменялось, но использовать его как критерий для поиска старой записи просто глупо. Вот нам и нужно то самое поле ID_ПОЛЬЗОВАТЕЛЯ — ведь оно не меняется ни в какой ситуации. Только вот проблема: в таблице нет поля с таким названием (этого захотел заказчик). Вы можете решить использовать в качестве критерия поиска записи ее порядковый номер. Что же, я ведь не зря разрешил таблицу сортировать.

В общем, вы должны придти к выводу о том, что данные, которые мы видим на экране, и те, которые необходимы нам для работы, — две разные вещи. И проще говорить "настоящие данные" и "отображение данных". Вот я и выделил первые два слоя из MVC: "M" — model, "V" — view. К виду можно добавить еще специализированные редакторы полей таблицы (помните еще, что поле ПОЛ должно выбираться из падающего списка?). Слой C — controller — выполняет операцию обработки событий, которые инициирует пользователь. Нажал пользователь на кнопку падающего списка для поля пол — view — будь добр, отобрази падающий список с вариантами. Выполнил переход на другую запись (читай: закончил редактирование) — model — пожалуйста, сохрани запись в базу данных. Самый простой компонент, построенный на базе MVC, — это JList, или список некоторых значений. Когда вы создаете объект списка, то при вызове конструктора можете указать его либо в качестве параметра, либо как некоторый массив явно заданных значений (в этом случае JList сам создаст объект модели и выполнит его наполнение элементами массива). Во втором случае вы можете передать уже непосредственно класс, поддерживающий интерфейс ListModel. Существует предопределенная реализация данного интерфейса: VectorListModel. Для того, чтобы изменять содержимое списка, вам необходимо получить ссылку на модель (в любом из случаев это можно сделать с помощью метода getModel). Единственная сложность в том, что интерфейс ListModel не содержит методов, позволяющих изменять содержимое модели. К счастью, есть другой интерфейс — MutableListModel, производный от ListModel, который и предоставляет услуги изменяемой модели данных (именно этот интерфейс реализует класс VectorListModel). В примере ниже я покажу эти два приема создания списка. Для того, чтобы реагировать на событие изменения текущего элемента в списке, вам следует добавить ссылки на функции "слушателя" данного события с помощью метода addSelectionListener. Для удаления ненужного "слушателя" используйте removeSelectionListener. Методы в составе интерфейса MutableListModel по большей части тривиальны. Так, вы можете добавлять/удалять элементы списка. Более интересен метод, который служит для добавления обработчика события "модель данных была изменена". Предположим, что у вас есть список, в который добавляется информация из множества мест (функций, других компонентов) в программе. Тогда, если вы хотите, чтобы при любом изменении списка сработал некоторый код, и боитесь сами реализовывать его во множестве мест (так ведь легко запутаться или забыть), то лучшей альтернативой будет установка специального метода, вызываемого автоматически всякий раз, когда этот список был изменен. Рекомендую всегда, когда вы создаете JList, помещать его внутрь контейнера JScrollPane, т.к. сам компонент списка не создает полос прокрутки в тех ситуациях, когда содержимое в нем не умещается. Так как список может содержать несколько элементов, бывает полезным указать правило, по которому можно выделять его элементы. Для этого служат две константы, определенные в составе класса JList: SINGLE_SELECTION — можно выделить только один элемент; MULTIPLE_SELECTION — можно выделять любые элементы. Разные способы выделения элементов диктуют разные правила получения информации о том, какой элемент (элемент ы) сейчас активен (активны). В первом случае используйте метод getSelectedIndex. Если же в списке можно выделить несколько элементов, то применяйте getSelectedIndices, который вернет массив, содержащий список выделенных номеров элементов. Есть сходная пара методов getSelectedValue и getSelectedValues, возвращающих собственно значения выделенных элементов списка. Пример созданного списка, а также падающего списка приводится на рис. 1.

// создаем список с явно перечисленным содержимым
var list1 = new JList (["apple", "orange", "grapes"]);
// создаем модель данных для списка
var model2:VectorListModel = new VectorListModel(["one", "two", "three", "four"]);
// и теперь на основе этой модели создаем сам список
var list2 = new JList (model2);// теперь можно установить различные модели выделения элементов списка
list1.setSelectionMode (JList.SINGLE_SELECTION);
list2.setSelectionMode (JList.MULTIPLE_SELECTION);// и последним шагом создаем функцию — обработчик события изменения/выделения элемента списка function __listSelection(e:Event):void{
var source:JList = e.target as JList;
trace ("selection: " + source.getSelectedValues() );}
// привязываем функцию обработки событий
list1.addSelectionListener (__listSelection);
panel_top.append(new JScrollPane (list1));
// внимание! не забудьте при добавлении списка обернуть его с помощью JScrollPane
panel_top.append(new JScrollPane (list2));

На примере списка JList мы разберем подробнее еще одну возможность из мира MVC —renderer и editor. Помните, в примере с таблицей, где содержатся сведения о людях, наш "клиент" хотел, чтобы поле "пол" было реализовано в виде падающего списка? Еще добавим требование, чтобы строка, содержащая сведения о человеке, была подсвечена, например, красным цветом, если он родился в пятницу 13-го. Обе эти возможности нестандартны, и, разумеется, ни в одной общей библиотеке эти функции не будут реализованы по умолчанию. Вам нужно, чтобы особые данные выглядели "так", кому-то другому — чтобы "вот так". Чтобы угодить всем, были созданы специальные классы, управляющие генерацией внешнего вида для других компонентов. Например, для списка мы можем переопределить правило отображения содержимого информационной модели. Если в JList хранится, например, список людей (каждый из которых реализован в виде экземпляра класса CHuman), то только специально созданный render может знать, как именно следует этот объект (CHuman) визуализировать. Но это еще не все. Следующий вопрос в том, откуда берутся эти render'ы, и как сказать, что JList будет отрисовываться нестандартно? Конструктор класса JList, кроме первого параметра, может получить еще один необязательный параметр, содержащий ссылку на специальный объект — "фабрику" render'ов.

Что такое фабрика объектов в мире программирования? Да почти то же самое, что и в реальной жизни. Фабрика — это специализированный объект, который предназначен для создания других объектов. Так, когда JList'у потребуется отрисовать элемент списка, то он попросит фабрику вернуть тот компонент, который знает, как выполнять такую отрисовку. Звучит просто, но на самом деле я пропустил десяток страниц текста, рассказывающего о паттернах проектирования/программирования — в частности, о паттернах создания, о фабриках, о фабриках фабрик и еще о всяких хитрых методиках, позволяющих экономить ресурсы при отрисовке "больших" элементов управления. Ведь если каждому элементу списка или таблицы размером под тысячу строк выделить свой render, то никаких ресурсов просто не хватит (в мире flash уж точно). Существует специальная фабрика GeneralListCellFactory, облегчающая работу с пользовательскими render'ами. Все, что вам нужно, — это создать объект этой фабрики, передав в качестве параметра объект метакласса render признак того, будут ли ячейки списка разделять один render (это рекомендуемая стратегия), а также признаки того, будут ли все элементы списка одной высоты, и каково значение этой высоты. Первое, что я сделал, — создал класс CHuman, в составе которого три поля, для инициализации которых я также реализовал конструктор. Все поля сделаны public — хоть это и неправильно — лучше придерживаться методики доступа к внутреннему устройству класса через специализированные методы getter и setter. Setter — это, проще говоря, метод, который устанавливает значение свойства (со всякими хитроумными проверками данных на корректность). Например, полю sex, которое у меня реализовано как число, можно по ошибке присвоить значение, не совпадающее с 0 и 1. Setter позволяет этой ошибки избежать. Getter служит для получения значения некоторого поля, закрытого с помощью модификатора private. Следующим шагом будет создание класса MyRenderer, поддерживающего интерфейс ListCell. В составе данного интерфейса есть различные методы. getCellComponent — этот метод должен вернуть визуальный компонент, который и будет использован для отрисовки значения поля. Пара методов getCellValue и setCellValue являются getter'ом и setter'ом для "значения". Перед тем, как будет вызван метод getCellComponent, среда должна будет вызвать метод setCellValue, внутри которого и следует выполнить настройку внешнего вида render'а. Функция setListCellStatus — она вызывается всякий раз, когда состояние компонента render'а должно измениться — например, пользователь выделил элемент списка, так что теперь его следует, например, выделить рамкой. Теперь я привожу исходный код примеров: сначала файл Chuman.as, затем MyRenderer.as, которые располагаются в том же каталоге, что и файл fla.

package {// класс, описывающий информацию о человеке
public class CHuman {
// поля класса
public var fio : String;
public var birthDate : Date;
// поле sex принимает два значения: 0 и 1 — м и ж
public var sex : int;
// конструктор, выполняющий инициализацю полей
public function CHuman(_fio : String, _birthDate : Date, _sex : int)
{ this.fio = _fio; this.birthDate = _birthDate; this.sex = _sex;}}}

// код класса render'а
package {
// импортируем нужные декларации классов ASwing
import org.aswing.*;
import org.aswing.border.*;
public class MyRenderer implements ListCell {
// компонент визуализации это текстовая надпись
private var cmp = new JLabel ("", new LoadIcon ("ico_fla9_1.PNG"));
public function getCellComponent():Component{ return cmp; }
// ccылка на информационный объект
private var human : CHuman;
public function getCellValue():* { return human; }
public function setCellValue(value:*):void{
human = value as CHuman;
cmp.setText (human.fio);
cmp.setForeground ( human.sex == 1? ASColor.RED : ASColor.BLUE ); }

public function setListCellStatus(list:JList, isSelected:Boolean, index:int):void{
// в зависимости от того, выделен ли сейчас элемент списка, я рисую либо черную рамку вокруг него, либо белую
cmp.setBorder(new LineBorder(null, isSelected?ASColor.BLACK:ASColor.WHITE, 3)); } }}

// и, наконец, собственно код, который создает форму со списком
// сначала создаем некоторое количество экземпляров объектов CHuman
var hum_1 = new CHuman ("bill", new Date (), 1);
var hum_2 = new CHuman ("jeorge", new Date (2005, 2, 5), 1);
var hum_3 = new CHuman ("mary", new Date (1998, 5, 6), 0);
// создаем список с явно перечисленным содержимым
var list1 = new JList ([hum_1, hum_2, hum_3], new GeneralListCellFactory (MyRenderer, true, true, 48) );
panel_top.append(new JScrollPane (list1));

Результаты работы показаны на рис. 2. Хотя при печати этого не видно, но цвет шрифта, которым написано имя человека, либо красный, либо синий в зависимости от его пола, а текущий элемент выделен с помощью жирной черной рамки. Для того, чтобы создать компонент "таблица", используйте класс JTable. В качестве параметров ему следует передать на модель данных. Класс модели данных должен реализовывать интерфейс AbstractTableModel. Удобно также применять стандартную заготовку модели данных в виде класса DefaultTableModel. После того, как вы создадите его экземпляр, вам необходимо указать названия его столбцов, а также значения строк. Применяйте метод, которому в качестве параметра следует передать ссылку на массив имен и значений столбцов. Результаты работы скрипта приведены на рис. 3.

var data:Array = [["Bill", new Date(), 'm', true], ["Jeorge", new Date(), 'm', false],["Mary", new Date(), 'f', true], ["Anna", new Date(), 'f', false]];
var column:Array = ["Fio", "BirthDate", "Sex", "Is_Good"];
var model:DefaultTableModel = (new DefaultTableModel()).initWithDataNames(data, column);
model.setColumnClass(0, "String");
model.setColumnClass(1, "Date");
model.setColumnClass(2, "String");
model.setColumnClass(3, "Boolean");
var table = new JTable(model);
panel_top.append(new JScrollPane (table));

Не забывайте разместить таблицу внутри области JScrollPane. На этом все.
Этой статьей я планировал завершить рассмотрение средства ASwing. Но 15 июня вышла новая версия библиотеки, в которой появилось много вкусного. Кроме того, остались нераскрытыми методика создания деревьев, алгоритмы работы с DnD... Так что продолжению быть.

black zorro, black-zorro@tut.by


Компьютерная газета. Статья была опубликована в номере 27 за 2007 год в рубрике программирование

©1997-2024 Компьютерная газета