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

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

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

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

75

тип — это промежуточные результаты, у нас это текст для кнопки, которые мы выводим на экран.

Для запуска задачи в отдельном потоке применяется метод execute(). Можно запускать экземпляры SwingWorker и в своих собственных потоках, так как класс этот реализует интерфейс Runnable. Далее все интересное происходит в переопределенных нами методах, все они вызываются внутренними механизмами класса SwingWorker. Сама долгая работа выполняется в методе doInBackground(), он исполняется в другом потоке, и в нем ни в коем случае не следует работать с интерфейсом программы. Зато из этого метода мы может вызывать метод publish(), передавая туда список данных, этот список попадает в метод process(), и вызывается он уже из потока рассылки событий, где мы вольны так обновить интерфейс, как нам пожелается. Мы просто обновляем текст на кнопке. Ну а метод done() вызывается после окончания работы метода doInBackground() и его можно использовать для окончательного обновления интерфейса после окончания задачи.

Преимуществ использования класса SwingWorker вместо ручного создания потоков несколько. Во-первых, его методы несут больше смысловой нагрузки по сравнению с беспорядочными вызовами очереди событий и созданием кучи объектов Runnable, и понять что именно делает задача, обладая нехитрым знанием SwingWorker, очень легко. Ну а главное — это оптимизация SwingWorker. Он не позволит вам расплодить очень много маленьких потоков (на данный момент максимальное количество потоков в нем ограничено 10), ставя новые потоки в очередь ожидания, попытается оптимизировать вызовы publish(), да и просто использование системного класса предполагает что поддержка программ с ним в будущем всегда проще, случись вдруг в Swing какие-то архитектурные перемены. В системных классах поддержка перемен всегда быстра, достаточно загрузить новую версию пакета JDK.

Почему мы так запускаем Swing-приложения

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

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

//StartingEventThread.java

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

import java.awt.*;

public class StartingEventThread {

public static void main(String[] args) {

//заменяем системную очередь событий своей

Toolkit.getDefaultToolkit(). getSystemEventQueue().push(new CustomQueue());

//создаем окно

76 ГЛАВА 3

JFrame frame = new JFrame("Тест"); System.out.println("(1) JFrame()"); // добавляем флажок

JCheckBox checkBox = new JCheckBox("Тест"); frame.add(checkBox, "South"); System.out.println("(2) Добавлен флажок"); // создаем список

DefaultListModel model = new DefaultListModel(); JList list = new JList(model);

frame.add(list);

System.out.println("(3) Добавлен список");

//обновляем модель model.addElement("Тест"); System.out.println("(4) Обновление модели");

//окончательно выводим интерфейс на экран frame.setVisible(true); System.out.println("(5) Интерфейс построен");

}

//специальная очередь событий, сообщающая

//отладочную информацию о событиях и потоках static class CustomQueue extends EventQueue {

//метод кладет событие в очередь public void postEvent(AWTEvent event) {

System.out.println("post(), поток: " + Thread.currentThread().toString());

System.out.println("post(), событие: " + event); super.postEvent(event);

}

//метод распределяет событие по компонентам protected void dispatchEvent(AWTEvent event) {

System.out.println("dispatch(), поток: " + Thread.currentThread().toString()); System.out.println("dispatch(), событие: " + event);

super.dispatchEvent(event);

}

}

}

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

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

77

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

Далее мы, нарушая архитектуру остальных примеров и программ книги, начинаем создавать и настраивать графические компоненты Swing прямо в методе main(), которые запускается в своем отдельном потоке. После каждого действия, произведенного с компонентами пользовательского интерфейса, в консоль выводится диагностическое сообщение, которое даст нам знать, на каком этапе создания интерфейса мы находимся. С другой стороны, наша специальная очередь, внедренная в самое сердце Swing, расскажет нам, откуда появятся первые события и кем они будут поставлены в обработку. Самих действий не так много — создается окно JFrame, флажок, список JList с моделью по умолчанию, и производится изменение этой модели, что согласно модели MVC, должно привести к оповещению списка о необходимости перерисовки, хотя он еще и не виден на экране.

Все, что произойдет далее, мы увидим в консоли. По сути, результаты могут разниться в зависимости от вашей версии JDK, операционной системы и любых других факторов, способных повлиять на внутренние механизмы виртуальной машины JVM и реализации AWT. В момент написания этой главы первые строчки на консоли выглядели следующим образом:

