В начало!  
Сделай закладку этой страницы в Digg Сделай закладку этой страницы в Del.icoi.us Сделай закладку этой страницы в Slashdot Сделай закладку этой страницы в Technorati
arrow Технологии arrow Java SE arrow Отзывчивый UI: используем SwingWorker


feed image

Отзывчивый UI: используем SwingWorker
Автор John O'Conner   
22.11.2008 г.

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

Содержание



Одна их типичных ошибок разработчика десктопного приложения (desktop application) - это  неправильное использование потока обработки событий Swing (Swing event dispatch thread или просто EDT). Кто-то просто не в курсе, что не стоит использовать графические компоненты из не-UI потоков, а кто-то не считает, что последствия могут быть серьезными.

В результате получаются приложения, которые время от времени перестают реагировать на действия пользователя или делают это с заметной задержкой, так как долго работающие задачи (long-running tasks) исполняются на EDT вместо исполнения их на отдельных рабочих потоках.

Продолжительные вычисления или операции ввода/вывода не должны выполняться на Swing EDT!

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

Класс javax.swing.SwingWorker , появившийся в Java SE 6 , позволяет значительно упростить создание пользовательского интерфейса, которые не выглядит медлительным (slow), инертным (sluggish) или нереагирующим на действия пользователя (unresponsive)

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

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

Знакомство с демонстрационным приложением

Image Search - приложение для поиска изображений на сайте Flickr по запросу пользователя. Для работы Image Search требуется выполнять "длительные" задачи, которые работают с веб сервисом Flickr, и такие задачи не должны выполняться на EDT. Вместо выполнения задач на EDT,  приложение использует класс SwingWorker, чтобы делегировать выполнение задачи в другие потоки.

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

На следующем рисунке показано поле поиска и индикатор процесса исполнения поискового запроса.


 
Рисунок 1. Поиск изображений и ход загрузки.
 

Загрузив пиктограмму изображения, ImageSearch помещает его в JList (см. рисунок ниже). Благодаря использованию SwingWorker добавление происходит сразу после загрузки изображения, не дожидаясь загрузки других пиктограмм.
 


Рисунок 2. Список с пиктограммами найденных изображений.

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


Рисунок 3. Загрузка полной версии выбранного изображения.

Наконец, после завершения загрузки полного изображения, приложение показывает его под списком.
 


Рисунок 4. Класс SwingWorker помогает создать быстро работающее приложение для поиска изображений.

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

В коде приложения используется два подкласса SwingWorker: ImageSearcher и ImageRetriever. Класс ImageSearcher отвечает за поиск и загрузку пиктограмм изображений для отображения в списке. Класс ImageRetriever отвечает за загрузку полной версии  выбранного изображения. Совместно, оба этих класса демонстрируют все основные возможности SwingWorker.

Краткий обзор Swing в контексте потоков

В модели Swing все потоки приложения делятся на 3 типа:
  • Начальный поток (initial thread)
  • Поток обработки событий (event dispatch thread (EDT))
  • Рабочий поток (worker threads)
В каждом приложении на Java есть метод main, с которого начинается запуск приложения. Этот метод исполняется на начальном (startup) потоке. Для типичного приложения с пользовательским интерфейсом на этом потоке выполняется немного работы по обработке аргументов командной строки и возможно инициализации вспомогательных объектов. Основная задача начального потока - инициализация и запуск пользовательского интерфейса. Как только это сделано, то для большинства приложений построенных на парадигме систем, управляемых событиями, работа начального потока на этом завершается.

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

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

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

Правильный запуск

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

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

public class MainFrame extends javax.swing.JFrame {
  ...

  public static void main(String[] args) {
    new MainFrame().setVisible(true);
  }
}


Однако, такой код нарушает правило, что все взаимодействия со Swing компонентами должны происходить на потоке EDT. В данном случае setVisible() вызывается на начальном потоке, а не в потоке EDT. Хотя эта проблема не кажется серьезной и на практике редко приводит к каким-то видимым последствиям, но при определенных условиях проблемы могут начать проявляться.
 
