WWW.DISS.SELUK.RU

БЕСПЛАТНАЯ ЭЛЕКТРОННАЯ БИБЛИОТЕКА
(Авторефераты, диссертации, методички, учебные программы, монографии)

 

Pages:     | 1 | 2 || 4 | 5 |

«Swing ЭФФЕКТНЫЕ ПОЛЬЗОВАТЕЛЬСКИЕ ИНТЕРФЕЙСЫ Java Foundation Classes Москва • Санкт-Петербург • Нижний Новгород • Воронеж • Новосибирск • Ростов-на-Дону • Екатеринбург • Самара • Киев • Харьков • ...»

-- [ Страница 3 ] --

Итак, что же такое очередь событий? Это экземпляр класса EventQueue из пакета Java.awt, и на самом деле класс этот очень прост. Он представляет собой контейнер, работающий по принципу FIFO (FirstIn, First-Out — первый пришел, первый вышел), то есть по принципу очереди (отсюда и название). В этой очереди графическая система Java хранит все приходящие в программу низкоуровневые события. Происходит это следующим образом: пользователь совершает действие, операционная система сообщает об этом, виртуальная машина Java создает соответствующий событию объект (например, объект класса MouseEvent, если пользователь щелкнул мышью) и добавляет его в конец очереди EventQueue. Класс EventQueue — это одиночка (singleton) 18; сколько бы обычных окон, диалоговых окон или компонентов приложение ни создавало, очередь событий всегда присутствует в единственном экземпляре. Единственное, что можно сделать, — это заменить существующую очередь событий своей собственной. В том, что очередь событий всегда хранится только в одном экземпляре и реализована в виде очереди, имеется немалый смысл. В очередь событий может поступать громадное количество разнообразных событий, и все из них в той или иной степени изменяют состояние компонента. Если бы события, пришедшие позже, обрабатывались перед событиями, пришедшими раньше, компонент мог бы прийти в противоречивое состояние.

Использование дисциплины FIFO позволяет этого избежать. То же самое относится и к наличию только одной очереди: будь их несколько, неизвестно, по каким правилам следовало бы распределять события по очередям и выбирать их оттуда.

Далее вступает в действие специальный поток выполнения EventDispatchThread. Он циклически «подкачивает» (pump) события из очереди событий EventQueue (это делает метод pumpEvents()), и если события в очереди имеются, он извлекает их и рассылает по соответствующим компонентам AWT (это выполняет метод dispatchEvent() класса EventQueue). Так как поток EventDispatchThread один, очередь событий одна, и события хранятся в порядке их поступления, обеспечивается последовательная обработка событий в соответствии с очередностью их поступления, то есть события обрабатываются одно за другим.

Одиночкой класс EventQueue является с точки зрения библиотеки AWT, которая одновременно задействует только один его экземпляр. Однако вы можете создать сколь угодно много объектов этого класса и заменить используемый AWT объект EventQueue своим собственным (это может быть и объект специальным образом унаследованного класса с новой функциональностью). Тем не менее используется только один объект.

Роль потока EventDispatchThread в любой графической программе на Java трудно переоценить, это настоящий «царь и бог» этих программ. Именно в этом потоке происходит вся работа, и он никогда не останавливается. Мы отмечали, что графическая программа в промежутке между событиями простаивает, это так лишь с нашей стороны, с другой стороны, всегда работает поток EventDispatchThread, вызывающий наш код только при наступлении событий (в графической программе наш код весьма пассивен). Стоит ему остановиться, как программа тут же прекратит свою работу. В этом предложении ответ на часто возникающий вопрос, почему программа, использующая графический интерфейс, не заканчивается после выполнения метода main(), как это делают обычные (консольные) программы. Вот маленький пример:

