Добавил:
СПбГУТ * ИКСС * Программная инженерия Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Портянкин И. Swing

.pdf
Скачиваний:
188
Добавлен:
07.10.2020
Размер:
4.63 Mб
Скачать

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

495

набором. Для хранения набора атрибутов мы применяем класс SimpleAttributeSet7, в котором указываем, что текст должен иметь красный цвет. Передав данный набор в метод и отказавшись от полной замены имеющегося стиля (третий параметр равен false), мы окрасим часть заголовка в красный цвет, не меняя остальных его атрибутов, подобных размеру и шрифту.

Напоследок мы задействуем кое-что из экзотических возможностей редактора JTextPane: добавляем в конец документа самый настоящий компонент — флажок JCheckBox. Предварительно флажок делается прозрачным (свойство opaque устанавливается в false), в противном случае он будет закрашивать свою область цветом фона и выпадать из общей картины документа. Также приходится переместить курсор в конец документа методом setCaretPosition(), иначе флажок появится вместо конца документа в его начале. Ну а сама вставка компонента на текущую позицию (указываемую курсором) выполняется методом insertComponent().

Запустив программу с примером, вы увидите, как отображается на экране размеченный разнообразными стилями документ. Легко видеть, что текстовый редактор JTextPane как нельзя лучше подходит там, где нужны многофункциональные мощные текстовые возможности. С его помощью вы сможете управлять стилями, обновлять произвольные фрагменты текста, вставлять в текст компоненты и значки, причем сделать все это будет совсем несложно. Для компонента JTextPane практически нет невозможного: любые ваши запросы он сможет удовлетворить, просто придется провести немного больше исследований и работы.

Форматированный вывод — компонент

JFormattedTextField

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

7 Набор атрибутов описывается интерфейсом AttributeSet, класс SimpleAttributeSet — самая простая реализация этого интерфейса, позволяющая быстро настроить набор. Кстати, стили по сути также являются наборами атрибутов: класс стилей Style реализует интерфейс AttributeSet, отличие лишь в том, что стиль обладает собственным уникальным именем.

496

ГЛАВА 16

Отличие между полем для форматированного вывода данных и обычным текстовым полемзаключаетсявналичиивполеJFormattedTextFieldособогоформатирующегообъекта, который должен быть унаследован от абстрактного внутреннего класса AbstractFormatter. Когда пользователь проводит в текстовом поле какие-либо действия — набирает новый символ, удаляет фрагмент текста, отменяет операцию — вызывается форматирующий объект. В его задачу входит анализ значения, которое оказывается в текстовом поле, и принятие решения о соответствии этого значения некоторому формату. Основными составляющими форматирующего объекта являются фильтр документа (DocumentFilter), который принимает решение, разрешать или нет очередное изменение в документе,

атакже навигационный фильтр (NavigationFilter). Навигационный фильтр получает исчерпывающую информацию о перемещениях курсора в текстовом поле и способен запрещать курсору появляться в некоторых областях документа (таких как разделители номеров, дат, и других данных, которые не должны редактироваться). Форматирующий объект также ответственен за действие, которое предпринимается в случае ввода пользователем неверного значения (по умолчанию раздается звуковой сигнал).

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

ав наличии нескольких уже готовых форматирующих объектов, способных помочь в организации эффективного ввода самых разнообразных данных. В табл. 16.3 представлен краткий перечень стандартных объектов (все описываемые форматирующие объекты находятся в пакете javax.swing.text).

Таблица 16.3. Стандартные форматирующие объекты для JFormattedTextField

Форматирующий Описание объект

MaskFormatter Организует ввод данных на основе простой маски: набора специальных символов, задающих символы, которые допустимы на определенных позициях документа. Подробное описание масок вы найдете в интерактивной

документации данного класса, а простой пример мы вскоре увидим. Вместо

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

DateFormatter Позволяет редактировать даты в любом удобном вам (или пользователю

ваших приложений) формате. Для форматирования дат используется класс

DateFormat из пакета java.text. Довольно удобен, так как позволяет быстро локализовать ввод дат для любого поддерживаемого JDK языка (а в со-

временных выпусках JDK поддерживаются практически все языки мира).

Формат для отображения даты вы сможете задать в конструкторе формати-

рующего объекта или поменять прямо во время работы программы. Впро-

чем, современные интерфейсы предполагают применение для выбора дат всплывающих диалогов, реализацию такого диалога можно найти в библио-

теке дополнений SwingX