Правильный способ запуска UI выглядит примерно так:

public class MainFrame extends javax.swing.JFrame {
  ...

  public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new MainFrame().setVisible(true);
      }
    });
  }
}

Здесь используется класс javax.swing.SwingUtilities, которые предоставляет набор полезных статических методов, облегчающих задачи взаимодействия с компонентами интерфейса. В частности, invokeLater метод, который исполняет полученный в качестве параметра объект типа Runnable на потоке EDT.

Применение invokeLater для запуска UI из начального потока - это рекомендованный подход. Это асинхронный метод, то есть вызов invokeLater завершается не дожидаясь завершения выполнения запущенной задачи.

Есть два способа вызвать этот метод:
  • SwingUtilities.invokeLater
  • EventQueue.invokeLater
Оба подхода корректны и вы можете использовать любой из них. В действительности,
SwingUtilities.invokeLater() внутри себя использует EventQueue.invokeLater(), но поскольку SwingUtilities все равно активно используется классами из библиотеки Swing, то вызов SwingUtilities.invokeLater() не вызовет загрузки лишнего кода вашим приложением.

Другой способ исполнения задач на EDT - это использование метода SwingUtilities.invokeAndWait(). В отличие от invokLater, invokeAndWait - это синхронный метод. Он также исполняет переданный Runnable на EDT, но дожидается завершения его работы прежде чем завершиться самому.

И invokeLater, и invokeAndWait не гарантируют немедленного исполнения задачи. Новая
задача добавляется в конец очереди EDT и будут исполнена после завершения всех задач стоящих в очереди до нее.

Оба этих метода технически можно использовать и в коде, который исполняется на EDT. Но, если для invokeLater это имеет смысл, то использование invokeAndWait на EDT - это плохая идея, так как она приведет к тем же проблемам производительности, которых мы хотим избежать.

Минимизируем нагрузку на EDT

Библиотека Swing использует поток EDT для отрисовки компонент на экран, их обновления и обработки событий. Каждое событие и действие пользователя работающего с UI проходят через этот поток. Задачи исполняемые в этом потоке должны завершаться быстро, иначе они будут задерживать выполнение других задач. Когда это происходит, это легко заметно пользователю - программа выглядит "тормозящей" и медленно реагирующей на действия пользователя, ведь обработчики событий ждут в очереди пока выполнится ваша длинаая задача.

Что такое "долго" в контексте EDT? Идеально, задачи исполняемые на EDT не должны исполняться более 30-100 миллисекунд. В противном случае пользователь начнет замечать задержку между своими действиями и реакцией программы на них.

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

В нашем демонстрационном приложении, Image Search, есть два обработчика событий, которые негативно повлияют на время отклика приложения на действия пользователя, если их исполнять на потоке EDT. А именно, обработчик поискового запроса и обработчик запроса на показ полной версии изображения.

Оба этих обработчика используют Веб сервисы, время ответа которых может измеряться и в секундах. Если запрос к Веб-сервису будет сделан на потоке EDT, то пока не будет получен ответ, пользователь не сможет взаимодействовать с программой - например, не сможет отменить запрос на поиск. Именно поэтому эти методы (и им подобные) не стоит исполнять на EDT.

Следующий рисунок иллюстрирует, что если код обработчика полностью работает на потоке EDT, то EDT не сможет обрабатывать события в интервале времени от A до B, в течении которого исполняется запрос к Веб-сервису Flickr.



Используя класс SwingWorker приложение может запустить отдельный поток для асинхронного выполнения подобных задач и вернуться к обработке других задач в очереди EDT. Выгода такого подхода проиллюстрирована на следующем рисунке, где интервал ожидания из-за обработки нашей задачи (от A до B) значительно меньше:


