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

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

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

Круговорот данных

575

их правильно поняли и в других приложениях, будь они системными или другими Javaприложениями.

Задачей адаптации данных Java-приложения для обмена ими с системой занимается интерфейс Transferable из хорошо известного нам пакета java.awt.datatransfer. Именно реализацию этого интерфейса необходимо передать в метод setContents() класса Clipboard, чтобы разместить свои данные в системном буфере. По сути, интерфейс просто связывает данные Java-приложения, представленные некоторым объектом, с описанием типа данных DataFlavor, так что реализовать его самостоятельно совсем несложно1.

Таблица 18.2. Методы интерфейса Transferable

Метод

Предназначение

getTransferDataFlavors()

Возвращает массив всех типов, поддерживаемых данными, которые

 

описываются интерфейсом. Подразумевается, что самым первым эле-

 

ментов массива идет самый удобный тип, последним — соответствен-

 

но самый мало подходящий тип данных

isDataFlavorSupported()

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

 

честве параметра тип

getTransferData()

Основной метод, возвращает объект, который будет представлять дан-

 

ные для указанного типа. К примеру, для типа-изображения необходимо

 

будет вернуть изображение Image

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

Как всегда, не обязательно для каждого случая с нуля писать свою реализацию интерфейса Transferable, есть и реализация по умолчанию, которая называется StringSelection. В конструктор этого класса можно передать строку String, и затем передать полученный объект в буфер обмена. Это прекрасный и быстрый способ вставить в буфер обмера строку с текстом, однако вы редко будете использовать этот класс напрямую, так как все текстовые компоненты Swing и без того обладают встроенной поддержкой операций копирования и вставки текста. Гораздо чаще встречается ситуация когда вам нужно хранить в буфере обмена не только текстовое представление данных, но и другое представление, к примеру, Java-объект или изображение. В этом случае реализовать интерфейс Transferable придется самостоятельно. Сделать это несложно, попробуем вставить

вбуфер обмена данные нескольких типов:

//PastingClipboard.java

//Вставка данных в системный буфер обмена import javax.swing.*;

import java.awt.*;

import java.awt.image.BufferedImage; import java.awt.datatransfer.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener;

1 Мы недаром используем в этом разделе термин «адаптация», ведь интерфейс Transferable по сути и является представителем известного шаблона проектирования «адаптер»

576

ГЛАВА 18

import java.io.IOException; import java.util.Arrays;

public class PastingClipboard extends JFrame { private JTextArea textArea;

public PastingClipboard() { super("PastingClipboard");

//выход при закрытии окна setDefaultCloseOperation(EXIT_ON_CLOSE);

//добавим кнопку для вставки данных

JButton copyButton = new JButton("Копировать"); // текстовое поле для получения текста textArea = new JTextArea();

copyButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) {

// буфер обмена

Clipboard clipboard = Toolkit.getDefaultToolkit(). getSystemClipboard();

// вставим в буфер наши адаптированные данные clipboard.setContents(new ComplexTransferable(), null);

}

});

add(new JScrollPane(textArea)); add(copyButton, "South");

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

}

// объект-адаптер для наших данных

class ComplexTransferable implements Transferable { // список поддерживаемых типов данных

private DataFlavor[] supportedTypes = new DataFlavor[]

{ DataFlavor.stringFlavor, DataFlavor.imageFlavor }; public DataFlavor[] getTransferDataFlavors() {

return supportedTypes;

}

public boolean isDataFlavorSupported(DataFlavor flavor) { return Arrays.asList(supportedTypes).contains(flavor);

}

public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {

Круговорот данных

577

if ( flavor == DataFlavor.imageFlavor ) {

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

BufferedImage bi = new

BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB); Graphics g = bi.getGraphics();

g.setColor(Color.RED); g.fillRect(0, 0, 100, 100); return bi;

}else if ( flavor == DataFlavor.stringFlavor ) {

//если запрошена строка, вернем выделенный текст return textArea.getSelectedText();

}

// исключение в случае неизвестного типа данных throw new UnsupportedFlavorException(flavor);

}

}

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

