Java 7

Хабибуллин Ильдар Шаукатович

Текстовые компоненты

 

При создании приложения с графическим интерфейсом очень часто приходится использовать поля для ввода текста. Такое поле может состоять из одной или нескольких строк, быть редактируемым или не редактируемым. В редактируемом окне часто приходится задавать возможность смены шрифта, цвета, вставку дополнительных символов. Библиотека Swing предоставляет для этого большие возможности, предлагая пакеты интерфейсов и классов javax.swing.text, javax.swing.text.html, j avax. swing.text. html .parser и j avax. swing. text. rtf.

На вершине иерархии текстовых компонентов стоит класс JTextComponent — непосредственное расширение класса JComponent. Это абстрактный класс, вобравший в себя общие свойства всех текстовых компонентов. У него три расширения: однострочное текстовое поле JTextField, многострочная текстовая область JTextArea и небольшой, но мощный текстовый редактор JEditorPane, имеющий расширение- класс JTextPane, ме

тодами которого можно оформить текст в каком-то определенном стиле.

У класса JTextField есть два расширения- поле для ввода пароля JPasswordField, в ко

тором вместо вводимых символов показывается один заранее определенный символ, по умолчанию звездочка, и поле для редактирования форматированных объектов JFormattedTextField, например даты — объекта класса Date, или чисел, заданных в определенном формате.

Рассмотрим текстовые компоненты подробнее и начнем с вершины их иерархии.

 

Компонент JTextComponent

Абстрактный класс JTextComponent стоит на вершине иерархии текстовых компонентов и содержит их общие свойства.

Текстовые компоненты построены по схеме "Model-View-Controller", которая отражена во внутреннем устройстве класса JTextComponent. Рассмотрим это устройство подробнее. Начнем с модели данных текстовых компонентов.

Модель данных — документ

Модель данных схемы MVC текстовых компонентов описана интерфейсом Document и называется документом. Документ может быть простым, "плоским", или сложным, структурированным. Интерфейс Document описывает простой текст (content), содержащийся в компоненте, как последовательность символов Unicode. Количество символов в тексте можно узнать методом getLength(). Последовательность символов нумеруется, начиная от нуля. Каждый символ имеет свой порядковый номер, называемый позицией (position, location или offset) символа в тексте. Точнее говоря, модель данных задает позицию не символа, а позицию между символами, перед текущим символом текста, что удобно для вставки текста.

Если текст документа сложный, структурированный, то, чтобы скрыть сложность определения текущей позиции при частых вставках и удалениях текста, удобно воспользоваться объектом, описанным интерфейсом Position. Экземпляр класса, реализующего интерфейс Position, создается методом createPosition (int). Методы getStartPosition( ) и getEndPosition() возвращают начальную и конечную позиции текста в виде объекта, реализующего интерфейс Position. В интерфейсе Position всего один метод — getOffset (), возвращающий позицию символа в виде целого числа типа int. Кроме того, в интерфейсе Position есть вложенный класс Bias, в котором определены два экземпляра класса Bias: поле Forward и поле Backward. Они уточняют символ, позиция которого определена: текущий или предыдущий. Позиция символа используется, например, при получении текста методами

String getText(int offset, int length);

void getText(int offset, int length, Segment text);

извлекающими из документа фрагмент текста длиной length символов, начиная от позиции offset. Второй метод заносит извлеченный фрагмент в экземпляр text класса Segment. Опишем этот небольшой, но удобный класс.

Строка символов Segment

Класс Segment представляет строку символов в виде, удобном для быстрого просмотра. У строки класса Segment всегда есть текущий символ, который можно получить методом current ( ). Позицию текущего символа можно узнать методом getIndex (), а установить — методом setIndex (int). Класс Segment содержит методы previous () и next (), возвращающие предыдущий и следующий символ строки, а также методы first () и last (), возвращающие первый и последний символ строки. Будьте внимательны: эти четыре метода меняют текущую позицию строки!