SwingWorker: немного теории

В этом разделе мы кратко рассмотрим доступные возможности SwingWorker.

Согласно javadoc, класс SwingWorker описывается так:

public abstract class SwingWorker<T,V> extends Object implements RunnableFuture

Поскольку SwingWorker - это абстрактный класс, то для его использования вам необходимо создать его потомка. Обратите внимание, что SwingWorker - это также и параметризованный класс. Параметр T определяет тип возвращаемого значения для метода doInBackground() и get(). Параметр V определяет тип аргумента с которым работают методы publish и process.
Мы подробнее расскажем зачем нужны эти методы далее.

Этот класс также реализует интерфейс java.util.concurrent.RunnableFuture, который является объединением двух интерфейсов - Runnable и Future.

Поскольку SwingWorker реализует Runnable, то его реализация должна иметь метод run(). Этот метод используется объектом Thread, для выполнения работы после запуска потока.

Поскольку SwingWorker реализует интерфейс Future, то он должен предоставлять доступ к результатам выполнения работы в виде объекта типа T и обеспечивать некоторые возможности взаимодействия с потоком исполнения работы, а именно:

     boolean cancel(boolean mayInterruptIfRunning)
     T get()
     T get(long timeout, TimeUnit unit)
     boolean isCancelled()
     boolean isDone()

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

protected T doInBackground() throws Exception
 
Этот метод исполняется в контексте рабочего потока. Его предназначение - исполнение  основной задачи, результат выполнения которой он возвращает. Таким образом вам необходимо реализовать метод doInBackground(), так чтобы он исполнял или делегировал исполнение искомой задачи.

Обратите внимание, что этот метод НЕ надо вызывать явным образом. Если вы вызовите его явным образом на EDT потоке, то это приведет к исполнению вашей длинной задачи на EDT потоке. Правильный подход - использовать метод execute() для запуска рабочего потока, который сам вызовет doInBackground().

Результат работы doInBackground() можно получить используя метод get() у SwingWorker.
Вы можете вызывать get() на EDT потоке, но если doInBackground() еще не завершил работу, то этот метод заблокируется пока рабочий поток не завершит работу. Тем самым будет заблокировано EDT. Чтобы этого не случилось метод get() надо вызывать только тогда, когла известно, что рабочий поток выполнил задачу. Чтобы избежать блокирования можно использовать метод isDone() для проверки готовности результата или метод get(long timeout, TimeUnit unit), который гарантирует, что он разблокируется если результаты не будут готовы в течении заданного времени.

Однако, обычно удобнее перегрузить метод done() и получать результаты выполнения задачи внутри его реализации:

protected void done()   
 
SwingWorker вызовет этот метод после завершения работы метода doInBackground, то есть
вызовы get() после момента вызова done() не будут блокироваться. Поэтому перегруженный метод done() и является хорошим местом для обработки резльтатов. Более того, метод done() исполняется на потоке EDT. Таким образом внутри этого метода можно
спокойно работать с любыми компонентами Swing.

Вообще говоря, не обязательно ждать завершения выполенения задачи для того, чтобы получить промежуточные результаты работы. Промежуточный результат - это фрагмент данных, который рабочий поток может предоставить до того как будет готов окончательный полный результат. Технически, рабочий поток может делать такие результаты доступными с помощью метода publish(), передавая ему объект с промежуточным результатом типа V. Для того, чтобы обрабатывать промежуточные результаты (например, показывать их пользователю) необходимо перегрузить метод process(). Мы расскажем об этом подробнее несколько позже.

SwingWorker также может уведомлять об изменении одного из двух своих свойств: state и progress.

Рабочий поток может находиться в одном из нескольких состояний (state), которые представляются следующими константами (см. SwingWorker.StateValue):     
  • PENDING
  • STARTED
  • DONE