new Runnable() {

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

}

}

В примере мы создадим окно JFrame и разместим в его центре многострочное текстовое поле JTextArea, текст, который в нем набирается, потом можно будет скопировать в буфер обмена. На юге окна разместится кнопка, по нажатию которой в буфере обмена будут размещаться наши данные. При нажатии кнопки мы получаем системный буфер обмена уже знакомым нам методом класса Toolkit, и вызываем его метод setContents(), который размещает переданный ему объект Transferable в буфере. Вторым параметром этого метода можно передать слушатель событий, который будет оповещаться в случае утраты «владения» буфером обмена, то есть если другое приложение разместит в нем свои данные и перезапишет ваши. Эта возможность, хоть и может быть полезной, редко востребуется, и вместо слушателя мы передаем в метод пустую ссылку null.

Самое интересное скрыто в классе ComplexTransferable, который адаптирует несколько видов данных для размещения в системном буфере. Класс реализует интерфейс Transferable и все три его метода, которые мы чуть ранее обсудили. Их очень несложно реализовать . Мы поддерживаем два типа данных — строки и изображения, оба эти типа стандартны и описаны как константы класса DataFlavor. В методе getTransferData() нам остается вернуть строку или изображение, в зависимости от переданного типа данных. Строку мы берем из текстового поля, это выделенный в данный момент текст, а вот изображение строим в памяти динамически с помощью класса BufferedImage. Для этого требуется задать размер и тип изображения в конструкторе (тип изображения у нас стандартный RGB), а затем можно получить для изображения объект для рисования Graphics и нарисовать на нем все что угодно. Мы для простоты просто закрашиваем все красным цветом. После этого изображение передается для хранения в буфере.

578

ГЛАВА 18

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

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

Как правило, работа со сложными данными и буфером обмена в большом приложении сводится примерно к такому решению, что мы описали в примере. Для обмена

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

Буфер обмена и стандартные компоненты Swing

Несмотря на то, что мы разобрались с буфером обмена и подготовкой для него данных и можем все сделать своими руками, многое для нас уже сделано по умолчанию в стандартных компонентах Swing. Прежде всего, это относится ко всем текстовым компонентам, унаследованным от базового класса JTextComponent. В нем определены методы cut(), copy() и paste(), которые простым вызовом позволяют манипулировать текстом в буфере обмена, а также зарегистрированы стандартные клавиатурные сокращения (Control-C и так далее), которые предоставят пользователям привычные операции с помощью клавиатуры. К сожалению, контекстное меню для текстовых компонентов по умолчанию не предусмотрено, и его придется создавать самостоятельно, впрочем, сделать это просто — слушатели пунктов меню будут просто вызывать упомянутые чуть выше методы.

Дело не ограничивается текстовыми компонентами, компоненты посложнее, вроде списков JList, деревьев JTree или таблиц JTable также обладают некоторым уровнем поддержки буфера обмена. В них тоже зарегистрированы стандартные клавиатурные сокращения, нажав которые, пользователь может получить текстовое представление выбранных в данный момент данных (для таблицы это будет даже текстовое представление всех столбцов выбранной строки). Хорошо, что по умолчанию есть и это, но как правило, это всего лишь приятная мелочь. Если сложные компоненты призваны обмениваться данными друг с другом или другими приложениями, обычно приходится писать собственную реализацию этого процесса, но, как мы поняли из изучения обмена данных, сделать это несложно.

Операции перетаскивания

Операции визуального перетаскивания и вставки данных (drag and drop, DnD) выглядят для пользователей чрезвычайно заманчиво, потому что они очень естественны — даже пользуясь мышью, вы, по сути, перетаскиваете ее с места на место. Увидеть такой же процесс на экране приятно и очень понятно — вы берете нечто важное для вас, та-

