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

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

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

Рисование в Swing

95

Программная перерисовка в Swing

Если вспомнить как мы рассматривали программную перерисовку в AWT (метод repaint()), то легко видеть что это по сути постановка события на перерисовку (PaintEvent) в очередь событий интерфейса. Именно эта простая идея подтолкнула создателей Swing включить оптимизацию и здесь, на этот раз применив принцип пакетной обработки.

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

Метод repaint() переопределен в классе JComponent и вместо прямой постановки события PaintEvent в очередь событий просит нашего старого знакомого RepaintManager добавить область, которую нужно перерисовать, в список «загрязненных» (dirty region). «Загрязненные» области нужно перерисовать, как только до них дойдут руки потока рассылки событий.

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

Следующая схема прекрасно иллюстрирует, что происходит и какие классы участвуют в процессе:

repaint()

RepaintManager.addDirtyRegion()

Список поврежденных областей

SystemEventQueueUtilities queueComponentWorkRequest()

postEvent()

collectDirtyRegions() paintDirtyRegions()

paintImmediately()

Поток EventDispatchThread

Прорисовка

Вспомогательный класс SystemEventQueueUtilities ставит то самое особое событие (его имя ComponentWorkRequest) в очередь, используя метод для постановки postEvent(). События PaintEvent в Swing вообще не применяются. Как только поток рассылки дохо-

96

ГЛАВА 4

дит до данного события, оно выполняется и вызывает метод все того же RepaintManager с названием paintDirtyRegions(). Опуская детали реализации, мы приходим к тому, что вызывается метод, определенный в базовом классе JComponent под названием paintImmediately(). Ну а в нем все уже совсем просто. В итоге методом getGraphics() создается графический объект и вызывается хорошо знакомый нам метод paint(). Работу этого метода в Swing мы подробно изучили чуть ранее, и все выполняется по той самой диаграмме, что мы составили. В результате компонент Swing перерисовывается, причем лишь один раз за определенный промежуток времени, и максимально оптимизировано.

Рисование в Swing: резюме

Подводя итоги всей системы рисования Swing, можно определить следующие самые важные выводы:

Метод paint() в Swing является частью внутренней системы и выполняет важную работу. Переопределять его для рисования компонента Swing не следует.

Для рисования компонента или просто графики применяется метод paint-

Component().

При рисовании в Swing необходимо учитывать свойство непрозрачности opaque. Если компонент непрозрачен, необходимо закрашивать его фон или не оставлять непрорисованных областей. Делается это либо вручную, либо вызовом super. paintComponent(), чтобы фон закрасил UI-представитель. Однако, второй способ не работает для компонентов, унаследованных напрямую от JComponent. Если не выполнить данное условие, мусор на экране неизбежен.

Перерисовка repaint() в Swing выполняется пакетами и оптимизируется. Если вас это не устраивает, придется переопределить или метод repaint(), или написать свой вариант класса RepaintManager. Впрочем, необходимости в этом практически не возникает.

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

Разбирая тонкости систем рисования библиотеки Swing и ее основы AWT, мы рассматривали все возникающие вопросы с точки зрения рисования с нуля, в пределах одного компонента, как это обычно и бывает. Однако легко себе представить и другую ситуацию — стандартные компоненты Swing весьма функциональны и полезны, и у вас может возникнуть желание «прорисовать» какой-либо из них в своем собственном компоненте. К примеру, надписи JLabel умеют отображать многострочный текст формата HTML и изображения, представьте, что вам пришлось бы заниматься тем же самым самостоятельно при создании любого нового компонента. Интересно, что сложные компоненты Swing, такие как таблицы JTable, используют те же самые надписи для отображения своих ячеек или текста, так что идея неплоха.

При работе с обычными легковесными компонентами AWT представить себе процесс их прорисовки в рамках другого компонента легко —нужно всего лишь вызвать метод paint(), предварительно задав размер компонента и возможно поменяв какие-то графические параметры объекта Graphics. Компонент никогда не знает, кто вызывает его прорисовку, и должен выполнять ее независимо от этого. Однако в Swing картина немного другая.

Если вспомнить процесс прорисовки компонентов Swing, то легко увидеть, что все действия идут через объект RepaintManager, а сам компонент, если он находится в другом компоненте, «подневолен» и настроен рисовать себя через буфер своего

Рисование в Swing

97

компонента-предка. Если мы создадим новый компонент Swing, не добавляя его на экран, он увидит, что предков у него нет, и решит, что является первым компонентом в иерархии компонентов, пытаясь нарисовать себя через объект RepaintManager и экранный буфер. Так как компонент на самом деле на экране не находится, то и содержимое буфера скопировать будет некуда и прорисовка не получится. В этом случае компоненты Swing приходится настраивать дополнительно: отключать для таких компонентов двойную буферизацию. Тогда они рисуют себя напрямую и появляются на экране.

