Глава 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()
}