Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

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

.pdf
Скачиваний:
210
Добавлен:
07.10.2020
Размер:
5 Мб
Скачать
☆

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

505

чуть позже, это необходимо нам для выделения текста. Заодно он сразу же присоединяет модель к этому компоненту методом setDocument(). Новые методы позволяют контролировать автоматическое заполнение: метод addWord() добавляет в список слов новое слово; именно список слов words содержит слова, которые могут быть дополнены автоматически, а свойство beforeCompletion (и метод set) определяет, какое количество букв в слове должен набрать пользователь, прежде чем слово будет анализироваться на предмет автоматического дополнения. По умолчанию свойство beforeCompletion равно трем.

Для автоматического заполнения потребуется метод, который вызывается при вставке в документ нового содержимого (это метод insertString()); именно при вставке (наборе) нового текста пользователь может ожидать подсказки от текстового поля. Удаление содержимого, как правило, не должно сопровождаться автоматическим заполнением, это может привести к путанице и излишне усложнить работу с полем. Замена же текста сводится к поэтапному удалению фрагмента текста и вставке нового текста на место старого. Она вполне подходит нашему алгоритму.

Первым делом в методе insertString() мы получаем необходимую для дальнейшей работы текущую позицию курсора в документе end (получить ее можно, суммируя смещение вставки в документе, переданное в метод как параметр offs, и длину нового текста для вставки). Далее нам необходимо определить, какое слово находится в данный момент на текущей позиции курсора. Здесь нам неоценимую услугу окажут статические методы класса Utilities8. Метод getWordStart() позволяет получить позицию начала слова. В качестве параметров им требуется передать текстовый компонент, в котором находится анализируемый текст, и позицию, с которой надо начинать поиск слова. В нашем случае позиция известна: это позиция начала вставки, переданная нам параметром. После получения позиции текущего слова мы проверяем, можно ли начинать его дополнение, то есть достаточна ли длина этого слова (она должна быть не меньше свойства beforeCompletion). Если слово недостаточно длинное, текст вставляется без изменений.

В случае если слово имеют достаточную длину, мы начинаем его поиск в списке слов, которые можно дополнять. Для этого мы перемещаемся по списку со словами words, проверяя, не начинается ли некое слово с набранных пользователем символов. Если такое слово обнаруживается в списке, оно сохраняется в переменной wholeWord, после чего поиск заканчивается. Если слово не обнаруживается, работа заканчивается. После обнаружения подходящего слова остается получить ту его часть, которую необходимо предложить пользователю в качестве подсказки, вставить эту часть в документ, а затем выделить ее.

В нашем примере вставка текста в документ очень проста, поскольку мы и являемся документов — мы просто добавляем часть для дополнения к новому тексту и передаем его родительскому методу, который и будет хранить его. Выделение добавленной части осуществляется из потока рассылки событий EventDispatchThread, с помощью метода invokeLater() класса EventQueue. Выделение позволит пользователю быстро отказаться от нашей подсказки, если она ему не подойдет, и продолжить печатать свой вариант как ни в чем не бывало. Текст необходимо выделять из потока рассылки событий, потому что, во-первых, мы будем проводить манипуляции с графическим компонентом Swing, а вовторых, нам нужно «подождать», пока новый текст окажется в поле. Это сделает наш предок, когда мы вызовем его родительский метод и выполнение метода insertString() закончится, а затем уже, когда дойдет очередь в потоке событий, вызовется наш метод run(), который выделит новый текст.

8 Это класс с набором статических методов, он находится в пакете javax.swing.text, и содержит несколько весьма полезных алгоритмов для работы с текстовыми компонентами и их моделями. Если вы собираетесь вплотную заниматься текстовыми возможностями Swing, обратите на данный класс свое внимание. Методы для нахождения слов в тексте к тому же полностью готовы к работе со всеми возможными языками, а не основаны на примитивных алгоритмах.

506

ГЛАВА 16

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

//TestAutoCompletion.java

//Проверка работы текстового поля с

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

import com.porty.swing.*; import javax.swing.*; import java.awt.*;

public class TestAutoCompletion extends JFrame { public TestAutoCompletion() {

super("TestAutoCompletion"); setDefaultCloseOperation(EXIT_ON_CLOSE);

//создаем и настраиваем поле

JTextField field = new JTextField();

field.setColumns(15);

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

AutoCompleteTextDocument doc =

new AutoCompleteTextDocument(field); doc.addWord("прекрасный"); doc.addWord("великолепный");

//добавляем поле в окно

setLayout(new FlowLayout()); add(field);

// выводим окно на экран setSize(300, 200); setVisible(true);

}

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

new Runnable() {

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

}

}

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

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