Вновь созданный рабочий поток имеет состояние PENDING. После начала работы doInBackground, поток переходит в состояние STARTED. По завершении doInBackground поток переходит в состояние DONE. Вы можете подписаться на уведомления об изменении статуса потока.

Рабочий поток также имеет свойство progress, являющееся индикатором прогресса выполнения работы. Работающий поток может обновлять значение этого свойства целыми числами от 0 до 100. Вы также можете подписаться на уведомления об изменении значения этого свойства.

Реализуем простой ImageRetriever

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

Создавая подкласс SwingWorker, вам необходимо указать тип значения, возвращаемого методами doInBackground и get. В нашей реализации ImageRetriever эти методы возвращают объект Icon. Поскольку ImageRetriever не предусматривает получение неполных результатов, то в качестве второго параметра класса SwingWorker используется специальный тип Void.

Основная часть нашей реализации ImageRetriever выглядит так:

public class ImageRetriever extends SwingWorker<Icon, Void> {
    private ImageRetriever() {}
   
    public ImageRetriever(JLabel lblImage, String strImageUrl) {
        this.strImageUrl = strImageUrl;
        this.lblImage = lblImage;
    }
   
    @Override
    protected Icon doInBackground() throws Exception {
        Icon icon = retrieveImage(strImageUrl);
        return icon;
    }
   
    private Icon retrieveImage(String strImageUrl)
            throws MalformedURLException, IOException {      
        InputStream is = null;
        URL imgUrl = null;
        imgUrl = new URL(strImageUrl);
        is = imgUrl.openStream();
        ImageInputStream iis = ImageIO.createImageInputStream(is);
        Iterator<ImageReader> it =
            ImageIO.getImageReadersBySuffix("jpg");
       
        ImageReader reader = it.next();
        reader.setInput(iis);
        ...
        Image image = reader.read(0);
        Icon icon = new ImageIcon(image);
        return icon;
    }
   
    @Override
    protected void done() {
        Icon icon = null;
        String text = null;
        try {
            icon = get();
        } catch (Exception ignore) {
            ignore.printStackTrace();
            text = "Image unavailable";
        }
        lblImage.setIcon(icon);
        lblImage.setText(text);
    }
   
    private String strImageUrl;
    private JLabel lblImage;
}
 
ImageRetriever загружает указанное изображение и вставляет его в JLabel, поэтому логично иметь конструктор, который получает JLabel для вставки результата и URL изображения для загрузки. Если бы мы использовали SwingWorker как внутренний (inner) класс в контексте другого класса, то можно было бы избежать дополнительного конструктора и прямо использовать поля объемлющего объекта. Однако, и в таком случае конструктор в принципе тоже имеет определенные преимущества - каждая экземпляр ImageRetriever будет иметь свою копию данных. В случае прямого доступа к полям объемлющего класса эта информация будет разделяемой, что может повлечь усложнение кода для обеспечения синхронизации доступа к данным.

Обратите внимание не декларацию класса. Как уже было сказано ранее, результат выполнения задачи - это объект типа Icon. Промежуточные результаты не предусмотрены, поэтому используется тип Void:

public class ImageRetriever extends SwingWorker<Icon, Void>
 
Поскольку класс Icon был указан в качестве параметра для возврщаемого значения класса SwingWorker, то и метод doInBackground и метод get у ImageRetriever возвращают объекты типа Icon. Метод get объявлен как final, таким образом перегрузить его нельзя. А вот метод doInBackground объявлен как asbtract и предоставить его реализацию необходимо. Собственно, в этом методе и происходит загрузка изображения по данному URL и создание объекта Icon, который возвращает в качестве результата работы:

