Портянкин И. Swing
.pdfУправление пространством |
385 |
Теперь рассмотрим пример, в котором создадим несколько разделяемых панелей JSplitPane, применив самые полезные и часто используемые их возможности, после чего обсудим все поподробнее:
//UsingSplitPanes.java
//Использование разделяемых панелей import javax.swing.*;
import java.awt.*;
public class UsingSplitPanes extends JFrame { // этот значок будем использовать в надписях
private Icon icon = new ImageIcon("image.jpg"); public UsingSplitPanes() {
super("UsingSplitPanes"); setDefaultCloseOperation(EXIT_ON_CLOSE); // первая разделяемая панель
JSplitPane splitMain = new JSplitPane(); splitMain.setOneTouchExpandable(true);
//размер полосы splitMain.setDividerSize(20);
//вертикальная разделяемая панель
JSplitPane split2 = new JSplitPane( JSplitPane.VERTICAL_SPLIT, true);
//настроим ее компоненты split2.setTopComponent(
new JScrollPane(new JLabel(icon))); split2.setBottomComponent(
new JScrollPane(new JLabel(icon)));
//настроим компоненты первой панели splitMain.setLeftComponent(
new JScrollPane(new JLabel(icon))); splitMain.setRightComponent(split2);
//добавим панель и выведем окно на экран add(splitMain);
setSize(600, 400); setVisible(true);
}
public static void main(String[] args) { SwingUtilities.invokeLater(
new Runnable() {
public void run() { new UsingSplitPanes(); } });
}
}
386 |
ГЛАВА 13 |
В примере мы создаем небольшое окно, в центре панели содержимого которого разместится разделяемая панель JSplitPane. Разделяемые панели могут быть вертикальными и горизонтальными, в вертикальных панелях компоненты располагаются друг над другом, а в горизонтальных — в ряд, слева и справа. По умолчанию создается горизонтальная разделяемая панель. Для первой разделяемой панели (splitMain) мы использовали конструктор без параметров, он создает горизонтальную разделяемую панель, а в качестве компонентов задействует две кнопки (чтобы не оставлять место пустым, но вы всегда можете установить в разделяемой панели собственные компоненты). Сразу же после создания компонента мы изменили два свойства разделяемой панели. Свойство oneTouchExpandable мы установили в true; оно позволяет добавить к вешке разбивки компонентов две маленькие кнопки со стрелками, щелкая на которых пользователь сможет мгновенно переместить вешку в крайнее положение, полностью предоставив все пространство разделяемой панели одному из компонентов. Мы также поменяли значение свойства dividerSize, которое управляет размером (в пикселах) полосы, образующей вешку разбивки. По умолчанию размер полосы задается UI-представителем разделяемой панели, мы выбрали его на свой вкус, немного большим, чем обычно.
Вторая разделяемая панель создается гораздо более интересным конструктором. Он принимает два параметра: первый задает тип панели (вертикальная или горизонтальная), а второй позволяет сразу же определить, будут ли компоненты при перемещении вешки разбивки непрерывно обновляться (перерисовываться и, если это сложный компонент, проводить проверку корректности). По умолчанию непрерывное обновление отключено (как для первой созданной нами разделяемой панели), мы включаем его. Непрерывное обновление можно включить (или выключить) и после создания разделяемой панели, меняя значение свойства continuousLayout. Как правило, непрерывное обновление стоит сразу же включать, тогда при перемещении вешки разбивки пользователь сразу увидит, как открываются новые части компонента, которому он предоставляет дополнительное место, так что оценить, сколько места понадобится компоненту для комфортной работы, будет гораздо проще.
После создания разделяемых панелей и нехитрой настройки настает пора задать компоненты, которые они должны разделять. Для панели, созданной второй по счету (вертикальной), мы задействуем для задания компонентов методы с красноречивыми именами setTopComponent() и setBottomComponent(), позволяющие указать верхний и нижний компонент, соответственно. Компонентами являются две надписи JLabel с одинаковыми значками. Обратите внимание на то, что даже для таких несложных компонентов, как надписи, были применены панели прокрутки JScrollPane. Использовать панели прокрутки для компонентов, содержащихся в разделяемой панели, приходится почти всегда: дело в том, что разделяемая панель учитывает минимальный размер компонента и не позволяет делать его размер меньше. Если минимальный размер компонента значим (к примеру, минимальный размер надписи со значком равен размеру значка), толку от разделяемой панели будет мало — она просто не позволит изменять размеры компонентов и гибко распределять пространство контейнера. Учитывая это, необходимо включать компоненты в панели прокрутки. Исключение составляют лишь компоненты, минимальный размер которых равен нулю или невелик: к таким компонентам относятся панели JPanel, «рабочий стол» JDesktopPane и практически все остальные вспомогательные контейнеры, служащие для размещения других компонентов, в том числе и сама разделяемая панель JSplitPane. Их вы можете добавлять в разделяемую панель напрямую.
Далее мы задаем компоненты для горизонтальной разделяемой панели, созданной нами первой. Названия методов здесь также красноречивы: методы setLeftComponent() и setRightComponent() позволяют задать левый и правый компоненты панели. Интересно, что уже использованные нами для вертикальной панели методы setTopComponent()
Управление пространством |
387 |
и setBottomComponent() на самом деле вызывают методы, задающие левый и правый компоненты, и определены только для ясности и удобства программиста. Левым компонентом будет надпись JLabel с тем же значком, включенная в панель прокрутки, а вот правым компонентом станет ничто иное, как уже созданная и настроенная нами вертикальная разделяемая панель. Такое поведение вполне возможно, так что вы без труда сможете сделать из своего контейнера настоящую мозаику, добавляя в разделяемые панели JSplitPane такие же панели. Вот только увлекаться этим не стоит: если в разделяемую панель вложено больше одной такой же панели, интерфейс запутает пользователя и не придаст вашему приложению привлекательности. Лучше вообще ограничиться единственным экземпляром JSplitPane.
Интересно, что разделяемая панель JSplitPane относится к тем немногим контейнерам, которые переопределяют метод isValidateRoot() базового класса JComponent и возвращают в нем true. Мы обсуждали это свойство и улучшенный механизм проверки корректности Swing в главе 5. Здесь все логично: при изменении размеров одного из компонентов разделяемой панели может измениться положение полосы и, соответственно, изменятся размеры другого компонента, но все изменения произойдут в пределах разделяемой панели, и проводить проверку корректности «за ее рубежами» не понадобится.
Свойства разделяемой панели
Запустив программу с примером, вы сможете увидеть разделяемые панели в действии, а также оценить удобство мгновенного перемещения полосы прокрутки (маленькими кнопками со стрелками) и непрерывного обновления компонентов. Ну а все использованные нами в примере свойства мы перепишем, чтобы держать их «под рукой» в качестве справочника (табл. 13.2).
Таблица 13.2. Самые полезные свойства разделяемой панели JSplitPane
Свойства |
Описание |
orientation |
Задает ориентацию разделяемой панели. Разделяемая панель может |
|
быть горизонтальной (по умолчанию создается именно горизонтальная |
|
панель) или вертикальной. В горизонтальной панели компоненты рас- |
|
полагаются слева и справа вешки разбивки, а в вертикальной — сверху |
|
и снизу |
388 |
ГЛАВА 13 |
Таблица 13.2 (продолжение)
Свойства |
Описание |
continuousLayout |
Позволяет включить или выключить режим непрерывного обновления |
|
компонентов при перемещении вешки разбивки. По умолчанию непре- |
|
рывное обновление выключено, но его имеет смысл включать: непре- |
|
рывное обновление позволяет создать более «отзывчивый» интерфейс, |
|
потому что пользователь сможет в реальном времени видеть, какая часть |
|
компонента открывается при перемещении вешки разбивки. Отключение |
|
обновления имеет смысл лишь при серьезных проблемах с производи- |
|
тельностью перерисовки. |
dividerSize |
Управляет размером (в пикселах) вешки разбивки |
oneTouchExpandable |
Установив данное свойство в true, вы добавляете к вешке разбивки две |
|
маленькие кнопки, позволяющие «одним мановением руки» предоста- |
|
вить все пространство панели только одному из компонентов. По умол- |
|
чанию это свойство равно false, но лучше установить его в true: особого |
|
труда это не требует, а мгновенное распределение пространства удобно |
|
для пользователя |
leftComponent, |
С помощью этих свойств (и методов get/set) вы сможете установить ле- |
topComponent |
вый (в терминах горизонтальной панели) или верхний (в терминах вер- |
|
тикальной) компонент. Используйте то свойство, которое вам больше по |
|
душе; как мы уже знаем, они устанавливают один и тот же компонент, |
|
различны лишь названия |
rightComponent, |
Эти свойства позволяют задать компонент для правой (или, что то же |
bottomComponent |
самое, для нижней) части разделяемой панели. Как и в случае с преды- |
|
дущими свойствами, компонент один и тот же, а выбрать вы можете то |
|
свойство, название которого лучше подходит для вашей панели (верти- |
|
кальной или горизонтальной) |
Действуя примерно так, как мы это делали в примере, и манипулируя описанными в таблице свойствами, вы сможете разделить любые компоненты и предоставить пользователю возможность в любой момент перераспределить занятое ими пространство контейнера.
Управлять положением вешки разбивки разделяемой панели можно и программно с помощью нескольких дополнительных свойств класса JSplitPane (табл. 13.3). Также есть возможность указать, как должно распределяться свободное место в том случае, если размер разделяемой панели увеличится.
Таблица 13.3. Дополнительные свойства разделяемой панели
Свойства Описание
dividerLocation Для этого свойства имеется два перегруженных метода set: первый при-
нимает в качестве параметра целое число (int), указывающее, сколько
пикселов от левой (или, что то же самое, верхней) границы разделяемой панели должно быть до полосы. Вторая версия метода требует параметр
типа double, причем значение его должно лежать в границах от нуля до единицы. Фактически это значение в процентах (если умножить его на сто, конечно), показывающее, какую часть разделяемой панели будет занимать левая (верхняя) ее часть. Указав, к примеру, значение 0,35, вы
выделите под левую часть панели 35% доступного пространства, правой
части достанется 65% этого пространства, соответственно расположится и полоса
Управление пространством |
389 |
|
Таблица 13.3 (продолжение) |
|
|
|
|
|
Свойства |
Описание |
|
resizeWeight |
Позволяет указать, как должно распределяться избыточное пространство, ес- |
|
|
ли разделяемая панель увеличивается в размерах. Это свойство имеет тип |
|
|
double и работает точно так же, как и вторая версия предыдущего свойства. |
|
|
Значение свойства должно лежать в пределах от нуля до единицы, оно задает, |
|
|
сколько (в процентах, если умножить значение на сто) от избыточного про- |
|
|
странства достанется левой (верхней) части разделяемой панели. Остальная |
|
|
часть достанется правой (нижней) панели. |
|
Интересный факт — если вы установите значение этого свойства в 0 или 1,
это будет означать, что все дополнительное пространство будет доставаться исключительно одной из частей разделяемой панели (левой или правой, соответственно), а вторая не будет менять свой размер. Однако это не отменяет тот факт, что пользователь сможет менять размер компонентов панели.
События разделяемой панели
У разделяемой панели JSplitPane нет собственных событий. «Как же так?» — спросите вы: «А если понадобится узнать о перемещениях полосы разделяемой панели, чтобы предпринять при этом какие-либо особые действия? Как это сделать?». Тут надо вспомнить, что все свойства компонентов Swing являются привязанными (мы обсуждали основы Swing и JavaBeans в главе 1). Это означает, что каждый компонент библиотеки поддерживает слушателей PropertyChangeListener, которые оповещаются при смене любого свойства компонента, имеющего значение для его создателя-программиста. Создатели класса JSplitPane решили, что не стоит плодить новые классы и интерфейсы только для того, чтобы следить за свойством dividerLocation. Ведь сделать это можно и с помощью слушателя PropertyChangeListener. Принцип действия прост: вы регистрируете в компоненте своего слушателя, так что при смене какого-либо свойства вызывается метод этого слушателя propertyChange(). В качестве параметра этого метода выступает объект PropertyChangeEvent. Вызвав его метод getPropertyName(), вы узнаете имя свойства, а метод getNewValue() поможет получить новое значение свойства (но вы должны знать его тип, для свойства dividerLocation это будет объект Integer). Следя за событием с именем dividerLocation, вы сможете вовремя узнавать, куда и когда пользователь перетаскивает вешку разбивки.
Хотя мы и отмечали, что события PropertyChangeEvent в основном предназначены для визуальных инструментов построения GUI и обмена информацией между компонентами и UI-представителями, они могут быть полезны и в повседневном программировании. Если вам вдруг понадобится следить за изменениями какого-либо свойства, не стоит сетовать на судьбу, если отдельного события для этого свойства создатели компонента не предусмотрели. К вашим услугам всегда механизм привязанных свойств JavaBeans, которому четко следует библиотека Swing.
Разделяемая панель в насыщенных интерфейсах
Одним из признаков пользовательского интерфейса, спроектированного не самым лучшим образом, является переизбыток так называемого «визуального шума», разнообразных рамок, линий, жирного текста, подчеркиваний и тому подобных эффектов. Наш разум достаточно хорош, чтобы структурировать интерфейс и просто по тому, что его компоненты разделены некоторым пустым пространством. Эта
390 |
ГЛАВА 13 |
проблема часто возникает с разделяемыми панелями — внутри них, как правило, находятся панели прокрутки со своими собственными рамками или панели с какиминибудь украшениями, и ко всему этому великолепию разделяемая панель добавляет свою вешку разбивки. Интерфейс чересчур сильно насыщается рамками и границами.
Чтобы избежать негативного эффекта, достаточно считать, что вешка разбивки разделяемой панели представляет собой и так достаточно сильную визуальную рамку, и ни в коем случае не применять внутри разделяемой панели других рамок и эффектов отделения содержимого. На крайний случай, если вам совершенно необходима рамка или что-то подобное в компоненте, который содержится в разделяемой панели, вы можете отделить его от вешки разбивки с помощью пустой рамки EmptyBorder определенного размера, соблюдая при этом рекомендации дизайнеров вашего внешнего вида. Ну а панель прокрутки можно избавить от рамки по умолчанию, заменив ее на все ту же пустую рамку нулевого размера, и вы будете избавлены от лишних линий и «шума».
Панель прокрутки JScrollPane
Панель прокрутки JScrollPane — настоящая «волшебная палочка» пользовательских интерфейсов Swing. Она уже не раз позволяла нам добиваться многого всего строчкой кода и, как мы сейчас увидим, способна на большее. Несложно догадаться по названию, что панель JScrollPane дает возможность наделить компонент (обычно большого размера) возможностью прокрутки (на экране появляется одна или две полосы прокрутки, используя которые, вы можете переходить к той или иной части компонента, выводя ее на экран). Как правило, для добавления панели прокрутки достаточно следующей записи:
add(new JScrollPane(ваш_компонент));
Этой строкой вы добавляете в контейнер некоторый компонент, размеры которого не позволяют ему разместиться на экране полностью2. Передав компонент в простейший конструктор класса JScrollPane, вы получаете прекрасную возможность ограничить размеры компонента (применив для этого подходящие менеджеры расположения или вспомогательные компоненты), не беспокоясь о том, что пользователь не сможет получить доступ к какой-либо его части.
Прежде чем начать обсуждение более сложных деталей, рассмотрим небольшой пример. В этом примере мы настроим самые простые свойства панели прокрутки JScrollPane, которые, тем не менее, могут быть весьма полезны для тонкой настройки пользовательского интерфейса:
//SimpleScrollPanes.java
//Настройка некоторых простых свойств панелей прокрутки import javax.swing.*;
import java.awt.*;
public class SimpleScrollPanes extends JFrame { public SimpleScrollPanes() {
super("SimpleScrollPanes");
2 Впрочем, это не обязательно — компонент может и полностью умещаться на экране. В таком случае панель прокрутки обеспечит гибкость вашему пользовательскому интерфейсу на тот случай, если размеры компонента изменятся (в нем появится больше данных для отображения), и к тому же изменит внешность компонента (панель прокрутки обладает специальной рамкой, зависящей от ее UI-представителя).
Управление пространством |
391 |
setDefaultCloseOperation(EXIT_ON_CLOSE); // надпись
JLabel label = new JLabel(new ImageIcon("image.jpg"));
//особый конструктор панели прокрутки
JScrollPane scrollPane = new JScrollPane(label, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
//некоторые свойства scrollPane.setViewportBorder(
BorderFactory.createLineBorder(Color.BLUE));
scrollPane.setWheelScrollingEnabled(false);
//выводим окно на экран
add(scrollPane); setSize(400, 300); setVisible(true);
}
public static void main(String[] args) { SwingUtilities.invokeLater(
new Runnable() {
public void run() { new SimpleScrollPanes(); } });
}
}
Пример замечательно прост: мы создаем надпись JLabel со значком ImageIcon, который был получен из файла с изображением в формате JPEG. Предположительно надпись большого размера. Мы размещаем ее в панели прокрутки, используя довольно сложный конструктор с тремя параметрами. Первый параметр, как нетрудно видеть, — это сам компонент, нуждающийся в прокрутке, то есть наша надпись с изображением. Вторые два параметра, прежде не применяемые нами при создании панелей прокрутки, управляют полосами прокрутки (JScrollBar), которые собственно и позволяют пользователю выполнять прокрутку. Первый из этих двух параметров определяет поведение вертикальной полосы прокрутки, второй — горизонтальной. По умолчанию используются описанные в классе JScrollPane в качестве констант значения XXX_SCROLLBAR_AS_NEEDED (где XXX может принимать значение VERTICAL или HORIZONTAL). Они указывают, что полосы прокрутки должны появляться на экране только в том случае, если компонент в панели прокрутки действительно велик и ему требуется прокрутка. Мы использовали значение SCROLLBAR_ALWAYS, которое означает, что полосы прокрутки будут находиться на экране в любом случае, даже если прокрутка не требуется. Иногда это позволяет ясно показать пользователю, что в случае необходимости компонент поддерживает прокрутку (к примеру, такое поведение вполне подходит текстовым компонентам: текста в них может быть немного, но полосы прокрутки должны быть всегда, как показатель того, что объем текста ничем не ограничен). Есть и еще один режим вывода полос прокрутки — XXX_SCROLLBAR_NEVER. При выборе этого значения полосы прокрутки не появляются на экране вообще, даже если в этом возникает необходимость. Обычно такое поведение требуется там, где прокрутка реализована программно. Помимо задания режима появления полос прокрутки в конструкторе вы можете изменять его с помощью свойств verticalScrollBarPolicy и horizontalScrollBarPolicy.
392 |
ГЛАВА 13 |
Далее мы настраиваем еще несколько свойств панели прокрутки. Как известно, панель прокрутки всегда добавляет особую рамку к видимой части компонента (видимая часть компонента выводится на экран окном просмотра JViewport, незаменимым помощником класса JScrollPane — вскоре мы о нем узнаем), которая определяется UIпредставителем. Иногда именно эта рамка позволяет придать компоненту, добавленному в панель прокрутки, законченный вид (в главе 11 мы отмечали, что списки JList из-за отсутствия своей собственной рамки выглядят без панели прокрутки не слишком презентабельно, то же относится к большинству текстовых компонентов). Менять ее стоит лишь тогда, когда у вас и так хватает визуального подчеркивания компонента, как мы говорили в этой главе про разделяемые панели. Как правило, лишнюю рамку можно просто убрать методом setBorder(), передав в него экземпляр пустой рамки EmptyBorder нулевого размера.
Однако рамка самой панели прокрутки не одинока. Есть еще и рамка вокруг содержимого панели прокрутки. Ее можно сменить с помощью свойства viewportBorder, в примере мы меняем ее рамкой LineBorder синего цвета, так что вы сможете оценить то, где она находится и как выглядит. Не сказать, чтобы это было очень полезное свойство, но знать все возможности своих компонентов определенно стоит.
Наконец, свойство wheelScrollingEnabled определяет, будет ли поддерживаться прокрутка колесиком мыши, по умолчанию оно включено. Если такое поведение по какойлибо причине вас не устраивает, вы можете легко отключить его и использовать движение колесика так, как вам хочется. В нашем примере мы как раз отключили поддержку колесика мыши.
Запустив программу с примером, вы сможете увидеть (вернее, «пощупать») те небольшие, но часто требующиеся изменения, которые мы произвели с панелью прокрутки JScrollPane. Обратите внимание, что когда компоненту в панели прокрутки хватает места, он все равно занимает все доступное место в панели (растягивается), и то, как он при этом будет выглядеть, зависит уже исключительно от него. Надпись JLabel, к примеру, при избытке пространства по умолчанию помещает значок по центру, а текст бы разместила слева. При разработке интерфейса следует учесть все подобные «краевые условия», и удостовериться в том, что он всегда ведет себе подобающе. Для этого панель прокрутки предоставляет специальные инструменты, о которых мы сейчас узнаем.
Управление пространством |
393 |
Управление прокруткой
По умолчанию процесс прокрутки компонента, добавленного в панель прокрутки JScrollPane, проходит довольно очевидно: при нажатии на одну из кнопок полос прокрутки компонент сдвигается в ту или иную сторону на один пиксел. При щелчке на полосе прокрутки видимая часть изображения меняется «блоком», при этом прежняя видимая часть исчезает, а вместо нее появляется следующая или предыдущая (в зависимости от того, по какую сторону полосы прокрутки был сделан щелчок) часть такого же размера, если это возможно. С другой стороны, можно вспомнить разнообразные сложные компоненты Swing, такие как списки или таблицы — уж они-то точно прокручиваются не по одному пикселу, даже если пользователь щелкает на кнопке полосы прокрутки. Вместо этого содержимое этих компонентов перемещается на одну логическую единицу: на один элемент списка или столбец таблицы, и это именно то поведение, что ожидает от них пользователь. Как же реализовать особое поведение при прокрутке?
Оказывается, ничего сложного в этом нет. Все компоненты Swing, поддерживающие нестандартную прокрутку, просто пользуются предоставленным им панелью прокрутки JScrollPane механизмом управления. Это интерфейс Scrollable, который необходимо реализовать компоненту, требующему особый режим прокрутки. Когда такой компонент попадает в панель прокрутки JScrollPane, последняя видит это и получает (вызывая определенные в интерфейсе методы Scrollable) дополнительную информацию о компоненте, которая и позволяет ей проводить прокрутку компонента так, как тому необходимо. Интерфейс Scrollable несложен и назначение методов его очевидно. Рассмотрим пример, в котором реализуем особый режим прокрутки своего компонента:
//ControllingScrolling.java
//Управление процессом прокрутки с помощью
//интерфейса Scrollable
import javax.swing.*; import java.awt.*;
public class ControllingScrolling extends JFrame { public ControllingScrolling() {
super("ControllingScrolling"); setDefaultCloseOperation(EXIT_ON_CLOSE);
//добавим в центр панель прокрутки с
//нашим компонентом
add(new JScrollPane(new GridComponent())); // выводим окно на экран
setSize(400, 300); setVisible(true);
}
// компонент-"сетка" с особым режимом прокрутки class GridComponent extends JPanel
implements Scrollable { // размер ячейки сетки private int CELL_SIZE = 45;
394 |
ГЛАВА 13 |
//количество ячеек сетки private int CELL_COUNT = 50;
//предпочтительный размер компонента public Dimension getPreferredSize() {
return new Dimension(CELL_SIZE*CELL_COUNT, CELL_SIZE*CELL_COUNT);
}
//прорисовка компонента
public void paintComponent(Graphics g) { // нужно вызвать метод базового класса super.paintComponent(g);
for (int x=0; x<CELL_COUNT; x++) { for (int y=0; y<CELL_COUNT; y++) {
//рисуем ячейку g.setColor(Color.BLACK); g.drawRect(x*CELL_SIZE, y*CELL_SIZE,
CELL_SIZE, CELL_SIZE);
//текст с координатами g.drawString(""+x+","+y,
x*CELL_SIZE + 5, y*CELL_SIZE + CELL_SIZE/2);
}
}
}
//предпочтительный размер области прокрутки public Dimension getPreferredScrollableViewportSize() {
return getPreferredSize();
}
//приращение при прокрутке на один элемент public int getScrollableUnitIncrement(
Rectangle visible, int or, int dir) { return CELL_SIZE;
}
//приращение при прокрутке "блоком"
public int getScrollableBlockIncrement( Rectangle visible, int or, int dir) {
return CELL_SIZE*10;
}
// нужно ли следить за размером области прокрутки public boolean getScrollableTracksViewportWidth() {
return false;