Интересно, что в свежих версиях JDK отключение буферизации не требуется, так как она и так отключена и заменена на буферизацию системного уровня. Однако

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

вконтейнеры.

Рассмотрим небольшой пример с прорисовкой компонентов:

//PaintingOtherComponents.java

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

import java.awt.*;

public class PaintingOtherComponents extends JFrame { public PaintingOtherComponents() {

super("PaintingOtherComponents"); setDefaultCloseOperation(EXIT_ON_CLOSE); add(new CustomPaintComponent()); setSize(400, 300);

setVisible(true);

}

class CustomPaintComponent extends JPanel { // кнопка для прорисовки

private JButton button = new JButton("Привет!"); // метод для рисования в Swing

protected void paintComponent(Graphics g) {

//необходимо вызвать для обработки свойства opaque super.paintComponent(g);

//рисуем кнопки

Graphics2D g2 = (Graphics2D) g; button.setSize(80, 30);

//отключение двойной буферизации — не всегда нужно button.setDoubleBuffered(false);

//переместим позицию рисования

g2.translate(100, 100); for (int i=1; i<=8; i++) {

// кручение кнопки по кругу g2.rotate(2*Math.PI / i);

98

ГЛАВА 4

button.paint(g);

}

}

}

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

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

}

}

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

сграфическим объектом Graphics все, на что хватит фантазии.

Впримере мы будем рисовать кнопку JButton с помощью стандартных средств AWT для рисования, поворачивая ее вокруг своей начальной точки на полный круг. Для этого предназначен метод rotate(), которому необходимо задать угол поворота в радианах. Кнопка создается одна, а нарисовать ее на экране можно бесчисленное количество раз. Нужно лишь не забыть задать ей корректный размер методом setSize() и, настроив по вкусу свойства рисования в объекте Graphics, вызвать метод paint(). Обратите вни-

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

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

Рисование в Swing

99

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

Рисование вне рамок

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

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

RepaintManager как прикладной инструмент

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

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

Таким образом, можно использовать свой объект RepaintManager для контроля каких-либо критичных параметров в процессе рисования или же определять, какой

100

ГЛАВА 4

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

//RotatedUI.java

//Кручение и верчение стандартных компонентов import javax.swing.*;

import java.awt.*;

public class RotatedUI extends JFrame { public RotatedUI() {

super("RotatedUI");

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

//добавляем особую панель

RotatingPanel rp = new RotatingPanel(); add(rp);

//добавляем в панель компоненты rp.add(new JButton("Привет!")); rp.add(new JTextField(20));

//устанавливаем свой RepaintManager RepaintManager.setCurrentManager(

new RotatingRepaintManager());

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

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

}

// компонент, который поворачивает всех потомков class RotatingPanel extends JPanel {

// отвечает за прорисовку потомков protected void paintChildren(Graphics g) {

Graphics2D g2 = (Graphics2D) g; g2.translate(50, 200);

//поворот на 45 градусов g2.rotate(-Math.PI/4);

//небольшое растяжение g2.shear(-0.1, -0.1);

//обычное рисование предков super.paintChildren(g);

Рисование в Swing

101

}

}

// особый тип RepaintManager

class RotatingRepaintManager extends RepaintManager { // все запросы на перерисовку попадают сюда

public void addDirtyRegion(JComponent c, int x, int y, int w, int h) {

// ищем нужного предка

Container parent = c;

while (! (parent instanceof RotatingPanel)) { parent = parent.getParent();

if ( parent == null ) {

// мы не нашли нашего предка, сброс parent = c;

break;

}

}

// перерисовываем весь компонент полностью super.addDirtyRegion((JComponent) parent,

0, 0, parent.getWidth(), parent.getHeight());

}

}

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

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

}

}

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

Если остановиться на этом и запустить пример, то поначалу интерфейс будет прокручен и растянут, однако любое обновление кнопки или поля приведет к тому, что они будут рисовать себя привычным образом. Замена стандартного объекта RepaintManager на собственный позволяет нам перехватывать желания кнопки и поля. Наш объект унаследован от стандартного и переопределяет метод addDirtyRegion(), который вызывается при любом запросе любого компонента Swing на перерисовку. Мы смотрим, не находится ли компонент в нашей «крутящей» панели, и, если да, просто полностью перерисовываем ее, а если нет, позволяем рисовать оригинальному «просителю». Производительность перерисовки при таком грубом подходе конечно упадет, но это просто пример. Запустив его, вы убедитесь, что интерфейс выглядит более чем авангардно.