Интересно, что для быстроты доступа строка класса Segment хранится в открытом (public) поле — массиве с именем array типа char [ ], начиная с индекса offset, и занимает в этом массиве count элементов. Это открытые поля, так что к массиву символов можно обращаться напрямую и менять его элементы, хотя при этом теряется главное назначение класса Segment- быстро просматривать текст. Метод getBeginIndex() воз

вращает индекс начала строки в массиве array, т. е. число offset, а getEndIndex( ) индекс элемента массива, следующего за последним символом строки.

Заканчивая обзор класса Segment, скажем, что для большей надежности выполнения метода getText(int, int, Segment) в класс Segment введен метод setPartialReturn(boolean). Если в этом методе задать параметр true, то передача текста методом getText () в экземпляр класса Segment будет происходить, по возможности, без дополнительного копирования. Значение параметра по умолчанию — false. С учетом этого, работа с классом Segment начинается примерно так:

Segment seg = new Segment(); seg.setPartialReturn(true); doc.getText(0, doc.getLength(), seg);

// Работаем с объектом seg...

Запись текста в документ

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

void insertString(int offset, String text, AttributeSet attr);

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

Атрибуты текста

Как видно из сигнатуры метода insertString(), у вносимого текста могут быть атрибуты, например: имя шрифта, размер шрифта, цвет. Если у текста нет атрибутов, то третьему параметру метода надо дать значение null.

Атрибуты записываются в виде пар "имя — значение" в объект, реализующий интерфейс AttributeSet. Этот интерфейс описывает неизменяемое множество атрибутов, в нем не описаны методы добавления и удаления атрибутов. Такие методы внесены в его расширение — интерфейс MutableAttributeSet. В библиотеке Swing есть реализация данного интерфейса — класс SimpleAttributeSet. С помощью этого класса можно определить любые пары "имя — значение", но общепринятые атрибуты удобнее задавать с использованием констант и статических методов класса StyleConstants и четырех его подклассов, которые в то же время вложены в него: CharacterConstants, ColorConstants, FontConstants и ParagraphConstants.

Объект, реализующий интерфейс AttributeSet, может содержать ссылку на другой, "родительский" объект того же типа. Ссылка хранится как значение атрибута, имеющего имя ResolveAttribute. Так можно получить цепочку объектов, содержащих атрибуты текста. Если какая-то пара "имя — значение" не найдена в первом объекте методом getAttribute (Object), то она отыскивается в родительском объекте, который определяется методом getResolveParent (), затем поиск идет далее по цепочке.

У интерфейса MutableAttributeSet есть свое расширение- интерфейс Style. Это расши

рение дает возможность получить имя множества атрибутов методом getName ( ), создав так называемый стиль (style), и присоединить к множеству слушателя события

ChangeEvent методом

void addChangeListener(ChangeListener chl);

Заданное имя затем можно использовать как значение другого атрибута, а с помощью слушателя отслеживать добавление и удаление атрибутов.

Для создания стилей и работы с ними очень полезен класс StyleContext. Его экземпляр с общепринятым набором атрибутов можно получить следующим образом:

StyleContext stc = StyleContext.getDefaultStyleContext();

Затем в полученный объект stc можно добавить новые атрибуты методами addAttribute (), удалить атрибуты методами removeAttribute(), создать цепочку стилей.

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

Обратная операция — удаление части или всего текста из документа — выполняется методом

remove(int offset, int length);

Он удаляет length символов, начиная от символа, находящегося в позиции offset.

Фильтрация документа

Операции занесения текста в документ методом insertString() и удаления методом remove () можно изменить так, чтобы они производили различные проверки и модификации, фильтруя таким способом вставляемый или удаляемый текст. Это можно сделать прямым расширением класса, реализующего Document, и переопределением его методов insertString () и remove (). Но есть еще один способ, не изменяющий документ.

Сначала надо расширить класс DocumentFilter, переопределив его методы. В классе DocumentFilter всего три метода:

void insertString(DocumentFilter.FilterBypass fb, int offset,

String text, AttributeSet attr);