NumberFormatter Рассматривает вводимые пользователем значения как числа, записанные

в определенном формате. Для форматирования чисел использует класс NumberFormat из пакета java.text. Также как и предыдущий класс, позволяет

легко локализовать приложение и настроить формат прямо во время рабо-

ты программы

Все стандартные форматирующие объекты унаследованы от класса DefaultFormatter, реализующего основные функции базового класса AbstractFormatter. Как правило, ваша работа заключается именно в настройке одного из доступных форматирующих объек-

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

497

тов и подключении его к полю JFormattedTextField. Создание собственных форматирующих объектов — довольно кропотливая задача. Зачастую проще создать собственную модель документа Document (мы ее вскоре обсудим).

Кроме того, что вы можете указать форматирующий объект, который следует использовать в текстовом поле JFormattedTextField, имеется еще одна возможность настраивать форматирование. Фабрика AbstractFormatterFactory, описанная, как и форматирующий объект AbstractFormatter, в виде абстрактного внутреннего класса, служит для создания форматирующих объектов для текстовых полей JFormattedTextField. Если вы создадите поле JFormattedTextField, передав в конструктор фабрику форматирующих объектов, то поле при редактировании будет каждый раз запрашивать ее для получения форматирующего объекта. Это дает дополнительную гибкость: вы можете возвращать различные форматирующие объекты, в зависимости от того, что в данный момент находится в текстовом поле. В пакете javax.swing.text имеется стандартная реализация фабрики DefaultFormatterFactory. С ее помощью вы сможете задать различные форматирующие объекты для трех ситуаций: когда поле не пусто и обладает фокусом ввода, когда поле не пусто и не обладает фокусом ввода, и, наконец, когда поле пусто. Хотя фабрика форматирующих объектов и предоставляет дополнительную гибкость, чаще всего достаточно одного форматирующего объекта «на все случаи жизни».

А теперь рассмотрим пример, в котором используем все три стандартных форматирующих объекта, поставляемых вместе со Swing, а заодно применим некоторые наиболее полезные свойства текстового поля JFormattedTextField:

//FormattedFields.java

//Применение полей JFormattedTextField import javax.swing.*;

import javax.swing.text.*; import java.text.*; import java.util.Date; import java.awt.*;

public class FormattedFields extends JFrame { // поля для форматированного ввода данных private JFormattedTextField

phoneField, dateField, numberField; public FormattedFields() {

super("FormattedFields"); setDefaultCloseOperation(EXIT_ON_CLOSE);

//ограниченный ввод на основе маски

//телефонный номер

try {

MaskFormatter phone =

new MaskFormatter("+#-###-###-##-##"); phone.setPlaceholderCharacter('0'); phoneField = new JFormattedTextField(phone); phoneField.setColumns(15);

}catch (Exception ex) { ex.printStackTrace();

498

ГЛАВА 16

}

//редактирование даты

//формат даты

DateFormat date =

new SimpleDateFormat("dd MMMM yyyy, EEEE");

//настройка форматирующего объекта

DateFormatter formatter = new DateFormatter(date); formatter.setAllowsInvalid(false); formatter.setOverwriteMode(true);

// настройка текстового поля

dateField = new JFormattedTextField(formatter); dateField.setColumns(15); dateField.setValue(new Date());

//редактирование чисел

//формат числа с экспонентой

NumberFormat number = new DecimalFormat("##0.##E0"); numberField = new JFormattedTextField(

new NumberFormatter(number));

//настройка поля numberField.setColumns(10); numberField.setValue(1500);

//добавляем поля в панель содержимого setLayout(new FlowLayout());

add(new JLabel("Телефон:")); add(phoneField);

add(new JLabel("Дата:")); add(dateField);

add(new JLabel("Число:")); add(numberField);

//выводим окно на экран

setSize(400, 300); setVisible(true);

}

public static void main(String[] args) { SwingUtilities.invokeLater(

new Runnable() {

public void run() { new FormattedFields(); } });

}

}

Мы создаем небольшое окно с рамкой, в нем разместятся три текстовых поля для вывода форматированных данных JFormattedTextField. Каждое из полей продемонстрирует работу одного из стандартных форматирующих объектов.

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

499

