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

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

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

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

585

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

new Runnable() {

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

}

}

В примере мы создаем надпись, объект TransferHandler которой будет передавать при перетаскивании значение свойства text, то есть собственно текст надписи. В центр окна мы добавляем текстовую область JTextArea. У нее тоже есть свойство text, так что мы сможем перетаскивать значение свойства с надписи на текстовое поле. Главное же — определить момент перетаскивания. Мы присоединяем к надписи слушатель движений мыши и при любом перетаскивании мыши над надписью указываем объекту TransferHandler начать системную процедуру передачи данных. Запустив пример, вы с легкостью перетащите текст надписи в текстовое поле, однако не сможете «утащить» его за пределы своего приложения. Для сложных деловых данных больших приложений такое поведение вряд ли подходит, необходима интеграция со всеми приложениями системы, однако для приложения с визуальными редакторами, например с перетаскиванием текста на компоненты, представляющие собой элементы некой схемы, может быть очень кстати.

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

TransferHandler и буфер обмена

Нетрудно видеть, что при перетаскивании и приеме данных мы фактически делаем все то же самое, что и при работе с буфером обмена: адаптируем данные для обмена и отдаем их «наружу» или же принимаем данные и думаем, куда их «пристроить». Так и просится встроить в класс TransferHandler поддержку буфера обмена, и что неудивительно, эта поддержка там есть (в Swing наши желания, как правило, сбываются). Если в случае с перетаскиванием пользователь визуально «тащит» или «бросает» данные с компонента, то в случае с буфером обмена он обычно использует какую-то кнопку или пункт меню для копирования в буфер или вставки его содержимого.

Класс TransferHandler предоставляет нам все три действия (CCP, cut-copy-paste) в виде команд Action, и мы можем их присоединить к своим компонентами или меню. Когда вызывается команда копирования (getCopyAction()), она вызывает метод createTransferable() и полученные данные вставляет в буфер обмена, команда же вставки из буфера (getPasteAction()) вызывает знакомый нам метод importData(). Таким образом, благодаря

586

ГЛАВА 18

объекту TransferHandler разница между перетаскиванием данных и получением их из буфера обмена практически неотличима. Есть лишь одно «но»: для всех команд из объекта необходимо правильно задавать источник события ActionEvent. Это должен быть компонент, данные которого нужно импортировать или экспортировать.

Вспоминая наш достаточно универсальный объект TransferHandler для списков JList на основе стандартной модели со строковыми элементами, мы можем быстро дополнить список операциями копирования и вставки в буфер обмена, благо адаптация данных уже готова. Давайте попробуем:

//TransferHandlerClipboard.java

//Копирование в буфер с помощью TransferHandler import javax.swing.*;

import java.awt.event.ActionEvent; import java.awt.event.ActionListener;

public class TransferHandlerClipboard extends JFrame { public TransferHandlerClipboard() {

super("TransferHandlerClipboard");

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

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

DefaultListModel model = new DefaultListModel(); model.addElement("Раз"); model.addElement("Два");

// список со строками

final JList list = new JList(model);

list.setTransferHandler(new ListDrag. ListTransferHandler(list));

list.setDragEnabled(true);

list.setDropMode(DropMode.INSERT); add(new JScrollPane(list));

JButton copy = new JButton("Копировать в буфер"); copy.addActionListener(new ActionListener() {

public void actionPerformed(ActionEvent e) {

//необходимо для работы команды e.setSource(list);

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

TransferHandler.getCopyAction().actionPerformed(e);

}

});

add(copy, "South");

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

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

587

}

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

new Runnable() {

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

});

}

}

Впримере нам уже все хорошо знакомо — список на основе стандартной модели

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

таскивать данные из списка, так и копировать их в буфер обмена. Обратите внимание, что перед вызовом стандартного действия необходимо задать правильный источник события — компонент с данными, в примере это наш список. Есть лишь одна маленькая неприятность: вставка данных работать не будет, так как в классе ListTransferHandler мы зависим от позиции на экране, где данные «бросаются» на компонент, а при вставке из буфера ее не существует и в методе getDropLocation() возникнет исключение. Для вставки из буфера надо доработать метод importData() и методом isDrop() проверять, перетаскиваются ли данные визуально, и только в этом случае вставлять их на определенную позицию, в противном случае методом getDropLocation() получать позицию нельзя. Пусть доработка примера и класса ListTransferHandler станет для вас простым упражнением.

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

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

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

Любое приличное приложение, в котором пользователи тратят хоть какие-то усилия по созданию и изменению данных, должно позволять им избавляться от случайных ошибок, сохраняя историю их изменений и позволяя отказываться от них. Поддержке отказа от изменений посвящен отдельный пакет библиотеки Swing javax.swing.undo, однако этот пакет довольно абстрактный и большую часть работы приходится делать своими руками.

Основой пакета служит класс UndoManager, который, по сути, представляет собой список изменений, описанных интерфейсом UndoableEdit. Изменение в данных, описанное как UndoableEdit, должно знать, как отменить или повторить себя, возможна ли в данный момент отмена или повтор, и предоставлять пользователям удобное описание изме-

588

ГЛАВА 18