void remove(DocumentFilter.FilterBypass fb, int offset, int length); void replace(DocumentFilter.FilterBypass fb, int offset, int length,

String text, AttributeSet attr);

Их стандартная реализация просто вызывает соответствующие методы вложенного абстрактного класса FilterBypass, которые оставлены абстрактными.

После расширения класса DocumentFilter, вызванного переопределением методов, полученный фильтр надо установить в документ методом

void setDocumentFilter(DocumentFilter filter);

класса AbstractDocument. Далее всякое обращение к методам insertString () и remove () документа будет пропускаться через методы созданного фильтра.

Пример использования фильтра, пропускающего только цифры, приведен в листинге 12.1.

Внесение структуры в документ

В модель данных можно внести структуру дерева, например разбить документ на главы, параграфы, разделы. Каждый элемент разбиения описывается интерфейсом Element. Элемент занимает какую-то область текста с начальной позицией, возвращаемой методом getStartOffset (), и конечной позицией getEndOffset (). У элемента может быть родительский элемент, который легко получить методом getParentElement(). Область текста, занимаемая родительским элементом, полностью включает в себя область исходного элемента. У элемента могут быть дочерние элементы, чья область текста полностью лежит внутри области самого элемента. Их можно получить методом getElement(int). Число дочерних элементов возвращает метод getElementCount (), а индекс дочернего элемента — метод getElementIndex (int). Элементу можно дать имя, а затем получить его методом getName ().

Интерфейс Element частично реализован абстрактным классом AbstractElement, вложенным в класс AbstractDocument, и полностью реализован еще двумя вложенными

в AbstractDocument классами — BranchElement и LeafElement, расширяющими класс AbstractElement. Основная разница между ними в том, что у класса BranchElement могут быть дочерние элементы, а у класса LeafElement — нет.

Класс BranchElement, в свою очередь, расширяется классом SectionElement, вложенным в класс DefaultStyledElement, и классом BlockElement, вложенным в класс HTMLDocument.

Класс LeafElement расширяется классом RunElement, вложенным в класс HTMLDocument. Элементы создаются методами

Element createLeafElement(Element parent, AttributeSet attr,

int pos1, int pos2);

Element createBranchElement(Element parent, AttributeSet attr);

Для каждого структурного элемента можно задать свое множество атрибутов методом

addAttribute(Object name, Object value) или методом addAttributes(AttributeSet).

В документе допускается задание нескольких независимых структур. Их корневые элементы можно получить методом getRootElements ( ), возвращающим массив типа Element [ ]. Один из корневых элементов можно сделать корневым элементом по умолчанию и получать его методом getDefaultRootElement(), возвращающим элемент в виде объекта, реализующего интерфейс Element. Получив корневые элементы структурного дерева, легко обойти его, используя метод getElement(int) интерфейса Element или метод children (), имеющийся в его реализациях.

События в документе

При всяком изменении документа или его структуры происходит событие, описанное интерфейсом DocumentEvent. Он предлагает метод getDocument ( ), позволяющий узнать, в каком документе произошло событие, методы getOffset () и getLength (), сообщающие о начальной позиции и длине измененного текста. Вложенный интерфейс ElementChange содержит метод getElement (), позволяющий узнать элемент, в котором произошло событие, и методы, помогающие отследить добавление и удаление элемента из документа.

Реализация интерфейса DocumentEvent — класс DefaultDocumentEvent, вложенный в класс AbstractDocument, — добавляет к методам интерфейса метод undo (), отменяющий изменения, метод redo (), восстанавливающий изменения, и еще несколько информационных методов.

Если модель данных позволяет отменять и восстанавливать изменения (undo/redo), то при каждом таком действии в ней происходит событие класса UndoableEditEvent.

Реализации документа

Интерфейс Document частично реализован абстрактным классом AbstractDocument. Этот класс вносит понятие блокировки документа. Документ могут просматривать несколько подпроцессов-"читателей" и один подпроцесс-"писатель". Доступ их к документу блокируется методами readLock() и writeLock(). Блокировки снимаются методами

readUnlock() Рё writeUnlock().

