Материал: Портянкин И. Swing

Внимание! Если размещение файла нарушает Ваши авторские права, то обязательно сообщите нам

70

ГЛАВА 3

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

Ну и, наконец, есть самый гибкий способ изменить компонент из другого потока. Если ваша задача, выполняющаяся в отдельном потоке, так или иначе приводит к необходимости изменить что-либо в компоненте, и в этом вам не могут помочь безопасные вызовы, остается только одно. Во избежание неприятностей с потоком рассылки событий и неожиданного тупика все действия с компонентами все равно нужно выполнять из потока рассылки событий. И у вас есть возможность выполнить некоторое действие в потоке рассылки событий. Для этого предназначены методы invokeLater() и invokeAndWait() класса EventQueue. Данным методам нужно передать ссылку на интерфейс Runnable, метод run() этого интерфейса будет выполнен потоком рассылки событий, как только он доберется до него. (Переданная в метод invokeLater() или invokeAndWait() ссылка на интерфейс Runnable «оборачивается» в событие специального типа InvocationEvent, обработка которого сводится к вызову метода run().) В итоге вы действуете следующим образом: выполняете в отдельном потоке сложные долгие вычисления, получаете некоторые результаты, а действия, которые необходимо провести после этого с графическими компонентами, выполняете в потоке рассылки событий с помощью метода invokeLater() или invokeAndWait(). Рассмотрим небольшой пример:

//InvokeLater.java

//Метод invokeLater() и работа с потоком рассылки событий import java.awt.*;

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

public class InvokeLater extends JFrame { public InvokeLater() {

super("InvokeLater");

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

//добавим кнопку со слушателем

button = new JButton("Выполнить сложную работу"); button.addActionListener(new ActionListener() {

public void actionPerformed(ActionEvent e) { // запустим отдельный поток

new ComplexJobThread().start(); button.setText("Подождите...");

}

}); // настроим панель содержимого и выведем окно на экран

setLayout(new FlowLayout()); add(new JTextField(20)); add(button);

setSize(300, 200);

За кулисами системы обработки событий

71

setVisible(true);

}

private JButton button;

// поток, выполняющий "сложную работу" class ComplexJobThread extends Thread {

public void run() { try {

//изобразим задержку sleep(3000);

//работа закончена, нужно изменить интерфейс

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

button.setText("Работа завершена");

}

});

}catch (Exception ex) {

ex.printStackTrace();

}

}

}

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

new Runnable() {

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

}

}

Мы создаем небольшое окно, в панели содержимого которого размещается кнопка JButton и вспомогательное текстовое поле. При нажатии кнопки будет вызван слушатель ActionListener, и мы предполагаем, что работа, которая предстоит слушателю, довольно сложна и займет приличное время, поэтому выполнять ее нужно в отдельном потоке (на самом деле, выполнение долгих вычислений в слушателе, который был вызван потоком рассылки событий, приведет к блокированию остальных событий, ждущих в очереди рассылки тем же потоком, и в итоге интерфейс программы станет неотзывчивым). Отдельный поток, выполняющий долгие сложные вычисления, реализован во внутреннем классе ComplexJobThread, унаследованном от базового класса всех потоков Thread. Проблема состоит в том, что по окончании вычислений нам нужно сменить надпись на кнопке, а мы к этому моменту будем находиться в отдельном потоке. Менять надпись из потока, отличного от потока рассылки событий, не стоит: это может привести к неопределенности данных в компонентах и ошибкам, и даже к тупикам

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

иприйти в нестабильное состояние.

Здесь нам и пригодится статический метод invokeLater() класса EventQueue. Он позволяет выполнить некоторый фрагмент кода из потока рассылки событий. В качестве

72 ГЛАВА 3

параметра данному методу нужно передать ссылку на объект, который реализует интерфейс Runnable: метод run(), определенный в этом интерфейсе, и будет выполнен потоком рассылки событий. Так мы и поступаем в примере: код, меняющий надпись на кнопке, будет выполнен из потока рассылки событий, так что можно не опасаться возникновения конфликтов. Запустив программу с примером и нажав кнопку, вы увидите, что во время вычислений пользовательский интерфейс доступен (вы сможете набрать что-ли- бо в текстовом поле или нажать кнопку еще раз). Сразу по завершении вычислений вы узнаете об этом по изменению надписи не кнопке, и никаких конфликтов при этом не возникнет.