public class ExitTest { public static void main(String[] args) { new javax.swing.JFrame().pack();

Если вы запустите эту программу, то увидите, что она и не подумает закончить работу после того, как создаст окно JFrame, установит для него оптимальный размер методом раск(), и метод main() завершится. В спецификации виртуальной машины Java сказано, что программа завершает свою работу, когда в ней не останется работающих потоков выполнения (не являющихся демонами). В графической программе такой поток выполнения остается — это как раз поток распределения событий EventDispatchThread. Прямого доступа к нему нет, так что выход из программы приходится осуществлять «грубой силой»: вызывая метод System.exit().

Более того, поток EventDispatchThread рассылает не только события, возникающие в результате действий пользователя, в его обязанности входит рассылка событий даже такого экзотического типа, как PaintEvent и InvocationEvent. События этого типа вы не сможете обработать в своей программе, они используются внутри графической подсистемы: событие PaintEvent означает необходимость перерисовать компонент, целиком или частично, а событие InvocationEvent позволяет вмешаться в «плавное течение» потока EventDispatchThread и произвести некоторые дополнительные действия, ненадолго приостановив его.

Чуть позже мы вернемся к роли потока выполнения EventDispatchThread в графических программах, а сейчас давайте все-таки посмотрим, как события, происходящие в результате действий пользователя, добираются до компонентов Swing. Надо сказать, что низкоуровневые события, возникающие в операционной системе, рассылаются только тяжеловесным компонентам AWT, потому что легковесных компонентов (к которым относятся и компоненты Swing) операционная система не видит. «Прекрасно», саркастически улыбаясь, говорите вы, «мы договорились до того, что легковесные компоненты Swing уведомлений о событиях не получают. Что же, слушатели работают по мановению волшебной палочки?» На самом деле легковесные компоненты узнают о событиях, только не напрямую, а через один скрытый механизм. Сейчас мы с ним познакомимся.

';

Доставка событий методам processXXXEvent() Итак, когда поток выполнения EventDispatchThread обнаруживает, что в очереди событий появилось новое событие, он извлекает его и проверяет тип этого события, чтобы определить, что делать с ним дальше. Если извлеченное из очереди событие относится к графическому компоненту (это может быть событие, относящееся к прорисовке, или более интересное нам событие, возникшее в результате действия пользователя), то поток EventDispatchThread вызывает метод dispatchEvent() очереди событий EventQueue. Этот метод не делает ничего экстраординарного, а просто узнает, к какому компоненту относится событие (то есть определяет источник события — об этом сообщает операционная система), и вызывает метод dispatchEvent() этого компонента.

Все компоненты AWT наследуют метод dispatchEvent() от своего базового класса java.awt.Component, но ни один из них не в состоянии вмешаться в процесс первоначальной обработки событий, потому что метод этот является неизменным (final) и переопределить его невозможно. Сделано это неспроста.

Именно в методе dispatchEvent() происходит самая важная внутренняя работа по распределению событий, в том числе координация действий с помощниками компонентов, преобразование событий PaintEvent в вызовы методов paint() и update() 19, а также еще целая череда действий, вмешиваться в Благодаря непрерывной работе потока EventDispatchThread и событиям PaintEvent графический интерфейс программы способен вовремя перерисовываться, если какая-либо часть его была закрыта другим окном, а затем снова представлена глазам пользователя. Операционная система сообщает Java-приложению о необходимости перерисовки части окна, виртуальная машина создает соответствующий этому сообщению объект-событие PaintEvent, а поток рассылки событий мгновенно, сразу после обнаружения в очереди событий EventQueue нового события PaintEvent, передает это событие в принадлежащий ему компонент которые нет смысла, потому что они относятся к внутренней реализации библиотеки и постоянно меняются без всякого предупреждения. Для нас гораздо важнее то, что, в конце концов, метод dispatchEvent() передает событие (если это было событие в ответ на действие пользователя) методу processEvent().

Метод processEvent() — это следующий этап на пути следования событий к слушателям. Ничего особенного в нем не происходит, его основная функция — передать событие согласно его типу одному из методов processXXXEvent(), где XXX — название события, например Focus или Key.

Именно методы processXXXEvent() распределяют приходящие события по слушателям. Например, метод processKeyEvent() определяет, что именно за событие от клавиатуры пришло, и вызывает соответствующий метод слушателя KeyListener (если, конечно, такие слушатели были зарегистрированы в компоненте). После этого мы наконец-то получаем уведомление о событии в свою программу (посредством слушателя) и выполняем необходимые действия. Так заканчивается путь события, начинавшийся в очереди событий. В целом все выглядит так, как показано на рисунке 2.2.

Вся описанная цепочка действий относится к тяжеловесным компонентам AWT, которые «видны»

операционной системе и которым она посылает низкоуровневые события. Как же легковесные компоненты получают уведомления о событиях?

Оказывается, что происходит это с помощью тяжеловесного контейнера (обычно окна или апплета), в котором расположены легковесные компоненты. Он вмешивается в цепочку обработки событий еще на уровне метода dispatchEvent() 20 и, прежде чем продолжить обработку события обычным образом, «ворошит» все содержащиеся в себе легковесные компоненты, проверяя, не принадлежит ли пришедшее событие им. Если это так, то обработка события тяжеловесным контейнером прекращается, а событие передается в метод dispatchEvent() соответствующего легковесного компонента. Эти действия встроены в базовый класс контейнеров java.awt.Container, и любой унаследованный от него контейнер может быть уверен, что содержащиеся в нем легковесные компоненты получат необходимую им поддержку. Для программистов-клиентов Swing все эти хитрости остаются незаметными, и цепочка обработки событий кажется одинаковой как для тяжеловесного, так и для легковесного компонентов.

Методы processXXXEvent() мы упомянули здесь не для «красного словца», они могут быть полезными и в обычных программах. Чаще всего эти методы используют там, где необходимо выполнить некоторое действие перед поступлением события к слушателю или вообще запретить распространение события. Для этого необходимо переопределить метод processXXXEvent(), соответствующий нужному нам событию. Давайте рассмотрим простой пример, в котором мы попытаемся вмешаться в цепочку обработки событий от мыши:

// PreProcessMouse.java // Перехват событий от мыши до их поступления к слушателям import javax.swing.*;

import java.awt.event.*;

(как правило, перерисовка выполняется для главного окна приложения), где данное событие преобразуется в вызов метода paint().

Именно в методе paint() и производится прорисовка любого компонента. Программисту-клиенту не нужно знать всех подробностей движения события PaintEvent: довольно знания того, что код, прорисовывающий компонент, необходимо поместить в метод paint().

Хотя метод dispatchEvent() является неизменным (final), вмешаться в его работу контейнер все-таки может, потому что метод dispatchEvent() просто вызывает метод dispatchEventImpl(), в котором и выполняется вся работа. Метод dispatchEventImpl() обладает видимостью в пределах пакета, так что все классы из пакета java.awt могут привнести в него что-то новое.

public class PreProcessMouse extends JFrame { setDefaultCloseOperation(EXIT_ON_CLOSE);

public void processMouseEvent(MouseEvent e) { // в этом слушателе будем следить за щелчками мыши class MouseL extends MouseAdapter { public static void main(String[] args) { В примере мы создаем небольшое окно JFrame, при его закрытии приложение будет заканчивать свою работу. К этому окну (которое является обыкновенным компонентом, унаследованным от базового класса Component), мы присоединяем слушателя событий от мыши MouseL, который унаследован от адаптера MouseAdapter и следит за щелчками мыши в окне (о них сообщается в метод слушателя mouseClicked()). Однако слушатель этот — не единственное заинтересованное в щелчках мыши «лицо»: мы переопределили метод processMouseEvent() нашего окна JFrame. Как нам теперь известно, именно в этом методе происходит рассылка событий от мыши слушателям, в нем мы также отслеживаем щелчки мыши еще до поступления события к слушателям. Посмотрите, что происходит:

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

Запустив программу с примером, вы (глядя на консольный вывод) сможете увидеть, какие щелчки мыши добираются до слушателя событий MouseL, а какие щелчки, несмотря на то, что вы их старательно производите в окне, так и не появляются в этом слушателе. В методы processXXXEvent() попадают всевозможные события одного типа, для мыши это щелчки, перетаскивание, нажатия, вход в область компонента и выход из нее, и многое другое. Определяя, какие из этих событий вам нужны, вы можете встроить в свои графические интерфейсы поддержку самой тщательной фильтрации событий и тонко настроить их поведение. Слушатели находятся на самом верху пирамиды рассылки событий и не знают, что событие до них может не добраться: они просто терпеливо ожидают, когда оно произойдет. Вы, со своей стороны, всегда можете опуститься ниже по цепочке обработки событий, и чем ниже вы будете опускаться, тем более тонкие возможности будут оказываться в ваших руках.

Тем не менее, как бы заманчиво ни выглядела перспектива фильтрации событий до их поступления к слушателям, возможности этого подхода весьма ограничены. Вспомните еще раз, как происходит рассылка событий: все основные действия происходят в недоступных для нас механизмах класса Component и Container, и в итоге событие попадает в наши руки уже после того, как система определила, какому компоненту оно принадлежит. То есть фактически фильтровать события мы можем только для того компонента, которому они принадлежат, а смысла в этом немного: чаще всего фильтрация событий имеет смысл для контейнеров, содержащих другие компоненты. Фильтруя или особым образом обрабатывая события для контейнера, к примеру, отключая все щелчки правой кнопки мыши, мы, как правило, хотим, чтобы подобная фильтрация работала и для всех содержащихся в контейнере компонентов. Но реализовать такое поведение, переопределяя методы processXXXEvent(), нельзя: события обрабатываются для каждого компонента отдельно. Создатели Swing учли ситуацию и добавили в контейнеры высшего уровня особый компонент, прозрачную панель; она позволяет фильтровать и предварительно обрабатывать события сразу для всего пользовательского интерфейса программы. Мы рассмотрим прозрачную панель в главе 4.

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

Фокус ввода определяет, какие компоненты получают события от клавиатуры первыми, а также то, в какой последовательности происходит обработка этих событий. На самом деле, операционная система не видит легковесных компонентов, так что с ее точки зрения все события от клавиатуры происходят только в контейнере высшего уровня, в котором, однако, может находиться множество легковесных компонентов. Система передачи фокуса ввода должна четко определять, какой компонент получит событие от клавиатуры. Эта система встроена в работу метода dispatchEvent() базового класса Component и благодаря этому всегда имеет шанс перехватить любое необходимое для передачи фокуса ввода событие от клавиатуры. Клавиатурные сокращения — один из основополагающих инструментов Swing, поддерживаются благодаря особой реализации метода processKeyEvent() базового класса библиотеки JComponent. Поэтому, переопределение метода processKeyEvent() для специальной обработки или фильтрации событий от клавиатуры нужно производить с осторожностью и всегда вызывать метод базового класса super.processKeyEvent(), а также осознавать, что некоторые нажатия клавиш до этого метода могут так и не добраться. Мы будем подробно обсуждать систему передачи фокуса ввода и клавиатурные сокращения в главе 3 и там же узнаем, какие дополнительные способы специальной обработки событий от клавиатуры у нас есть.

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

Решением стало маскирование событий (event masking). С каждым компонентом графической системы, способным получать уведомления о событиях, теперь ассоциирована специальная маска в виде длинного (long) целого, которая и определяет, какие события компонент получает. По умолчанию отключено получение всех событий, кроме уведомлений о необходимости перерисовки.

Но, как только вы (или другой компонент) присоединяете к компоненту слушателя события определенного типа, например слушателя событий от мыши MouseListener, компонент тут же включает в свою маску признак обработки данного события (от мыши), так что система обработки событий начинает присылать ему все связанные с этой маской события. Принцип прост: нет слушателей — нет и событий, как только слушатели появляются — автоматически появляются события. Маскирование скорее относится к внутренним механизмам системы обработки событий, и чаще всего вам достаточно того, что при присоединении слушателей маска компонента автоматически обновляется. Однако вы можете и вручную управлять маскированием, быстро включая (или отключая) доставку интересующих вас событий. Это менее гибкий способ управления событиями, чем, к примеру, методы processXXXEvent(), но иногда он позволяет добиться желаемого гораздо быстрее. Одним из самых распространенных вариантов «ручного» маскирования можно назвать как раз переопределение методов processXXXEvent(): если вы не присоедините к компоненту слушателей, а попытаетесь сразу же получить информацию о событиях из методов processXXXEvent(), у вас ничего не выйдет. Вспомните, по умолчанию события в маску не включаются. Это распространенная ошибка, а решение ее просто: надо просто включить в маску нужное вам событие, после чего метод processXXXEvent() заработает «как по маслу».

Добавить событие в маску компонента или удалить его оттуда позволяет пара методов enableEvents() и disableEvents(). Правда, вызвать их напрямую не так-то просто: методы эти описаны как защищенные (protected), то есть они доступны только подклассам компонента. В качестве параметров им требуется передать набор объединенных логическим «ИЛИ» констант из класса AWTEvent, которые и определят, какие низкоуровневые события вы включаете или отключаете. Давайте рассмотрим небольшой пример:

// MaskingEvents.java // Маскирование событий import java.awt.*;

import javax.swing.*;

public class MaskingEvents extends JFrame { setDefaultCloseOperation(EXIT_ON_CLOSE);

disableEvents(AWTEvent.WINDOW_EVENT_MASK);

JPanel contents = new JPanel();

contents.add(new CustomButton("Привет!"));

class CustomButton extends JButton { public static void main(String[] args) { Здесь мы создаем небольшое окно с рамкой JFrame и сразу же указываем ему, что при закрытии окна нужно будет завершить работу приложения. Когда мы вызываем для этого метод setDefaultCloseOperation(), он включает в маску события от окна, так что окно начинает проверять (в методе processWindowEvent()), когда пользователь закрывает его, чтобы завершить работу приложения. Но в нашем примере все меняется: мы властно приказываем компоненту (методом disableEvents(), он нам доступен, потому что класс в примере унаследован от окна JFrame) исключить из маски события от окна, и после запуска примера вы убедитесь, что сколько бы вы ни пытались закрыть окно и выйти из приложения, сделать это вам не удастся, потому что события до окна не дойдут (придется завершать работу программы грубой силой, к примеру, нажав в консольном окне сочетание клавиш Ctrl+C).

Пример также демонстрирует, как управлять маскированием событий от клавиатуры; мы отключаем их для особой кнопки, унаследованной от обычной кнопки JButton библиотеки Swing. Запустив программу с примером, вы увидите, что созданную кнопку легко нажать мышью, но невозможно активизировать с клавиатуры (с помощью клавиши Enter или пробела в зависимости от используемых внешнего вида и поведения). Все работает как обычно, слушатели и UI-представители готовы к обработке событий, но они просто не доходят до них: маскирование выполняется на самых нижних уровнях системы обработки событий, и если событие не включено в маску компонента, обработать его оказывается невозможно. Маскирование редко требуется в обычных приложениях, однако его можно применить в специальных целях, таких как автоматизированное тестирование интерфейса или проверка работоспособности приложения в условиях отказа одного или нескольких видов событий.

Впрочем, стоит помнить, что, так же как и в случае с методами processXXXEvent(), маскирование событий работает только для одного компонента, что может быть неудобно. В таких случаях нам снова может пригодиться прозрачная панель, которую мы будем обсуждать в главе 4.

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

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

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

// ConsumingEvents.java // Поглощение событий import java.awt.*;

import java.awt.event.*;

import javax.swing.*;

public class ConsumingEvents extends JFrame { public ConsumingEvents() { setDefaultCloseOperation(EXIT_ON_CLOSE);

public static void main(String[] args) { Мы создаем небольшое окно, к которому присоединяются два слушателя событий от мыши MouseListener. В первом слушателе мы проверяем, сколько раз была нажата кнопка мыши (неважно, какая кнопка), и если число нажатий равно единице, событие поглощается. Второй слушатель вежливо проверяет, не поглощено ли пришедшее к нему событие, и если поглощено, он не пытается его обработать. Запустив программу с примером, вы увидите, что единичные щелчки мышью, хотя и доходят до второго слушателя, им не воспринимаются, потому что поглощаются первым слушателем 21. Тем не менее, если к этой компании из двух слушателей присоединится кто-либо посторонний, все усилия первого слушателя по поглощению события могут быть напрасными, потому что новый слушатель может обрабатывать событие, совершенно не обращая внимания, что оно было поглощено (чаще всего так и бывает). В таком случае первый слушатель добьется только того, что поглощенное им событие не доберется до помощника компонента. Компоненты Swing являются легковесными, то есть полностью написанными на Java, и события они обрабатывают с помощью обычных слушателей или методов processXXXEvent(), поэтому поглощенные события до них в любом случае дойдут. Если вы хотите перехватить некоторое событие и «не пустить» его к легковесному компоненту, используйте подходящий метод processXXXEvent() или маскирование.

Поглощение вам в этом не поможет.

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

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

Получить используемую в данный момент очередь событий позволяет метод getSystemEventQueue() класса Toolkit, а получить объект Toolkit можно методом getToolkit(), который имеется в каждом унаследованном от класса Component компоненте (кстати, класс Toolkit — это абстрактная фабрика, используемая для создания основных частей AWT). Полученный экземпляр очереди событий позволяет проделать многое: вы сможете узнать, какие события находятся в данный момент в очереди событий, вытащить их оттуда, поместить в очередь новые события (которые могут быть созданы вами вручную, а всем компонентам будет «казаться», что события эти возникли в результате действий пользователя). Помещение в очередь сгенерированных вами событий — прекрасный способ автоматизированного тестирования вашего интерфейса или демонстрации некоторых его возможностей. Приложение при этом будет вести себя точно так же, как если бы с ним работал самый настоящий пользователь. Давайте рассмотрим небольшой пример:

// UsingEventQueue.java // Использование очереди событий import java.awt.*;

import java.awt.event.*;

import javax.swing.*;

public class UsingEventQueue extends JFrame { public UsingEventQueue() { setDefaultCloseOperation(EXIT_ON_CLOSE);

JButton button = new JButton("Генерировать событие");

button.addActionListener(new ActionListener() { getContentPane().setLayout(new FlowLayout());

public static void main(String[] args) { В примере мы создаем небольшое окно, при закрытии которого программа будет завершать свою работу. В панель содержимого окна (для нее мы устанавливаем последовательное расположение компонентов FlowLayout) добавляется кнопка JButton с присоединенным к ней слушателем. При нажатии кнопки мы создаем событие WindowEvent типа WINDOW_CLOSING, именно такое событие генерируется виртуальной машиной, когда пользователь пытается закрыть окно. Созданное событие (помимо типа ему нужно указать источник, то есть окно, которое закрывается пользователем) мы помещаем в очередь событий, используя для этого метод postEvent(). Запустив программу с примером, вы увидите, что при нажатии кнопки приложение заканчивает свою работу, точно так же, как если бы мы нажали кнопку закрытия окна. Система обработки событий «играет по честному» — те события, которые виртуальная машина генерирует в ответ на действия пользователя, вы можете с тем же успехом генерировать самостоятельно — результат будет точно таким же.

Заменить стандартную очередь событий собственным вариантом очереди позволяет метод push() класса EventQueue. Ему нужно передать экземпляр вашей очереди событий, унаследованной от класса EventQueue. Наибольший интерес при наследовании представляет метод dispatchEvent(), который, как мы знаем, вызывается при распределении событий всех типов. Переопределив данный метод, вы получите исчерпывающую информацию о том, как, когда и какие события распределяются в вашей программе. Это может быть весьма ценно при отладке и диагностике вашей программы или при реализации особого поведения ваших компонентов 22.

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

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

• раск() — служит для придания окну оптимального размера;

• setVisible(true) или show() — выводит окно на экран.

Если вы вызвали один из этих методов, можете быть уверены в том, что поток рассылки событий уже начал свою работу, и вы находитесь в многозадачном окружении. До вызова одного из этих методов графические компоненты являются просто объектами, и вы можете работать с ними из любого потока. В качестве наглядной иллюстрации можно вспомнить любой пример этой книги: мы создаем интерфейс в потоке main, но до вывода окна на экран. После вывода окна на экран (а значит, после запуска потока рассылки событий) никаких действий с компонентами из других потоков не производится. Вся работа с ними происходит в слушателях или методах processXXXEvent(), а слушатели и методы для обработки событий вызываются все тем же потоком рассылки событий.

Вопросы синхронизации с потоком рассылки событий становятся особенно важными при работе с компонентами библиотеки Swing, потому что последние практически полностью лишены каких бы то ни было средств для работы в многозадачном окружении. Сделано так не случайно: при разработке библиотеки рассматривалось множество альтернатив, и было выяснено, что наделение компонентов механизмами поддержки многозадачного окружения приведет не только к их усложнению, но и к значительному падению скорости их работы. Поэтому с компонентами Swing работать имеет смысл только из потока рассылки событий, то есть только из слушателей или методов, служащих для обработки событий. Но тут возникает резонный вопрос: «А как же обеспечить отзывчивость интерфейса? Ведь длительные вычисления в потоке рассылки событий блокируют остальной Есть чуть менее сложный способ узнать обо всех распределяемых в вашей программе низкоуровневых событиях — использовать особый слушатель AWTEventListener. Вы присоединяете его к системе обработки событий с помощью класса Toolkit, указывая при этом, о событиях каких типов вы хотите знать. После присоединения слушателя AWTEventListener события всех указанных вами типов будут перед распределением попадать этому слушателю.

интерфейс программы. И неужели придется забыть обо всех удобствах, которые дает параллельное выполнение нескольких потоков?»

На самом деле все не так плохо. Во-первых, у вас есть несколько методов, которые можно вызывать из любого потока выполнения, не опасаясь соревнования с потоком рассылки событий. Они обладают встроенной синхронизацией. Вот эти методы:

• repaint() — служит для перерисовки компонента;

• revalidate(), validate(), invalidate() — позволяют заново расположить компоненты в контейнере и удостовериться в правильности их размеров.

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

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

Ну и, наконец, есть еще один, самый гибкий способ изменить компонент из другого потока. Если ваша задача, выполняющаяся в отдельном потоке, так или иначе приводит к необходимости изменить что-либо в компоненте, и в этом вам не могут помочь ни безопасные вызовы, ни модели, остается только одно. Во избежание неприятностей с потоком рассылки событий и неожиданного тупика все действия с компонентами все равно нужно выполнять из потока рассылки событий. И у вас есть возможность выполнить некоторое действие в потоке рассылки событий. Для этого предназначены методы invokeLater() и invokeAndWait() класса EventQueue. Данным методам нужно передать ссылку на интерфейс Runnable, метод run() этого интерфейса будет выполнен потоком рассылки событий, как только он доберется до него. (Переданная в метод invokeLater() или invokeAndWait() ссылка на интерфейс Runnable «оборачивается» в событие специального типа InvocationEvent, обработка которого сводится к вызову метода run().) В итоге вы действуете следующим образом: выполняете в отдельном потоке сложные долгие вычисления, получаете некоторые результаты, а действия, которые необходимо провести после этого с графическими компонентами, выполняете в потоке рассылки событий с помощью метода invokeLater() или invokeAndWait(). Давайте рассмотрим небольшой пример:

// InvokeLater.java // Метод invokeLater() и работа с потоком рассылки событий import java.awt.*;

import java.awt.event.*;

import javax.swing.*;

public class InvokeLater extends JFrame { setDefaultCloseOperation(EXIT_ON_CLOSE);

button.addActionListener(new ActionListener() { getContentPane().setLayout(new FlowLayout());

// поток, выполняющий "сложную работу" class ComplexJobThread extends Thread { public static void main(String[] args) { Мы создаем небольшое окно, в панели содержимого которого размещается кнопка JButton и вспомогательное текстовое поле. При нажатии кнопки будет вызван слушатель ActionListener, и мы предполагаем, что работа, которая предстоит слушателю, довольно сложна и займет приличное время, так что выполнять ее нужно в отдельном потоке (на самом деле, выполнение долгих вычислений в слушателе, который был вызван потоком рассылки событий, приведет к блокированию остальных событий, ждущих в очереди рассылки тем же потоком, и в итоге интерфейс программы станет неотзывчивым). Отдельный поток, выполняющий долгие сложные вычисления, реализован во внутреннем классе ComplexJobThread, унаследованным от базового класса всех потоков Thread.

Проблема состоит в том, что по окончании вычислений нам нужно сменить надпись на кнопке, а мы к этому моменту будем находиться в отдельном потоке. Менять надпись из потока, отличного от потока рассылки событий, не стоит: это может привести к тупику (мы уже знаем, что компоненты Swing не обладают встроенной синхронизацией). Здесь нам и пригодится статический метод invokeLater() класса EventQueue. Он позволяет выполнить некоторый фрагмент кода из потока рассылки событий. В качестве параметра данному методу нужно передать ссылку на объект, который реализует интерфейс Runnable: метод run(), определенный в этом интерфейсе, и будет выполнен потоком рассылки событий. Так мы и поступаем в примере: код, меняющий надпись на кнопке, будет выполнен из потока рассылки событий, так что можно не опасаться возникновения конфликтов.

Запустив программу с примером и нажав кнопку, вы увидите, что во время вычислений пользовательский интерфейс доступен (вы сможете набрать что-либо в текстовом поле или нажать кнопку еще раз). Сразу по завершении вычислений вы узнаете об этом по изменению надписи не кнопке, и никаких конфликтов при этом не возникнет. Помимо метода invokeLater() в вашем распоряжении также имеется дополнительный метод invokeAndWait(). Он аналогичным образом позволяет выполнить фрагмент кода из потока рассылки событий, но в отличие от invokeLater() делает это синхронно: приостанавливая работу потока, из которого вы его вызвали, до тех пор, пока поток рассылки событий не выполнит ваш код. С другой стороны, метод invokeLater() работает асинхронно: он немедленно возвращает вам управление, так что вы можете продолжать работу, зная, что рано или поздно ваш фрагмент кода будет выполнен потоком рассылки событий. В большинстве ситуаций предпочтительнее метод invokeLater(), а метод invokeAndWait() стоит использовать только там, где немедленное выполнение потоком рассылки фрагмента вашего кода обязательно для дальнейших действий. Работая с методом invokeAndWait(), следует быть внимательнее: поток, из которого вы его вызвали, будет ожидать, когда поток рассылки событий выполнит переданные ему фрагмент кода, а в этом фрагменте кода могут быть обращения к ресурсам, принадлежащим первому потоку, тому самому, что находится в ожидании. В итоге возникнет взаимная блокировка, и программа окажется в тупике. Метод invokeLater() позволяет всего этого избежать. Практически все компоненты Swing не обладают встроенной синхронизацией, но благодаря описанным двум методам класса EventQueue вы всегда сможете выполнить некоторые действия с компонентами из потока рассылки событий. Правда, есть несколько исключений, к примеру, текстовые компоненты Swing, такие как многострочные поля JTextArea или редакторы JEditorPane, позволяют изменять свой текст из другого потока (и это очень удобно, особенно при загрузке больших текстов). Таких исключений немного, и если компонент может работать в многозадачном окружении, вы увидите упоминание об этом в его интерактивной документации.

Принципы работы потока рассылки событий и рассмотренный только что пример плавно подводят нас к «золотому правилу» Swing: пишите слушатели короткими и быстрыми. Здесь все просто:

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

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

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

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

Глава 3 В глубинах Swing Библиотека Swing чаще всего предстает в виде простой, легко настраиваемой и удобной в работе системы, предоставляющей прекрасные возможности для программирования и творчества. Но сейчас мы поговорим о внутренних механизмах функционирования Swing, взглянем на нее изнутри и беззастенчиво исследуем под микроскопом наиболее тайные детали библиотеки. Получилось так, что самые сложные и потаенные части Swing сконцентрированы в базовом классе библиотеки JComponent. Именно этот класс исправно трудится за кулисами библиотеки, кропотливо выполняя наиболее низкоуровневую работу и позволяя остальным частям библиотеки и программистамклиентам не задумываться о ней.

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

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

Рисование в AWT Прежде чем говорить о деталях системы рисования, использованной в библиотеке Swing, имеет смысл вспомнить, что представляло собой рисование в те времена, когда Swing еще не было и единственной графической библиотекой была библиотека AWT (это будет совсем не зря — ведь Swing основана на AWT). Мы уже отмечали, что в первом выпуске пакета JDK легковесных компонентов не было и вся работа по созданию компонента и определению его внешнего вида производилась операционной системой. Библиотека AWT лишь давала программисту видимость того, что компоненты управляются из Java, и фактически представляла собой тонкую прослойку между графической подсистемой операционной системы и Java-кодом. Во всех современных операционных системах процесс рисования происходит примерно одинаково. При этом ваша программа играет пассивную роль и терпеливо ожидает нужного момента. Момент этот определяют механизмы операционной системы, и настает он, когда необходимо перерисовать часть окна, принадлежащего вашему приложению, например, когда окно впервые появляется на экране или прежде скрытая его часть открывается глазам пользователя. Операционная система при этом определяет, какая часть окна нуждается в перерисовке, и вызывает специальную часть вашей программы, отвечающую за рисование, либо посылает вашему приложению сообщение о перерисовке, которое вы при необходимости обрабатываете. Реализовать такую схему создателям Java не представляло особого труда. Когда операционная система присылала какому-либо компоненту сообщение о перерисовке, система AWT преобразовывала его в необходимый Java-приложению объект (это был объект класса Graphics, содержащий всю информацию о текущем процессе рисования и позволяющий рисовать на компоненте) и передавала данный объект в метод paint(), в котором и производились действия по прорисовке компонента. Метод paint() можно было найти в любом графическом компоненте Java, потому что он был определен в базовом классе любого компонента Java.awt.Component. В итоге, для того чтобы вмешаться в процесс рисования, нужно было лишь переопределить метод paint(), записав в нем все ваши действия по рисованию (как уже упоминалось, такая техника называется обратным вызовом и исправно служит программистам уже многие годы). Код при этом выглядел примерно следующим образом:

// AWTPainting.java // Процесс рисования в AWT очень прост import java.awt.*;

import java.awt.event.*;

public class AWTPainting extends Frame { // в этом методе производится рисование public void paint(Graphics g) { public static void main(String[] args) { В данном простом примере создается небольшое окно и его пространство заливается красным цветом.

Для рисования в AWT чаще всего использовали классы окон (как у нас в примере мы рисовали в окне Frame), чтобы «приукрасить» фон приложения, или специально предназначенный для создания компонентов с собственной процедурой прорисовки класс «холста» Canvas, по умолчанию представляющий собой прямоугольник, залитый цветом фона. Рисовать на остальных компонентах, таких как кнопки (Button) и других компонентах с определенным внешним видом, было неудобно, потому что возникал конфликт с помощником (peer) компонента. Как вы помните, помощник связывает компонент с операционной системой, которая в AWT ответственна не только за возникновение событий и процесс прорисовки, но и за собственно внешний вид компонентов.

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

У вас также была возможность вмешиваться в процесс прорисовки компонентов, не ожидая запроса операционной системы. Это было особенно полезно в программах с интенсивным обновлением графики, где изображение обновляется не столько в результате действий пользователя, сколько из-за изменений данных самой программы, например в программах с анимацией. Для этого AWT предоставляла компонентам метод repaint(), который помещал в очередь событий сообщение о необходимости перерисовки, так, как будто бы этого требовала сама операционная система. В результате по вашему запросу компонент должен был перерисовать себя (AWT вызывала для него метод paint() 23). Вызывать напрямую метод paint() не рекомендовалось, так как это могло привести к конфликту с системой прорисовки и мусору на экране. Метод repaint() был достаточно гибок и обладал несколькими перегруженными версиями, позволяющими перерисовывать как весь компонент, так и его определенные части, что очень полезно при выводе сложной графики.

В том случае, когда программе требовался «экстренный» вывод графики на экран (без какой-либо задержки), метод repaint() был не совсем уместен. Дело в том, что, как мы уже сказали, данный метод помещает в очередь событий сообщение о необходимости перерисовки, но не дает никаких гарантий относительно того, когда эта перерисовка (вызов метода paint()) будет произведена. В очереди событий уже могут находиться довольно много событий, в том числе и связанных с прорисовкой компонента, так что до его обработки может пройти некоторое время, причем время неопределенное.

Чтобы дать возможность программам выводить что-либо на экран без задержки, в базовый класс всех компонентов Component был добавлен метод getGraphics(), позволяющий незамедлительно получить для любого компонента его объект Graphics, ведающий всеми операциями прорисовки. Таким образом, обеспечивалась возможность незамедлительного вывода графики на экран. Тем не менее надеяться на результат работы метода getGraphics() не стоило, потому что он оставался на экране На самом деле здесь есть небольшая хитрость, но она настолько редко требуется (а теперь, когда AWT фактически стала всего лишь основой Swing, она почти бесполезна), что о ней можно и не знать. На самом деле при вызове repaint() AWT вызывает метод update(), также определенный в классе Component. Этот метод может быть использован для реализации пошаговой прорисовки (incremental painting) — при каждом вызове repaint() в общую картину компонента добавляются новые детали без удаления того, что уже было. Но применить на практике эту технику сложно, а по умолчанию update() просто заливает перерисовываемую область цветом фона и вызывает paint(). Подробнее об этом можно узнать в соответствующих материалах на сайте java.sun.com.

только до первого вызова метода paint(). Поэтому все, что требовалось рисовать незамедлительно, должно было дублироваться и в методе paint(), с которого никто не снимал полномочий основного метода прорисовки компонента. В противном случае на экране возникала бы неразбериха. Более того, пользоваться методом getGraphics() следовало с особой осторожностью, потому что работал он не со всеми компонентами (при этом возникало странное сообщение об ошибке), кроме того, требовалось, чтобы компонент был виден на экране, работа происходила из потока рассылки событий, и система AWT была загружена.

В итоге рисование в AWT выглядело довольно стройной системой. Основной код, прорисовывающий компонент, вы помещали в метод paint(), который и вызывался системой прорисовки для обновления изображения. При необходимости нетрудно было также обновить изображение из программы — для этого служили несколько вариантов метода repaint(). Ну и, наконец, была возможность мгновенного рисования на компоненте, которая обеспечивалась методом getGraphics(). Использовалась она нечасто, но для некоторых программ была необходима.

Легковесные компоненты в AWT С появлением в Java легковесных компонентов система AWT изменилась, но не намного. Как мы знаем, легковесный компонент представляет собой область экрана тяжеловесного контейнера. Для того чтобы он мог правильно отображаться на экране, ему также необходимо получать от системы прорисовки вызовы своего метода paint(), однако операционная система, прежде заведовавшая этим процессом в AWT, ничего не знает о существовании легковесных компонентов, она видит лишь «родные» тяжеловесные компоненты и контейнеры, которым и отправляет запросы на перерисовку.

Решение здесь очевидно — нужно встроить поддержку легковесных компонентов в тяжеловесные контейнеры, и именно так поступили создатели AWT.

После встраивания поддержки легковесных компонентов любой тяжеловесный контейнер AWT (к таковым относятся окна Frame и Window, а также диалоговое окно Dialog) при вызове своего метода paint(), прежде всего, проверял, содержатся ли в нем легковесные компоненты. Если такие компоненты в контейнере содержались, последний после прорисовки себя последовательно перебирал содержащиеся в нем легковесные компоненты, вызывая их метод paint() и передавая в этот метод полученный от системы AWT объект Graphics. При этом контейнер отслеживал порядок размещения компонентов (он был довольно прост: компоненты, добавленные в контейнер раньше, перекрывали компоненты, добавленные позднее), потому что области экрана, занимаемые компонентами, могли перекрываться, и их методы paint() необходимо было вызывать в определенном порядке (здесь также все было просто: компоненты, добавленные позднее, прорисовывались раньше, так как ранее добавленные компоненты считались расположенными в стопке выше). Для легковесного компонента вся эта «кухня» оставалась за кадром — он просто ожидал вызова своего метода paint(), в котором и производил прорисовку. Единственным новым требованием стала необходимость обязательно вызывать при переопределении контейнера базовый метод super.paint(), производящий прорисовку легковесных компонентов, иначе в вашем новом контейнере легковесные компоненты не стали бы прорисовываться.

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

сквозь «верхние» компоненты. Идиллия нарушалась лишь при добавлении в контейнер одновременно тяжеловесных и легковесных компонентов. Все тяжеловесные компоненты получают сообщение о перерисовке от операционной системы (не считая вызовов repaint()), когда принадлежащая им часть экрана «повреждается». Выходило так, что операционная система принимала легковесные компоненты, перекрывающие тяжеловесные, за мусор на экране и инициировала перерисовку тяжеловесных компонентов, так что в каком бы порядке ни находились компоненты в контейнере, тяжеловесные компоненты в любом случае перекрывали легковесные. Отсюда и пошло известное правило: не совмещайте легковесные и тяжеловесные компоненты. Чтобы все окончательно прояснилось, рассмотрим небольшой пример.

// AWTLightweights.java // Использование легковесных компонентов в AWT import java.awt.*;

import java.awt.event.*;

public class AWTLightweights extends Frame { public AWTLightweights() { // укажем координаты вручную, чтобы компоненты перекрывались // легковесный компонент - синий квадрат class Lightweight1 extends Component { // легковесный компонент - красный кружок class Lightweight2 extends Component { public static void main(String[] args) { В этом примере мы создаем небольшое окно, в которое помещаем два легковесных компонента, унаследованных непосредственно от базового класса AWTComponent, и тяжеловесный компонент — кнопку Button, полностью принадлежащую операционной системе. Так как ни один из простых менеджеров расположения не позволяет компонентам перекрываться, мы удалили из нашего окна используемый менеджер расположения, вызвав метод setLayout(null) (подробнее о расположении компонентов рассказывается в главе 5), и указали для наших компонентов позиции «вручную»

методом setBounds(). При этом легковесные компоненты занимают все пространство окна, а кнопка лишь часть, достаточную для ее правильного отображения. Запустив программу с примером, вы увидите все в действии. Несмотря на то, что легковесные компоненты занимают все пространство окна, они могут позволить себе рисовать лишь на части его, оставив остальное пространство нетронутым. При этом синий квадрат, добавленный позже красного кружка, а значит, находящийся в стопке ниже, «просвечивает» сквозь кружок, потому что последний не закрашивает свою область цветом фона. «Безобразничает» лишь тяжеловесная кнопка. Она хоть и была добавлена последней, то есть имеет самую нижнюю позицию в стопке, оказывается выше обоих легковесных компонентов 24.

Причины этого мы уже выяснили. Впрочем, если ваши компоненты не перекрываются и вы не планируете этого в своих программах, легковесные и тяжеловесные компоненты прекрасно Если у вас есть инструмент, выводящий список окон для приложения (такой как Spy++ для Windows), вы можете провести интересный эксперимент. Запустите программу с нашим примером и выведите ее список окон. Вы увидите, что их всего два — это само окно Frame и кнопка Button. Как мы и говорили, легковесных компонентов «родная» система не видит. Тем не менее с точки зрения Java это самые настоящие компоненты.

уживаются в одном окне. Здесь есть только одно «но» — зачем вам ограниченные и плохо управляемые тяжеловесные компоненты?

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

Получается, что для легковесных компонентов способы рисования ничуть не изменились. Все также «рисующий» код помещается в метод paint(), все также для перерисовки используется метод repaint(), не отменили и действие метода getGraphics(). Изменились лишь внутренние механизмы библиотеки AWT, на которые и легла ответственность за правильную работу легковесных компонентов.

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

Для начала стоит сказать о том, что весь графический вывод в Swing выполняется с двойной буферизацией (double buffering). Эта техника Swing упоминается часто и на самом деле в ней все достаточно просто — вместо того чтобы рисовать прямо на компоненте, вы создаете совместимое с форматом экрана изображение в памяти, на котором и рисуете. После того как процесс прорисовки компонента заканчивается, полученное в памяти изображение (или его часть) копируется непосредственно на экран. В любой графической системе копирование изображения из памяти на экран (при условии, если их форматы совместимы) выполняется очень быстро, и это позволяет избавиться от мерцания при прорисовке компонентов, так досаждавшего пользователям ранних версий JDK и AWT. Сейчас, в новейших версиях пакета JDK, проблемы со скоростью вывода графики на экран уже не так актуальны, тем не менее двойная буферизация остается незаменимой частью Swing — в любом случае вывод графики идет с помощью операционной системы и требует приличных затрат.

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

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

RepaintManager.setDoubleBufferingEnabled(false). Таким образом, можно запретить классу RepaintManager хранить внеэкранное изображение. Однако ситуации, в которых отключение встроенной двойной буферизации оправдано, можно пересчитать по пальцам (наиболее часто это необходимо при отладке графики, мы об этом узнаем чуть позже), так что увлекаться им не стоит — качество вывода графики ухудшится, а скорость упадет. Мы еще вернемся к деталям двойной буферизации, когда более подробно начнем обсуждать класс RepaintManager. Ну а теперь, чтобы в полной мере ощутить, что же происходит при прорисовке компонентов Swing, станем на некоторое время частью системы прорисовки AWT и посмотрим, какие процессы протекают в базовом классе JComponent. Итак, компонент необходимо вывести на экран — для этого вызывается метод paint().

Метод paint() Когда операционная система присылает тяжеловесному контейнеру, в котором содержатся легковесные компоненты, сообщение о необходимости перерисовки, он прежде всего прорисовывает себя, а затем переходит к содержащимся в нем (то есть дочерним) компонентам, вызывая для них метод paint(). Самое интересное начинается, когда очередь доходит до первого компонента Swing, то есть до первого компонента, унаследованного от базового класса JComponent. Здесь последний и берет всю власть в свои руки. Начиная с этого момента, вся область экрана, принадлежащая первому встреченному тяжеловесным контейнером компоненту Swing, полностью обслуживается механизмами рисования класса JComponent. Именно метод paint() класса JComponent вызывается каждый раз при прорисовке любого компонента Swing, потому что ни один из компонентов библиотеки не переопределяет этого метода. Сделано так не зря. В классе JComponent при прорисовке компонента тщательно отделяется то, что изменяется часто, оттого, что изменяется крайне редко 26.

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

Вы просто рисуете свой компонент, оставляя низкоуровневые детали механизмам базового класса.

Итак, метод paint() класса JComponent начинает работу. Прежде всего, он с помощью метода RepaintManager.currentManager() получает задействованный в данный момент экземпляр класса RepaintManager, необходимый ему для выяснения того, используется ли в программе двойная буферизация, и если используется, то для получения собственно буфера. Далее рассчитывается так называемый прямоугольник отсечения (clip rectangle), который передается в метод paint() (системой AWT, если прорисовка происходит по команде от операционной системы, или программой, если используется метод repaint()) и определяет, какую именно область компонента необходимо перерисовать. Наличие прямоугольника отсечения позволяет значительно сократить объем работы рисующего метода, особенно для сложных и больших компонентов, таких как текстовые редакторы или таблицы. Перерисовка их целиком привела бы к очень низкой производительности, а прямоугольник отсечения значительно ускоряет их вывод на экран. Все, что находится за прямоугольником отсечения, отбрасывается и не прорисовывается (за этим следит класс Graphics).

Далее метод paint() определяет, как именно компонент будет выводиться на экран. Если двойная буферизация включена, он определяет, используется ли двойная буферизация в родительском компоненте, выясняя значение флага ANCESTOR_USING_BUFFER 27. Если оказывается, что данный флаг имеет значение true, то буфер для нашего компонента не потребуется, потому что объект Graphics, переданный в метод paint(), создан родительским компонентом и уже рисует в памяти.

Далее, если метод paint() выясняет, что буфер используется, он вызывает метод paintDoubleBuffered().

В методе paintDoubleBuffered() выполняется основная работа по настройке внеэкранного буфера.

Начиная с версии JDK 1.4, появилась возможность использовать в графических компонентах так называемые нестабильные изображения (volatile images). Они отличаются от обычных изображений тем, что размещаются прямо в памяти видеоадаптера (если он это позволяет) и выводятся на экран во экземпляр, который можно лишь заменить своим. Грубо говоря, это одиночка в смысле экземпляра, применяемого в библиотеке Swing.

Хороший пример реализации одного из важнейших принципов программирования: отделяйте неизменную составляющую программы от переменной.

В классе JComponent налажена целая система флагов, позволяющая синхронизировать операцию прорисовки между различными компонентами, чаще всего между родительским компонентом и его потомками.

много раз быстрее обычных изображений. Обратной стороной ускорения является то, что такие изображения нестабильны и в любой момент времени могут быть разрушены, так что за ними приходится внимательно следить. Как раз метод paintDoubleBuffered() и проверяет, используются ли классом RepaintManager нестабильные изображения и действительны ли они. Если нестабильное изображение было повреждено, предпринимается попытка восстановить его. Если эта попытка не удается, в качестве промежуточного используется обычное (неускоренное) изображение. В конечном итоге метод paintDoubleBuffered() получает внеэкранное изображение необходимого компоненту размера, создает для него объект Graphics и начинает процесс собственно прорисовки компонента, вызывая метод paintWithOffScreenBuffer().

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

Метод 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() 28, в котором и рисуйте компонент. Базовый класс позаботится о свойстве непрозрачности. Если же вам просто нужно что-либо нарисовать, унаследуйте свой компонент от любого подходящего вам класса (лучше всех для этого подходят непосредственно классы JComponent или JPanel, потому что сами они ничего не рисуют) и переопределите метод paintComponent(), в котором и рисуйте. Правда, при таком подходе нужно позаботиться о свойстве непрозрачности (если оно равно true) самостоятельно: потребуется закрашивать всю область прорисовки или вызывать перед рисованием базовую версию метода super.paintComponent() 29.

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

Он вызывается вторым, после метода paintComponent(), смотрит, установлена ли для компонента какая-либо рамка, и если рамка имеется, прорисовывает ее, вызывая определенный в интерфейсе Border метод paintBorder(). Единственный вопрос, который при этом возникает: где именно рисуется рамка? Прямо на пространстве компонента или для нее выделяется отдельное место? Ответ прост — никакого специального места для рамки нет. Она рисуется прямо поверх компонента после прорисовки последнего. Так что при рисовании компонента, если вы не хотите неожиданного наложения рамки на занятое место, учитывайте место, которое она занимает. Как это делается, мы узнаем в главе 6, часть которой полностью посвящена рамкам. Переопределять метод paintBorder() вряд ли стоит. Работу он выполняет нехитрую, и как-либо улучшить ее или коренным образом изменить не представляется возможным. Если вам нужно создать для своего компонента фантасмагорическую рамку, лучше воспользоваться услугами интерфейса Border или совместить несколько стандартных рамок.

Метод paintChildren() Заключительную часть процесса рисования выполняет метод paintChildren(). Как вы помните, при обсуждении легковесных компонентов в AWT мы отмечали, что уш их правильного отображения в контейнере, если вы переопределили их метода paint(), необходимо вызвать базовую версию paint() из класса Container, иначе легковесные компоненты на экране не появятся. Базовый класс JComponent библиотеки Swing унаследован от класса Container и вполне мог бы воспользоваться его услугами по прорисовке содержащихся в нем компонентов-потомков. Однако создатели Swing решили от услуг класса Container отказаться и реализовали собственный механизм прорисовки потомков. Причина проста — недостаточная эффективность механизма прорисовки AWT. Улучшенный оптимизированный механизм и реализуется методом paintChildren(). Для придания ему максимальной скорости компоненты Swing используют два свойства: уже известное нам свойство непрозрачности, а также свойство isOptimizedDrawingEnabled.

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

На этом этапе «вступают в бой» два вышеупомянутых свойства. С первым свойством все более или менее понятно: если свойство непрозрачности компонента равно true, это означает, что, сколько бы компонентов ни находилось под ним и ни пересекало бы его, всю свою область он обязуется закрасить, а значит, продолжать поиск потомков в этой области не имеет смысла — их все равно не будет видно. Теперь понятно, почему так важно выполнять требование заполнения всей области экрана при использовании свойства непрозрачности: в противном случае на экране неизбежен мусор, который по договоренности должен убирать сам компонент, а не система прорисовки. Со свойством isOptimizedDrawingEnabled картина немного другая. Данное свойство определено в классе С названиями рисующих методов в Swing сплошная путаница: все они называются одинаково. Здесь речь идет конечно о методе paint(), определенном в классе ComponentUI, а не об основном рисующем методе любого компонента. Видимо, создатели Swing хотели наглядно показать, что UI-представитель буквально забирает часть методов у компонента.

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

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

Методы рисования — краткий итог После вызова рисующих методов paintComponent(), paintBorder() и paintChildren() система прорисовки Swing завершает свою работу. Если при рисовании использовался буфер, а значит, вызывался метод paintWithOffscreenBuffer(), изображение копируется из внеэкранного буфера на экран (копируется только та его часть, которая соответствует прямоугольнику отсечения). Мы еще не говорили, что происходит, когда рисование производится без применения буфера. В этом случае метод paintDoubleBuffered() не вызывается, а метод paint() напрямую вызывает три рисующих метода, которые рисуют непосредственно на экране. Чтобы все стало окончательно ясно, рассмотрим небольшую диаграмму (рисунок 3.1).

Рисунок 3.1. Взаимоотношения наблюдателей и субъектов Итак, система прорисовки Swing максимально оптимизирует процесс рисования компонентов, используя внеэкранный буфер и специальные алгоритмы прорисовки потомков компонента. Для того чтобы сразу же включить рассмотренные нами механизмы в работу, все ваши компоненты рекомендуется размещать в контейнерах высшего уровня Swing, а не AWT, потому что контейнеры высшего уровня Swing размещают всех своих потомков в специальном компоненте, занимающем все их пространство, — корневой панели JRootPane. При этом процесс прорисовки всех компонентов, размешенных в корневой панели, полностью управляется оптимизированными механизмами класса JComponent, потому что тяжеловесный контейнер сразу же передает управление методу paint() корневой панели, правда, немного меняется смысл выключения двойной буферизации для отдельных компонентов — мы уже видели, что компонент рисует с помощью буфера, если буфер используется его родительским компонентом, независимо от того, включена или выключена двойная буферизация для него самого. Если все компоненты находятся в корневой панели, выключать двойную буферизацию имеет смысл только для нее (это будет равносильно выключению двойной буферизации для всех компонентов, находящихся внутри этого контейнера высшего уровня). Увидеть работу рисующих механизмов Swing «вживую» нам поможет небольшой пример.

// SwingPainting.java // Работа рисующих механизмов Swing import javax.swing.*;

import java.awt.*;

public class SwingPainting extends JFrame { public SwingPainting() { setDefaultCloseOperation(EXIT_ON_CLOSE);

getContentPane().add(new SwingComponent());

// компонент, использующий возможности Swing class SwingComponent extends JComponent { public static void main(String[] args) { Пример демонстрирует, как и в каком порядке работают рассмотренные нами рисующие методы.

Создается небольшое окно, в панель содержимого которого (панель содержимого — часть корневой панели, этому посвящена глава 4) добавляется унаследованный от класса JComponent небольшой компонент. Он переопределяет все три рисующих метода и выводит при их вызове сообщение на консоль, чтобы вы смогли убедиться, что методы эти вызываются и действуют по порядку. Рисуем мы в методе paintComponent(), как и положено, вызывая перед этим базовый метод super.paintComponent(), который позаботится о правильной обработке свойства непрозрачности. Два других метода выводят сообщение о вызове и передают управление своим базовым версиям, которые беспокоятся о правильной прорисовке рамки и потомков. Запустив программу с примером и взглянув на консоль, вы сможете увидеть, как функционирует система прорисовки Swing.

Программная перерисовка — метод repaint() и класс RepaintManager Теперь, когда мы узнали, как происходит перерисовка компонентов Swing в ответ на запрос системы (при вызове тяжеловесным контейнером метода paint()), пришла пора обратиться к процессу программной перерисовки компонентов. Программная прорисовка происходит, когда запрос на обновление изображения компонента или его части приходит не от операционной системы, а от самой программы. Разница невелика — дело все равно заканчивается методом paint(). В AWT для этого служил метод repaint(). Как вы помните, в итоге его вызов означал отложенный вызов метода paint() — соответствующий запрос помещался в очередь сообщений. Для легковесных компонентов перерисовка производилась с помощью тяжеловесного контейнера. В принципе, эту схему можно было использовать и для Swing.

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

Программной перерисовкой в Swing «заведует» класс RepaintManager. Вместо того, чтобы в методе repaint() вызывать услуги своего тяжеловесного контейнера, базовый класс JComponent вызывает в нем метод addDirtyRegion() класса RepaintManager, в качестве параметров для него указывая прямоугольник, который следует перерисовать, и ссылку на сам компонент. Дальше происходит следующее. RepaintManager проверяет, что прямоугольник для перерисовки не нулевой, и, прежде всего, смотрит, не было ли раньше запросов на перерисовку частей того же компонента. Чтобы справиться с этим, класс RepaintManager хранит ассоциативный массив Hashtable, в котором сопоставляет компонент и его текущий прямоугольник для перерисовки. Если в массиве ссылка на компонент есть, значит, запрос на перерисовку уже делался, и имеется некоторый прямоугольник для перерисовки. В таком случае RepaintManager объединяет два прямоугольника для перерисовки, используя для этого вспомогательный метод SwingUtilities.computeUnion(), и заканчивает работу.

Если же в массиве ссылки на компонент нет, то RepaintManager производит проверку всех родительских компонентов и удостоверяется в том, что компонент видим и действительно нуждается в перерисовке. Если ответ на последний вопрос положителен, в таблицу компонентов добавляется ссылка на новый компонент и его прямоугольник перерисовки, после чего остается поместить запрос в очередь событий.

Как и в AWT, в Swing метод repaint() безопасен для работы в многозадачном окружении. Чтобы обеспечить это условие, RepaintManager после появления компонентов, нуждающихся в перерисовке, вызывает метод SystemEventQueueUtilities.queueComponentWorkRequest(), который помещает в очередь событий (используя метод invokeLater()) запрос на выполнение метода paintDirtyRegions() класса RepaintManager. Последний метод мы сейчас разберем, но прежде стоит отметить, что в очереди событий никогда не находится больше одного запроса на перерисовку компонентов Swing.

Способствуют этому совместные усилия классов RepaintManager и SystemEventQueueUtilities: первый сообщает второму, в каком контейнере верхнего уровня находятся компоненты, нуждающиеся в перерисовке, а второй инициирует лишь один запрос для этого контейнера (следующий запрос не создается, пока не выполнен предыдущий). В итоге, пока поток рассылки событий EventDispatchThread доберется до исполнения запроса, в классе RepaintManager может набраться довольно много прямоугольников перерисовки, которые к тому же по возможности объединяются.

Это здорово повышает эффективность перерисовки Swing по сравнению с AWT.

В конечном итоге, поток рассылки событий добирается до запроса на перерисовку компонентов Swing и вызывает метод paintDirtyRegions(). Данный метод для начала выясняет, с каких родительских компонентов следует начинать прорисовку, вызывая для этого метод collectDirtyRegions(). В этом есть смысл — среди родительских компонентов вполне могут быть компоненты без прозрачных областей (со свойством непрозрачности, равным true), и перерисовывать компоненты ниже них в стопке не нужно, более того, при определении родительских компонентов можно увидеть, для каких из них можно объединить запрос на прорисовку в один. В конце концов, paintDirtyRegions() получает список компонентов, с которых необходимо начинать прорисовку, и прямоугольники, которые нужно прорисовать. Для каждого полученного компонента вызывается метод paintImmediately(), определенный в базовом классе JComponent.

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

Если родительский компонент использует буфер, то paintImmediately() тоже будет его использовать, и в этом его отличие от метода paint(). Связано это с тем, что если paint() вызывается из родительского компонента и уже задействует подготовленное в нем внеэкранное изображение, то paintImmediately() вызывается из потока рассылки событий, и ему приходится заботиться о буфере самостоятельно.

Когда все детали утрясены, выполняется сама прорисовка. Метод paintImmediately() получает объект для рисования методом getGraphics() и вызывает знакомый нам метод paintDoubleBuffered(), если буфер используется, или просто paint(), если буфер не нужен. Как дело происходит далее, мы уже знаем. На рисунке 3.2 показана итоговая картина программной перерисовки.

Рисунок 3.2. Итоговая картина программной перерисовки Что же, можно смело сказать — класс RepaintManager является настоящим ядром системы рисования Swing. Он тщательно оптимизирует процесс перерисовки компонентов Swing, забирая все мелкие и сложные детали себе и предоставляя нам простой и прекрасно работающий метод repaint(). Вдобавок, как мы уже знаем, в этом классе хранится внеэкранное изображение, применяемое библиотекой для двойной буферизации. Для хранения изображения используется вспомогательный внутренний класс DoubleBufferInfo, хранящий вместе с изображением его размер и флаг, показывающий, не настала ли необходимость заново создать изображение. Для получения внеэкранного изображения применяется метод getOffscreenBuffer(). В новых версиях JDK появилась возможность двойной буферизации на уровне операционной системы, и в этом случае RepaintManager для лучшей производительности и во избежание лишней работы встроенную буферизацию отключает.

Проверка корректности компонентов Проверка корректности, или валидация (validation), компонентов — еще один механизм AWT, который усовершенствован создателями Swing. Хотя он имеет не самое непосредственное отношение к рисованию, мы рассмотрим его здесь, потому что проверкой корректности компонентов Swing «заведует» наш старый знакомый — класс RepaintManager. В стандартных механизмах AWT проверка корректности компонентов требуется для того, чтобы расположение компонентов в контейнере и их размеры соответствовали действительности. На самом деле, если вы прямо во время работы программы увеличите размер шрифта на кнопке, то прежнего размера кнопки (которого, как правило, в аккурат хватает для размещения надписи) уже будет недостаточно, и часть надписи окажется непонятно где. Чтобы исправить ситуацию, и проверялась корректность компонентов, в ходе которой заново вычислялись размеры компонента и его позиция в контейнере (фактически проверка сводилась к повторному распределению пространства контейнера менеджером расположения).

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

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



Pages:     | 1 | 2 || 4 | 5 |
Похожие работы:

«Е.А. Торчинов ФИЛОСОФИЯ БУДДИЗМА МАХАЯНЫ 8 Санкт-Петербург 2002 УДК 294.3 ББК Э353-4 Федеральная целевая программа Культура России (подпрограмма Поддержка полиграфии и книгоиздания России) Е. А. Торчинов. Философия буддизма Махаяны. — СПб. Петербургское Востоковедение, 2002 — 320 с Т 61 (Мир Востока, XII) Книга известного петербургского буддолога и китаеведаЕ.А.Тор¬ чинова посвящена философии буддизма Махаяны, или Великой Ко­ лесницы, направления буддизма, получившего широчайшее рас­...»

«РАССМОТРЕНА И ПРИНЯТА УТВЕРЖДАЮ на заседании педагогического совета директор МОУ лицея Политэк протокол от 28.08.2014 г. № 1 Т.А.Самсонюк приказ от 01.09.2014г. № 173 ОСНОВНАЯ ОБРАЗОВАТЕЛЬНАЯ ПРОГРАММА СРЕДНЕГО ОБЩЕГО ОБРАЗОВАНИЯ 10-11 классы муниципального общеобразовательного учреждения лицея Политэк на 2014-2015 учебный год г. Волгодонск 2014 год Содержание Общие положения 1. Целевой раздел 1.1. Пояснительная записка 1.2. Общие учебные умения, навыки и способы деятельности 1.2.1....»

«Фонд поддержки детей, находящихся в трудной жизненной ситуации Государственное краевое учреждение социального обслуживания населения Социально-реабилитационный центр для несовершеннолетних г. Перми ООО Пермский центр политической инициативы и информационно-правового сопровождения ПРЕДОТВРАЩЕНИЕ ЖЕСТОКОГО ОБРАЩЕНИЯ С ДЕТЬМИ В СЕМЬЕ Пермь 2009 Составители: Зырина А.И.; Индейкина Т.Л. Методические материалы подготовлены на основе обобщения научных работ российских и зарубежных авторов по проблеме...»

«Современная экономическая наука и образование в России ПОСОБИЕ по ЭКОНОМИКЕ ДЛЯ ПОСТУПАЮЩИХ В РОССИЙСКУЮ ЭКОНОМИЧЕСКУЮ ШКОЛУ на программу Магистр экономики в 2013 году МОСКВА 2013 Пособие по экономике для поступающих в РЭШ на программу Магистр экономики в 2013 г. ОГЛАВЛЕНИЕ ЭКЗАМЕН ПО ЭКОНОМИКЕ КРИТЕРИИ ОЦЕНИВАНИЯ ТЕКСТЫ И РАЗБОР ЭКЗАМЕНОВ РАЗНЫХ ЛЕТ ТЕКСТ И РАЗБОР ЭКЗАМЕНА № 1 ТЕКСТ И РАЗБОР ЭКЗАМЕНА № 2 ТЕКСТ И РАЗБОР ЭКЗАМЕНА № 3 ТЕКСТ ЭКЗАМЕНА 2007 г. ТЕКСТ ЭКЗАМЕНА 2008 г. ТЕКСТ ЭКЗАМЕНА...»

«ОБРАЗОВАТЕЛЬНАЯ СИСТЕМА ШКОЛА 2100 РАБОЧАЯ ПРОГРАММА ПО ПРЕДМЕТУ ИЗОБРАЗИТЕЛЬНОЕ ИСКУССТВО 1 КЛАСС О.А. Куревина, Е.Д. Ковалевская Пояснительная записка Рабочая программа составлена на основе примерной программы по изобразительному искусству, Федеральных государственных стандартов общего образования второго поколения, с учтом основных идей и положений программы развития и формирования универсальных учебных действий для начального общего образования, межпредметных и внутрипредметных связей,...»

«Муниципальное образовательное учреждение средняя общеобразовательная школа №2 Согласовано Согласовано Утверждаю Руководитель МО Заместитель директора по УВР Директор школы /_/ Золотина Т.И. _Теплова Н.А. Протокол № 1от 27.08 2012 г. 28.08. 2012 г. Приказ от 29.08.2012г. №01-18/73-Б РАБОЧИЕ ПРОГРАММЫ по иностранному языку 5-11классы. Авторы: Буракова В.А., учитель английского языка. Мальгинова А.А., учитель английского языка. г. Великий Устюг 2012г. Место курса в образовательной программе...»

«ООО Интеллектуальные технологии / ООО ИнТех Россия, 630058, Новосибирск, ул. Русская 39, офис: 421, 427, 429 Intellectual technologies Consulting group © Интернет: www.intech.gorodok.net, [email protected] Тел/факс: (383) 333-25-54, 333-21-48 Консалтинг медиа - рынка Уважаемые господа, В рамках совместного проекта c Голландской компанией Burrelco (www.intech.gorodok.net/burrelco.html), предлагаем всем заинтересованным организациям в создании совместного предприятия с Голландской компанией, на...»

«МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РОССИЙСКОЙ ФЕДЕРАЦИИ УРАЛЬСКИЙ ГОСУДАРСТВЕННЫЙ ЭКОНОМИЧЕСКИЙ УНИВЕРСИТЕТ УТВЕРЖДАЮ Проректор по учебной работе Л. М. Капустина _ _2011 г. ПРОГРАММА УЧЕБНОЙ ДИСЦИПЛИНЫ ЭКОНОМИКА Специальность 260601 МАШИНЫ И АППАРАТЫ ПИЩЕВЫХ ПРОИЗВОДСТВ Специализация ОБОРУДОВАНИЕ ПРЕДПРИЯТИЙ ТОРГОВЛИ И ОБЩЕСТВЕННОГО ПИТАНИЯ Квалификация инженер Екатеринбург 1. ЦЕЛИ ОСВОЕНИЯ УЧЕБНОЙ ДИСЦИПЛИНЫ Современной российской экономике требуется подготовка специалистов с новым типом...»

«Семинар-тренинг “Взаимодействие с Глобальным фондом: решение проблем в сферах укрепления систем сообществ и снижения вреда в Евразии” 25-27 июня 2012 г., Стамбул (Турция) ОТЧЕТ Список сокращений 1. Цели и задачи 2. Организаторы и финансирующие организации 3. Участники и эксперты 4. Методология 5. Основные темы 5.1. Структуры, задействованные в страновых программах, поддерживаемых ГФ. 4 5.2. Надзорная функция СКК 5.3. Перепрограммирование 5.4. Реализация программ и взаимодействие ОР и СР 5.4.1....»

«Муниципальное бюджетное общеобразовательное учреждение Лицей №23 ОСНОВНАЯ ОБРАЗОВАТЕЛЬНАЯ ПРОГРАММА ОСНОВНОГО ОБЩЕГО ОБРАЗОВАНИЯ (ФГОС ООО) УТВЕРЖДЕНА на заседании педагогического совета № 1 от августа 2013 г. г. Озерск, 2013 СОДЕРЖАНИЕ ЦЕЛЕВОЙ РАЗДЕЛ 1.1. ПОЯСНИТЕЛЬНАЯ ЗАПИСКА 1.2 ПЛАНИРУЕМЫЕ РЕЗУЛЬТАТЫ ОСВОЕНИЯ ОБУЧАЮЩИМИСЯ ОСНОВНОЙ ОБРАЗОВАТЕЛЬНОЙ ПРОГРАММЫ ОСНОВНОГО ОБЩЕГО ОБРАЗОВАНИЯ 1.2.1 Общие положения 1.2.2. Ведущие целевые установки и основные ожидаемые результаты 1.2.3. Планируемые...»

«Министерство образования и наук и Российской Федерации ВЕСТНИК ДАЛЬНЕВОСТОЧНОГО РЕГИОНАЛЬНОГО УЧЕБНОМЕТОДИЧЕСКОГО ЦЕНТРА №20/2013 Владивосток 2013 УДК 378.12 ISSN 2078-3906 Серия основана в 1994 году Редакционная коллегия: С.В. Иванец, А.А. Фаткулин, Ю.М. Сердюков, П.Ф. Бровко, Г.Н. Ким, Ю.Г. Плесовских, Е.В. Крукович, Т.В. Селиванова Вестник Дальневосточного регионального учебно-методического центра. – Владивосток: ДВФУ, 2013. – 217 с. Предлагаемый Вестник ДВ РУМЦ продолжает серию сборников...»

«МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РОССИЙСКОЙ ФЕДЕРАЦИИ ГОСУДАРСТВЕННОЕ ОБРАЗОВАТЕЛЬНОЕ УЧРЕЖДЕНИЕ ВЫСШЕГО ПРОФЕССИОНАЛЬНОГО ОБРАЗОВАНИЯ Московский государственный горный университет Кафедра горных машин и оборудования УТВЕРЖДАЮ Проректор по методической работе и качеству образования В. Л. Петров _2011 г. РАБОЧАЯ УЧЕБНАЯ ПРОГРАММА ПО ДИСЦИПЛИНЕ СД.Ф.01. ГОРНЫЕ МАШИНЫ И ОБОРУДОВАНИЕ Направление подготовки 130400 Горное дело Специальность Физические процессы горного или нефтегазового производства...»

«МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РОССИЙСКОЙ ФЕДЕРАЦИИ Федеральное государственное бюджетное образовательное учреждение высшего профессионального образования Кемеровский государственный университет Новокузнецкий институт (филиал) Факультет информационных технологий Кафедра экологии и естествознания УТВЕРЖДАЮ Декан ФИТ Каледин В.О. 14 марта 2013 г. РАБОЧАЯ ПРОГРАММА учебной дисциплины ОПД.Ф.15 Геоэкологическое проектирование и экспертиза Для специальности 020804.65 Геоэкология Специализация...»

«ФЕДЕРАЛЬНОЕ ГОСУДАРСТВЕННОЕ БЮДЖЕТНОЕ ОБРАЗОВАТЕЛЬНОЕ УЧРЕЖДЕНИЕ ВЫСШЕГО ПРОФЕССИОНАЛЬНОГО ОБРАЗОВАНИЯ САРАТОВСКАЯ ГОСУДАРСТВЕННАЯ ЮРИДИЧЕСКАЯ АКАДЕМИЯ УТВЕРЖДАЮ Первый проректор, проректор по учебной работе _ 2012 года УЧЕБНО-МЕТОДИЧЕСКИЙ КОМПЛЕКС ДИСЦИПЛИНЫ Дипломатическое и консульское право Специальность 03050165 Юриспруденция Саратов – 2012 Учебно-методический комплекс дисциплины обсужден на заседании кафедры европейского права и сравнительного правоведения 05 июня 2012 г. Протокол №...»

«Приложение 4: Программа-минимум кандидатского экзамена по иностранному языку ФЕДЕРАЛЬНОЕ ГОСУДАРСТВЕННОЕ БЮДЖЕТНОЕ ОБРАЗОВАТЕЛЬНОЕ УЧРЕЖДЕНИЕ ВЫСШЕГО ПРОФЕССИОНАЛЬНОГО ОБРАЗОВАНИЯ ПЯТИГОРСКИЙ ГОСУДАРСТВЕННЫЙ ЛИНГВИСТИЧЕСКИЙ УНИВЕРСИТЕТ Утверждаю Проректор по научной работе и развитию интеллектуального потенциала университета профессор З.А. Заврумов __2013 г. ПРОГРАММА-МИНИМУМ кандидатского экзамена Иностранный язык по специальности 10.02.04 Германские языки Дисциплина: Иностранный язык...»

«Министерство образования и науки Российской Федерации Федеральное государственное бюджетное образовательное учреждение высшего профессионального образования Кемеровский государственный университет Факультет романо-германской филологии ПРОГРАММА кандидатского экзамена по специальности 10.02.19 – Теория языка КЭ.А.03; цикл КЭ.А.00 Кандидатские экзамены основной профессиональной образовательной программы подготовки аспиранта по отрасли 10.00.00 – филологические науки, 10.02.00 – Языкознание...»

«2013 ГОДОВОЙ ОТЧЕТ ПРОИЗВОДСТВЕННЫЕ ПОКАЗАТЕЛИ ТРАНСФОРМАТОРНАЯ МОЩНОСТЬ 743,6 ГВА ПРОТЯЖЕННОСТЬ ЛИНИЙ ЭЛЕКТРОПЕРЕДАЧИ 2262 тыс. км ОТПУСК ЭЛЕКТРОЭНЕРГИИ 706 млрд кВтч КОЛИЧЕСТВО ПОДСТАНЦИЙ 473 тыс. шт. ЧИСЛЕННОСТЬ СОТРУДНИКОВ 221,6 тыс. чел. ФИНАНСОВЫЕ ПОКАЗАТЕЛИ ВЫРУЧКА 759,8 млрд руб. ЧИСТАЯ ПРИБЫЛЬ (СКОРРЕКТИРОВАННАЯ*) 74 млрд руб. Скорректированная чистая прибыль рассчитывается * как чистая прибыль за период, без учета убытков от обесценения...»

«И.М.Лифиц СТАНДАРТИЗАЦИЯ, МЕТРОЛОГИЯ И СЕРТИФИКАЦИЯ УЧЕБНИК Рекомендовано Министерством образования Российской Федерации в качестве учебника для студентов высших учебных заведений, обучающихся по специальностям Коммерция, Маркетинг, Товароведение и экспертиза товаров 5-е издание, переработанное и дополненное МОСКВА • ЮРАЙТ • 2005 УДК 389 ББК 30.10ц; 65.2/4-80я73 Л64 Рецензенты: М.А. Николаева — доктор технических наук, профессор, действительный член Международной академии информатизации: Г.Н....»

«МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РОССИЙСКОЙ ФЕДЕРАЦИИ федеральное государственное автономное образовательное учреждение высшего профессионального образования Северный (Арктический) федеральный университет имени М.В. Ломоносова УТВЕРЖДАЮ Первый проректор по образованию и науке Л.Н. Шестаков 18 июня 2012 г. Основная образовательная программа высшего профессионального образования Направление подготовки: 250700.62 Ландшафтная архитектура Квалификация (степень): бакалавр Архангельск 1. Общие...»

«1 Формирование ответственности за состояние окружающей среды подрастающего поколения российских граждан через реализацию права на выборы Экологические деловые игры для старшеклассников: воспитание заботы об окружающей среде Цели и задачи проекта, описание мероприятий, основные результаты В 2004-2005гг. Институт консалтинга экологических проектов реализовал новый проект, направленный на решение задачи вовлечения молодежи в природоохранный процесс и формирования экологически ориентированного...»






 
2014 www.av.disus.ru - «Бесплатная электронная библиотека - Авторефераты, Диссертации, Монографии, Программы»

Материалы этого сайта размещены для ознакомления, все права принадлежат их авторам.
Если Вы не согласны с тем, что Ваш материал размещён на этом сайте, пожалуйста, напишите нам, мы в течении 1-2 рабочих дней удалим его.