@Override
protected Icon doInBackground() throws Exception {
  Icon icon = retrieveImage(strImageUrl);
  return icon;
 
По завершении doInBackground, SwingWorker вызовет метод done(), причем сделает это на EDT. Это произойдет автоматически, не надо вызывать done() самостоятельно.

Метод done() получает результат загрузки в виде Icon и вставляет его в искомый JLabel  (переменная lblImage):

@Override
protected void done() {
  ...
  icon = get();
  ...
  lblImage.setIcon(icon);
  ...
}

Для того, чтобы информировать о прогрессе исполнения задачи, можно использовать свойство progress, которое принимает целочисленные значения от 0 до 100. Для обновления этого свойства можно воспользоваться методом setProgress.

В нашем случае, загрузка изображения происодит с помощью библиотеки ImageIO, которая предоставляет отслеживать прогресс работы по загрузке используя IIOReadProgressListener. Мы реализуем обработку событий от ImageIO и передаем эту информацию нашему расширению SwingWorker:

reader.addIIOReadProgressListener(new IIOReadProgressListener() {
  ...          
  public void imageProgress(ImageReader source, float percentageDone) {
    setProgress((int) percentageDone);
  }
           
  public void imageComplete(ImageReader source) {
    setProgress(100);
  }
});

Изменение свойства progress повлечет рассылку уведомлений о его изменении классом SwingWorker. Таким образом, в нашем примере уведомление от ImageIO будет передано SwingWorker, с помощью метода setProgress(), который в свою очередь уведомит UI о необходимости обновить состояние индикатора прогресса загрузки.

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


 
ImageRetriever иллюстрирует простое применение класса SwingWorker. В простом случае, единственное обязательное действие - реализовать doInBackground. Однако, поскольку результат доступен только после полного завершения работы, то обычно стоит также перегрузить и метод done. Этот метод вызывается после завершения doInBackground и удобен для обработки результатов выполнения вашей длинной задачи.

Использование ImageRetriever

Для того чтобы воспользоваться классом на основе SwingWorker достаточно создать экземпляр объекта этого класса и вызвать метод execute(). Класс MainFrame в нашем демонстрационном приложении использует рабочие потоки ImageRetriever каждый раз, когда пользователь выбирает пиктограмму в списке, т.е. каждый раз когда вызывается обрабочик события связанного с выделением элемента (listImagesValueChanged).

Метод listImagesValueChanged определяет, какой элемент списка был выбран пользователем, и создает URL соответствующий запросу к Flickr на получение полной копии изображения. Далее он вызывает retrieveImage, который собственно и создает ImageRetriever и инициирует загрузку.

private void listImagesValueChanged(ListSelectionEvent evt) {
  ...
  ImageInfo info = (ImageInfo) listImages.getSelectedValue();
  String id = info.getId();
  String server = info.getServer();
  String secret = info.getSecret();
  // Нет смысла пытаться загружать полную версию некорректного изображения
  if (id == null || server == null || secret == null) {
    return;
  }
  String strImageUrl = String.format(IMAGE_URL_FORMAT,
    server, id, secret);
  retrieveImage(strImageUrl);
  ...
}                                      
   
private void retrieveImage(String imageUrl) {
  // Объекты SwingWorker нельзя переиспользовать => создаем новый
  ImageRetriever imgRetriever =
        new ImageRetriever(lblImage, imageUrl);
  progressSelectedImage.setValue(0);
  // Отслеживаем изменения свойства "progress"
  // Обработчик событий в этом случае можно переиспользовать для разных рабочих потоков
  imgRetriever.addPropertyChangeListener(listenerSelectedImage);
 
  progressSelectedImage.setIndeterminate(true);
 
  // Инициируем асинхронный запуск рабочего потока
  imgRetriever.execute();
  // Поток EDT может продолжать обработку других событий, не ожидая завершения рабочего потока
}   
 
Обратите внимание, что retrieveImage создает новый экземпляр ImageRetriever каждый раз, когда загружается новое изображение. Экземпляры SwingWorker нельзя переиспользовать, необходимо создавать новый объект каждый раз, когда вы хотите выполнить новую задачу.

Правильная последовательность действий при использовании SwingWorker - это: создать экземпляр объекта, зарегистрировать подписки на уведомления о событиях, вызвать метод execute. Метод execute асинхронный, он быстро завершит работу и EDT перейдет к обработке следующего события сразу после завершения работы метода execute. То есть, длительный запрос к Веб-сервису не повлияет на работу EDT.

Созаваемый в методе retrieveImage объект ImageRetriever содержит ссылку на JLabel, который будет использоваться для показа изображения, то есть у него есть вся информация необходимая для самостоятельного завершения работы. Поэтому нет необходимости в дальнейшем контроле со стороны retrieveImage или EDT.

Перед тем как запустить рабочий поток, метод retrieveImage подписывается на уведомления об изменении свойств SwingWorker. Класс MainFrame содержит индикатор прогресса загрузки изображения и с подписка помогает отображать в нем статус исполнения работы.

Связь между события об изменении свойства progress у SwingWorker и визуальным индикатором прогресса осуществляется при помощи класса ProgressListener, который обновляет визуальную компоненту по получении события от SwingWorker.

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

/**
 * ProgressListener получает уведомления об изменении свойства progress
 * рабочего потока SwingWorkers, который производит поиск и загрузку пиктограмм.
 */
class ProgressListener implements PropertyChangeListener {
  // Нельзя создать без задания индикатора прогресса (JProgressBar)
  private ProgressListener() {}
 
  ProgressListener(JProgressBar progressBar) {
    this.progressBar = progressBar;
    this.progressBar.setValue(0);
  }
 
  public void propertyChange(PropertyChangeEvent evt) {
    String strPropertyName = evt.getPropertyName();
    if ("progress".equals(strPropertyName)) {
      progressBar.setIndeterminate(false);
      int progress = (Integer)evt.getNewValue();
      progressBar.setValue(progress);
    }
  }
 
  private JProgressBar progressBar;
}

ImageSearcher: более сложный пример использования SwingWorker

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

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

Расширяя SwingWorker, необходимо указать типы для полного и частичного результатов работы в декларации класса. ImageSearcher ищет и загружает пиктограммы соответствующие поисковому запросу. Полный результат - это список пиктограмм, соответствующий тип данных - List<ImageInfo>. В качестве частичных результатов мы хотим публиковать каждую скачанную пиктограмму. Поэтому тип частичного результата - ImageInfo. Вот так выглядит соответствующая часть кода ImageSearcher:

public class ImageSearcher
extends SwingWorker<List<ImageInfo>, ImageInfo> {
  public ImageSearcher(DefaultListModel model, String key,
        String search, int page) {
    this.model = model;
    this.key = key;
    this.search = search;
    this.page = page;
  }
  ...
}
 
Из этого небольшого кусочка можно почерпнуть несколько вещей. Согласно декларации класса методы doInBackground и get возвращают список объектов типа ImageInfo. Также известно, что

ImageSearcher будет публиковать промежуточные результаты типа ImageInfo. Поскольку конструктор получает в качестве одного из параметром модель данных для списка, то можно предположить, что ImageSearcher будет напрямую добавлять элементы в модель данных списка. Так как используется Веб-сервис Flickr, то необходим ключ для доступа к API, а также запрос для поиска и информация какую страницу результатов использовать. Для простоты реализации мы всегда используем только первую страницу результатов.

Метод doInBackground - сердце SwingWorker, поэтому начнем с его реализации:

@Override
protected List<ImageInfo> doInBackground() {
  ...
  Object strResults = null;
  InputStream is = null;
  URL url = null;
  List<ImageInfo> infoList = null;
  try {
    url = new URL(searchURL);
    is = url.openStream();
    infoList = parseImageInfo(is);
    retrieveAndProcessThumbnails(infoList);
  } catch(MalformedURLException mfe) {
    ...
  }
  return infoList;
}

Этот код устанавливает соединение с Веб-сервисом по заданому URL, потом вызывает parseImageInfo, который в свою очередь загружает ответ от сервиса в формате XML и заполняет список информацией о найденных пиктограммах. Метод retrieveAndProcessThumbnails загружает пиктограммы из списка. По завершении загрузки всех пиктограмм возвращается список пиктограмм.

Во многом этот класс похож на ImageRetriever и поэтому, в частности, мы не будем далее подробно рассматривать методы doInBackground, done, get, и setProgress, так как их реализация схожа с тем, что уже было описано ранее.

Однако, ImageSearcher загружает не одно изображение, а до 100 пиктограмм. Это позволяет продемонстрировать другие возможности SwingWorker, а именно методы publish и process.

Метод publish можно использовать для публикации частичных результатов выполнения задачи. Завершив загрузку пиктограммы, поток ImageSearcher может не просто добавить ее в список загруженных пиктограмм и перейти к следующей загрузке, но также и опубликовать загруженную пиктограмму для того, чтобы пользователь умел возможность увидеть ее как можно раньше. Если вы используете publish, то вам необходимо перегрузить метод process, который предназначен для обработки опубликованных результатов. Этот метод вызывается на EDT, так что он может свободно использовать компоненты Swing.

Вот как методы publish и process используются в ImageSearcher. По завершении загрузки питограммы мы вызываем publish, чтобы сделать ее доступной. Это влечет вызов метода process() на потоке EDT, который добавляет эту пиктограмму в модель данных JList, что в свою очередь вызовет отображение загруженной пиктограммы на экране:

private void retrieveAndProcessThumbnails(List<ImageInfo> infoList) {
  for (int x=0; x <infoList.size() && !isCancelled(); ++x) {           
    // http://static.flickr.com/{server-id}/{id}_{secret}_[mstb].jpg
    ImageInfo info = infoList.get(x);
    String strImageUrl = String.format("%s/%s/%s_%s_s.jpg",
    IMAGE_URL, info.getServer(), info.getId(), info.getSecret());
    Icon thumbNail = retrieveThumbNail(strImageUrl);
    info.setThumbnail(thumbNail);
    publish(info);
    setProgress(100 * (x+1)/infoList.size());
  }
}
   
/**
 * Метод process вызывается в результате вызова метода publish у этого рабочего потока.
 * Метод process выполняется на EDT.
 *
 * Добавляем загруженные пиктограммы в модель для компоненты-списка.
 *
 */
@Override
protected void process(List<ImageInfo> infoList) {
  for(ImageInfo info: infoList) {
    if (isCancelled()) {
      break;
    }
    model.addElement(info);
  }     
}

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

Для того, чтобы предоставить пользователю возможность отмены запущенной задачу, в рабочем потоке задачи необходимо время от времени проверять на наличие запроса на отмену задачи. Проверка производится с помощью метода isCancelled и ее стоит выполнять во всех осмысленных промежуточных точках, в частности, в теле циклов или других итераторов. Например, в реализации ImageSearcher проверка на необходимость отмены производится в следующих местах:
  • Перед загрузкой каждой пиктограммы, в методе doInBackground
  • Перед добавлением промежуточных результатов в модель данных списка, в методе process
  • Перед добавлением окончательных результатов в модель данных списка, в методе done
Метод doInBackground вызывает retrieveAndProcessThumbnails, который перебирает список описаний изображений и загружает их пиктограммы. Однако, пользователь может передумать и инициировать новый поиск, пока пользователь находится в этом цикле. Поэтому внутри цикла имеет смысл проверять на наличие запроса на отмену:

private void retrieveAndProcessThumbnails(List<ImageInfo> infoList) {
  for (int x=0; x<infoList.size(); ++x) {
    // Проверяем не отменили ли задание для этого рабочего потока.
    // Если отменили - прекразаем загружать пиктограммы.
    if (isCancelled()) {
      break;
    }
    ...
}

Обрабатывая очередную пиктограмму, происходит ее публикация. Сама публикация выполняется в методе process(), исполняемом на EDT. Поэтому, если запрос на отмену или выполенение нового поиска пришел ПОСЛЕ проверки в retrieveAndProcessThumbnails, но ДО того как начал исполняться метод process (который мог ожидать исполнения в очереди EDT), то пиктограмма от отмененного запроса появится в списке на экране. Предотвратить это можно, добавив проверку в метод process:

protected void process(List<ImageInfo> infoList) {
  for (ImageInfo info: infoList) {
    if (isCancelled()) {
      break;
    }
    model.addElement(info);
  }
}
 
Аналогично, по завершении работы рабочего потока может произойти обновление модели или пользовательского интерфейса и тут может возникнуть схожая проблема с отображением уже ненужных результатов. Для того чтобы этого не случилось, стоит добавить проверку и в метод done:

@Override
protected void done() {
  ...
  if (isCancelled()) {
    return;
  }
  ...
  // Обновляем модель
}
 
Класс ImageSearcher демонстрирует больше возможностей SwingWorker, чем ImageRetriever. В частности, отображение промежуточных результатов и корректную подержку отмены запущенной операции. В обоих классах работа выполняется в фоновых рабочих потоках и информация о прогрессе ее выполнения отслеживается с помощью событий.

Использование ImageSearcher

Наше демонстрационное приложение имеет поле ввода поискового запроса. Для каждого нового запроса в методе searchImages создается и запускается новый экземпляр ImageSearcher:

private void searchImages(String strSearchText, int page) {
  if (searcher != null && !searcher.isDone()) {
    // Отменяем текущую операцию поиска перед началом новой.
    // Мы не хотим выполнять более одного поиска одновременно.
    searcher.cancel(true);
    searcher = null;
  }
  ...
  // передаем модель для списка (list model) => ImageSearcher может публиковать
  // пиктограммы по мере загрузки
  searcher = new ImageSearcher(listModel, API_KEY, strEncodedText, page);
  searcher.addPropertyChangeListener(listenerMatchedImages);
  progressMatchedImages.setIndeterminate(true);
  // Инициируем запуск поиска!
  searcher.execute();
  // Поток EDT может продолжать работу, не дожидаясь заверешения операции поиска.
}

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

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


 
Рисунок 8. Пиктограммы - это промежуточный результат рабочего потока.
 
Ввод нового запроса пользователем, до того как завершился текущий запрос, подразумевает отмену текущей работы. Это достигается с помощью вызова метода cancel в обработчике события ввода нового запроса:

private void searchImages(String strSearchText, int page) {
  if (searcher != null && !searcher.isDone()) {
    // Отменяем текущую операцию поиска и начинаем новую.
    // Мы не хотим выполнять более одного поиска одновременно.
    searcher.cancel(true);
    searcher = null;
  }
  ...
}


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

Резюме

Все события и код обновляющий пользовательский интерфейс исполняются на потоке EDT. Исполнение длительных (например, связанных с оперцаями ввода-вывода) активностей на этом потоке приводит замедлению визуальной реакции на действия пользователя в пользовательском интерфейсе. Такие операции рекомендуется выносить из EDT, используя добавленный в Java SE 6 класс SwingWorker.

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

Демонстрационное приложение Image Search иллюстрирует как расширять и применять класс SwingWorker. В этом приложении рабочие потоки используются для поиска и загрузки изображений с сайта Flickr. Обратите внимание, что использование Веб-сервисов Flickr требует регистрации и получения ключа, необходимого для доступа к API.

Дополнительная информация:

     Исходный код Image Search
     Скачать Java SE 6
     Документация по SwingWorker
     Документация по аннотации @Override
     Документация по ImageIO
     Введение в разработку приложений с использованием Flickr API
     Java tutorial: разработка пользовательского интерфейса на основе JFC/Swing

Оригинал


 
 

Добавить комментарий


Защитный код
Обновить