507

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

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

Рассмотренный нами в этом параграфе подход, когда мы берем стандартную модель документапростыхтекстовыхполейPlainDocument ипереопределяемееметодinsertString(), очень полезен, и его определенно стоит взять на заметку. Если с простыми случаями ввода специальных данных может справиться поле с форматированием JFormattedTextField, то в более сложных ситуациях очень ценится возможность полностью контролировать процесс ввода данных и незамедлительно их менять , что мы и увидели. Еще одним вариантом является применение слушателя документа DocumentListener, но в этом случае модифицировать текст гораздо труднее, поскольку слушателю достается информация уже после того, как изменения произошли, и любые изменения текста в нем опять же приводят к вызову методов слушателя, а это не совсем элегантно.

Отмена и повтор операций

К модели документа Document можно присоединить не только слушателя DocumentListener. Есть событие и соответствующий ему слушатель еще одного типа — это событие UndoableEditEvent и слушатель UndoableEditListener. Данное событие модель документа обеспечивает путем поддержки операций повтора и отмены (undo/redo). Эти операции можно встретить во всех современных текстовых компонентах (и не только в текстовых), и они весьма удобны: пользователь может не опасаться совершить какуюлибо ошибку, к примеру, случайно удалить набранный текст. К его услугам всегда имеется возможность отмены операций, позволяющая привести документ к прежнему виду. Повтор операции аналогичным образом дает пользователю возможность снова выполнять ту операцию, от которой он по ошибке отказался. Все текстовые компоненты в ваших программах должны поддерживать по меньшей мере отмену операций, иначе ваш интерфейс рискует стать очень неудобным.

Если уже знакомое нам событие DocumentEvent несет в себе подробную информацию об изменениях в структуре и содержимом документа, то у события UndoableEditEvent совершенно иная цель. Оно должно сообщать, что пользователь совершил в документе какую-то операцию, которую можно отменить или повторить. Такая операция описана в классе UndoableEdit, объект именно этого класса возвращает метод getEdit() события UndoableEditEvent. В данном объекте описано немного: имя операции, если оно есть (у некоторых операций могут быть различные имена для отмены и повтора), возможность отмены или повторения операции, и два основных метода, которые и занимаются отменой и повторением. Больше никакой информации объект UndoableEdit в себе не таит, да

508

ГЛАВА 16

это ему и не нужно: единственная его цель состоит в описании операции, совершенной в документе. Подробности сообщает событие DocumentEvent.

Теперь вы можете наделять свои текстовые компоненты возможностью отмены и повторения операций: достаточно зарегистрировать в модели документа слушателя UndoableEditListener, получить уведомления о произведенных пользователем операциях, хранить историю операций (например, в виде списка объектов UndoableEdit) и при запросе пользователя вызывать для последних метод undo() или redo(). Правда, есть еще один путь: для удобства программистов библиотека Swing предоставляет небольшой пакет javax.swing.undo. Ядром данного пакета является класс UndoManager, вы можете регистрировать его в любом текстовом компоненте как слушателя UndoableEditListener. Этот класс позаботится о хранении истории операций и предоставит удобный программный интерфейс, позволяющий быстро наладить отмену и повторение операций в вашем приложении.

Рассмотрим пример, в котором мы обеспечим многострочное текстовое поле JTextArea специальной панелью инструментов, позволяющей отменять и повторять произведенные в поле операции. Для удобства работы хранить операции и работать с ними мы будем посредством услуг класса UndoManager. Вот что получается:

//UndoAndRedo.java

//Поддержка отмены и повтора операций

//в текстовых компонентах Swing import javax.swing.*;

import javax.swing.undo.*; import java.awt.event.*; import java.awt.*;

public class UndoAndRedo extends JFrame { // поддержка отмены/повтора операций

private UndoManager undoManager = new UndoManager(); public UndoAndRedo() {

super("UndoAndRedo"); setDefaultCloseOperation(EXIT_ON_CLOSE); // панель инструментов

JToolBar toolBar = new JToolBar(); toolBar.add(new UndoAction()); toolBar.add(new RedoAction());

// текстовое поле

JTextArea textArea = new JTextArea();

//добавляем слушателя операций textArea.getDocument().

addUndoableEditListener(undoManager);

//добавляем компоненты в окно add(toolBar, "North");

add(new JScrollPane(textArea));

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

setSize(400, 300);

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

509

setVisible(true);

}

// команда — отмена операции

class UndoAction extends AbstractAction { public UndoAction() {

// настройка команды putValue(AbstractAction.SMALL_ICON,

new ImageIcon("undo16.gif"));

}

public void actionPerformed(ActionEvent e) { if ( undoManager.canUndo() )

undoManager.undo();

}

}