(1)JFrame()

(2)Добавлен флажок

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

(3) Добавлен список

post(), поток: Thread[main,5,main]

post(), событие: java.awt.event.InvocationEvent[...]

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

(4) Обновление модели

dispatch(), поток: Thread[AWT-EventQueue-1,6,main] dispatch(), событие: java.awt.event.InvocationEvent[...] post(), поток: Thread[main,5,main]

post(), событие: java.awt.event.InvocationEvent[...] dispatch(), поток: Thread[AWT-EventQueue-1,6,main] dispatch(), событие: java.awt.event.InvocationEvent[...] post(), поток: Thread[AWT-Windows,6,main]

post(), событие: java.awt.event.InvocationEvent[...] post(), поток: Thread[main,5,main]

post(), событие: java.awt.event.InvocationEvent[...]

(5) Интерфейс построен

78

ГЛАВА 3

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

СОВЕТ

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

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

Отладка потоков в системе событий

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

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

Прежде всего, это хорошо известный сторонний внешний вид Swing, называемый Substance, который по умолчанию запрещает работу с компонентами Swing из «неправильных» потоков, создавая исключения. Таким образом, небрежно написанная программа вообще не имеет шансов стабильно работать в этом внешнем виде. Даже если вы используете другой внешний вид, вы можете тестировать свою программу во внешнем виде Substance для поиска возможных проблем с множественными потоками. Это не единственный вариант: существует также целая библиотека для модульного тестирования Swing-приложений под названием FEST, которая вызовом одного метода включает режим запрета работы с компонентами Swing из потоков, отличных от потока рассылки событий. В противном случае возникает исключение. Библиотека FEST очень полезна для автоматизированного тестирования Swing-приложений, попробуйте найти ее в Интернете, после ознакомления со Swing.

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

79

Резюме

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

Глава 4. Рисование в Swing

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

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

Система рисования

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

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

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

Проще всего оценить схему рисования, использованную по умолчанию в Java и AWT, на простой диаграмме, и мы сразу увидим всю ее подноготную:

Рисование в Swing

81

Операционная

система

Рисование

компонента

системы

Доп. рисование в программе

PAINT

paint()

Программные

вызовы

Очередь

repaint()

событий

 

PaintEvent (компонент, UPDATE область, тип)

Событие ..

Событие ..

Точка

update()

выполнения

по умолчанию вызывает paint()

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

спомощью специального события PaintEvent.

Впервом варианте призыв нарисовать фрагмент экрана присылает операционная система, когда, по ее мнению, он был «поврежден», то есть свернут, закрыт другим окном и т.п. Если в этот фрагмент входит системный компонент (тот самый, что представлен компонентами AWT, такими как кнопки Button или списки List), он перерисовывает себя сам, и выглядит именно так, как ему положено в данной операционной системе. После этого помощник (peer) компонента или системная часть Java создаст событие PaintEvent

стипом PAINT, укажет в нем, какую область экрана необходимо перерисовать и в каком компоненте и поместит его в очередь событий EventQueue.

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

Вспоминая архитектуру событий Swing, логично было бы подумать, что для получения сигналов о прорисовке можно присоединять слушателей типа PaintEventListener, которые затем оповещаются при рассылке событий из методов dispatchEvent() и processPaintEvent(). Однако мы не можем обрабатывать сигналы о прорисовке как обычные события. Вместо этого события PaintEvent обрабатываются самой системой (в помощниках), когда поток рассылки событий «вытаскивает» их из очереди и передает в метод dispatchEvent() компонента, к которому они принадлежат. Для событий типа PAINT вызывается метод

82

ГЛАВА 4

paint() компонента, в котором необходимо провести перерисовку. Для событий UPDATE вызывается метод update(), который по умолчанию все также вызывает метод paint(). Все эти методы определены в базовом классе любого компонента Component.

В качестве средства для рисования в каждый из этих «рисующих» методов передается графический объект Graphics (часто его называют графическим контекстом). Именно с его помощью графические примитивы выводятся на экран. Получить его можно не только в этих методах, но и создать самому, вызвав метод getGraphics() (доступный в любом компоненте). Это позволяет нарисовать что-то мгновенно, не дожидаясь вызова «рисующего» метода, однако, это практически бесполезно. Любой следующий вызов «рисующего» метода все равно нарисует на экране то, что определено в нем, так что лучше все сводить к методу paint(). Кстати, объект Graphics для рисующих методов системная часть Java также создает методом getGraphics().