нения. По сути, объект UndoableEdit — не что иное как «команда» (command), известный шаблон проектирования, объединяющий всю информацию об изменении и возможности его отмены или повтора. Добавить новое изменение к списку UndoManager можно методом addEdit(), а отменить последнее изменение в списке методом undo(). Таким образом нам предоставляется просто удобный список операций, а что это за операция и как она должна отменяться, мы решаем самостоятельно.

Чуть проще обстоят дела с текстовыми компонентами, которые автоматически генерируют команды для отмены и повтора операций. Мы уже кратко обсуждали эту возможность в главе 16, где обсуждали такие компоненты. Интересно, что класс UndoManager может работать как «наблюдатель» (observer), наблюдая за объектами, которые генерируют изменения в виде события UndoableEditEvent. Это позволяет отделиться от конкретного объекта UndoManager, именно так с ним работают текстовые компоненты, к которым его нужно присоединить в виде слушателя событий. В собственных компонентах мы вольны выбирать любой способ: добавлять команды для отмены напрямую методом addEdit() либо оповещать неизвестный заранее объект UndoManager обо всех изменениях с помощью событий UndoableEditEvent.

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

Описывать изменения в терминах моделей весьма просто. Например, добавление элемента в список элементов можно описать как объект UndoableEdit, внутри него хранить список с данными и добавленный элемент и при вызове отмены (undo()) просто удалять его из списка. При вызове же повтора операции (redo()) элемент вновь должен попасть в список. Какой из компонентов отображает список, и как, нас при этом не волнует. Останется лишь обновить экран, чтобы компоненты заново получили данные из моделей и отобразили новое состояние дел. Таким образом, отмена и повтор операций в идеале производятся только над данными приложения и с графическими компонентами никак не связаны.

Атеперь рассмотрим пример, в котором мы будем добавлять элементы в список JList

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

//UndoListAdd.java

//Отмена операций в списках и UndoManager

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

import java.awt.event.ActionListener; import java.awt.event.ActionEvent;

public class UndoListAdd extends JFrame { // объект для отмены операций

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

super("ListDrag");

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

589

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

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

final DefaultListModel model = new DefaultListModel(); // создаем списки

JList list1 = new JList(model); JList list2 = new JList(model); // добавим списки в окно

JPanel listPanel = new JPanel(new GridLayout(1, 2)); listPanel.add(new JScrollPane(list1)); listPanel.add(new JScrollPane(list2)); add(listPanel);

// кнопка добавление элемента

JButton add = new JButton("Добавить"); add.addActionListener(new ActionListener() {

public void actionPerformed(ActionEvent e) { String newElement = "Новый!"; model.addElement(newElement);

// регистрация новой операции для отмены undoManager.addEdit(new

ListAddUndoableEdit(model, newElement));

}

}); // кнопки отмены и повтора

final JButton undo = new JButton("Отменить"); final JButton redo = new JButton("Повторить"); // обработчик нажатий кнопок

ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e) {

// отмена и повтор при возможности if ( e.getSource() == undo

&& undoManager.canUndo() ) { undoManager.undo();

} else if ( undoManager.canRedo() ) { undoManager.redo();

}

}

};

undo.addActionListener(al);

redo.addActionListener(al); // добавим кнопки на юг окна

590

ГЛАВА 18

JPanel buttons = new JPanel(); buttons.add(add); buttons.add(undo); buttons.add(redo); add(buttons, "South");

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

}

// класс, описывающий добавление в список

class ListAddUndoableEdit extends AbstractUndoableEdit { // модель и новый элемент

private DefaultListModel model; private Object element;

public ListAddUndoableEdit(DefaultListModel model, Object element) {

this.model = model; this.element = element;

}

@Override

public void undo() throws CannotUndoException { super.undo();

model.removeElement(element);

}

@Override

public void redo() throws CannotRedoException { super.redo();

model.addElement(element);

}

}

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

new Runnable() {

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

}

}

В примере в центре окна разместятся два списка одинакового размера. У них будет одна стандартная модель DefaultListModel, поначалу пустая. На юге окна у нас разместится панель с последовательным расположением, с тремя кнопками. Первая кнопка будет добавлять к модели новый элемент, вторая отменять последнюю операцию, а третья повторять отмененную операцию. Для того чтобы класс UndoManager смог работать с нашей операцией, необходимо описать ее с помощью интерфейса UndoableEdit. Он довольно громоздкий, поэтому проще воспользоваться абстрактным классом AbstractUndoableEdit,

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

591

в котором уже многое реализовано, что мы и делаем. В конструктор операции следует передать стандартную модель списка и новый элемент. В общем же требуется лишь описать методы undo() и redo(), в которых мы удаляем новый элемент или заново добавляем его. Все довольно очевидно, есть лишь одна деталь — необходимо вызвать метод базового класса, так как он должен знать, когда мы отменили или повторили операцию, чтобы затем класс UndoManager смог узнать, доступна ли в данный момент отмена или повтор.

Когда изменение данных описано в требуемом виде, остается регистрировать его

вобъекте UndoManager в момент изменения, что мы и делаем методом addEdit(). Оставшееся является делом техники — кнопки отмены и повтора операций просто вызывают методы undo() и redo() для объекта UndoManager, предварительно удостоверившись

втом, что это возможно, методами canUndo() и canRedo(). Запустив пример, вы убедитесь

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

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

Резюме

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