Первое поле применяет в качестве форматирующего объекта MaskFormatter, данный объект позволяет организовать ввод данных на основе особой маски, составленной из специальных символов. Подробное описание масок, как мы уже отмечали, находится в интерактивной документации. В примере маска составляется для ввода расширенного телефонного номера: с кодом страны и города. Обратите внимание, как в маске задаются десятичные числа — они обозначаются символом #. Остальные символы, использованные при создании объекта MaskFormatter, обычные, а это означает, что редактировать их пользователю будет нельзя, так как они служат разделителями данных. Объект MaskFormatter приходится создавать в блоке try/catch — если при анализе маски будет обнаружена ошибка синтаксиса, возникнет исключение. Помимо собственно создания форматирующего объекта мы настраиваем некоторые дополнительные свойства. Свойство placeholderCharacter отвечает за то, какой символ будет заменять незаполненные пользователем части маски. По умолчанию используется пробел, но для нашей маски он не очень хорошо подходит, потому что размер текстового поля с пробелами намного меньше того же текстового поля, но уже заполненного цифрами. К тому же по маске, заполненной пробелами, трудно определить, что в ней требуется записывать цифры, а не что-либо иное. Поэтому мы заменяем пробел символом нуля. Более того, мы увеличиваем размер текстового поля до 15 символов. Поле с избыточным количеством символом смотрится лучше, а за пределы маски пользователю все равно выходить запрещено (за этим следит форматирующий объект).

Второе поле служит для набора дат, заданных в определенном формате. Формат даты задается объектом DateFormat. В классе DateFormat имеются несколько статических методов, позволяющих создавать стандартные форматы дат, принятых в различных странах, однако мы настраиваем формат даты по своему вкусу, создавая объект SimpleDateFormat. Он позволяет указать формат даты с помощью несложной текстовой маски; полное описание правил создания таких масок вы найдете в документации класса SimpleDateFormat. Наш формат для даты содержит число месяца (за него отвечают символы маски «dd»), полное название месяца («MMMM»), четырехзначные число года и название дня недели («yyyy» и «EEEE», соответственно). Созданный формат даты мы передаем в форматирующий объект DateFormatter, после чего настраиваем для него несколько дополнительных свойств. В отличие от объекта MaskFormatter форматирующий объект для дат по умолчанию разрешает ввод неверных (не соответствующих формату даты) значений. Мы запрещаем ввод таких значений, изменяя свойство allowsInvalid. Для удобства изменения даты мы также включаем режим перезаписи значений (по умолчанию работает режим вставки), применяя свойство overwriteMode. После настройки форматирующий объект передается в текстовое поле, размер которого мы также увеличиваем до 15 символов. Обратите внимание, как задается новое значение, которое будет отображаться текстовым полем: в метод setValue() мы передаем объект Date, инкапсулирующий дату. За преобразование этого объекта в текст отвечает как раз форматирующий объект DateFormatter.

Наконец, третье поле JFormattedTextField настраивается для ввода чисел в определенном формате. Формат чисел определяется объектом NumberFormat. Как и в случае с объектом для формата дат DateFormat, в нем имеется несколько статических методов для получения стандартных форматов чисел различных стран и языков, однако мы применим для создания формата числа объект DecimalFormat. Он позволяет настраивать формат числа с помощью несложной текстовой маски. В примере мы настроили маску для целых чисел в экспоненциальном формате: две десятичных цифры в самом числе, две возможных десятичных цифры в мантиссе. Число и мантисса разделяются точкой и заканчиваются нулями. Подробное описание маски для определения формата числа вы найдете в документации класса DecimalFormat. Созданный формат числа присоединяется к форматирующему объекту NumberFormatter, а тот, в свою очередь, передается текстовому полю. Для текстового поля мы немного увеличиваем размеры (увеличиваем количество столбцов).

500

ГЛАВА 16

После настройки всех трех текстовых полей JFormattedTextField они добавляются в окно с последовательным расположением FlowLayout. Каждое поле снабжается небольшой поясняющей надписью. Запустив программу с примером, вы увидите поле JFormattedTextField в действии и сможете оценить достоинства и недостатки ввода ограниченных данных. Поле, действующее на основе маски, наиболее удобно и интуитивно понятно для пользователя, поле для ввода чисел также довольно удобно, хотя неожиданная смена записи числа, введенного в обычной форме, на запись экспоненциальную, иногда может быть некстати, и об этом пользователя стоит предупреждать. А вот поле для ввода дат действует не лучшим образом: если ввести новую дату или год еще возможно, то название месяца или дня недели поменять одним символом нельзя. К тому же при изменении дат или года любое случайно введенное значение может привести к скачкам на десятки месяцев, а то и лет. Так что для ввода дат гораздо лучше применять уже изученные нами в главе 12 счетчики JSpinner. Как мы помним, для вывода дат они применяют именно поля JFormattedTextField, так что вместо самостоятельной настройки поля JFormattedTextField для вывода даты вы сможете настроить поле счетчика, а для выбора собственно даты применить сам счетчик. Еще лучше для выбора дат использовать отдельные всплывающие окна. Ну а самым удобным и полезным (и к тому же простым) форматирующим объектом без сомнения является MaskFormatter.