Круговорот данных

579

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

Реализовать перетаскивание не совсем просто. Мало следить за всевозможными комбинациями движения мыши и клавиатуры, надо еще узнавать, готов ли компонент под курсором мыши принять данные, и понять, в каком месте компонента они окажутся. Поэтому, как правило, операционные системы предоставляют способы управления перетаскиванием более высокого уровня. В AWT перетаскиванию данных посвящен немаленький пакет java.awt.dnd, и пользоваться им возможно, но требуются усилия. К счастью, библиотека Swing предоставляет инструменты более высокого уровня, и чаще всего перетаскивание встроить в приложение довольно просто.

Важнейшую роль играет объект TransferHandler, присоединенный к любому компоненту Swing, унаследованному от класса JComponent. Объект этот хранится в свойстве transferHandler, и вы можете получить его или задать новый, собственный. Задачей этого объекта является предоставление данных в готовом виде из компонента для обмена данными (то есть в виде объекта Transferable, который нам уже знаком по разделу о буфере обмена) и обработка различных ситуаций при перетаскивании и вставке данных в компонент, который, как правило, пользуется моделью и не знает, как добавить в нее новые данные. Все эти спорные моменты улаживает объект TransferHandler, либо стандартными путями, либо с вашей помощью, когда вы переопределяете его методы.

Перетаскивание и вставка данных встроена не во все компоненты, а лишь в те, в которых это, по мнению создателей Swing, имеет смысл. Прежде всего, это текстовые компоненты, унаследованные от класса JTextComponent, там и вставка текста перетаскиванием не только поддерживается, но и включена по умолчанию, в чем вы легко можете убедиться, запустив пример с текстовыми компонентами Swing и перетаскивая в них текст. Не включено по умолчанию перетаскивание данных в списках JList, деревьях JTree и таблицах JTable, но оно там есть и настраивается очень легко. Достаточно установить свойство этих классов dragEnabled в true, как сразу же вы сможете перетаскивать данные с этих компонентов.

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

Таблица 18.3. Методы класса TransferHandler, необходимые для описания данных и их перетаскивания

Метод Предназначение

createTransferable() В этом методе необходимо создать и вернуть прекрасно нам знакомый объект-адаптер Transferable, который описывает все поддерживаемые типы данных и возвращает их по запросу извне. К примеру, для списка имеет

смысл вернуть строку или объект, который выделен в списке в данный мо-

мент. Этот метод вызывается когда пользователь начинает «тащить» чтото с вашего компонента

580

ГЛАВА 18

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

Метод

Предназначение

 

 

getSourceActions ()

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

 

перетаскивании. По умолчанию никаких действий не поддерживается. Дей-

 

ствия определены как константы в класса TransferHandler, чаще всего это

 

COPY (копирование перетаскиванием), MOVE (перемещение данных), или

 

оба этих действия. Если метод не переопределить, включение перетаски-

 

вания ни к чему не приведет

canImport ()

Если компонент сможет принимать данные, этот метод необходимо пе-

 

реопределить и возвращать true, когда данные подходящие. Этот метод

 

вызывается когда пользователь пытается что-то «бросить» на компонент.

 

Как правило, вы анализируете тип данных и решаете, подходит ли он вам.

 

Доступна также текущая позиция курсора.

importData()

Если предыдущий метод «одобрил» данные, они передаются в этот ме-

 

тод, если пользователь решил вставить их в ваш компонент. Здесь данные

 

необходимо каким-то образом обработать, вставить в модель или куда-то

 

еще, и желательно показать пользователю на экране, что данные добавле-

 

ны. Доступна также текущая позиция курсора, так что данные можно встав-

 

лять очень точно.

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

//ListDrag.java

//Перетаскивание и вставка данных и TransferHandler import javax.swing.*;

import java.awt.*;

