Частая причина путаницы в Swing: paint(), repaint(), paintComponent(), revalidate()

November 30, 2011
java swing paint validate Graphics RepaintManager

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

Если вам понадобится более глубокое погружение в 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 {
        g2d.dispose();
      }

Метод 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() (мы только что выяснили это в предыдущем разделе). В нем, согласно порядку добавления компонентов, от последнего к первому, происходят следующие шаги:

Идея проста – каждый легковесный компонент вносит свою лепту в процесс рисования контейнера, в то время как операционная система считает что контейнер просто рисует себя. Так как установлена область отсечения, легковесный компонент никогда не сможет «залезть» в область, которая ему не принадлежит, и пририсовать там что-то лишнее. Так как рисование идет от последних добавленных компонентов к первым, первые всегда будут перекрывать последние в случае пересечения.

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

Когда-то тяжеловесные компоненты выпадали из этой идиллии, если их добавляли в тот же контейнер. Вне зависимости от их позиции по оси 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() для легковесного компонента, он переадресует запрос контейнеру, в котором содержится, указав в качестве области перерисовки ту область, что он занимает в контейнере. Так продолжается до тих пор, пока запрос не достигнет тяжеловесного контейнера (легковесные компоненты могут содержаться в легковесных контейнерах, так что тяжеловесный контейнер можно искать долго). В итоге запрос на перерисовку в очередь событий помещает именно тяжеловесный контейнер, причем перерисовка «заказывается» лишь для области нужного легковесного компонента. Это позволяет сократить затраты, потому что легковесный компонент может занимать лишь малую часть контейнера.

Легковесные компоненты: резюме

Легковесные компоненты не доставляют проблем, если помнить некоторые детали:

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

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

Отличие системы рисования Swing от стандартной состоит в оптимизации и поддержке UI-представителей. Другими словами, библиотека Swing в своей системе рисования руководствуется всего тремя принципами, которые позволяют нам избавиться от лишней работы и дают уверенность в том, что рисование максимально оптимизировано:

Кэширование

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

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

  1. Операционная система просит окно заново нарисовать содержимое
  2. Окно проходит по списку содержавшихся в нем легковесных компонентов и находит там компонент Swing
  3. Вызывается метод paint(). Чтобы встроить оптимизацию во все компоненты Swing, метод paint() в них не переопределяется, и таким образом, работу всегда выполняет базовый класс JComponent.
  4. Если (согласно флагам), это первый вызванный для рисования компонент Swing, то он запрашивает рисование через буфер у класса RepaintManager. Тот настраивает буфер и вызывает метод paintToOffscreen() вызвавшего его компонента, передавая ему объект Graphics для рисования в памяти. Выставляется флаг, который отныне говорит, что рисование пошло в буфер.
  5. Снова вызывается метод paint(), но так как флаг рисования в буфер уже выставлен, RepaintManager больше не применяется. Вызываются «рабочие» методы прорисовки компонента, рамки и компонентов-потомков (об этих методах чуть ниже).
  6. Компоненты-потомки рисуют своих потомков (напрямую, согласно флагам), и так далее до полной прорисовки всей иерархии данного компонента Swing.
  7. После окончания этой процедуры управление (согласно шагу 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, можно определить следующие самые важные выводы:

Проверка корректности компонентов

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

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

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

В 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(), мы получаем следующие выводы:

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 powered by Disqus