Модель документа Document

Как и у всех достаточно сложных компонентов библиотеки Swing, у текстовых компонентов есть модель. Именно в модели хранятся данные, отображаемые текстовыми компонентами; нетрудно догадаться, что в качестве данных выступает текст, набранный пользователем или вставленный программно. Модель всех текстовых компонентов описывается не слишком сложным интерфейсом Document из пакета javax.swing.text. Поддержка интерфейса Document встроена в базовый класс всех текстовых компонентов JTextComponent библиотеки Swing, так что во всех текстовых компонентах вы сможете манипулировать данными посредством модели или менять саму модель (модель хранится в свойстве document). Для манипуляции текстом применяются несколько методов интерфейса Document, перечисленных в табл. 16.4.

Таблица 16.4. Методы модели Document, служащие для манипуляции текстом

Метод

Описание

getText(позиция, длина)

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

 

и длиной. Позиция должна быть не меньше нуля и не больше длины тек-

 

ста, иначе возникнет исключение. Длина получаемого фрагмента также не

 

должна выходить за пределы документа

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

501

Таблица 16.4 (продолжение)

Метод

Описание

insertString(позиция,

Вставляет на произвольную позицию документа текст, который к тому же

текст, атрибуты)

может быть снабжен набором некоторых атрибутов (о них чуть позже). По-

 

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

 

метод вызывается при любых дополнениях в текстовом документе, так что

 

его можно использовать для анализа и модификации текста перед тем как

 

он попадет в текстовый компонент

remove(позиция,

Удаляет из документа фрагмент текста, заданный позицией и длиной. По-

длина)

зиция и длина фрагмента должны укладываться в пределы текста

getLength()

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

 

документа

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

Помимо текста и назначенных ему атрибутов модель Document позволяет определять, как текст распределен по элементам. Элемент документа, который может содержать произвольный фрагмент текста, размеченный различными атрибутами, описывает какуюто единую логическую единицу текста, например, абзац, маркированный список или таблицу. Таким образом, элементы документа, представленные интерфейсом Element, позволяют описывать структуру текста. Элементы могут находиться в иерархии. В простых текстовых полях элементами являются строки текста, в сложных редакторах — абзацы, находящиеся в отношениях «родитель-потомок». Методы, предназначенные для получения корневых элементов модели, перечислены в табл. 16.5.

Таблица 16.5. Методы модели Document для получения элементов документа

Метод Описание

getDefaultRootEleВозвращает основной корневой элемент документа. С его помощью можment() но получить элементы, являющеюся потомками данного элемента, и та-

ким образом полностью раскрыть структуру документа. Основной корне-

вой элемент обычно вмещает в себя весь текст документа

getRootElements() Позволяет получить массив корневых элементов документа. Как правило,

корневой элемент в документе один (именно он возвращается предыду-

щим методом), так что массив будет состоять из одного элемента. Впро-

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

данный метод

Для того чтобы вид (текстовый компонент) смог вовремя узнавать об изменениях в тексте или структуре модели документа Document и перерисовать себя для отображения этих изменений, необходим механизм оповещения об изменениях. Данный механизм реализуется событием DocumentEvent и его слушателем DocumentListener, которого вы сможете присоединить к модели Document. Событие DocumentEvent запускается каж-

502

ГЛАВА 16

дый раз при изменении текста документа, именно его следует обрабатывать, если вы заинтересованы в отслеживании каждого изменения текста. В интерфейсе слушателя DocumentListener определены три метода; каждый из них вызывается при конкретном типе события в документе: удалении, обновлении или вставке текста.