import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable;

public class ListDrag extends JFrame { // начальные данные для списков private String[] listData =

new String[] { "Раз", "Два", "Три" }; public ListDrag() {

super("ListDrag");

//выход при закрытии окна setDefaultCloseOperation(EXIT_ON_CLOSE);

//создаем модели

DefaultListModel model1 = new DefaultListModel(), model2 = new DefaultListModel();

for (String element: listData) {

Круговорот данных

581

model1.addElement(element);

model2.addElement(element);

}

// создаем списки

JList list1 = new JList(model1); list1.setTransferHandler(new ListTransferHandler(list1)); list1.setDragEnabled(true);

JList list2 = new JList(model2); list2.setTransferHandler(new ListTransferHandler(list2)); list2.setDragEnabled(true);

//добавим списки на экран setLayout(new GridLayout(1, 2)); add(new JScrollPane(list1)); add(new JScrollPane(list2));

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

}

// объект-адаптер для списков и их данных

class ListTransferHandler extends TransferHandler { private JList list;

public ListTransferHandler(JList list) { this.list = list;

}

@Override

public int getSourceActions(JComponent c) {

// данные могут копироваться или перемещаться return TransferHandler.COPY_OR_MOVE;

}

@Override

public boolean canImport(TransferSupport support) { // поддерживается импорт только строковых данных return support.isDataFlavorSupported(

DataFlavor.stringFlavor);

}

@Override

protected Transferable createTransferable(JComponent c) { // стандартный способ адаптировать строку для обмена return new StringSelection(

list.getSelectedValue().toString());

}

582

ГЛАВА 18

 

@Override

 

public boolean importData(TransferSupport support) {

 

if ( support.isDataFlavorSupported(DataFlavor.stringFlavor) ) {

 

try {

 

// добавляем в модель новую строку на выбранную позицию

 

((DefaultListModel)list.getModel()).

 

add(list.locationToIndex(

 

support.getDropLocation().getDropPoint()),

 

// получение строки из Transferable

 

support.getTransferable().

getTransferData(DataFlavor.stringFlavor));

return true;

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

}

}

return false;

}

}

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

new Runnable() {

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

}

}

Итак, приступим. В примере создаются два списка JList, которые будут добавлены

вокно, разделенное на две одинаковые части с помощью табличного расположения. Для того чтобы мы смогли что-то добавлять в списки, нам необходима модель, которая допускает добавление элементов, здесь как нельзя лучше пригодится стандартная модель списков DefaultListModel. Мы создаем по модели для каждого списка и добавляем

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

вконструкторы списков.

Создание списков и моделей очень просто и нам прекрасно известно, теперь нужно заставить их уметь отдавать и принимать данные при перетаскивании. Само перетаскивание включается для списков свойством dragEnabled. Этого вполне достаточно. Далее вся работа переносится на объект TransferHandler, который будет отдавать данные при перетаскивании и принимать их при вставке2. В примере мы написали универсальный объект ListTransferHandler, годный для любого списка, работающего со стандартной моделью. Он требует список, данные которого требуется «адаптировать», в качестве параметра своего конструктора. Вспоминая таблицу 18.3 с описанием основных методов, требуемых для обмена данными, реализация нашего класса становится довольно очевидной. Сначала в методе getSourceActions() необходимо указать, какие действия поддерживает

2 Как нетрудно видеть, объект TransferHandler представляет собой пример классического ша- блона «стратегия», когда сторонний объект определяет, как следует обмениваться данными и их описывать, и объект этот легко поменять.

Круговорот данных

583

компонент при перетаскивании. У нас это копирование и перемещение данных, хотя, строго говоря, мы лишь копируем данные из списка, а перемещение добавлено только для удобства (копирование требует нажатия клавиши Control при перетаскивании).