// команда — повтор операции

class RedoAction extends AbstractAction { public RedoAction() {

// настройка команды putValue(AbstractAction.SMALL_ICON,

new ImageIcon("redo16.gif"));

}

public void actionPerformed(ActionEvent e) { if ( undoManager.canRedo() )

undoManager.redo();

}

}

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

new Runnable() {

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

}

}

В центре нашего окна размещается многострочное текстовое поле JTextArea, предварительно «обернутое» в панель прокрутки JScrollPane. На севере окна мы размещаем панель инструментов JToolBar; она состоит из двух кнопок, отвечающих за отмену и повторение операций. Кнопки создаются на основе команд Action, описанных в виде внутренних классов. Настройка команд выполняется в конструкторе: мы меняем свойство команды SMALL_ICON, задавая для кнопок значок9. Само же действие, выполняемое кнопкой, происходит в методе actionPerformed().

Для хранения происходящих в текстовом поле операций и их последующей отмены или повторения мы воспользуемся услугами класса UndoManager. Для того чтобы он

9 Значки для кнопок из этого примера были взяты из коллекции Java Look and Feel Graphics Repository. Вы можете найти ее на сайте java.sun.com.

510

ГЛАВА 16

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

Команды отмены и повторения операций очень просты, во многом благодаря услугам объекта UndoManager. Они проверяют, возможны ли сейчас отмена или повторение операций, и если да, выполняют отмену (методом undo()) или повторение (redo()). Управлять списком операций UndoableEdit будет класс UndoManager. Запустив программу с примером и работая с текстом, вы увидите, каким образом кнопки панели инструментов позволяют отменять или повторять операции.

Класс UndoManager обладает несколькими дополнительными интересными возможностями и зачастую его хватает для достаточно гибкого управления операциями, происходящими в ваших текстовых компонентах. Если же возникнет необходимость в особом поведении, вы всегда сможете самостоятельно реализовать интерфейс слушателя UndoableEditListener, присоединить его к модели документа и распоряжаться информацией об операциях так, как вам потребуется. Увы, но по умолчанию никакой отмены или повторения операций в текстовых компонентах Swing не поддерживается, так что вам всегда придется добавлять ее самостоятельно — в противном случае работать с вашими текстовыми компонентами будет не слишком удобно. Для поддержки отмены и повтора операций через клавиатуру чрезвычайно хорошо подходят карты клавиатурных сокращений InputMap, которые вы можете настроить на вызов тех же самых команд, что настроены на панели инструментов, в случае нажатия подходящего сокращения (обычно

Control-Z).

Управление курсором — интерфейс Caret

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

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

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

вбазовом классе JTextComponent имеется несколько удобных методов, но изначально вы-

деление текста находится в ведении текстового курсора.

Два основных свойства курсора — его позиция (вы сможете управлять ею посредством свойства dot) и начало отсчета (узнать его позволяет метод getMark()). Позиция курсора показывает, где будут появляться набираемые пользователем или вставляемые вами программно символы. Начало отсчета служит для поддержки выделения текста или позиции курсора, когда выделенного текста нет. Если же выделение текста происходит, начало отсчета указывает на начало выделенного фрагмента, а позиция указывает на конец этого фрагмента (в конце выделенного фрагмента находится курсор). В зависимости от направления выделения (вперед по тексту или назад) начало отсчета может быть больше позиции или меньше ее.

Курсор, описанный интерфейсом Caret, поддерживает событие ChangeEvent, оно запускается каждый раз при изменении позиции курсора. Присоединив к курсору слушателя ChangeListener, вы сможете получать подробную информацию обо всех

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

511

перемещениях курсора в вашем текстовом компоненте. Кроме того, базовый класс JTextComponent поддерживает событие CaretEvent. Оно также запускается при перемещениях курсора, но если событие ChangeEvent содержит только источник события (сам курсор), то событие CaretEvent позволяет сразу же узнать текущую позицию курсора и начало отсчета.

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

особытиях:

//ControllingCaret.java

//Управление текстовым курсором import javax.swing.*;

import javax.swing.event.CaretListener; import javax.swing.event.CaretEvent; import javax.swing.text.*;

import java.awt.*;