Простейший пример покажет нам все в действии:

//AWTPainting.java

//Процесс рисования в AWT очень прост import java.awt.*;

import java.awt.event.*;

public class AWTPainting extends Frame {

public AWTPainting() { super("AWTPainting");

//выход при закрытии окна addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0);

}

});

setLayout(new FlowLayout());

//попробуем закрасить часть кнопки add(new Button("Перерисуем кнопку!") { public void paint(Graphics g) { g.setColor(Color.BLUE);

g.fillRect(2, 2, getWidth() — 5, getHeight() — 5);

}

});

setSize(200, 200);

}

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

public void paint(Graphics g) { // заполняем все красным цветом g.setColor(Color.RED);

g.fillRect(0, 0, getWidth(), getHeight());

Рисование в Swing

83

}

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

}

}

В примере создается окно с рамкой Frame (мы наследуем свой класс от него), в нем мы в качестве менеджера расположения используем последовательное расположение FlowLayout и добавляем кнопку Button c незамысловатым текстом. И в окне, и в кнопке процедура прорисовки компонента paint() заменена на нашу собственную. Окно полностью закрашивается красным цветом, а кнопка, не считая маленького ободка, синим (мы попросту стираем ее текст).

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

Что касается кнопки, то она на первый взгляд выглядит, как положено, но если присмотреться (все будет зависеть от вашей версии пакета JDK и вашей операционной системы), можно будет заметить, как время от времени системная кнопка пытается прорваться на экран через наш синий «заслон». Проблема все та же: AWT банально не успевает ее закрасить, и мы можем увидеть, что именно приложение пыталось от нас скрыть. Но если тут мы хоть что-то закрасили, то, например, список List на платформе Windows просто не даст себя закрасить. Так уж он устроен в операционной системе. Вывод прост — компоненты AWT просто не предназначены для модификации и смены их внешнего вида из кода Java.

Метод paint() — резюме

Итак, в методе paint() размещается код, прорисовывающий компонент. Причем учитывать, что именно в компоненте поменялось, вовсе не обязательно, так как в метод передается графический контекст Graphics, которому уже задан прямоугольник отсечения (clip) (переданный или системой, или из метода repaint()). За преде-

84

ГЛАВА 4

лами этого прямоугольника прорисовка не производится и время не нее не затрачивается.

Добавлять к рисованию системных компонентов AWT свои детали не стоит — слишком неопределенна система взаимодействия системной процедуры прорисовки и метода paint(). Как правило, системный компонент будет «прорываться» через нарисованное вами, а то и вообще не даст ничего на себе нарисовать. Специально для рисования в AWT предусмотрен компонент-«холст» Canvas.

Если вам понадобится сменить какие-либо глобальные параметры графического объекта Graphics, например включить сглаживание, изменить прямоугольник отсечения, а ваш компонент может содержать другие компоненты, особенно легковесные, создавайте копию объекта, вызывая метод create() класса Graphics. В противном случае все ваши настройки перейдут по наследству всем компонентам, которые могут прорисовываться после вашего компонента. К тому же существует один нюанс: после завершения рисования для такого объекта придется явно вызвать метод dispose(), иначе ресурсы системы рисования могут быстро закончиться1.

Метод repaint() — пара дополнений

Как мы увидели, метод программной перерисовки repaint() для стандартных компонентов AWT вызывает сначала метод update(). Когда-то создатели AWT полагали, что это поможет реализовать эффективную технику инкрементальной прорисовки. Это значило, что каждый раз, когда вы вызывали repaint(), в методе update() к уже прорисованному компоненту можно было добавить какие-то детали, а потом перейти к основной картине в методе paint() (установив предварительно нужный прямоугольник отсечения (clip), чтобы не пропало то, что было только что нарисовано).

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

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

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

Рисование легковесных компонентов

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

1 Можно предложить применять интересный вариант кода, позволяющий всегда гарантировать удаление созданного объекта Graphics. Как мы все знаем, всегда вызывается секция finally. Так как исключений при рисовании практически не возникает, секцию catch можно не указывать: try {

создаем новый объект Graphics, рисуем…

}finally { объект.dispose()

}