Рисование на канве компонентов
Традиционно в программировании канвой называют область компонента, на которой можно рисовать графические примитивы, текст, а также отображать готовые изображения. Каждая точка канвы имеет координаты x и y . Начало системы координат (точка с координатами 0, 0 ) находится в верхне-левом углу канвы. Координата x возрастает при перемещении слева направо, а координата y — при перемещении сверху вниз. Координаты измеряются в пикселях, наименьших элементах поверхности изображения, с которыми можно манипулировать. Важнейшее свойство пикселя — его цвет.
В отличие от других библиотек для создания графического интерфейса пользователя, Juce, как уже упоминалось ранее, позволяет рисовать не только на клиентской области окна компонента, но и на его заголовке, т.е. канвой является вся поверхность компонента.
За перерисовку компонента отвечает виртуальная функция virtual void Component::paint(Graphics& g) , с которой мы уже знакомы по примерам из предыдущих лекций. В классах, наследуемых от Component , эта функция должна быть переопределена для того, чтобы отобразить их содержимое.
До сих пор мы использовали этот метод лишь для того, чтобы производить заливку цветом фона компонента с помощью функции void Graphics::fillAll() const , которая, как мы видим, является методом класса Graphics .
Класс Graphics представляет собой платформ-независимый контекст рисования для прорисовки компонента или изображения. В каждом компоненте Juce есть хотя бы один объект этого класса, который передаётся в функцию virtual void Component::paint(Graphics& g) в качестве параметра. Понятно, что класс обладает большим набором методов для рисования фоновых изображений компонентов, не ограничивающихся фоновой заливкой цветом. Он содержит методы для рисования линий, прямоугольников, окружностей и др. Рассмотрим их на простых примерах.
Рисование точек
Для отображения точек используется метод класса Graphics void Graphics::setPixel(int x, int y) const , который закрашивает пиксель с координатами x и y текущим цветом. В листинге 7.1 приведён пример отображения восьми точек, расположенных по кругу (далее в этой главе для краткости в листингах будет приводиться лишь реализация метода paint ).
#include "TCentralComponent.h" //------------------------------------------------------ #include // Функции вычисления синуса и косинуса //------------------------------------------------------ TCentralComponent::TCentralComponent() : Component ("Central Component") < setSize(100, 100); >//------------------------------------------------------ TCentralComponent::~TCentralComponent() < >//------------------------------------------------------ void TCentralComponent::paint(Graphics& Canvas) < // Фон виджета - белый Canvas.fillAll(Colours::white); // Цвет рисуемых точек - чёрный Canvas.setColour(Colours::black); int iPointsNumber = 8; // Число точек // Вычисляем координаты точек for(int i = 0; i < iPointsNumber; ++i) < float fAngle = 2 * float_Pi * i / iPointsNumber; int iX = 50 + cos(fAngle) * 40; int iY = 50 + sin(fAngle) * 40; // Рисуем точку Canvas.setPixel(iX, iY); >> //------------------------------------------------------ void TCentralComponent::resized() < >//------------------------------------------------------
Листинг 7.1. Пример рисования точек на канве компонента (файл TCentralComponent.cpp)
Внешний вид работающей программы показан на рисунке 7.1 .
Рис. 7.1. Программа, рисующая восемь точек на канве компонента содержимого
Обратите внимание, что для расчёта координат точек мы использовали константу float_Pi из заголовочного файла juce_MathsFunctions.h. В этом же файле объявлена константа double_Pi , отличающаяся от первой, как понятно из названия, точностью числа .
Как называется метод для перерисовки содержимого компонента
Как показывает опыт (и статистика вопросов пользователей), неизменно возникают вопросы и путаница когда речь заходит о том, как же Java Swing непосредственно рисует пикселы на экране, когда, и в каком порядке? Выдержки из книги в виде этой статьи помогут вам быстро разобраться в этом.
В любой библиотеке, предназначенной для построения графических интерфейсов, важнейшим является процесс вывода содержимого непосредственно на экран. Любое добавление к внешнему виду приложения требует знать этот процесс в деталях. Система рисования Swing обосновалась в классе JComponent и его помощнике RepaintManager, и основана на уже имеющейся системе рисования AWT. Все это мы рассмотрим в деталях, чтобы ни один пиксел на экране не был для нас загадкой.
Проверка корректности (валидация) компонентов, процесс довольно простой, однако на практике иногда упорно «показывающий зубы», также получил свою интерпретацию в классе JComponent. Мы рассмотрим, как проверка производится классически, и узнаем механику работы метода revalidate() .
Система рисования
Прежде чем говорить о деталях системы рисования, использованной в библиотеке Swing, имеет смысл увидеть, как рисование происходит в базовой библиотеке AWT. Именно она связывает абстрактные вызовы Java и реальные действия операционной системы на экране.
Во всех современных операционных системах процесс рисования происходит примерно одинаково. При этом ваша программа играет пассивную роль и терпеливо ожидает, когда настанет нужный нужного момента. Момент этот определяют механизмы операционной системы, и настает он, когда необходимо перерисовать часть окна, принадлежащего вашему приложению, например, когда окно впервые появляется на экране или прежде скрытая его часть открывается глазам пользователя. Операционная система при этом определяет, какая часть окна нуждается в перерисовке, и вызывает специальную часть вашей программы, отвечающую за рисование, либо посылает вашему приложению сообщение о перерисовке, которое вы при необходимости обрабатываете. Частью программы в графических компонентах AWT, которая вызывается системой для прорисовки компонента, является метод paint() .
Программная, или принудительная, перерисовка, также необходима – вам необходимо иметь возможность вручную указывать системе, что пора заново нарисовать тот или иной фрагмент вашего экрана. Этого требует анимация или любые динамические изменения в интерфейсе, не ждать же, в самом деле, пока операционная система соизволит перерисовать ваше окно. Перерисовку позволяет вызвать еще один доступный всем компонентам AWT метод repaint() .
Проще всего оценить схему рисования, использованную по умолчанию в Java и AWT, на простой диаграмме, и мы сразу увидим всю ее подноготную:
Отталкиваться приходится от того, что графическая система хранит все свои события в очереди (мы выяснили это в подробностях в Главе 2), чтобы избежать мусора на экране, вызванного его непоследовательным обновлением. Вызовы о прорисовке того или иного фрагмента экрана также необходимо разместить в очереди, и делается это с помощью специального события PaintEvent.
В первом варианте призыв нарисовать фрагмент экрана присылает операционная система, когда, по ее мнению, он был «поврежден», то есть свернут, закрыт другим окном и т.п. Если в этот фрагмент входит системный компонент (тот самый, что представлен компонентами AWT, такими как кнопки Button или списки List), он перерисовывает себя сам, и выглядит именно так, как ему положено в данной операционной системе. После этого помощник (peer) компонента или системная часть Java создаст событие PaintEvent с типом PAINT, укажет в нем, какую область экрана необходимо перерисовать и в каком компоненте и поместит его в очередь событий EventQueue.
Программная перерисовка много проще и никаких системных вызовов не касается. В методе repaint() просто создается событие PaintEvent с типом UPDATE, указывается компонент (тот самый для которого и был вызван repaint() ) и область перерисовки (ее можно указать вручную или будет использован весь размер компонента), и также помещается в очередь событий.
Вспоминая архитектуру событий Swing, логично было бы подумать, что для получения сигналов о прорисовке можно присоединять слушателей типа PaintEventListener, которые затем оповещаются при рассылке событий из методов dispatchEvent() и processPaintEvent() . Однако, обрабатывать сигналы о прорисовке как обычные события мы не можем. Вместо этого события PaintEvent обрабатываются самой системой (в помощниках), когда поток рассылки событий «вытаскивает» их из очереди и передает в метод dispatchEvent() компонента, к которому они принадлежат. Для событий типа PAINT вызывается метод 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()); >public static void main(String[] args) < new AWTPainting().setVisible(true); >>
В примере создается окно с рамкой Frame (мы наследуем свой класс от него), в нем мы в качестве менеджера расположения используем последовательное расположение FlowLayout и добавляем кнопку Button c незамысловатым текстом. И в окне, и в кнопке процедура прорисовки компонента paint() заменена на нашу собственную. Окно полностью закрашивается красным цветом, а кнопка, не считая маленького ободка, синим (мы попросту стираем ее текст).
Запустив пример, мы ничего, кроме красочных цветов, не увидим, самое интересное происходит в движении, для чего размер окна надо увеличивать. Легко видеть что окно закрашивается полностью – каждый раз вызывается paint() , в котором методы getWidth() и getHeight() возвращают новые актуальные размеры.
Метод paint() — резюме
Итак, в методе paint() размещается код, прорисовывающий компонент. Причем учитывать что именно в компоненте поменялось, не обязательно, так как в метод передается графический контекст Graphics, которому уже задан прямоугольник отсечения (clip) (переданный или системой, или из метода repaint() ). За пределами этого прямоугольника прорисовка не производится и время не нее не затрачивается.
Добавлять к рисованию системных компонентов AWT свои детали не стоит – слишком неопределенна система взаимодействия системной процедуры прорисовки и метода paint() . Как правило, системный компонент будет «прорываться» через нарисованное вами, а то и вообще не даст ничего на себе нарисовать. Специально для рисования в AWT предусмотрен компонент-«холст» Canvas.
Если вам понадобится сменить какие-либо глобальные параметры графического объекта Graphics, например включить сглаживание, изменить прямоугольник отсечения, а ваш компонент может содержать другие компоненты, особенно легковесные, создавайте копию объекта, вызывая метод create() класса Graphics. В противном случае все ваши настройки перейдут по наследству всем компонентам, которые могут прорисовываться после вашего компонента. При том, имеется одна деталь — после завершения рисования для такого объекта придется явно вызвать метод dispose() , иначе ресурсы системы рисования могут быстро закончиться. Чтобы это гарантировать, обычно применяют блок try-finally :
Graphics2D g2d = (Graphics2D) g.create(); try < // код рисования, преобразований и т.п. . >finally
Метод repaint() – пара дополнений
Как мы увидели, метод программной перерисовки repaint() для стандартных компонентов AWT вызывает сначала метод update() . Когда-то создатели AWT полагали, что это поможет реализовать эффективную технику инкрементальной прорисовки. Это значило что каждый раз когда вы вызывали repaint() , в методе update() к уже прорисованному компоненту можно было добавить какие-то детали, а потом перейти к основной картине в методе paint() (установив предварительно нужный прямоугольник отсечения (clip), чтобы не пропало то что было только что нарисовано).
Однако такой подход редко требуется, а эффективную прорисовку можно осуществить и просто ограничив область рисования в методе paint() (применяя тот самый прямоугольник отсечения). Так что задумка создателей AWT не удостоилась внимания масс.
Самой же главное рекомендацией остается вызов repaint() с максимально суженной областью перерисовки компонента. Это особенно верно для сложных, наполненных динамическим содержимым и анимацией компонентов, так как это приносит огромную экономию по времени прорисовки. Как мы сейчас увидим, Swing старается взять большую часть этой задачи на себя, однако где возможно, все равно стоит отслеживать минимальную область прорисовки самим.
Рисование легковесных компонентов
Как обрабатывается рисование системных компонентов, мы только что увидели. Однако они настолько редко применяются (и мерцание при прорисовке еще раз доказывает что не зря), что их можно расценивать лишь как приятное дополнение к легковесным компонентам. Как мы знаем, легковесный компонент представляет собой область экрана тяжеловесного контейнера. Для того чтобы он мог правильно отображаться на экране, ему также необходимо получать от системы прорисовки вызовы своего метода paint() , однако операционная система, заведующая этим процессом в AWT, ничего не знает о существовании легковесных компонентов, она видит лишь «родные» тяжеловесные компоненты и контейнеры, которым и отправляет запросы на перерисовку. Решение здесь очевидно — нужно встроить поддержку легковесных компонентов в тяжеловесные контейнеры, и именно так поступили создатели AWT.
Все контейнеры в AWT (а значит и в Swing) унаследованы от своего базового класса Container. Именно там легковесные компоненты и становятся полноправными участниками процесса вывода на экран. Никаких хитростей здесь нет, нужно лишь четко очертить круг участников этого процесса. Итак – легковесный компонент – это нечто, унаследованное от класса Component, и не связанное с операционной системой помощником (peer). Как он выглядит на экране, определяет исключительно его рисующий метод paint() . Эти компоненты можно добавлять в контейнер (любой, лишь бы он был унаследован от класса Container).
В контейнерах поддерживается иерархия компонентов, выстроенная «по оси Z» (z-order). Они как бы нанизываются на ось Z, устремленную от нас. Первый компонент имеет индекс 0, второй 1, и так далее. Рисование идет в обратном порядке, так что первый добавленный компонент всегда закрывает остальные, если они перекрывают друг друга. Диаграмма окончательно все прояснит:
В итоге, когда операционная система запрашивает прорисовку принадлежащего ей тяжеловесного контейнера (в случае приложения Swing это будет как правило окно JFrame либо диалог JDialog), вызывается его метод paint() (мы только что выяснили это в предыдущем разделе). В нем, согласно порядку добавления компонентов, от последнего к первому, происходят следующие шаги:
- Устанавливается область отсечения для рисования (clip), соответствующая области, занимаемой легковесным компонентов в контейнере.
- Для прорисовываемого компонента в объекте Graphics выставляются текущие цвета фона и шрифта (берутся из контейнера, впрочем использовать их компоненту совсем не обязательно).
- Устанавливается текущий шрифт (из контейнера)
- Настроенный объект Graphics передается в метод paint() легковесного компонента, который и рисует себя (а фактически, просто участвует в процессе прорисовки все того же тяжеловесного контейнера).
Идея проста – каждый легковесный компонент вносит свою лепту в процесс рисования контейнера, в то время как операционная система считает что контейнер просто рисует себя. Так как установлена область отсечения, легковесный компонент никогда не сможет «залезть» в область, которая ему не принадлежит, и пририсовать там что-то лишнее. Так как рисование идет от последних добавленных компонентов к первым, первые всегда будут перекрывать последние в случае пересечения.
Отсюда же возникает чудесная способность легковесных компонентов быть прозрачными и принимать любые формы – их никто не заставляет хоть что-то рисовать и не обязывает закрашивать фоновым цветом занимаемым на экране прямоугольник, как это происходит с тяжеловесными компонентами. Таким образом, внутри тяжеловесного компонента руки у нас развязаны и компонентам можно придать любые формы, хотя конечно же по сути они остаются прямоугольными.
Когда-то тяжеловесные компоненты выпадали из этой идиллии, если их добавляли в тот же контейнер. Вне зависимости от их позиции по оси Z, они перекрывали легковесные компоненты, и их не рекомендовалось совмещать их в одном контейнере во избежание проблем с прорисовкой и расположением на экране. Сейчас эта проблема наконец исправлена и вы можете делать это свободно.
Давайте рассмотрим простой пример и убедимся в справедливости всего сказанного:
// AWTLightweights.java // Использование легковесных компонентов в AWT import java.awt.*; import java.awt.event.*; public class AWTLightweights extends Frame < public AWTLightweights() < super("AWTLightweights"); // при закрытии окна приложение завершается addWindowListener(new WindowAdapter() < public void windowClosing(WindowEvent e) < System.exit(0); >>); // добавляем пару легковесных компонентов LightweightRect rect1 = new LightweightRect(Color.BLUE, true); LightweightRect rect2 = new LightweightRect(Color.RED, true); LightweightRect transparentRect = new LightweightRect(Color.BLACK, false); // укажем координаты вручную, чтобы компоненты // перекрывались setLayout(null); rect1.setBounds(40, 40, 100, 100); rect2.setBounds(50, 50, 100, 100); transparentRect.setBounds(35, 35, 150, 150); add(transparentRect); add(rect1); add(rect2); // последним добавляем тяжеловесный компонент Button button = new Button("Тяжелая!"); button.setBounds(50, 175, 80, 30); add(button); // выводим окно на экран setSize(250, 250); setVisible(true); > // легковесный компонент - цветной прямоугольник class LightweightRect extends Component < private Color color; private boolean fill; // параметы - цвет и нужно ли зарисовывать всю область public LightweightRect(Color color, boolean fill) < this.color = color; this.fill = fill; >public void paint(Graphics g) < g.setColor(color); if (fill) g.fillRect(0, 0, getWidth() - 1, getHeight() - 1); else g.drawRect(0, 0, getWidth() - 1, getHeight() - 1); >> public static void main(String[] args) < new AWTLightweights(); >>
В примере в качестве тяжеловесного контейнера используется окно Frame, от которого мы наследуем. Легковесным компонентом выступит класс LightweightRect, который унаследован от базового класса Component и переопределяет метод для рисования paint() . Это прямоугольник, которому через конструктор можно задать цвет и свойство заполнения цветом — будет он закрашен или нет(нарисуется просто рамка).
Чтобы заставить компоненты перекрываться, придется отказаться от менеджера расположения (применяя метод setLayout(null) — подробнее о расположении компонентов рассказывается в главе 7), так как они не рассчитаны на перекрывание компонентов. Мы просто задаем позицию компонентов и их размеры методом setBounds() . Порядок добавления компонентов определяет их место и «первенство» на экране. Сначала идет незакрашенный прямоугольник, затем два закрашенных, и в последнюю очередь тяжеловесная кнопка Button. Несмотря на то, что прозрачный прямоугольник должен закрыть оба закрашенных, последние прекрасно видны — все благодаря прозрачности легковесных компонентов. Закрашенные прямоугольники перекрываются согласно старшинству — наверху тот, что был добавлен первым. Тяжеловесная кнопка находится в самом низу, так как была добавлена последней, то есть имеет самую дальнюю позицию по оси Z. Однако прозрачность легковесного компонента на нее не действует, и вы легко это увидите, запустив пример. Причины кроются во внутренних механизмах библиотеки AWT, ну а нам придется аккуратнее совмещать тяжеловесные и легковесные компоненты, если это все же понадобится.
Для полноты картины остается добавить, что для легковесных компонентов немного изменилось поведение метода repaint() . Суть его осталась прежней — он все также помещает в очередь событий сообщение о необходимости перерисовки, но происходит это не напрямую из метода repaint() легковесного компонента. Когда вы вызываете repaint() для легковесного компонента, он переадресует запрос контейнеру, в котором содержится, указав в качестве области перерисовки ту область, что он занимает в контейнере. Так продолжается до тих пор, пока запрос не достигнет тяжеловесного контейнера (легковесные компоненты могут содержаться в легковесных контейнерах, так что тяжеловесный контейнер можно искать долго). В итоге запрос на перерисовку в очередь событий помещает именно тяжеловесный контейнер, причем перерисовка «заказывается» лишь для области нужного легковесного компонента. Это позволяет сократить затраты, потому что легковесный компонент может занимать лишь малую часть контейнера.
Легковесные компоненты: резюме
Легковесные компоненты не доставляют проблем, если помнить некоторые детали:
- Легковесные компоненты хранятся в контейнере в порядке добавления (по оси Z), и рисуются наоборот – значит, те компоненты, что добавлены сначала, имеют на экране приоритет.
- Легковесные компоненты могут быть прозрачны или рисовать себя в любой форме – через незатронутые области могут «просвечивать» другие компоненты или фон контейнера.
- Метод repaint() работает для легковесных компонентов – но за кулисами перерисовывается часть тяжеловесного контейнера. Метод update() не вызывается.
- Если вы переопределяете контейнер и его метод paint() , и планируете затем добавлять в него другие компоненты, не забудьте вызвать базовый метод super.paint() , иначе о прорисовке легковесных компонентов придется позаботиться самостоятельно.
Система рисования Swing
По большому счету, легковесные компоненты ничем не ограничены, и создать с их помощью, не мудрствуя лукаво, даже такую богатую библиотеку, как Swing, не представляет собой технических трудностей. Однако во времена зарождения Swing пользовательские интерфейсы Java страдали от проблем с производительностью. Богатые возможностями, иногда даже прозрачные компоненты Swing просто сделали бы из пользовательского интерфейса мало удобоваримого тихохода. Действительно, все легковесные компоненты рисуются один за другим, вне зависимости от того, есть ли среди них непрозрачные области, и каждая операция с графикой — это долгая операция по обращения к «родному» коду операционной системы. Оптимизация была необходима как воздух, и оптимизация серьезная.
Отличие системы рисования Swing от стандартной состоит в оптимизации и поддержке UI-представителей. Другими словами, библиотека Swing в своей системе рисования руководствуется всего тремя принципами, которые позволяют нам избавиться от лишней работы и дают уверенность в том, что рисование максимально оптимизировано:
- Кэшируй
- Разделяй и властвуй
- С глаз долой, из сердца вон
Кэширование
Стратегия кэширования, применяемая в Swing, крайне проста и стара как мир. Если устройство выводит данные на экран слишком медленно (не то чтобы сейчас медленные видеокарты, но помните что в Java это вызовы «родного» кода), необходимо заранее подготовить все в быстром хранилище-буфере (в памяти), а затем одним махом (одной операцией) записать все в устройство (на экран). В качестве буфера применяется область в памяти в формате экрана, по возможности — область в памяти видеокарты. Буфер этот хранит вспомогательный класс для рисования в Swing под названием RepaintManager. Он же проверяет, что буфер корректен, при необходимости заново создает его и выполняет тому подобную техническую работу. Также этот класс позволяет компонентам запросить прорисовку в буфер с последующим выводом на экран.
Интересная и часто ведущая к проблемам при сложной прорисовке особенность компонентов Swing состоит в том, что они «общаются» друг с другом посредством системы флагов. Вся эта система встроена в базовый класс JComponent. Таким образом, рассчитывать на то, что компоненты Swing являются автономными сущностями, каждый со своей жизнью и процедурой прорисовки (как по большому счету это обстоит в AWT), не стоит. Посмотрим, как рисуется компонент Swing, добавленный в тяжеловесный контейнер:
- Операционная система просит окно заново нарисовать содержимое
- Окно проходит по списку содержавшихся в нем легковесных компонентов и находит там компонент Swing
- Вызывается метод paint() . Чтобы встроить оптимизацию во все компоненты Swing, метод paint() в них не переопределяется, и таким образом, работу всегда выполняет базовый класс JComponent.
- Если (согласно флагам), это первый вызванный для рисования компонент Swing, то он запрашивает рисование через буфер у класса RepaintManager. Тот настраивает буфер и вызывает метод paintToOffscreen() вызвавшего его компонента, передавая ему объект Graphics для рисования в памяти. Выставляется флаг, который отныне говорит, что рисование пошло в буфер.
- Снова вызывается метод paint() , но так как флаг рисования в буфер уже выставлен, RepaintManager больше не применяется. Вызываются «рабочие» методы прорисовки компонента, рамки и компонентов-потомков (об этих методах чуть ниже).
- Компоненты-потомки рисуют своих потомков (напрямую, согласно флагам), и так далее до полной прорисовки всей иерархии данного компонента Swing.
- После окончания этой процедуры управление (согласно шагу 4) возвращается в RepaintManager, которые копирует изображение компонентов из буфера на реальный экран.
Данная процедура верна для любого компонента Swing. Как видно, наиболее удачным решением было бы наличие одного компонента, который занимал бы всю площадь тяжеловесного контейнера, а значит, именно этот компонент и был бы единственным участником, передающим управление от AWT в Swing, и копировать изображение из буфера на экран пришлось бы только один раз. Именно так и сделано, а роль такого компонента в контейнерах высшего уровня Swing играет корневая панель JRootPane.
Кэширование (буферизацию) графики в Swing можно отключить либо методом RepaintManager.setDoubleBufferingEnabled() , либо непосредственно на компоненте методом setDoubleBuffered() . Правда, смысл выключения двойной буферизации для отдельных компонентов немного меняется. Мы уже видели, что компонент рисует с помощью буфера, если буфер используется его родительским компонентом, независимо от того, включена или выключена двойная буферизация для него самого. Если все компоненты находятся в корневой панели, выключать двойную буферизацию имеет смысл только для нее (это будет равносильно выключению двойной буферизации для всех компонентов, находящихся внутри этого контейнера высшего уровня). Учтите, что с появлением в AWT, на- чиная с версий Java 1.4, системной поддержки буферизации, система Swing может быть отключена во избежание дублирования. С другой стороны, отключать оптимизацию самостоятельно приходится редко, один случай мы вскоре рассмотрим, а вообще лучше довериться Swing.
Разделение обязанностей
Как видно, метод paint() в компонентах Swing занят довольно-таки важным делом — он кэширует вывод графики компонентов на экран, пользуясь помощью класса RepaintManager. Переопределять этот метод в каждом компоненте чтобы нарисовать его больше нельзя — мы помним про систему флагов и что метод paint() действует по разному в зависимости от ситуации. Здесь создатели Swing применили лозунг «разделяй и властвуй» — рисование отныне производится в других методах, а метод paint() является носителем внутренней логики библиотеки и переопределять его без дела не следует. Это значительно упрощает обновление компонентов и изменение их внешнего вида, позволяя вам при создании нового компонента не думать о том, как реализовать для него эффективный вывод графики. Вы просто рисуете свой компонент, оставляя низкоу- ровневые детали механизмам базового класса. Рисование компонента в Swing, в отличие от простой до невозможности процедуры в библиотеке AWT, разбито на три этапа, и за каждый отвечает свой метод в классе JComponent. Как раз эти методы вы будете переопределять, если вам захочется нарисовать по-своему что-либо на компоненте Swing.
Метод paintComponent()
Метод paintComponent() вызывается при прорисовке компонента первым, и именно он рисует сам компонент. Разница между ним и классическим методом paint() , используемым в AWT, состоит в том, что вам не нужно заботиться ни об оптимизации рисования, ни о правильной прорисовке своих компонентов-потомков. Обо всем этом позаботятся механизмы класса JComponent. Все, что вам нужно сделать, — нарисовать в этом методе компонент и оставить всю черновую работу базовому классу.
Как вы помните, в Swing используется немного модифицированная архитектура MVC, в которой отображение компонента и его управление выполняются одним элементом, называемым UI-представителем. Оказывается, что прорисовка компонента с помощью UI-представителя осуществляется именно из метода paintComponent() , определенного в базовом классе JComponent. Действует метод очень просто: определяет, есть ли у компонента UI-представитель (не равен ли он пустой ссылке null) и, если представитель есть, вызывает его метод update() . Метод update() для всех UI- представителей работает одинаково: по свойству непрозрачности проверяет, нужно ли закрашивать всю свою область цветом фона, и вызывает метод paint() , определенный в базовом классе всех UI-представителей — классе ComponentUI. Последний метод и рисует компонент. Остается лишь один вопрос: что такое свойство непрозрачности?
Мы отмечали, что одним из самых впечатляющих свойств легковесных компонентов является их способность быть прозрачными. Однако при написании библиотеки создатели Swing обнаружили, что набор из нескольких десятков легковесных компонентов, способных «просвечивать» друг сквозь друга, приводит к большой загрузке системы рисования и соответствующему замедлению работы программы. Действительно, перерисовка любого компонента оборачивалась настоящей каторгой: сквозь него просвечивали другие компоненты, которые задевали еще одни компоненты, и так могло продолжаться долго. В итоге, перерисовка даже небольшой части одного компо- нента приводила к перерисовке доброго десятка компонентов, среди которых могли оказаться и очень сложные. С другой стороны, компоненты вроде текстовых полей или кнопок редко бывают прозрачными, и лишняя работа для них совершенно не к чему. Так и появилось свойство непрозрачности (opaque), имеющееся у любого компонента Swing.
Если в AWT любой легковесный компонент автоматически считается прозрачным, то в Swing все сделано наоборот. Свойство непрозрачности определяет, обязуется ли компонент закрашивать всю свою область, чтобы избавить Swing от дополнительной работы по поиску и прорисовке всего того, что находится под компонентом. Если свойство непрозрачности равно true (а по умолчанию оно равно true), то компонент обязан закрашивать всю свою область, иначе на экране вместо него появится мусор. Дополнительной работы здесь немного: всего лишь необходимо зарисовать всю свою область, а облегчение для механизмов прорисовки получается значительное. Ну а если вы всетаки решите создать компонент произвольной формы или прозрачный, вызовите для него метод setOpaque( false), и к вам снова вернутся все чудесные возможности легковесных компонентов — система прорисовки будет предупреждена. Однако злоупотреблять этим не стоит: скорость прорисовки такого компонента значительно падает. Во многом из-за этого в Swing не так уж и много компонентов, имеющих прозрачные области.
Вернемся к методу paintComponent() . Теперь роль его вполне очевидна: он прорисовывает компонент, по умолчанию используя для этого ассоциированного с компонентом UI-представителя. Если вы собираетесь создать новый компонент с собственным UI-представителем, то он будет прекрасно вписываться в эту схему. Унаследуйте своего UI-представителя от базового класса ComponentUI и переопределите метод paint() , в котором и рисуйте компонент. Базовый класс позаботится о свойстве непрозрачности. Если же вам просто нужно что-либо нарисовать, унаследуйте свой компонент от любого подходящего вам класса (лучше всех для этого подходят непосредственно классы JComponent или JPanel, потому что сами они ничего не рисуют) и переопределите метод paintComponent() , в котором и рисуйте. Правда, при таком подходе нужно позаботиться о свойстве непрозрачности (если оно равно true) самостоятельно: потребуется закрашивать всю область прорисовки или вызывать перед рисованием базовую версию метода super.paintComponent() .
Метод paintBorder()
Благодаря методу paintBorder() в Swing имеется такая замечательная вещь, как рамка (border). Для любого компонента Swing вы можете установить рамку, используя метод setBorder() . Оказывается, что поддержка рамок целиком и полностью обеспечивается методом paintBorder() класса JComponent. Он вызывается вторым, после метода paintComponent() , смотрит, установлена ли для компонента какая-либо рамка, и если рамка имеется, прорисовывает ее, вызывая определенный в интерфейсе Border метод paintBorder() . Единственный вопрос, который при этом возникает: где именно рисуется рамка? Прямо на пространстве компонента или для нее выделяется отдельное место? Ответ прост — никакого специального места для рамки нет. Она рисуется прямо поверх компонента после прорисовки последнего. Так что при рисовании компонента, если вы не хотите неожиданного наложения рамки на занятое место, учитывайте место, которое она занимает. Как это делается, мы узнаем в главе 8, часть которой полностью посвящена рамкам.
Переопределять метод paintBorder() вряд ли стоит. Работу он выполняет нехитрую, и как-либо улучшить ее или коренным образом изменить не представляется возможным. Если вам нужно создать для своего компонента фантасмагорическую рамку, лучше воспользоваться услугами интерфейса Border или совместить несколько стандартных рамок.
Метод paintChildren()
Заключительную часть процесса рисования выполняет метод paintChildren() . Как вы помните, при обсуждении легковесных компонентов в AWT мы отмечали, что для их правильного отображения в контейнере, если вы переопределили их методы paint() , необходимо вызвать базовую версию paint() из класса Container, иначе легковесные компоненты на экране не появятся. Базовый класс JComponent библиотеки Swing унаследован от класса Container и вполне мог бы воспользоваться его услугами по прорисовке содержащихся в нем компонентов-потомков. Однако создатели Swing решили от услуг класса Container отказаться и реализовали собственный механизм прорисовки потомков. Причина проста — по сравнению с AWT компоненты Swing намного сложнее и требуют иного подхода. Улучшенный оптимизированный механизм и реализуется методом paintChildren() . Для придания ему максимальной скорости компоненты Swing используют два свойства: уже известное нам свойство непрозрачности opaque, а также свойство isOptimizedDrawingEnabled.
Метод paintChildren() действует по алгоритму, который был слегка вольнодумно назван нами «с глаз долой, из сердца вон». Он получает список содержащихся в компоненте потомков и начинает перебирать их, используя при этом текущий прямоугольник отсечения. Основной задачей его является нахождение «слепых зон», то есть зон, где компоненты закрываются друг другом. Если компонент закрыт или вообще не попадает в область отсечения, то зачем его рисовать — никто труда не заметит. Можно нарисовать только тот компонент, что виден сверху.
На этом этапе «вступают в бой» два вышеупомянутых свойства. С первым свойством все более или менее понятно: если свойство непрозрачности компонента равно true, это означает, что, сколько бы компонентов не находилось под ним и не пересекало бы его, он обязуется закрасить всю свою область, а значит продолжать поиск потомков в этой области не имеет смысла — их все равно не будет видно. Теперь понятно, почему так важно выполнять требование заполнения всей области экрана при использовании свойства непрозрачности: в противном случае на экране неизбежен мусор, который по договоренности должен убирать сам компонент, а не система прорисовки. Со свойством isOptimizedDrawingEnabled картина немного другая.
Данное свойство определено в классе JComponent как предназначенное только для чтения: вы не можете изменить его, кроме как унаследовав собственный компонент и переопределив метод isOptimizedDrawingEnabled() . По умолчанию для большинства компонентов свойство isOptimizedDrawingEnabled равно true. Это позволяет снизить загрузку системы прорисовки. Проще говоря, это свойство гарантирует, что из под одними потомками не «просвечивают» другие и не задевают их, при этом на них не наложен дополнительный прозрачный компонент и т. д. Когда речь идет о простых компонентах, это свойство приносит небольшие дивиденды, однако, если у вас есть сложные и медленно рисующиеся компоненты, оно значительно ускоряет процесс. Когда свойство isOptimizedDrawingEnabled равно true, метод paintChildren() просто перебирает потомков и перерисовывает их поврежденные части, не разбираясь, что они собой представляют и как друг с другом соотносятся. Данное свойство переопределяют лишь три компонента: это многослойная панель JLayeredPane, рабочий стол JDesktopPane и область просмотра JViewport. В них компоненты-потомки часто перекрываются и требуют особого внимания.
Общая диаграмма рисования в Swing
Теперь, когда все детали оптимизации и разделения прорисовки в Swing нам извест- ны, мы можем все объединить в несложную диаграмму, на которую можно смотреть если вдруг что-то на экране перестанет рисоваться как нужно:
Программная перерисовка в Swing
Если вспомнить как мы рассматривали программную перерисовку в AWT (метод repaint() ), то легко видеть что это по сути постановка события на перерисовку (PaintEvent) в очередь событий интерфейса. Именно эта простая идея подтолкнула создателей Swing включить оптимизацию и здесь, на этот раз применив принцип пакетной обработки.
Глядя на очередь, очевидно, что каждое событие из очереди выполняется некоторое время, и пока дело дойдет до нашего события на перерисовку PaintEvent, в очереди могут появится новые события такого же типа. Весьма вероятно, что они относятся не только к тому же окну, в котором была запрошена перерисовка в первый раз, но и даже к одному и тому же компоненту, и возможно, последующие события делают первые и вовсе ненужными, потому что полностью зарисуют то, что нарисуют первые. Вообразите активную прокрутку большой таблицы – подобный процесс будет генерировать огромное количество вызовов метода repaint() , и следующие вызовы уже будут перерисовывать области, которые должны бы были нарисованы первыми вызовами. Определенно здесь можно оптимизировать процесс и избавиться от лишнего рисования.
Метод repaint() переопределен в классе JComponent и вместо прямой постановки события PaintEvent в очередь событий просит нашего старого знакомого RepaintManager добавить область, которую нужно перерисовать, в список «загрязненных» (dirty region). «Загрязненные» области нужно перерисовать, как только до них дойдут руки потока рассылки событий.
RepaintManager объединяет все приходящие запросы на прорисовку «загрязненных» областей в один пакет, ставя в очередь событий особое событие, которое при срабатывании разом перерисует все накопившиеся (за время ожидания исполнения этого особого события, ведь очередь событий может быть занята) «грязные» области. Таким образом, мы избавляемся от избыточных событий. В дополнение к этому, в классе RepaintManager области для прорисовки оптимизируются – они объединяются в одну область для компонента, области, которые закрыты другими компонентами или более не видны на экране, выбрасываются из списка прорисовки и так далее.
Следующая схема прекрасно иллюстрирует что происходит и какие классы участвуют в процессе:
Вспомогательный класс SystemEventQueueUtilities ставит то самое особое событие (его имя ComponentWorkRequest) в очередь, используя метод для постановки postEvent() . События PaintEvent в Swing вообще не применяются. Как только поток рассылки доходит до данного события, оно выполняется, и вызывает метод все того же RepaintManager с названием paintDirtyRegions() . Опуская детали реализации, мы приходим к тому, что вызывается метод, определенный в базовом классе JComponent под названием paintImmediately() . Ну а в нем уже все совсем просто – в итоге создается графический объект методом getGraphics() , и вызывается прекрасно знакомый нам метод paint() . Работу этого метода в Swing мы изучили в деталях чуть ранее, и все выполняется по той же самой диаграмме, что мы составили. Итог – компонент Swing перерисовывается, причем лишь один раз за определенный промежуток времени, и максимально оптимизировано.
Рисование в Swing: резюме
Подводя итоги всей системы рисования Swing, можно определить следующие самые важные выводы:
- Метод paint() в Swing является частью внутренней системы и выполняет важную работу. Переопределять его для рисования компонента Swing не следует.
- Для рисования компонента или просто графики применяется метод paintComponent() .
- При рисовании в Swing необходимо учитывать свойство непрозрачности opaque. Если компонент непрозрачен, необходимо закрашивать его фон или не оставлять непрорисованных областей. Делается это либо вручную, либо вызовом super. paintComponent() , чтобы фон закрасил UI-представитель. Однако, второй способ не работает для компонентов, унаследованных напрямую от JComponent. Если не выполнить данное условие, мусор на экране неизбежен.
- Перерисовка repaint() в Swing выполняется пакетами и оптимизируется. Если вас это не устраивает, придется переопределить или метод repaint() , или написать свой вариант класса RepaintManager. Впрочем, необходимости в этом практически не возникает.
Проверка корректности компонентов
Проверка корректности компонентов требуется для того, чтобы расположение компонентов в контейнере и их размеры соответствовали действительности. На самом деле, если вы прямо во время работы программы увеличите размер шрифта на кнопке, то прежнего размера кнопки (которого, как правило, в аккурат хватает для размещения надписи) уже будет недостаточно, и часть надписи окажется непонятно где.
Если компонент инициализируется перед выводом на экран, то последовательность действий четко определена – вы создаете его, задаете свойства компонента, в том числе те, что влияют на его внешний вид, к примеру размер шрифта и значков, а затем добавляете в некоторый контейнер. В контейнере работает менеджер расположения, который выясняет, какой размер желает иметь компонент, и выделяет ему пространство на экране, где его можно будет увидеть, как только приложение выведет контейнер высшего уровня на экран.
Однако, если контейнер уже на экране, а вы меняете свойство содержащегося в нем компонента, влияющее на его внешний вид, а самое главное, размеры, для обновления контейнера, его размеров и размеров всех соседей-компонентов необходимо провести проверку корректности (валидацию), которая заново распределит пространство на экране и перерисует компоненты в их новом состоянии.
В AWT, в котором, как мы помним, компоненты лаконичны до крайности, каждый из них хранит булевский флаг valid, показывающий, находится ли сейчас компонент в корректном состоянии. Как только происходит нечто, нарушающее размеры компонента (к примеру, смена шрифта или текста), флаг устанавливает в false специальным методом invalidate() . Так как обычно компонент хранится в контейнере, а тот в другом контейнере, и так далее, изменение его размеров влияет на все содержащие его контейнеры. Чтобы пометить этот факт, метод invalidate() вызывает точно такой же метод контейнера, в котором компонент хранится, и все идет по цепочке до самого верха.
Сам же процесс приведения к корректному виду осуществляется методом validate() . Для обычных компонентов AWT, таких как кнопки или надписи, он просто перерисовывает компонент. А вот для контейнеров все сложнее: метод заново вызовет менеджер расположения контейнера и перераспределит пространство между компонентами в контейнере и изменит их размеры согласно их новым желаниям, вызовет для всех компонентов в контейнере их метод validate() , а потом и перерисует сам контейнер. Так что, если вы хотели привести конкретный компонент к нужному размеру, вряд ли стоило вызывать для него validate() , если только вы уже не задали ему новый подходящий размер вручную – это будет равносильно его перерисовке. В общем же всегда вызывается метод validate() того контейнера, в котором находится измененный компонент. Стоит заметить, что validate() работает лишь тогда, когда компоненты уже выведены на экран.
Рассмотрим небольшой пример:
// Базовая валидация AWT - при изменении размеров // или других параметров остается вызвать validate() import java.awt.*; public class AWTValidateShow extends Frame < private static Button button; public AWTValidateShow() < setSize(400, 300); Panel contents = new Panel(); button = new Button("Текст"); Button button2 = new Button("Текст 2"); contents.add(button); contents.add(button2); add(contents); >public static void main(String[] args) throws InterruptedException < new AWTValidateShow().setVisible(true); Thread.sleep(2000); button.setLabel("Очень длинный текст"); // С этого момента размер поменялся - вызван invalidate() // можно вызывать validate() в контейнере Thread.sleep(2000); // будет заново расположен весь контейнер // и все его содержимое (кнопка) button.getParent().validate(); >>
В примере мы создаем окно и помещаем в него в панель с двумя кнопками. Обратите внимание, пример написан исключительно с использованием AWT, так что беспокоиться о многозадачности нам не придется – компоненты AWT синхронизированы, и мы можем напрямую вызывать из других потоков, отличных от потока рассылки событий. Маленький нюанс примера – окна AWT не умеют сами закрывать себя, а мы не написали слушателя окна, который заканчивал бы приложение при закрытии окна, так что приложение это придется заканчивать вручную, снимая задачу.
Когда у кнопки меняется надпись, она помечает себя и все содержащие ее контейнеры как некорректные. Нам же остается вызвать validate() для содержащего ее контейнера (мы получаем его методом getParent() ), чтобы он получил данные о новом размере кнопки и заново расположил ее. В примере специально вставлены задержки – вы успеете увидеть, как выглядит некорректная кнопка. В качестве упражнения попробуйте вызвать validate() только для кнопки, чтобы убедиться, что это не поможет ей обрести корректный размер.
Метод Swing: revalidate()
Одной из основных задач Swing является более быстрая и эффективная работа компонентов библиотеки, в сравнении с AWT. Проверка корректности не стала исключением. Прежде всего, компоненты Swing намного более сложны, и некоторые из них в любом случае «поглотят» любые изменения содержащихся в них компонентов. К примеру, это внутренние окна JInternalFrame (все, что меняется в окне, остается в его рамках) или панель прокрутки JScrollPane (изменение размеров содержимого лишь меняет полосы прокрутки, но не размер самой панели). Однако мы увидели, что при изменении компонентов некорректными становятся все компоненты, вплоть до контейнера высшего уровня – так устроен метод invalidate() .
Чтобы каким-то образом помечать компоненты, на которых проверку корректности можно остановить, в базовом классе JComponent появился метод isValidateRoot() . Те компоненты, которые утверждают, что все изменения их содержимого не отразятся на их размерах (и значит, размерах всего окружающего), возвращают в этом методе true (к ним относится JScrollPane и корневая панель JRootPane, которая является основой любого окна в Swing). Такие компоненты мы называем корнем валидации. По умолчанию метод возвращает false.
Как мы уже убедились (в описании прорисовки), Swing старается всегда скомпоновать похожие события графической системы, что минимизировать издержки. Эта же схема применена и для проверки корректности, и делает это новый метод revalidate() из класса JComponent. Он не проводит мгновенной проверки корректности, вместо этого, он помечает компонент как некорректный в классе RepaintManager. Тот, в свою очередь, находит корень валидации для компонента, и помещает в очередь событий отложенное задание для окна, в котором этот корень валидации находится (все это происходит только в том случае, если компонент уже выведен на экран, до вывода на экран в проверке просто нет смысла). Когда очередь до этого события дойдет, в данном корне валидации и данном окне могут накопиться несколько компонентов, требующих проверки корректности, и выполнение проверки за один раз позволяет сократить издержки.
Само же задание выполняет проверку корректности уже знакомым нам методом validate() из AWT. Он вызывается для корня валидации, а это значит, что все находящиеся в нем компоненты будут заново расположены с учетом их новых пожеланий по размерам. Компоненты, которые находятся в контейнерах «выше» корня валидации, проверяться таким образом не будут, что сэкономит драгоценные секунды.
Таким образом эффект revalidate() коренным образом отличается от validate() . Если validate() нужно вызывать с умом, выбирая тот контейнер, проверка которого заново расположит все нужные нам компоненты, то revalidate() совершенно безразличен к тому, откуда его вызывают. Он в любом случае найдет корень валидации и вызовет проверку именно для него, что гарантирует нам нужный результат. Более того, большая часть компонентов Swing вызывает revalidate() при смене свойств, меняющих внешний вид и размер, автоматически, что совсем избавляет нас от раздумий, свойственных системе проверки корректности в AWT.
// Валидация Swing - большинство компонентов // позаботятся о себе сами. В остальном метод revalidate() // позволяет не задумываться о деталях import javax.swing.*; public class SwingValidateShow extends JFrame < private static JButton button, newButton; public SwingValidateShow() < setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); setSize(400, 300); JPanel contents = new JPanel(); button = new JButton("Текст"); JButton button2 = new JButton("Текст 2"); contents.add(button); contents.add(button2); add(contents); >public static void main(String[] args) throws InterruptedException < SwingUtilities.invokeLater(new Runnable() < public void run() < new SwingValidateShow().setVisible(true); >>); Thread.sleep(2000); // Кнопка при смене параметра сама вызовет // revalidate() и мы сразу же увидим изменения SwingUtilities.invokeLater(new Runnable() < public void run() < button.setText("Очень длинный текст"); >>); // при добавлении в контейнер revalidate() // автоматически не вызывается SwingUtilities.invokeLater(new Runnable() < public void run() < newButton = new JButton("Новичок"); button.getParent().add(newButton); >>); Thread.sleep(2000); // revalidate() может быть вызван из любого потока newButton.revalidate(); > >
В этом примере мы снова используем Swing, а значит должны работать со всеми компонентами из потока рассылки событий. Как и в примере с AWT, создается окно с двумя кнопками. Однако, Swing тут же доказывает свое удобство, автоматически проверяя корректность кнопки при смене ее надписи методом setText() . Затем мы добавляем в панель с кнопками еще одну кнопку, что не приведет к автоматической проверке. Чтобы ее провести, мы вызываем revalidate() для самой кнопки. Это найдет корень валидации (корневую панель нашего окна) и заново расположит все компоненты в ней. Для какого компонента вызывается revalidate() , не так уж и важно.
Сам метод revalidate() может быть безопасно вызван из любого потока – он автоматически заботится о том, чтобы дальнейшие действия происходили в потоке рассылке событий.
На деле пример оказывается не так прост, как кажется. Если вам повезет, вы сможете увидеть как новая кнопка появляется в окне сразу же, еще до вызова revalidate() . Секрет тут прост: в таких случаях мы успеваем добавить ее еще до того, как выполнилась задача проверки корректности, запущенная методом setText() , и она «прихватывает» с собой и новый компонент в панели.
Интересно, что удобство Swing, благодаря которому компоненты сами вызывают revalidate() при смене своих размеров, является чуть ли не главной причиной того, что даже до вывода приложения на экран с компонентами можно работать только из потока рассылки событий. На самом деле, revalidate() помещает задание в очередь событий (это не так лишь если компонент пока не добавлен в контейнер), а это автоматически запускает поток рассылки. Насколько это оправдывает себя, вопрос спорный. Иногда возможность заранее подготовить части сложного интерфейса в фоновом режиме бывает очень кстати, но Swing это сделать не позволяет.
Проверка корректности в Swing: резюме
Соединяя вместе правила работы классического метода validate() и более изощренного revalidate() , мы получаем следующие выводы:
- revalidate() эффективнее и пытается объединить похожие события в пакет. Однако вы не можете рассчитывать на мгновенный эффект, и должны быть уверены в том, что среди контейнеров, в которых находится ваш компонент, найдется корень валидации ( isValidateRoot() возвращает true). В противном случае проверки не произойдет.
- validate() проводит проверку немедленно, но только среди потомков контейнера, для которого вы его вызываете. Контейнер нужно выбрать правильно, чтобы привести к корректному виду все нужные вам компоненты. Для Swing этот метод можно вызывать только из потока рассылки событий.
- При разработке собственных сложных компонентов, не меняющих свои размеры (к примеру, легковесных плавающих диалогов), вам может пригодится метод isValidateRoot() . Правильный выбор корня валидации увеличит производительность, не «пуская» валидацию за пределы нужного контейнера. Это важно в Swing, если помнить что практически любое изменение моделей или свойств стандартных компонентов автоматически запускает revalidate() .
- Оба этих метода сработают только лишь если компонент, который вы хотите привести к корректному виду, уже находится на экране и виден. Действительно, для невидимых компонентов нужные размеры будут заданы в момент их вывода на экран.
RepaintManager как прикладной инструмент
Подводя итоги рассмотрения механизмов рисования Swing, мы видим, какую се- рьезную роль играет в них класс RepaintManager. Весь процесс рисования компонентов Swing, благодаря особой реализации базового класса JComponent, проходит через его двойную буферизацию, он также полностью заведует программной перерисовкой ком- понентов, оптимизируя ее. С этой точки зрения он всего лишь внутренний «винтик» Swing, однако с учитом того, что все «нити» при рисовании сходятся в одно место, мы получаем уникальную возможность вмешаться в рабочий процесс рисования, так как можем заменить экземпляр класса RepaintManager на свой собственный, методом setCurrentManager() .
Прежде чем задуматься о собственной реализации, важно узнать, какие дополни- тельные реализации этого класса уже существуют. Прежде всего, это инструменты, которые мы обсуждали в предыдущей главе, для слежения за тем, чтобы все рисо- вание в Swing велось из потока рассылки событий. Обращение к экземпляру класса RepaintManager из другого потока сразу же обозначает ошибку программиста и необ- ходимость вынести вызывающий код в поток рассылки событий. Настолько же инте- ресно применяет свой экземпляр класса RepaintManager библиотека вспомогательных инструментов SwingX. В ней существует панель, позволяющая настроить уровень про- зрачности, как свой, так и всех содержащихся в ней компонентов. Однако, как мы знаем, при необходимости компоненты сами перерисовывают друг друга, и со сторо- ны никак нельзя «заставить» их рисовать себя полупрозрачными. Решением является особый объект RepaintManager, который при запросе на перерисовку компонентов, на- ходящихся в прозрачной панели, направляет их самой полупрозрачной панели так, чтобы она смогла настроить все параметры рисования и только потом нарисовать потомков.
Таким образом, можно использовать свой объект RepaintManager для контроля каких-либо критичных параметров в процессе рисования или же определять, какой компонент будет перерисовываться, вне зависимости от того, кто запросил перери- совку. Чтобы убедиться в том, что все сказанное не пустые слова, напишем малень- кий, но интересный, пример. В нем мы попытаемся создать панель, которая будет крутить и растягивать все компоненты, попадающие в нее. Чтобы компоненты не могли рисовать себя сами нормальным образом, необходимо будет перехватывать их запросы на перерисовку и вместо этого направлять их нашей панели. Вот что по- лучится:
// 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); >> // особый тип 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 на перерисовку. Мы смотрим, не находится ли компонент в нашей «крутящей» панели, и, если да, просто полностью перерисовы- ваем ее, а если нет, позволяем рисовать оригинальному «просителю». Производитель- ность перерисовки при таком грубом подходе конечно упадет, но это просто пример. Запустив его, вы убедитесь, что интерфейс выглядит более чем авангардно.
Компоненты также будут реагировать на клавиатуру, печать клавиш, будет мерцание кур- сора — все это можно незамедлительно видеть растянутым и повернутым. Однако события от мыши не знают о наших манипуляциях с графикой и попрежнему будут приходить из оригинального местоположения компонентов в левом верхнем углу экрана. Впрочем, зада- ча преобразования координат для мыши при поворотах интерфейса уже совсем другая.
Как мы видим, власть у RepaintManager имеется порядочная. Однако не стоит забы- вать, что на все Swing-приложение имеется лишь один объект RepaintManager, и он может быть уже заменен такой библиотекой, как SwingX или даже сторонним внешним видом. Использовать свой собственный объект RepaintManager стоит в случае крайней необхо- димости и при этом тщательно проверять, в полном ли объеме работают все дополни- тельные библиотеки, компоненты и инструменты. Стандартные же компоненты Swing без труда переносят замену объекта RepaintManager .
Comments
© All rights reserved. Powered by Hugo and Minimal
Как называется метод для перерисовки содержимого компонента
После создания компонента фреймворк Angular вызывает у этого компонента ряд методов, которые представляют различные этапы жизненного цикла:
- ngOnChanges : вызывается до метода ngOnInit() при начальной установке свойств, которые связаны механизмом привязки, а также при любой их переустановке или изменении их значений. Данный метод в качестве параметра принимает объект класса SimpleChanges , который содержит предыдущие и текущие значения свойства.
- ngOnInit : вызывается один раз после установки свойств компонента, которые участвуют в привязке. Выполняет инициализацию компонента
- ngDoCheck : вызывается при каждой проверке изменений свойств компонента сразу после методов ngOnChanges и ngOnInit
- ngAfterContentInit : вызывается один раз после метода ngDoCheck() после вставки содержимого в представление компонента кода html
- ngAfterContentChecked : вызывается фреймворком Angular при проверке изменений содержимого, которое добавляется в представление компонента. Вызывается после метода ngAfterContentInit() и после каждого последующего вызова метода ngDoCheck() .
- ngAfterViewInit : вызывается фреймворком Angular после инициализации представления компонента, а также представлений дочерних компонентов. Вызывается только один раз сразу после первого вызова метода ngAfterContentChecked()
- ngAfterViewChecked : вызывается фреймворком Angular после проверки на изменения в представлении компонента, а также проверки представлений дочерних компонентов. Вызывается после первого вызова метода ngAfterViewInit() и после каждого последующего вызова ngAfterContentChecked()
- ngOnDestroy : вызывается перед тем, как фреймворк Angular удалит компонент.
Каждый такой метод определен в отдельном интерфейсе, который называется по имени метода без префикса «ng». Например, метод ngOnInit определен в интерфейсе OnInit . Поэтому, если мы хотим отслеживать какие-то этапы жизненного цикла компонента, то класс компонента должен применять соответствующие интерфейсы:
import < Component, OnInit, OnDestroy >from '@angular/core'; @Component(< selector: 'my-app', template: `Hello Angular 2
` >) export class AppComponent implements OnInit, OnDestroy < name:string="Tom"; constructor()< this.log(`constructor`); >ngOnInit() < this.log(`onInit`); >ngOnDestroy() < this.log(`onDestroy`); >private log(msg: string) < console.log(msg); >>
ngOnInit
Метод ngOnInit() применяется для какой-то комплексной инициализации компонента. Здесь можно выполнять загрузку данных с сервера или из других источников данных.
ngOnInit() не аналогичен конструктору. Конструктор также может выполнять некоторую инициализацию объекта, в то же время что-то сложное в конструкторе делать не рекомендуется. Конструктор должен быть по возможности простым и выполнять самую базовую инициализацию. Что-то более сложное, например, загрузку данных с сервера, которая может занять продолжительное время, лучше делать в методе ngOnInit .
ngOnDestroy
Метод ngOnDestroy() вызывается перед удалением компонента. И в этом методе можно освобождать те используемые ресурсы, которые не удаляются автоматически сборщиком мусора. Здесь также можно удалять подписку на какие-то события элементов DOM, останавливать таймеры и т.д.
ngOnChanges
Метод ngOnChanges() вызывается перед методом ngOnInit() и при изменении свойств в привязке. С помощью параметра SimpleChanges в методе можно получить текущее и предыдущее значение измененного свойства. Например, пусть у нас будет следующий дочерний компонент:
import < Component, Input, OnInit, OnChanges, SimpleChanges >from '@angular/core'; @Component(< selector: 'child-comp', template: `Привет >
` >) export class ChildComponent implements OnInit, OnChanges < @Input() name: string = ""; constructor()< this.log(`constructor`); >ngOnInit() < this.log(`onInit`); >ngOnChanges(changes: SimpleChanges) < for (let propName in changes) < let chng = changes[propName]; let cur = JSON.stringify(chng.currentValue); let prev = JSON.stringify(chng.previousValue); this.log(`$: currentValue = $, previousValue = $`); > > private log(msg: string) < console.log(msg); >>
И пусть этот компонент используется в главном компоненте:
import < Component, OnChanges, SimpleChanges>from '@angular/core'; @Component( ` >) export class AppComponent implements OnChanges < name:string="Tom"; age:number = 25; ngOnChanges(changes: SimpleChanges) < for (let propName in changes) < let chng = changes[propName]; let cur = JSON.stringify(chng.currentValue); let prev = JSON.stringify(chng.previousValue); this.log(`$: currentValue = $, previousValue = $`); > > private log(msg: string) < console.log(msg); >>
То есть значение для свойства name передается в дочерний компонент ChildComponent из главного — AppComponent. Причем в главном компоненте тоже реализован метод ngOnChanges() .
И если мы запустим приложение, то сможем заметить, что при каждом изменении свойства name в главном компоненте вызывается метод ngOnChanges:
В то же время надо отметить, что данный метод вызывается только при изменении входных свойств с декоратором @Input . Поэтому изменение свойства age в AppComponent здесь не будет отслеживаться.
Реализация всех методов
Определим следующий дочерний компонент:
import < Component, Input, OnInit, DoCheck, OnChanges, AfterContentInit, AfterContentChecked, AfterViewChecked, AfterViewInit>from '@angular/core'; @Component(< selector: 'child-comp', template: `Привет >
` >) export class ChildComponent implements OnInit, DoCheck, OnChanges, AfterContentInit, AfterContentChecked, AfterViewChecked, AfterViewInit < @Input() name: string = ""; count:number = 1; ngOnInit() < this.log(`ngOnInit`); >ngOnChanges() < this.log(`OnChanges`); >ngDoCheck() < this.log(`ngDoCheck`); >ngAfterViewInit() < this.log(`ngAfterViewInit`); >ngAfterViewChecked() < this.log(`ngAfterViewChecked`); >ngAfterContentInit() < this.log(`ngAfterContentInit`); >ngAfterContentChecked() < this.log(`ngAfterContentChecked`); >private log(msg: string) < console.log(this.count + ". " + msg); this.count++; >>
И используем этот компонент в главном компоненте:
import < Component>from ‘@angular/core’; @Component( ` >) export class AppComponent
И при обращении к приложению мы получим следующую цепочку вызовов:
Простое и понятное введение в React Native
React Native является относительно простым фреймворком для разработки кроссплатформенных приложений для iOS и Android на JavaScript, но во многих введениях по нему материал даётся сложным и запутанным образом. Перед началом работы нужно что-то устанавливать, настраивать, выяснять необходимость наличия компьютер Mac и прочее. Официальная документация написана неплохо, но введение в React Native можно сделать проще и понятнее.
В самом простом случае для создания и тестирования приложений React Native требуется только браузер и Интернет-соединение. В официальной документации есть возможность редактировать код примеров и сразу видеть результат изменений, но для экспериментов удобнее использовать браузерную игровую площадку Expo Snack, при помощи которой можно создавать проекты и тестировать их в браузере, на эмуляторе или устройстве.
Нюансы фреймворка мы будем рассматривать на примерах из документации, чтобы в дальнейшем вам было удобнее с ней работать.
Рассмотрим простой пример.
Для создания приложений в React Native используется расширенный JavaScript стандарта ES2015.
import React from ‘react’;
необходима для поддержки JSX. JSX — это синтаксис, позволяющий встраивать XML в JavaScript. Если строку импорта убрать, то блок кода
будет помечен как ошибочный.
происходит подключение к проекту модуля Component. Без этой строки при создании класса пришлось бы указать export React.Component.
Далее в коде создаётся класс HelloWorldApp, который является компонентом. Обратите внимание на то, что наш класс содержит в себе дочерние компоненты — View и Text. Это нужно понимать при работе с контекстом.
В React Native используется компонентный подход для создания приложений, согласно которому приложение рекомендуется составлять из повторно используемых компонентов, код каждого из которых располагается в отдельных файлах. React Native поставляется с готовыми встроенными компонентами, функциональность которых можно расширить при помощи собственных пользовательских компонентов, как показано в примере ниже.
Здесь LotsOfGreetings является родительским пользовательским компонентом, а Greeting — дочерним пользовательским компонентом, который выводит приветствие в зависимости от параметра name.
— это структурный элемент, соответствующий компоненту Greeting.
В нашем случае простой компонент располагается в главном файле приложения, но с увеличением функциональности приложения компоненты размещают в отдельных файлах и подключают к главному файлу приложения при помощи директивы import.
Передача данных от родительского компонента дочернему производится посредством параметров props. Когда родитель перерисовывает дочерний компонент, то он отправляет ему параметры. Они доступны через this.props.
Фигурные скобки <> используются для вставки кода javascript в JSX структуру. Блочный javascript комментарий в JSX также необходимо заключить в фигурные скобки:
Если требуется расширить функциональность какого либо встроенного компонента, например, Button, то для этого мы не раздуваем его функциональность при помощи добавления атрибутов и свойств, как это происхоит в html и javascript, а создаём новый компонент, наследующийся от базового Component. Можно написать и extends Button, но это не приведёт к наследованию от кнопки и создания кастомной кнопки. Внешний вид компонента и его функциональность задаются явно, а не наследуются от родительского класса, как будет видно далее.
Вернёмся к исходному примеру.
import < Text, View >from ‘react-native’;
подключает к проекту классы компонентов, использующиеся в нём: Text — текстовая область и View — контейнерный компонент.
В каждом классе компонента необходимо определить метод render(), предназначенный для отрисовки его содержимого. Данный метод должен вернуть либо элемент React, либо null, если ничего отрисовывать не нужно.
В контексте React Native компоненты являются виджетами — объектами, имеющими видимое представление. Под отрисовкой (рендерингом) компонента понимается преобразование движком React Native разметки JSX в нативный вид путём вызова соответствующих методов API. В результате на экране устройства мы видим нативный интерфейс, а не его имитацию средствами web. При этом полученный интерфейс на iOS и Android будет отличаться. То есть, данный фреймворк не позволяет создать интерфейс, одинаково отображающийся и на iOS, и на Android.
Вся разметка компонента должна находиться в одном корневом контейнерном элементе. Следующий код вызовет ошибку:
Для устранения ошибки второй контейнерный элемент нужно вложить в первый.
Контейнерные компоненты могут содержать внутри себя другие компоненты. В контексте JSX контейнерный элементом является тот, который может отображать содержимое вложенных в него элементов. Например, элемент может содержать между открывающим и закрывающим тегом другие элементы, но это не делает его контейнером, так как данное содержимое не отображается.
Контейнерные и неконтейнерные элементы можно задать в форме самозакрывающегося тега:
Настройка компонентов происходит при помощи параметров двух видов — props (начальные неизменяемые параметры) и state (изменяемое состояние). Props часто называют свойствами, но это вносит путаницу так как под свойствами мы привыкли понимать изменяемую сущность. Тогда что это, атрибут, реквизит, свойство для чтения? Props следует понимать как параметры, которые можно передать компонентам в момент их создания и которые остаются фиксированными на протяженни всего жизненного цикла компонента. В примере
title является встроенным обязательным параметром, который нужно указать. Значение данного параметра можно получить, но его изменение не изменит надписи на кнопке:
this.props.title = «Новая кнопка»; // надпись на кнопке останется прежней — «Кнопка»
Более того, при перерисовке компонента (которое происходит при каждом изменении его состояния, о чём будет рассказано далее, изменении контекста или получении props от родителя) значения параметров сбрасываются в начальное состояние. Например, мы решили ввести свой параметр myParams:
Где-то в коде можно будет обратиться к нему и изменить его значение:
В другом месте кода можно будет его использовать, но при перерисовки компонента значение this.params.myParams останется равным «100».
Наряду со встроенными параметрами, которые изначально имеют встроенные компоненты, можно использовать пользовательские параметры при создании пользовательского компанента:
, где name — пользовательский параметр
или в конструкторе компонента:
constructor(props) super(props)
this.props.title = ‘myTitle’
>
Для доступа к параметру внутри JSX используются фигурные скобки:
Как изменить значение параметра (например, title), если оно является неизменяемым? Для этого используется состояние state. Данные, которые нужно изменить, необходимо преобразовать в состояния:
title — это параметр, а myTitle — это состояние, определённое в конструкторе так:
Имя состояния можно не заключать в кавычки. Если состояний несколько, то они перечисляются через запятую:
this.state = ‘myTitle’:’Показать приветствие’,
‘name’:’Mate’,
‘age’:’25’
>
Преимущество данной записи состоит в том, что в случае организации цикла по свойствам компонента, в объект state попадут все указанные состояния. Если каждое состояние будет задано в отдельной строке при помощи this.state, то в выборку попадёт состояние, определённое последним.
Для получения состояни используется запись this.state.myTitle, а для изменения состояния используется метод setState:
Изменение состояния должно производиться только при помощи указанного метода. При любом изменении состояния компонента вызывается его метод render(). Каждое каждое лишнее действие в данном методе будет влиять на скорость отрисовки. При помощи специального метода можно откелючить перерисовку компонента.
В терминах React Native компонент может иметь много изначально заданных параметров (props), но находить в каждый момент времени в одном состоянии (state). При работе с компонентом мы меняем его состояние, а не параметры (или свойства). Предположим, в момент создания кнопка имеет какую-то надпись. Это значит то, что кнопка находится в состоянии отображения данной надписи. Если при помощи состояния надпись или её цвет были изменены, то это означает то, что теперь кнопка находится в другом состоянии с изменённой надписью или её цветом. Аналогичным образом можно описать состояние всех компонентов в проекте.
Атрибут ref
Вернёмся к кнопке:
Текущее значение параметра title в нашем случае можно получить при помощи состояния this.state.myTitle. Но как получить значение параметра title, если ему была присвоена строка? Для этого используется атрибут ref, принимающий функцию обратного вызова, параметром которого является ссылка на компонент, в котором она используется:
Теперь ссылка this.myButton будет указывать на компонент Button и можно будет получить значение его параметра title так:
или обратиться к методу компонента:
Рассмотрим более сложный пример c использованием стилей и событий.
Стили похожи на CSS, но фреймворк не использует CSS, хотя и заимствует многое из web разработки. Стиль представляет собой объект. При использовании встроенного стиля он заключается в двойные фигурные скобки, так как объекты в javascript заключаются в фигурные скобки:
Для вызова событий можно использовать обычный вызов функции, функцию стрелки и привязку.
У функции стрелки отсутствует свой контекст, поэтому внутри данной функции this будет таким же, как и снаружи. При вызове обычной функции внешний контекст this не передаётся и, соответственно, внутри функции нельзя обратиться к параметрам и состоянию компонента. Если это и не требуется, то можно использовать вызов функции без привязки контекста:
Если внутри функции требуется контекст, то используется вызов с привязкой:
Здесь под this понимается контекст родительского компонента, а не дочернего элемента , как может показаться. В методе _onPress компонента невозможно получить значение параметра title элемента . Для ссылки на параметр элемента (дочернего компонента) необходимо получить ссылку на него при помощи атрибута ref, как было показано выше.
Для вызова функции с контекстом и параметрами последние перечисляются после this через запятую:
Пользовательские аргументы передаются функции перед системными. При возникновении события onValueChange система передаёт функции обработки один параметр, в котором находится название выбранной опции компонента Picker. Тогда в первом параметре функции updateUser будет находится строка ‘Привет’, а во-втором — название выбранной опции.
Для уменьшения накладных расходов привязку можно заранее сделать в конструкторе и использовать состояние в качестве параметра:
constructor(props) super(props);
this.state = ;
this.updateUser = this.updateUser.bind(this,this.state.name);
>
Обратите внимание на то, что при использовании функции стрелки можно получить оба параметра, передаваемых системой обработчику события onValueChange — название выбранной опции и её индекс, тогда как использовании обычной функции системой передаётся один параметр — название выбранной опции.
При изменении выбранного элемента Picker система вызывает встроенный метод onValueChange. Аналогичным образом в дочернем компоненте можно создать свой метод и затем вызвать его. Для этого создаём метод:
_onMySend = (par) => Alert.alert(par)
>
Добавляем его дочернему элементу в качестве параметра, например, onSend:
А где-нибудь в дочернем компоненте вызываем наш метод:
Упростим вызов метода при помощи деструктуризации объекта:
const = this.props;
onSend(‘Параметр 1’)
Параметры props используются не только для хранения в них неизменяемых данных, но и в качестве ссылок на методы. Проще говоря, в элементе:
title и onPress являются параметрами props.
Как динамически добавить или удалить компонент? Для этого можно изменить состояние компонента, сделав его видимым или невидимым, или настроить отображение компонентов в функции отрисовки render(), которая в зависимости от того или иного условия будет отрисовывать нужную компонентную структуру. Разметка в React Native производится при помощи Flexbox.
В основе React Native лежит JavaScript и кажется, что это позволяет использовать такие же трюки и фокусы, как и в web разработке. Но это не так. И на мой взгляд основная сложность фреймворка как раз и заключается в понимании этого. На JavaScript объекты, методы и свойства можно создавать «на лету», есть возможность напрямую взаимодействовать со свойствами и др., а здесь используется традиционный подход к разработке приложений, при котором работа происходит в рамках определённой компонентной структуры, а не динамически создаваемой и загружаемой, как в web среде.