public class ControllingCaret extends JFrame { public ControllingCaret() {

super("ControllingCaret"); setDefaultCloseOperation(EXIT_ON_CLOSE); // многострочное текстовое поле

final JTextArea textArea = new JTextArea(); // добавим текст

textArea.append("Просто какой-то текст");

//настройка курсора и выделение текста

Caret caret = textArea.getCaret(); caret.setBlinkRate(50); caret.setDot(5); caret.moveDot(10);

//добавим текстовое поле в окно add(new JScrollPane(textArea));

//надпись для слова у курсора

final JLabel label = new JLabel(); add(label, "South");

// слушатель перемещений курсора textArea.addCaretListener(new CaretListener() {

public void caretUpdate(CaretEvent e) { // выведем слово у курсора

try {

// начало и конец слова на позиции int wordStart =

Utilities.getWordStart(textArea, e.getDot()); int wordEnd =

512

ГЛАВА 16

Utilities.getWordEnd(textArea, e.getDot()); label.setText(textArea.getText(

wordStart, wordEnd — wordStart));

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

}

}

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

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

}

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

new Runnable() {

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

}

}

Мы создаем небольшое окно, в центре его разместится многострочное текстовое поле JTextArea, «обернутое» в панель прокрутки. Для того чтобы можно было работать с курсором, необходим какой-либо текст, так что мы добавляем в поле строку текста, вызывая метод append(). Далее мы получаем используемый в текстовом поле курсор (для этого служит метод getCaret()) и настраиваем для него некоторые свойства.

Прежде всего меняется частота мерцания курсора, задаваемая свойством blinkRate. Это промежуток между мерцаниями в миллисекундах. В нашем случае курсор будет мерцать довольно быстро: мы задаем промежуток равным всего 50 миллисекундам. Мерцание можно и отключить: достаточно задать нулевой промежуток. Далее мы устанавливаем позицию курсора равной 5 и выделяем фрагмент текста. Текст удобно выделять с помощью метода moveDot(): он перемещает позицию в то место, что вы ему указываете, а начало отсчета делает равным прежнему значению позиции. В примере мы выделяем фрагмент текста в пять символов с пятой позиции по десятую.

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

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

513

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

Дополнительное выделение текста

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

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

Именно рисование фона применяется для прорисовки выделенного текста, однако это решение доступно и нам, и не ограничивается выделением. В архитектуру текстовых компонентов встроен рисующий объект, описанный интерфейсом Highlighter, который прорисовывает фон текста, перед тем как будет прорисован сам текст. Вы можете реализовать его самостоятельно и задать для любого текстового компонента, используя свойство с одноименным названием. Однако, как это бывает в Swing, почти всегда есть возможность встать на плечи гигантов и воспользоваться стандартной реализацией. Дело в том, что интерфейс Highlighter предусматривает добавление новых регионов для прорисовки фона и их последующее удаление. Прорисовкой же самого фона для заданного региона текста занимается отдельный объект HighlightPainter. Таким образом, вы можете взять стандартный объект, рисующий фон, из любого текстового компонента и добавить к нему дополнительный регион текста, а прорисовку реализовать в интерфейсе HighlightPainter, или же и здесь использовать стандартную реализацию DefaultHighlightPainter, которая рисует фон в одном цвете.

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

//TextHighlights.java

//Дополнительное выделение текста import javax.swing.*;

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

public class TextHighlights extends JFrame { public TextHighlights() {

super("TextHighlights"); setDefaultCloseOperation(EXIT_ON_CLOSE); // создаем и настраиваем поле

JTextArea area =

new JTextArea("Привет мир!");

514 ГЛАВА 16

try {

// добавим выделенный фрагмент желтого цвета

Object reference = area.getHighlighter().

addHighlight(0, 6,

new DefaultHighlighter. DefaultHighlightPainter(Color.YELLOW));

} catch (BadLocationException e) { e.printStackTrace();

}

//добавляем поле в окно add(new JScrollPane(area));

//выводим окно на экран setSize(300, 200); setVisible(true);

}

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

new Runnable() {

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

}

}

В примере мы создаем многострочное текстовое поле и размещаем его в центре нашего окна. Далее мы просто получаем используемый в нем по умолчанию рисующий объект фона, и методом addHighlight() добавляем новую выделенную область. Обратите внимание на параметры метода — это начало и конец фрагмента текста, фон которого вы собираетесь рисовать, а также рисующий объект. Мы использовали стандартный объект, попросив его рисовать фон желтым цветом. Метод же возвращает ссылку на добавленный фрагмент прорисовки фона. В дальнейшем эту ссылку можно использовать, чтобы поменять фрагмент или совсем удалить его методами changeHighlight() и removeHighlight(). Запустив программу, вы увидите, как работает прорисовка фона текста.

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