Рисование в Swing |
105 |
UI-представителя. Эта небольшая проблема имеет два решения: можно написать для своего компонента, даже для такого простого как наш, UI-представителя, который, используя ресурсы своего базового класса ComponentUI, реально ничего делать не будет, но мы еще не добрались до подробного обсуждения UI-представителей. Поэтому в примере применено второе решение: мы унаследовали компонент от класса JPanel, на котором тоже можно спокойно рисовать (сам он ничего не рисует), к тому же у него есть свой UI-представитель.
Далее мы, так как в примере будет использоваться отладка с условием FLASH_OPTION, полностью отключаем двойную буферизацию методом класса RepaintManager() (почти то же самое можно было бы сделать, вызвав метод setDoubleBuffered(false) для корневой панели нашего окна). Для нашего компонента мы включаем два режима отладки: вывод диагностических сообщений и выделение цветом, для этого константы класса DebugGraphics объединяются операцией логического «ИЛИ». Напоследок идет небольшая настройка режима выделения цветом (мы увеличиваем количество мерцаний и задержку, чтобы лучше видеть процесс рисования). Остается запустить приложение и посмотреть на результат.
Диагностические сообщения, выводимые в режиме отладки графики на консоль (хотя можно выводить их в любой поток вывода), имеют простой формат:
Graphics(N1-N2) Операция: Параметры
Здесь N1 — номер используемого в данный момент объекта DebugGraphics (каждый раз при создании объекта DebugGraphics он нумеруется), а N2 — код режима отладки. Значение N2 в нашем примере равно 3 — это именно то число, которое мы передаем в метод setDebugGraphicsOptions() (оно получается при логическом объединении констант). Оно дает возможность узнать, какие операции отладки используются в данный момент. Далее следует краткое текстовое описание графической операции (которое не слишком информативно и фактически повторяет название вызываемого метода) и список параметров, переданных вызванному методу.
Данный пример может быть полезен не только в качестве наглядного пособия по использованию встроенной системы отладки графики, но и как пример работы системы прорисовки Swing. После запуска закройте область нашего окна другим окном, потом откройте ее и посмотрите, как перерисовывается графика. Если была закрыта только часть окна приложения, то заново прорисуется только закрытая часть, хотя диагностические сообщения показывают, что вызываются все операции прорисовки компонента PaintingComponent. Механизмы Swing сами заботятся о прямоугольнике отсечения и максимально эффективном выводе на экран, а нам остается лишь нарисовать компонент и получить готовый результат. Отладку графики также удобно проводить для стандартных компонентов Swing, если с ними возникают затруднения, например, если они неправильно перекрываются или неверно располагаются в контейнере. В любом случае систему отладки графики, любезно предоставленную нам Swing, стоит «держать под рукой».
Отладка графики вдохновила создателей некоторых библиотек на встраивание в них на том или ином уровне подобных механизмов. К примеру, в популярном стороннем менеджере расположения MigLayout, о котором мы вкратце расскажем в главе 7, есть визуальная отладка, позволяющая отследить, где располагаются все компоненты и какое именно пространство им выдал менеджер расположения.
Резюме
Внутренние механизмы системы рисования Swing, незаметно выполняющие для нас разнообразную «грязную» работу, довольно сложны и в основном скрыты в базовом классе библиотеки JComponent. Все компоненты библиотеки наследуют от него и поэтому эффективно рисовать в Swing можно лишь четко представляя, что при этом происходит внутри библиотеки. К тому же знание «внутренностей» системы открывает широкие возможности для отладки проблем и смелых экспериментов с графикой.
Глава 5. Внутренние винтики Swing
Эта глава будет несколько отличаться от всех остальных глав книги, в которых мы рассматриваем конкретные вопросы, компоненты или способы работы с библиотекой. Здесь мы ближе взглянем на некоторые внутренние механизмы библиотеки Swing, которые обычно работают как часы, однако при возникновении более сложных ситуаций требуют нашей осведомленности в них. Во-первых, речь будет идти о проверке корректности («валидации») компонентов. Процесс это довольно простой, однако на практике иногда упорно «показывающий зубы», он имеет свою интерпретацию в классе JComponent. Мы рассмотрим, как проверка производится классически, и узнаем механику работы метода revalidate().
Далее мы разберемся с более тонкими способами работы с клавиатурой. Обработка событий от клавиатуры с помощью слушателей во многих ситуациях недостаточна, так как предназначена для конкретного компонента. Клавиатурные сокращения Swing, вкупе с системой передачи фокуса ввода, позволяют тоньше настроить работу с клавиатурой, и мы рассмотрим, как это делается и почему это так.
Эта глава не предназначена для новичков в Swing. Многое в Swing очень доступно и очень просто, и прежде чем переходить к деталям и мелким концепциям настолько мощной библиотеки, необходимо изучить ее основы, особенно способ рассылки событий и размещение компонентов в контейнерах, в том числе и высшего уровня. Если же вам будет недостаточно, можно прочитать и эту главу.
Проверка корректности компонентов
Проверка корректности компонентов требуется для того, чтобы расположение компонентов в контейнере и их размеры соответствовали действительности. На самом деле, если вы прямо во время работы программы увеличите размер шрифта на кнопке, то прежнего размера кнопки (которого, как правило, как раз хватает для размещения надписи) уже будет недостаточно, и часть надписи окажется непонятно где.
Если компонент инициализируется перед выводом на экран, то последовательность действий четко определена — вы создаете его, задаете свойства компонента, в том числе те, что влияют на его внешний вид, к примеру размер шрифта и значков, а затем добавляете в некоторый контейнер. В контейнере работает менеджер расположения, который выясняет, какой размер желает иметь компонент, и выделяет ему пространство на экране, где его можно будет увидеть, как только приложение выведет контейнер высшего уровня на экран.
Однако, если контейнер уже на экране, а вы меняете свойство содержащегося в нем компонента, влияющее на его внешний вид, а самое главное, размеры, для обновления контейнера, его размеров и размеров всех соседей-компонентов необходимо провести проверку корректности («валидацию»), которая заново распределит пространство на экране и перерисует компоненты в их новом состоянии.
В AWT, в котором, как мы помним, компоненты лаконичны до крайности, каждый из них хранит булевский флаг valid, показывающий, находится ли сейчас компонент в корректном состоянии. Как только происходит нечто, нарушающее размеры компонента (к примеру, смена шрифта или текста), флаг устанавливается в false специальным методом invalidate(). Так как обычно компонент хранится в контейнере, а тот в другом контейнере, и так далее, изменение его размеров влияет на все содержащие его контейнеры.
Внутренние винтики Swing |
107 |
Чтобы пометить этот факт, метод invalidate() вызывает точно такой же метод контейнера, в котором компонент хранится, и все идет по цепочке до самого верха.
Сам же процесс приведения к корректному виду осуществляется методом validate(). Для обычных компонентов AWT, таких как кнопки или надписи, он просто перерисовывает компонент. А вот для контейнеров все сложнее: метод заново вызовет менеджер расположения контейнера и перераспределит пространство между компонентами в контейнере и изменит их размеры согласно их новым желаниям, вызовет для всех компонентов в контейнере их метод validate(), а потом и перерисует сам контейнер. Так что, если вы хотели привести конкретный компонент к нужному размеру, вряд ли стоило вызывать для него validate(), если только вы уже не задали ему новый подходящий размер вручную — это будет равносильно его перерисовке. В общем же всегда вызывается метод validate() того контейнера, в котором находится измененный компонент. Стоит заметить, что validate() работает лишь тогда, когда компоненты уже выведены на экран.
Рассмотрим небольшой пример:
//Базовая валидация AWT — при изменении размеров
//или других параметров остается вызвать validate() import java.awt.*;
public class AWTValidateShow extends Frame { private static Button button;
public AWTValidateShow() { setSize(400, 300);
Panel contents = new Panel(); button = new Button("Текст");
Button button2 = new Button("Текст 2"); contents.add(button); contents.add(button2);
add(contents);
}
public static void main(String[] args) throws InterruptedException {
new AWTValidateShow().setVisible(true); Thread.sleep(2000); button.setLabel("Очень длинный текст");
//С этого момента размер поменялся — вызван invalidate()
//можно вызывать validate() в контейнере
Thread.sleep(2000);
//будет заново расположен весь контейнер
//и все его содержимое (кнопка)
button.getParent().validate();
}
}
108 |
ГЛАВА 5 |
В примере мы создаем окно и помещаем в него в панель с двумя кнопками. Обратите внимание, пример написан исключительно с использованием AWT, так что беспокоиться о многозадачности нам не придется — компоненты AWT синхронизированы, и мы можем напрямую вызывать из других потоков, отличных от потока рассылки событий. Маленький нюанс примера — окна AWT не умеют сами закрывать себя, а мы не написали слушателя окна, который заканчивал бы приложение при закрытии окна, так что приложение это придется заканчивать вручную, снимая задачу.
Когда у кнопки меняется надпись, она помечает себя и все содержащие ее контейнеры как некорректные. Нам же остается вызвать validate() для содержащего ее контейнера (мы получаем его методом getParent()), чтобы он получил данные о новом размере кнопки и заново расположил ее. В примере специально вставлены задержки — вы успеете увидеть, как выглядит некорректная кнопка. В качестве упражнения попробуйте вызвать validate() только для кнопки, чтобы убедиться, что это не поможет ей обрести корректный размер.
Метод Swing: revalidate()
Одной из основных задач Swing является более быстрая и эффективная работа компонентов библиотеки, в сравнении с AWT. Проверка корректности не стала исключением. Прежде всего, компоненты Swing намного более сложны, и некоторые из них в любом случае «поглотят» любые изменения содержащихся в них компонентов. К примеру, это внутренние окна JInternalFrame (все, что меняется в окне, остается в его рамках) или панель прокрутки JScrollPane (изменение размеров содержимого лишь меняет полосы прокрутки, но не размер самой панели). Однако мы увидели, что при изменении компонентов некорректными становятся все компоненты, вплоть до контейнера высшего уровня — так устроен метод invalidate().
Чтобы каким-то образом помечать компоненты, на которых можно остановить проверку корректности, в базовом классе JComponent появился метод isValidateRoot(). Те компоненты, которые утверждают, что все изменения их содержимого не отразятся на их размерах (и значит, на размерах всего окружающего), вернут в этом методе true (к ним относится JScrollPane и корневая панель JRootPane, которая является основой любого окна в Swing). Такие компоненты мы называем корнем валидации. По умолчанию метод возвращает false.
Как мы уже убедились (в описании прорисовки), Swing старается всегда скомпоновать похожие события графической системы, что минимизировать издержки. Эта же схема применена и для проверки корректности, и делает это новый метод revalidate() из класса JComponent. Он не проводит мгновенной проверки корректности, вместо этого, он помечает компонент как некорректный в классе RepaintManager. Тот, в свою очередь, находит корень валидации для компонента, и помещает в очередь событий отложенное задание для окна, в котором этот корень валидации находится (все это происходит только в том случае, если компонент уже выведен на экран, до вывода на экран в проверке просто нет смысла). Когда очередь до этого события дойдет, в данном корне валидации и данном окне могут накопиться несколько компонентов, требующих проверки корректности, и выполнение проверки за один раз позволяет сократить издержки.
Само же задание выполняет проверку корректности уже знакомым нам методом validate() из AWT. Он вызывается для корня валидации, а это значит, что все находящиеся в нем компоненты будут заново расположены с учетом их новых пожеланий по размерам. Компоненты, которые находятся в контейнерах «выше» корня валидации, таким образом проверяться не будут, что сэкономит драгоценные секунды.
Теперь понятно, что эффект revalidate() коренным образом отличается от validate(). Если validate() нужно вызывать с умом, выбирая тот контейнер, проверка которого заново расположит все нужные нам компоненты, то revalidate() совершенно безразличен к тому,
Внутренние винтики Swing |
109 |
откуда его вызывают. Он в любом случае найдет корень валидации и вызовет проверку именно для него, что гарантирует нам нужный результат. Более того, большая часть компонентов Swing вызывает revalidate() при смене свойств, меняющих внешний вид и размер, автоматически, что совсем избавляет нас от раздумий, свойственных системе проверки корректности в AWT.
Рассмотрим пример:
//Валидация Swing — большинство компонентов
//позаботятся о себе сами. В остальном метод revalidate()
//позволяет не задумываться о деталях
import javax.swing.*;
public class SwingValidateShow extends JFrame { private static JButton button, newButton;
public SwingValidateShow() { setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); setSize(400, 300);
JPanel contents = new JPanel(); button = new JButton("Текст");
JButton button2 = new JButton("Текст 2"); contents.add(button); contents.add(button2);
add(contents);
}
public static void main(String[] args) throws InterruptedException {
SwingUtilities.invokeLater(new Runnable() { public void run() {
new SwingValidateShow().setVisible(true);
}
});
Thread.sleep(2000);
//Кнопка при смене параметра сама вызовет
//revalidate() и мы сразу же увидим изменения
SwingUtilities.invokeLater(new Runnable() { public void run() {
button.setText("Очень длинный текст");
}
});
//при добавлении в контейнер revalidate()
//автоматически не вызывается