Помимо метода invokeLater() в вашем распоряжении также имеется метод invokeAndWait(). Он аналогичным образом позволяет выполнить фрагмент кода из потока рассылки событий, но в отличие от invokeLater(), делает это синхронно: приостанавливая работу потока, из которого вы его вызвали, до тех пор, пока поток рассылки событий не выполнит ваш код. С другой стороны, invokeLater() работает асинхронно: он немедленно возвращает вам управление, так что вы можете продолжать работу, зная, что рано или поздно ваш фрагмент кода будет выполнен потоком рассылки событий. В большинстве ситуаций предпочтительнее метод invokeLater(), а метод invokeAndWait() стоит использовать только там, где немедленное выполнение потоком рассылки фрагмента вашего кода обязательно для дальнейших действий. Работая с методом invokeAndWait(), следует быть внимательнее: поток, из которого вы его вызвали, будет ожидать, когда поток рассылки событий выполнит переданные ему фрагмент кода, а в этом фрагменте кода могут быть обращения к ресурсам, принадлежащим первому потоку, тому самому, что находится в ожидании. В итоге возникнет взаимная блокировка, и программа окажется в тупике. Метод invokeLater() позволяет всего этого избежать.

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

Принципы работы потока рассылки событий и рассмотренный только что пример плавно подводят нас к «золотому правилу» Swing: пишите слушатели короткими и быстрыми. Здесь все просто: рассылка событий и вызов соответствующих слушателей, в которых они обрабатываются, происходят из одного потока выполнения, потока рассылки событий EventDispatchThread. Как только какой-либо слушатель начинает производить долгие вычисления, все остальные события «застревают» в очереди событий, и интерфейс программы перестает отзываться на любые действия пользователя, а работать с такой неповоротливой программой — удовольствие ниже среднего.

СОВЕТ

Если в слушателе или методе обработки событий выполняются достаточно длинные вычисления, выносите их в отдельный поток. Манипулировать графическими компонентами из другого потока выполнения вы всегда сможете с помощью методов класса EventQueue или класса SwingUtilities из пакета javax.swing. Последний позволяет избежать импорта ненужных пакетов AWT и именно его рекомендуется применять в Swing .

За кулисами системы обработки событий

73

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

Отзывчивость программы и SwingWorker

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

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

Давайте попробуем переделать предыдущий пример с «долгими» вычислениями

спомощью SwingWorker. Вот что у нас получится:

//UsingSwingWorker.java

//Класс SwingWorker для отзывчивости интерфейса import javax.swing.*;

import java.awt.*; import java.awt.event.*; import java.util.List;

public class UsingSwingWorker extends JFrame { private JButton button;

public UsingSwingWorker() { super("UsingSwingWorker");

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

//добавим кнопку со слушателем

button = new JButton("Выполнить сложную работу"); button.addActionListener(new ActionListener() {

public void actionPerformed(ActionEvent e) { // запустим отдельную долгую работу

new ComplexJob().execute(); button.setText("Подождите...");

}

});

74

ГЛАВА 3

// настроим панель содержимого и выведем окно на экран setLayout(new FlowLayout());

add(new JTextField(20)); add(button); setSize(300, 200); setVisible(true);

}

// класс, выполняющий "сложную работу"

class ComplexJob extends SwingWorker<String,String> { // здесь выполняется работа, это отдельный поток! public String doInBackground() throws Exception {

Thread.sleep(2000);

// публикуем промежуточные результаты publish("Половина работы закончена..."); Thread.sleep(2000);

return "";

}

//обработка промежуточных результатов

//это поток рассылки событий!

protected void process(List<String> chunks) { button.setText(chunks.get(0));

}

// окончание работы - и вновь это поток рассылки public void done() {

button.setText("Работа завершена");

}

}

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

new Runnable() {

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

}

}

Мы вновь создаем небольшое окно, в панели содержимого которого размещается кнопка JButton и вспомогательное текстовое поле. При нажатии кнопки нас ожидает тяжелая, долгая, неповоротливая задача, и во имя наших пользователей мы обязаны выполнить ее вне потока рассылки событий. Теперь нам помогает класс SwingWorker, специально предназначенный для этих задач.

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

Источник: https://studfile.net/preview/15935977/