Класс AbstractDocument обычно не расширяется непосредственно, а используются или расширяются его подклассы PlainDocument и DefaultStyledDocument.

Класс PlainDocument задает модель простого документа с "плоским" текстом, которая используется полями ввода JTextField, JPasswordField, JTextArea. Текст в этой модели имеет структуру: структурные элементы текста — это строки. Корневой элемент структуры можно получить методом getDefaultRootElement(). Метод getParagraphElement(int offset) возвращает элемент структуры — строку в виде объекта типа Element, к которому принадлежит позиция offset. Каждой строке, как любому элементу структуры, можно придать атрибуты. Отдельные символы атрибутов не имеют.

Более сложную структуру вносит в документ модель класса DefaultStyledDocument, используемая в текстовом редакторе JTextPane. В этой модели не только строке, но и каждому символу текста можно задать свой стиль.

Наконец, класс DefaultStyledDocument расширяется классом HTMLDocument. Это модель разметки языка HTML. В ней можно определить таблицы стилей (style sheets).

Установка модели данных

После того как новая модель данных определена, ее надо установить в компонент методом setDocument(Document) класса JTextComponent.

Р’РёРґ

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

Эта идея частично реализована абстрактным классом View. Он задает для каждого документа целую структуру Видов, отвечающую структуре элементов документа. Каждый вид из этой структуры при своем создании получает ссылку на определенный элемент документа, которую можно отследить методом getElement (). Документ, к которому относится Вид, можно получить методом getDocument(). У каждого Вида есть родительский вид, который определяется методами getParent() и setParent ( ), и множество дочерних видов. Их число можно определить методом getViewCount(), получить дочерние виды можно методом getView(int).

Создание дочерних Видов выполняется методом create(Element), описанным в интерфейсе ViewFactory. В документе у каждого элемента есть объект, реализующий этот интерфейс, который можно получить методом getViewFactory(). Еще несколько методов занимаются добавлением видов в иерархию и удалением их оттуда.

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

Shape modelToView(int startPos, Position.Bias b0,

int endPos, Position.Bias b1, Shape fig);

который возвращает двумерную фигуру класса Shape. Параметры метода задают начальную startPos и конечную endPos позиции текста, направления b0 и b1 от этих позиций к преобразуемому тексту, которые могут принимать одно из двух значений:

Position.Bias.Backward или Position.Bias. Forward, и окружающую фигуру fig.

Обратное преобразование выполняется методом

int viewToModel(float x, float y, Shape fig, Position.Bias[] b);

возвращающим позицию символа, имеющего координаты (x, y) на экране в фигуре fig. Кроме того, метод вычисляет массив направлений b, уточняющий положение символа в модели данных.

Для вывода на экран в каждом виде создается графический контекст — экземпляр класса Graphics. Непосредственный вывод элемента на экран выполняется методом paint (Graphics) подобно выводу компонента. Графическим контекстом можно воспользоваться не только в методе paint(Graphics), но и непосредственно, получив его методом getGraphics ( ).

Каждый Вид устанавливает свой размер методом setSize(float width, float height), у него есть минимальный, предпочтительный и максимальный размер.

В этом класс View напоминает класс Component. Сходство усиливается тем, что подобно тому, как класс Component порождает множество компонентов, создающих на экране графический интерфейс, класс View порождает множество подклассов-видов, отображающих на экране различные типы документов. Их иерархия показана на рис. 12.1.

Object

L View-г CompositeView— BoxView

PlainView

ImageView - IconView

-1— BlockView — ListView

— FlowView-ParagraphView

FieldView — TableView

L PasswordView — TableView.TableRow

— WrappedPlainView

— ZoneView

-GlyphView-LabelView—InlineView

-AsyncBoxView

- ComponentView-p FormView LobjectView

Рис. 12.1. Иерархия классов-видов

Как видно из рис. 12.1, иерархия Видов обширна и разветвлена. Большинство классов связано с интерпретацией языка HTML, например класс ImageView обрабатывает тег

, класс FormView тег

и относящиеся к нему теги ,