Метод createTransferable() создает данные для обмена в виде объекта Transferable. Нам этот процесс уже хорошо знаком, так как именно в таком виде мы вставляли данные в буфер обмена. Так как в списках у нас данные в виде строк, мы вполне можем ограничиться стандартным адаптером для строк StringSelection, ну а как реализовать более сложные случаи, мы уже знаем.

Самое сложное — это вставка данных в список. Прежде всего мы указываем в методе canImport(), что будем поддерживать лишь строковые данные. Вся необходимая информация о данных, которые пользователь пытается «бросить» на компонент, в том числе и их тип DataFlavor, доступна через параметр метода TransferSupport. Ну а метод importData() уже непосредственно отвечает за вставку данных в компонент. На всякий случай мы проверяем, представляют ли собой данные строку, получаем модель списка, и, подразумевая, что это стандартная модель списка DefaultListModel, добавляем в него новый элемент. Посмотрите, как вычисляется позиция для вставки — метод getDropPoint() позволяет узнать точку, в которой пользователь «бросил» данные, а метод списка locationToIndex() преобразует точку в своих координатах в позицию списка.

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

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

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

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

Аналогичным образом работает перетаскивание для деревьев JTree и таблиц JTable — достаточно включить свойство dragEnabled, и описать, как будет организован круговорот данных из компонента и в него. У этих трех компонентов (включая списки), есть и еще одно свойство, которое позволяет указать, каким образом будет прорисовываться место «бросания» данных. По умолчанию в нашем примере используется режим выделения данных особым цветом (элементы списка под курсором выделяются), что не всегда верно, в этом случае скорее стоило показать, что вставка данных будет происходить перед указанным элементом или после него. Изменить графическое представление вставки позволяет свойство dropMode, в которое надо передать одно из значений классаперечисления DropMode. По умолчанию оно равно USE_SELECTION, а в случае с нашим списком скорее стоило использовать INSERT, что лучше отображает вставку на показанную позицию. Остальные возможности класса DropMode вы можете узнать в интерактивной документации JDK.

584

ГЛАВА 18

А как же остальные компоненты Swing? Можно ли перетаскивать данные с панелей или кнопок? Можно, однако в этом случае придется самостоятельно определять момент начала перетаскивания. Все так же необходимо задать для компонента объект преобразования данных TransferHandler, а затем, в подходящий момент, вызвать его метод exportAsDrag(), который начнет системный процесс перетаскивания данных. Как правило, вы просто присоединяете к компоненту слушатель событий от мыши, следите за перетаскиванием мыши на нем, и при событии перетаскивания вызываете данный метод. Здесь как нельзя лучше пригодится дополнительный конструктор класса TransferHandler, который принимает название свойства компонента для обмена. Это свойство должно иметь методы get и set (если подразумевается вставка данных). К сожалению, такой объект TransferHandler не в состоянии обмениваться данными с остальной операционной системой, даже если свойство представляет собой строку, потому что данные представлены как внутренний объект JVM, однако для простых визуальных операций это может пригодиться. Рассмотрим на простом примере:

//LabelManualDrag.java

//Перетаскивание и вставка данных "вручную" import javax.swing.*;

import java.awt.event.MouseMotionAdapter; import java.awt.event.MouseEvent;

public class LabelManualDrag extends JFrame { public LabelManualDrag() {

super("ListDrag");

//выход при закрытии окна setDefaultCloseOperation(EXIT_ON_CLOSE);

final JLabel label = new JLabel("Просто текст!");

//стандартный способ обмена свойством "text" label.setTransferHandler(new TransferHandler("text"));

//момент начала перетаскивания label.addMouseMotionListener(new MouseMotionAdapter() {

public void mouseDragged(MouseEvent e) { TransferHandler handler = label.getTransferHandler();

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

handler.exportAsDrag(label, e, TransferHandler.COPY);

}

});

//добавим текстовое поле и надпись на экран add(new JScrollPane(new JTextArea())); add(label, "South");

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

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

}