Все модели документов, применяемые различными текстовыми компонентами Swing, унаследованы от базового класса AbstractDocument, реализующего механизм обновления текста, безопасный с точки зрения работы нескольких потоков. Достигается это использованием в классе внутренней синхронизации при изменении и чтении текста. Проще говоря, это означает, что вы можете получать текст документа или изменять его из любого потока выполнения, даже если это не поток рассылки событий EventDispatchThread. Тем самым и отличается работа с текстовыми компонентами от работы с остальными компонентами Swing: если для основной массы компонентов Swing действует правило «одного потока» (все действия с компонентом должны выполняться из потока рассылки событий, мы обсудили данное правило в главе 3), то текстовые компоненты (точнее, операции с хранимым в них текстом) освобождены от него. Сделано это не случайно: работа с текстом почти всегда подразумевает, во-первых, загрузку текста из различных источников данных. Загрузка может занимать много времени, особенно если текст велик, а пользователю зачастую хочется сразу видеть, что же он загружает. Благодаря механизму синхронизации класса AbstractDocument загрузку можно проводить из отдельного потока, и пользователь будет видеть текст уже в процессе загрузки. Вовторых, текст довольно часто приходится анализировать непосредственно во время его набора пользователем, например чтобы вывести подсказки для пользователя или провести автоматическое форматирование. Без поддержки синхронизации в текстовых компонентах проводить такой анализ было бы практически невозможно.

Модели для разнообразных компонентов Swing очень часто приходится создавать и программистам-клиентам (с точки зрения таких программистов мы и рассматриваем библиотеку), реализуя соответствующие интерфейсы и описывая данные в нескольких методах этих интерфейсов. Поставляемые со Swing стандартные модели применяются реже, хотя и требуют меньше усилий. Текстовые компоненты и здесь отличаются от своих собратьев. Модель документа Document не так сложна, чтобы ее нельзя было реализовать самому, однако поддержка этой модели, создание подходящей структуры элементов и ее отображение могут потребовать многих нетривиальных действий. Поэтому обработка текста обычно сводится к работе со стандартными моделями и фабриками классов — они достаточно гибкие, чтобы обеспечить программиста нужной информацией о документе и тексте, и поддерживают большую часть необходимых операций.

Текстовое поле с автоматическим заполнением

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

В текстовых полях по умолчанию применяется стандартная упрощенная модель документа под названием PlainDocument. Логично унаследовать от нее и переопределить метод, в который приходит любой новый текст, и в нем, прежде чем действительно добавлять текст в модель, анализировать и дополнять его. Для того чтобы было удобно использовать новую модель документа в других программах, мы снабдим его полезным программным интерфейсом и разместим в библиотечном пакете com.porty.swing. Вот что у нас получится:

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

503

//com/porty/swing/AutoCompleteTextDocument.java

//Модель документа с поддержкой автозаполнения package com.porty.swing;

import javax.swing.text.*; import javax.swing.*; import java.util.*;

public class AutoCompleteTextDocument extends PlainDocument {

//текстовый компонент в котором работает документ private JTextComponent comp;

//список слов для автозаполнения

private List<String> words = new ArrayList<String>(); // конструктор требует текстовый компонент

public AutoCompleteTextDocument(JTextComponent comp) { this.comp = comp;

comp.setDocument(this);

}

//добавляет слово в список public void addWord(String word) {

words.add(word);

}

//свойство, управляющее началом автозаполнения private int beforeCompletion = 3;

public void setBeforeCompletion(int value) { beforeCompletion = value;

}

//вызывается при вставке в документ нового текста

@Override

public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {

//текущая позиция в тексте

int end = offs + str.length();

// определяем позиции текущего слова

final int wordStart = Utilities.getWordStart(comp, offs); // длина текущего слова

int wordLength = end — wordStart;

// проверим, можно ли завершать слово if ( wordLength >= beforeCompletion) {

// получаем текущее слово

504 ГЛАВА 16

String word = getText(wordStart, offs — wordStart) + str; // пытаемся найти его полный вариант в списке

String wholeWord = "";

for (String next : words) { if (next.startsWith(word)) {

// слово найдено wholeWord = next; break;

}

}

// если слово найдено

if ( wholeWord.length() > 0) {

//вырезаем часть для автозаполнения

final String toComplete = wholeWord.substring(wordLength);

//позиции для выделения этой части

final int startPos = offs + str.length(); final int endPos = end + toComplete.length();

//добавляем добавку к тексту str = str + toComplete;

//отложенная задача для выделения добавки

SwingUtilities.invokeLater(new Runnable() { public void run() {

try {

//выделим добавленную часть comp.setSelectionStart(startPos); comp.setSelectionEnd(endPos);

}catch (Exception ex) { ex.printStackTrace();

}

}

});

}

}

// родительский метод добавит текст super.insertString(offs, str, a);

}

}

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