Материал: Портянкин И. Swing

Внимание! Если размещение файла нарушает Ваши авторские права, то обязательно сообщите нам

90

ГЛАВА 4

Кэширование

Стратегия кэширования, применяемая в 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(), либо непосредственно на компоненте методом

Рисование в Swing

91

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. Последний метод и рисует компонент. Остается лишь один вопрос: что такое свойство непрозрачности?

Мы отмечали, что одним из самых впечатляющих свойств легковесных компонентов является их способность быть прозрачными. Однако при написании библиотеки

92

ГЛАВА 4

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

Если в AWT любой легковесный компонент автоматически считается прозрачным, то в Swing все сделано наоборот. Свойство непрозрачности определяет, обязуется ли компонент закрашивать всю свою область, чтобы избавить Swing от дополнительной работы по поиску и прорисовке всего того, что находится под компонентом. Если свойство непрозрачности равно true (а по умолчанию оно равно true), то компонент обязан закрашивать всю свою область, иначе на экране вместо него появится мусор. Дополнительной работы здесь немного: всего лишь необходимо зарисовать всю свою область, а облегчение для механизмов прорисовки получается значительное. Ну а если вы всетаки решите создать компонент произвольной формы или прозрачный, вызовите для него метод setOpaque(false), и к вам снова вернутся все чудесные возможности легковесных компонентов — система прорисовки будет предупреждена. Однако злоупотреблять этим не стоит: скорость прорисовки такого компонента значительно падает. Во многом из-за этого в Swing не так уж и много компонентов, имеющих прозрачные области.

Вернемся к методу paintComponent(). Теперь роль его вполне очевидна: он прорисовывает компонент, по умолчанию используя для этого ассоциированного с компонентом UI-представителя. Если вы собираетесь создать новый компонент с собственным UI-представителем, то он будет прекрасно вписываться в эту схему. Унаследуйте своего UI-представителя от базового класса ComponentUI и переопределите метод paint()4, в котором и рисуйте компонент. Базовый класс позаботится о свойстве непрозрачности. Если же вам просто нужно что-либо нарисовать, унаследуйте свой компонент от любого подходящего вам класса (лучше всех для этого подходят непосредственно классы JComponent или JPanel, потому что сами они ничего не рисуют) и переопределите метод paintComponent(), в котором и рисуйте. Правда, при таком подходе нужно позаботиться о свойстве непрозрачности (если оно равно true) самостоятельно: потребуется закрашивать всю область прорисовки или вызывать перед рисованием базовую версию метода super.paintComponent()5.

Метод paintBorder()

Благодаря методу paintBorder() в Swing имеется такая замечательная вещь, как рамка (border). Для любого компонента Swing вы можете установить рамку, используя метод setBorder(). Оказывается, что поддержка рамок целиком и полностью обеспечи-

4 С названиями рисующих методов в Swing сплошная путаница: все они называются одинаково. Здесь речь идет конечно о методе paint(), определенном в классе ComponentUI, а не об основном рисующем методе любого компонента. Видимо, создатели Swing хотели наглядно показать, что UIпредставитель буквально забирает часть методов у компонента.

5 Однако нужно помнить, что вызов базового метода правильно сработает только для панели (JPanel), у которой есть свой UI-представитель, способный обработать свойство непрозрачности. У класса JComponent такого представителя нет (он абстрактный, вы не сможете вывести его на экран, так что UI-представитель ему на самом деле не нужен), и если вы наследуете от него, то заботьтесь о свойстве непрозрачности самостоятельно.

Рисование в Swing

93

вается методом 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. Это позволяет снизить загрузку системы прорисовки. Проще говоря, это свойство гарантирует, что из под одними по-

94

ГЛАВА 4

томками не «просвечивают» другие и не задевают их, при этом на них не наложен дополнительный прозрачный компонент и т. д. Когда речь идет о простых компонентах, это свойство приносит небольшие дивиденды, однако, если у вас есть сложные и медленно рисующиеся компоненты, оно значительно ускоряет процесс. Когда свойство isOptimizedDrawingEnabled равно true, метод paintChildren() просто перебирает потомков и перерисовывает их поврежденные части, не разбираясь, что они собой представляют и как друг с другом соотносятся. Данное свойство переопределяют лишь три компонента: это многослойная панель JLayeredPane, рабочий стол JDesktopPane и область просмотра JViewport. В них компоненты-потомки часто перекрываются и требуют особого внимания.

Общая диаграмма рисования в Swing

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

Компонент:JComponent RepaintManager.currentManager()

paint()

1

beginPaint()

2

paint()

paintToOffscren()

paintComponent()

paintBorder()

paintChildren()

endPaint()

Вывод на экран

Как мы выяснили, в методе paint() компонентов Swing есть два пути — первый работает в том случае, если компонент Swing рисуется кем-то, не являющимся компонентом Swing, — в этом случае включаются процессы RepaintManager и подготавливается буферкэш. Если же они включены, идет стандартный второй путь, рисующий все известными уже нам тремя методами. И тот же самый второй способ вызывается напрямую, если буферизация в компоненте отключена (вручную или если буферизация включена на уровне операционной системы).

Источник: https://studfile.net/preview/15935977/