102

ГЛАВА 4

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

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

Отладка графики

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

Класс DebugGraphics — это тонкая оболочка вокруг стандартного класса рисования Graphics. Всю работу по рисованию, как и прежде, выполняет Graphics, а DebugGraphics по вашему желанию замедляет графические операции и выделяет их специальным цветом (который потом стирается) или выводит о них диагностические сообщения6. Можно использовать DebugGraphics и напрямую, создав объект этого класса и передав

6 На языке шаблонов проектирования класс DebugGraphics представляет собой не что иное, как типичный декоратор.

Рисование в Swing

103

в его конструктор ссылку на стандартный объект Graphics, после чего производить рисование полученным объектом. Указать при этом, какой способ отладки графики вы предпочитаете, позволяет метод setDebugGraphicsOptions(). Однако базовый класс JComponent имеет встроенную поддержку класса DebugGraphics, и, чтобы включить для компонента Swing режим отладки графики, надо всего лишь указать способ отладки, вызвав тот же метод setDebugGraphicsOptions(), но на этот раз уже для самого компонента. После этого компонент и все его потомки будут использовать для рисования объект DebugGraphics. В качестве параметра методу setDebugGraphicsOptions() передается целое число, представляющее собой одну из констант, определенных в классе DebugGraphics, или комбинацию этих констант, составленную с помощью операции логического «ИЛИ» (табл. 4.1).

Таблица 4.1. Параметры метода setDebugGraphicsOptions()

Константа из класса

Действие

DebugGraphics

 

NONE_OPTION

Отключает режим отладки графики для используемого объекта

 

DebugGraphics

LOG_OPTION

Позволяет выводить диагностические сообщения обо всех про-

 

изводимых графических операциях (то есть обо всех вызы-

 

ваемых методах). По умолчанию сообщения выводятся в стан-

 

дартный поток вывода System.out, изменить поток вывода со-

 

общений позволяет статический метод класса DebugGraphics

 

setLogStream()

FLASH_OPTION

Установка этого флага приводит к тому, что все появляющееся на

 

экране рисуется в замедленном режиме и выделяется особым цве-

 

том, так что вы своими глазами можете проследить, как и где про-

 

исходит рисование. Настроить данный режим позволяют три стати-

 

ческих метода класса DebugGraphics: метод setFlashColor() устанав-

 

ливает цвет, которым выделяются все операции рисования, метод

 

setFlashCount() позволяет задать количество «мерцаний» при рисо-

 

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

 

указать задержку после каждой операции рисования. Имейте в виду,

 

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

 

феризации

BUFFERED_OPTION

В данном режиме операции рисования будут дополнительно вы-

 

водиться в отдельное окно, создаваемое классом DebugGraphics.

 

При этом учитываются остальные режимы отладки — они будут

 

действовать в созданном окне

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

//DebugPainting.java

//Демонстрация возможностей отладки графики в Swing import java.awt.*;

import javax.swing.*;

public class DebugPainting extends JFrame { public DebugPainting() {

super("DebugPainting");

104

ГЛАВА 4

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

//добавляем рисующий компонент

PaintingComponent pc = new PaintingComponent(); add(pc);

//включаем для него отладку графики

RepaintManager.currentManager(pc). setDoubleBufferingEnabled(false);

pc.setDebugGraphicsOptions(DebugGraphics.LOG_OPTION | DebugGraphics.FLASH_OPTION);

DebugGraphics.setFlashTime(50);

DebugGraphics.setFlashCount(3);

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

setSize(200, 200); setVisible(true);

}

// компонент, который что-то рисует class PaintingComponent extends JPanel {

public void paintComponent(Graphics g) { super.paintComponent(g);

// три простые фигуры g.setColor(Color.orange); g.fillRect(10, 10, 100, 100); g.setColor(Color.green); g.drawOval(50, 50, 50, 50); g.setColor(Color.blue); g.fillOval(100, 20, 50, 50);

}

}

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

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

}

}

В примере мы создаем небольшое окно, в которое добавляем собственный компонент, унаследованный от класса JPanel. Интересно, что если бы мы унаследовали компонент обычным образом, непосредственно от класса JComponent, система отладки графики работать бы с ним не стала. Связано это с тем, что у базового класса библиотеки нет собственного UI-представителя — ведь это абстрактный класс, его нельзя добавить в окно, и UIпредставитель ему просто не нужен. В обычных программах это не причиняет неудобств, однако система отладки графики отказывается работать с компонентом, у которого нет