Последнее изменение: 6 октября 2010г.

О менеджерах раскладки

До того, как я начал писать на Java, да и после этого тоже, я, разумеется, писал и на других языках. В том числе и приложения с пользовательским интерфейсом. На C++ – врать не буду, но, кажется, это была библиотека MFC. Ну и, естественно, на Delphi. Хорошо помню, как при переходе с версии 3 на версию 4 появилась новая возможность – привязывать край компоненты к краю формы. В результате чего при изменении размеров компонента двигалась и растягивалась. Тогда это показалось прорывом.

Однако такого подхода к пользовательскому интерфейсу как в Java я не встречал нигде. Я имею в виду способ расположения компонент в контейнере. Если для тех же C++ и Delphi привычным является позиционирование компонент по абсолютным координатам, то в Java это скорее исключение из правил. Когда сталкиваешься с таким подходом впервые – это повергает в легкую прострацию. Когда начинает получаться – ощущаешь чуть ли не восторг от того, как все работает при изменении размеров и Look&Feel-а.

Однако между прострацией и восторгом существует ощутимый промежуток времени. Когда очень хочется бросить все и работать по-старому, потому как по-новому не получается. Эта статья призвана сократить данный промежуток до минимума. Итак – мы поговорим о менеджерах раскладки, они же layout manager-ы.

Содержание разговора будет следующим:

Приступим!

Менеджер раскладки – что это и зачем нужно

Думаю, не сильно ошибусь, если скажу, что вопрос "а зачем это нужно?" задает подавляющее большинство тех, кто сталкивается с менеджерами раскладки. Ответ кроется в заявленной изначально особенности Java, а именно – переносимости. Не секрет, что вид одних и тех же компонент на разных платформах сильно различается. Вряд ли кто-нибудь спутает окно под Motif с окном под Windows или Mac. Более того, кроме внешнего вида компонент различаются их линейные размеры. Именно потому неприменим дизайн в абсолютных единицах. Возьмем такой простой пример:

package test;

import javax.swing.*;

/**
 * AbsoluteBoundsTest
 *
 * @author Eugene Matyushkin
 */
public class AbsoluteBoundsTest extends JFrame {

    public AbsoluteBoundsTest(){
        super("Absolute bounds test");
        JPanel content = new JPanel();
        content.setLayout(null);
        JLabel lblFirstName = new JLabel("First name");
        lblFirstName.setBounds(5,5,95,21);
        JLabel lblLastName = new JLabel("Last name");
        lblLastName.setBounds(5,30,95,21);
        JTextField tfFirstName = new JTextField(20);
        tfFirstName.setBounds(100,5,120,21);
        JTextField tfLastName = new JTextField(20);
        tfLastName.setBounds(100,30,120,21);
        JButton btnOk = new JButton("Ok");
        btnOk.setBounds(65,60,75,21);
        JButton btnCancel = new JButton("Cancel");
        btnCancel.setBounds(145,60,75,21);
        content.add(lblFirstName);
        content.add(lblLastName);
        content.add(tfFirstName);
        content.add(tfLastName);
        content.add(btnOk);
        content.add(btnCancel);
        setSize(230,130);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setContentPane(content);
    }

    public static void main(String[] args) {
        try {
//            UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
//            UIManager.setLookAndFeel("com.sun.java.swing.plaf.motif.MotifLookAndFeel");
//            UIManager.setLookAndFeel("com.sun.java.swing.plaf.gtk.GTKLookAndFeel");
            UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
        } catch (Throwable thrown) {
            thrown.printStackTrace();
        }
        AbsoluteBoundsTest abt = new AbsoluteBoundsTest();
        abt.setVisible(true);
    }
}

Хочу напомнить, что кроссплатформенный UI – Swing – появился гораздо позже, и был включен в Java только с версии 1.2. А до этого разработчики имели дело с AWT, базировавшемся на платформенном UI. Однако в примере я применяю именно Swing из-за наличия возможности переключиться в нужный UI.

Две метки, два поля ввода, две кнопки. Все в абсолютных координатах. И вот, как это окно выглядит в разных системах: Windows, Motif, GTK+ 2.0 (реализация Java 5), GTK+ 2.2 (реализация Java 6). В Windows и GTK 2.0 все неплохо. В GTK 2.2 хуже – не поместился текст "Cancel" на кнопке. И совсем плохо в Motif – кнопки смотрятся просто кошмарно.

Насчет GTK+ – этот вид пользовательского интерфейса наиболее распространен под X-Windows, хотя он, в общем-то, кроссплатформенный. В скомпилированном виде в Java он не входит, однако присутствует в стандартном файле исходников, src.zip. Я собрал их для версий Java 5 и Java 6. Обращаю особое внимание, что они не совместимы даже на уровне кода – версия под Java 6 не соберется под Java 5, и наоборот1.

И я взял всего лишь три простейших элемента. А если взять все имеющиеся?

Проблема в общем-то ясна. И решение для нее было найдено весьма оптимальное. По сути, что меняется при переходе между платформами? Размеры компонент, расстояния между ними. А взаимное расположение – оно остается тем же самым. Таким образом, разделив взаимное расположение – раскладку – и позиционирование, мы получим хорошее – и независимое от платформы! – средство для компоновки пользовательского интерфейса.

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

Теоретическая основа и принципы работы

Как я уже говорил, взаимное расположение компонентов – это еще не все. Для их реального позиционирования в контейнере нужен какой-то механизм определения размеров компонент в каждом конкретном случае. Для этого были введены такие понятия как предпочтительный размер (preferredSize) и минимальный размер (minimumSize). Эти параметры можно получить от каждой компоненты – у класса java.awt.Component есть соответствующие методы, – и менеджеры раскладки полагаются именно на них. А вот под этими методами лежат сильно разные механизмы.

В компонентах из пакета java.awt применяются специальные объекты... у меня всегда были затруднения с переводом слова peer, в русском я аналога не знаю, слово "ровня" дает слабое представление. В общем, это некий сопутствующий объект, реализованный для платформы, на которой идет исполнение. Этот объект – peer – инициализируется при привязке компонента к native-ресурсу где-то в недрах java.awt.ToolKit. Именно такие peer-объекты и отвечают за определение размеров компонентов, их отрисовку и многое другое.

В легковесных компонентах, находящихся в пакете javax.swing, применяется в чем-то схожий подход. peer-объекты заменены на т.н. делегатов пользовательского интерфейса. Наборы этих делегатов объединены в логические группы, которые называются Look&Feel. И точно так же как peer-объекты делегаты пользовательского интерфейса отвечают за определение размеров компонент, их отрисовку и т.п. Ключевое отличие следующее – LookAndFeel не привязан к платформе, он является эмуляцией ее внешнего вида. Точность эмуляции зависит исключительно от точности реализации, причем можно эмулировать и поведение компонент, например, подсветку при наведении мыши. И, соответственно, реализовав собственный LookAndFeel, можно получить принципиально иной вид приложения, не меняя ни строчки исходного кода. Именно потому в примере выше я вместо тестирования на различных платформах просто взял и поменял внешний вид. А Swing является хорошим примером реализации собственного LookAndFeel. И в варианте Swing приведенный выше пример выглядит вот так.

Впрочем, речь сейчас идет не о LookAndFeel-ах, достойных отдельной статьи. Речь о менеджерах компоновки. И для них основным является тот факт, что у любой компоненты можно получить ее предпочтительные и минимальные размеры, соответствующие текущему пользовательскому интерфейсу. Тем самым мы можем отделить логику абсолютного расположения от логики раскладки.

Второй теоретический момент, которого я хотел коснуться – внутреннее устройство менеджеров компоновки. Как я уже говорил, некоторые менеджеры позволяют задавать дополнительные пераметры, влияющие на позиционирование компонент. Иногда эти параметры общие, типа вертикального/горизонтального зазора, а иногда – отдельные для каждого компонента. Соответственно, все индивидуальные настройки хранятся в самом менеджере. Из этого вытекает вывод – менеджеры раскладки не рекомендуется разделять между несколькими контейнерами. Даже если вы уверены, что сейчас менеджер не держит внутри себя никакой дополнительной информации – в будущем это может измениться, ибо гарантируется исключительно API. В дальнейшем, когда я буду расказывать о самих менеджерах, я буду упоминать, с состоянием они или без него.

Ну, с теорией мы закончили и дошли до самого интересного. А именно –

Какие менеджеры раскладки есть в Java

Все менеджеры раскладки реализуют интерфейс java.awt.LayoutManager или java.awt.LayoutManager2, который унаследован от первого. Всего в API их насчитывается 30 штук (я сейчас говорю о Java 6). Хотя большая часть из них занимается раскладкой составных частей сложных компонент – выпадающие списки, полосы прокрутки и т.п. Мы рассмотрим девять их них, которые предназначены для стороннего использования: java.awt.BorderLayout, java.awt.CardLayout, java.awt.FlowLayout, java.awt.GridLayout, java.awt.GridBagLayout, javax.swing.BoxLayout, javax.swing.GroupLayout, javax.swing.OverlayLayout и javax.swing.SpringLayout. Сначала будут layout-менеджеры из AWT, потом из Swing. Да, поскольку все они реализуют один интерфейс, а все компоненты – и native, и легковесные – унаследованы от одного и того же java.awt.Component – любые менеджеры могут работать с любыми компонентами.

Однако смешивать native и легковесные компоненты не стоит из-за определенных проблем с отрисовкой. Да и вообще использование AWT на сегодняшний день, на мой взгляд, потеряло актуальность.

Итак, начнем по порядку! Первый по алфавиту у нас –

java.awt.BorderLayout

BorderLayout

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

Как рассчитывается положение компонент. Берутся предпочтительные размеры (preferred size), после чего верхняя и нижняя компоненты сохраняют высоту, не менее предпочтительной, а по ширине занимают все пространство контейнера. Боковые компоненты сохраняют ширину, не менее предпочтительной, а по высоте занимают столько, сколько осталось от высоты контейнера минус высоты верхней и нижней компоненты минус вертикальные зазоры (они задаются в конструкторе). Центральной компоненте достается то, что осталось в середине – по высоте она такая же, как боковые, по ширине – ширина контейнера минус ширины боковых компонент минус горизонтальные зазоры. Предпочтительные размеры при этом во внимание не принимаются.

Как осуществляется добавление компоненты в контейнер. Если использовать просто метод add(Component) – компонента добавится без указания положения и окажется в центре. Для того, чтобы расположить компоненту не в центре – нужно указать ограничения (constraints), в данном случае положение в контейнере. Это делается с помощью метода контейнера add(Component, Object), вторым параметром передается именно ограничение.

Вопрос на засыпку. Что передавать? И если вы думаете, что вопрос простой – вы глубоко заблуждаетесь. У рассматриваемого BorderLayout таких ограничений – аж 13! Вот они ( http://java.sun.com/javase/6/docs/api/java/awt/BorderLayout.html#field_summary):

  • AFTER_LAST_LINE
  • AFTER_LINE_ENDS
  • BEFORE_FIRST_LINE
  • BEFORE_LINE_BEGINS
  • CENTER
  • EAST
  • NORTH
  • SOUTH
  • WEST
  • PAGE_END
  • LINE_END
  • PAGE_START
  • LINE_START

И это при том, что позиций в контейнере всего пять. Вопрос – зачем столько?

Давайте разбираться. Прежде всего, CENTER. Эта константа не нуждается в комментариях. Позиция по центру контейнера и путаницы быть не может. Далее, первые 4 константы – синонимы последних четырех. Об этом сказано в API и рекомендуется применять именно последние. Таким образом остаются две группы:

  • NORTH, SOUTH, WEST, EAST
  • PAGE_START, PAGE_END, LINE_START, LINE_END

Т.е. вопрос, по существу, сводится к следующему. Зачем нужны две группы констант? Ответ кроется в таком классе как java.awt.ComponentOrientation. Для чего он служит:

The ComponentOrientation class encapsulates the language-sensitive orientation that is to be used to order the elements of a component or of text.

Т.е. этот класс отвечает за ориентацию текста, зависящую от языка. К примеру, в романских языках письмо идет слева направо, сверху вниз, а в арабских – справа налево, сверху вниз. В японском же вообще – сверху вниз справа налево.

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

package test;

import java.awt.*;
import javax.swing.*;

/**
 * ComponentOrientationTest
 *
 * @author Eugene Matyushkin
 */
public class ComponentOrientationTest extends JFrame {

    private final static int WIDTH = 410;
    private final static int HEIGHT = 220;

    public ComponentOrientationTest(boolean orientation, boolean useOrientation) {
        super("Component orientation test – RTL=" + orientation + ";use=" + useOrientation);
        JPanel content = new JPanel(new BorderLayout(5, 5));
        content.add(createLabel("Top"), (useOrientation) ? BorderLayout.PAGE_START : BorderLayout.NORTH);
        content.add(createLabel("Bottom"), (useOrientation) ? BorderLayout.PAGE_END : BorderLayout.SOUTH);
        content.add(createLabel("Left"), (useOrientation) ? BorderLayout.LINE_START : BorderLayout.WEST);
        content.add(createLabel("Right"), (useOrientation) ? BorderLayout.LINE_END : BorderLayout.EAST);
        content.add(createLabel("Center"), BorderLayout.CENTER);
        if (orientation) {
            content.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
        }
        setContentPane(content);
        setSize(WIDTH, HEIGHT);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
    }

    private JLabel createLabel(String caption) {
        JLabel lbl = new JLabel(caption);
        lbl.setPreferredSize(new Dimension(100, 50));
        lbl.setHorizontalAlignment(JLabel.CENTER);
        lbl.setBorder(BorderFactory.createLineBorder(new Color(0xff8000), 3));
        return lbl;
    }

    public static void main(String[] args) {
        JFrame.setDefaultLookAndFeelDecorated(true);
        Dimension size = Toolkit.getDefaultToolkit().getScreenSize();
        ComponentOrientationTest cot = new ComponentOrientationTest(false, false);
        cot.setLocation(size.width/2-WIDTH, size.height/2-HEIGHT);
        cot.setVisible(true);
        cot = new ComponentOrientationTest(false, true);
        cot.setLocation(size.width/2-WIDTH, size.height/2);
        cot.setVisible(true);
        cot = new ComponentOrientationTest(true, false);
        cot.setLocation(size.width/2, size.height/2-HEIGHT);
        cot.setVisible(true);
        cot = new ComponentOrientationTest(true, true);
        cot.setLocation(size.width/2, size.height/2);
        cot.setVisible(true);
    }
}

Что делается в этом примере. Создаются четыре окна, у каждого из которых раскладка – BorderLayout. В зависимости от переданных параметров компоненты добавляются в контейнер с использованием того или иного набора констант, в двух случаях из четырех ориентация ставится справа налево. Вот что мы имеем в результате:

components orientation

В двух нижних окнах используются константы, зависящие от ориентации, в двух правых – выставляется ориентация справа налево. Тест наглядно показывает, что при использовании набора констант NORTH, SOUTH, WEST, EAST указание ориентации не влияет на на расположение компонент, а при использовании набора PAGE_START, PAGE_END, LINE_START, LINE_END – влияет.

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

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

В заключение могу сказать, что BorderLayout является одним из наиболее удобных и часто используемых менеджеров раскладки. Именно этот менеджер установлен по умолчанию у панели содержимого (content pane) javax.swing.JFrame и javax.swing.JDialog, а также у java.awt.Frame и java.awt.Dialog.

Перейдем теперь к следующему менеджеру –

java.awt.CardLayout

CardLayout

Признаюсь честно – за все время знакомства с Java я не использовал CardLayout ни разу. Просто не было необходимости. Мне сложно представить, где его можно использовать кроме как при реализации закладок, а это уже сделано за нас. Но тем не менее, такой менеджер существует и мы его расмотрим. В окне, приведенном на рисунке, менеджер CardLayout выставлен в контейнере, лежащем в центре окна. Сверху и снизу – элементы, демонстрирующие работу менеджера.

Менеджер раскладки CardLayout позволяет разложить компоненты как колоду карт – одна над другой. При этом видна всегда только верхняя компонента – остальные спрятаны (посредством вызова setVisible(false)). Компоненты занимают все доступное пространство контейнера, предпочтительные размеры игнорируются. Менеджер хранит в себе все компоненты в порядке добавления, т.е. он с состоянием.

Немаловажный момент – каждой компоненте ставится в соответствие некое имя. Попытка добавить компоненту в контейнер без указания имени приведет к ошибке! Таким образом, при использовании CardLayout для добавления компонент в контейнер необходимо вызывать метод контейнера add(String, Component). Первым параметром передается имя, вторым компонента.

На самом деле тут ситуация несколько интереснее. Я не стал заострять на этом внимание при рассмотрении BorderLayout, но там то же самое. Дело в том, что имя используется в качестве ограничения (constraint), а потому – методы add(String, Component) и add(Component, Object) дадут одинаковый результат. В случае BorderLayout набор ограничений (по совместительству имен) содержит 13 элементов, в случае же CardLayout имена не ограничены.

Если в каждый момент времени видна только одна компонента, то возникает резонный вопрос – как увидель остальные? Очень просто. Менеджер раскладки позволяет позиционироваться по компонентам. Поскольку он держит их порядок – перемещаться можно на первую, последнюю, следующую и предыдущую компоненты. Если нет предыдущей или следующей – показывается последняя или первая соответственно, т.е. навигация по next/previous циклическая.

Существует также возможность спозиционироваться на нужную компоненту по ее имени, тому самому, которое выставляется при добавлении в контейнер. Формально уникальность имен не требуется, можно хоть все компоненты добавить с одним и тем же именем. Однако, если вы хотите использовать именя для позиционирования – они должны быть уникальны. Позиционирование производится с помощью метода show(Container, String) менеджера CardLayout. Обратите внимание, что контейнер передается в явном виде. Это нужно потому, что менеджер хранит только соответствия компонент именам и порядок добавления компонент (хотя реально он им не пользуется, а перебирает содержимое контейнера). Однако передать "чужой" контейнер не получится – у переданного контейнера проверяется менеджер раскладки.

Собственно, больше о CardLayout рассказать, пожалуй, нечего. Ниже приведен пример, иллюстрирующий описаные возможности.

package test;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

/**
 * CardLayoutTest
 *
 * @author Eugene Matyushkin
 */
public class CardLayoutTest extends JFrame {

    private static final Color[] COLORS = new Color[]{Color.red, Color.green, Color.blue, Color.black, Color.orange,
                                                      Color.cyan, Color.darkGray, Color.magenta, Color.lightGray,
                                                      Color.pink, Color.white, Color.yellow};

    private CardLayout layout;

    public CardLayoutTest(){
        super("CardLayout");
        layout = new CardLayout();
        final JPanel content = new JPanel(layout);
        String[] names = new String[COLORS.length];
        for(int i=0; i<COLORS.length; i++){
            String name = "Component"+i;
            content.add(name, createComponent(i));
            names[i] = name;
        }
        JPanel btnPanel = new JPanel(new GridLayout(1,4));
        JButton btnFirst = new JButton("<<");
        JButton btnPrevious = new JButton("<");
        JButton btnNext = new JButton(">");
        JButton btnLast = new JButton(">>");
        btnPanel.add(btnFirst);
        btnPanel.add(btnPrevious);
        btnPanel.add(btnNext);
        btnPanel.add(btnLast);
        btnFirst.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent e) {
                layout.first(content);
            }
        });
        btnPrevious.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent e) {
                layout.previous(content);
            }
        });
        btnNext.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent e) {
                layout.next(content);
            }
        });
        btnLast.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent e) {
                layout.last(content);
            }
        });
        final JComboBox cbxNames = new JComboBox(names);
        cbxNames.addItemListener(new ItemListener(){
            public void itemStateChanged(ItemEvent e) {
                if (e.getStateChange() == ItemEvent.SELECTED)
                    layout.show(content, (String)cbxNames.getSelectedItem());
            }
        });
        getContentPane().add(content, BorderLayout.CENTER);
        getContentPane().add(cbxNames, BorderLayout.SOUTH);
        getContentPane().add(btnPanel, BorderLayout.NORTH);
        setSize(410, 220);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
    }

    private JComponent createComponent(int number){
        JLabel lbl = new JLabel("Label "+number);
        lbl.setPreferredSize(new Dimension(100, 50));
        lbl.setHorizontalAlignment(JLabel.CENTER);
        lbl.setBorder(BorderFactory.createLineBorder(COLORS[number], 3));
        lbl.setForeground(COLORS[number]);
        return lbl;
    }

    public static void main(String[] args) {
        JFrame.setDefaultLookAndFeelDecorated(true);
        new CardLayoutTest().setVisible(true);
    }
}

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

Переходим теперь к следующему менеджеру компоновки –

java.awt.FlowLayout

FlowLayout

Это, пожалуй, самый простой менеджер компоновки. Он выстраивает компоненты в линию. Стоит упомянуть, что именно этот менеджер выставлен по умолчанию у обычной панели (java.awt.Panel/javax.swing.JPanel). В окне, приведенном на рисунке, менеджер FlowLayout выставлен на верхней панели, имеющей красную границу.

Размеры компонентам выставляются предпочтительные. Если компонента не помещается на текущей линии – она переносится на следующую. Причем обратите внимание – компонента может оказаться за пределами видимой границы контейнера! Это можно увидеть в примере, приведенном ниже – достаточно сильно уменьшить ширину окна, чтобы текстовое поле "убежало" за нижнюю границу.

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

Начнем с горизонтали. У FlowLayout есть пять режимов выравнивания: FlowLayout.LEFT, FlowLayout.CENTER, FlowLayout.RIGHT, FlowLayout.LEADING, FlowLayout.TRAILING. С первыми тремя понятно, а что означают последние два?

Честно сказать, я был абсолютно уверен, что только в случае использования этих двух типов выравнивания учитывается направление текста языка, пресловутый ComponentOrientation. Однако это не так. FlowLayout всегда учитывает направление текста и располагает компоненты согласно ему. Режимы выравнивания определяют только одно – куда будет выровнена уже сформированная строка. Так вот, в случае использования абсолютных режимов – FlowLayout.LEFT, FlowLayout.CENTER и FlowLayout.RIGHT – строка будет выровнена по левому краю, центру и правому краю соответственно. Вне зависимости от направления текста. А вот при использовании FlowLayout.LEADING и FlowLayout.TRAILING строка будет выравниваться по логическому началу и концу строки – левый/правый край соответственно при направлении текста слева направо и правый/левый край при направлении текста справа налево.

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

Что такое базовая линия. Это логическое понятие. У шрифта базовой называется линия, на которой расположены нижние края букв (имеются в виду буквы, не уходящие ниже строки – 'a', 'b', 'c', но не 'g', 'p', 'y'). Точно так же можно определить базовую линию компоненты. Для JLabel, JButton, к примеру, – это базовая линия надписи на них. Честно сказать, я не вдавался глубоко в этот вопрос, да и не важен он сейчас. Желающие могут посмотреть в исходном коде, как определяется базовая линия для различных компонент.

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

Ну, вот теперь можно и перейти к примеру:

package test;

import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.event.ItemListener;
import java.awt.event.ItemEvent;

/**
 * FlowLayoutTest
 *
 * @author Eugene Matyushkin
 */
public class FlowLayoutTest extends JFrame{

    private FlowLayout layout;

    private static final Alignment[] ALIGNMENTS =
            new Alignment[]{new Alignment(FlowLayout.LEFT, "FlowLayout.LEFT"),
                            new Alignment(FlowLayout.CENTER, "FlowLayout.CENTER"),
                            new Alignment(FlowLayout.RIGHT, "FlowLayout.RIGHT"),
                            new Alignment(FlowLayout.LEADING, "FlowLayout.LEADING"),
                            new Alignment(FlowLayout.TRAILING, "FlowLayout.TRAILING")};

    public FlowLayoutTest(){
        super("FlowLayout");
        layout = new FlowLayout(FlowLayout.LEFT);
        final JPanel content = new JPanel();
        content.setLayout(layout);
        JLabel lbl = new JLabel("Label");
        lbl.setFont(lbl.getFont().deriveFont(30.f));
        lbl.setPreferredSize(new Dimension(100, 50));
        lbl.setHorizontalAlignment(JLabel.CENTER);
        lbl.setBorder(BorderFactory.createLineBorder(Color.blue, 1));
        content.add(lbl);
        JButton btn = new JButton("Button");
        btn.setFont(btn.getFont().deriveFont(20.f));
        content.add(btn);
        content.add(new JTextField("Some text field", 10));
        content.setBorder(BorderFactory.createLineBorder(Color.red));
        final JCheckBox chkBaseLine = new JCheckBox("Align on baseline");
        final JCheckBox chkOrientation = new JCheckBox("Right-to-left orientation");
        final JComboBox cbxAlignments = new JComboBox(ALIGNMENTS);
        cbxAlignments.setSelectedIndex(0);
        cbxAlignments.setBorder(BorderFactory.createEmptyBorder(0,5,0,5));
        JPanel controls = new JPanel(new GridLayout(4,1));
        JLabel lblAlignment = new JLabel("Alignment:");
        lblAlignment.setVerticalAlignment(JLabel.BOTTOM);
        lblAlignment.setBorder(BorderFactory.createEmptyBorder(0,5,0,5));
        controls.add(lblAlignment);
        controls.add(cbxAlignments);
        controls.add(chkOrientation);
        controls.add(chkBaseLine);
        controls.setBorder(BorderFactory.createLineBorder(Color.orange));
        getContentPane().add(content, BorderLayout.CENTER);
        getContentPane().add(controls, BorderLayout.SOUTH);
        chkBaseLine.addChangeListener(new ChangeListener(){
            public void stateChanged(ChangeEvent e){
                layout.setAlignOnBaseline(chkBaseLine.isSelected());
                content.doLayout();
            }
        });
        chkOrientation.addChangeListener(new ChangeListener(){
            public void stateChanged(ChangeEvent e){
                content.setComponentOrientation( chkOrientation.isSelected() ?
                                                 ComponentOrientation.RIGHT_TO_LEFT :
                                                 ComponentOrientation.LEFT_TO_RIGHT);
                content.doLayout();
            }
        });
        cbxAlignments.addItemListener(new ItemListener(){
            public void itemStateChanged(ItemEvent e){
                if (e.getStateChange() == ItemEvent.SELECTED ){
                    layout.setAlignment(((Alignment)cbxAlignments.getSelectedItem()).alignment);
                    content.doLayout();
                }
            }
        });
        setSize(410, 220);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
    }

    public static void main(String[] args){
        JFrame.setDefaultLookAndFeelDecorated(true);
        new FlowLayoutTest().setVisible(true);
    }

    private static class Alignment{

        private int alignment;
        private String description;

        Alignment(int alignment, String description){
            this.alignment = alignment;
            this.description = description;
        }

        @Override
        public String toString(){
            return description;
        }
    }
}

Пожалуй, про FlowLayout больше сказать нечего. Разве что только то, что этот менеджер без состояния – раскладка пересчитывается каждый раз.

Идем дальше. Следующий по списку –

java.awt.GridLayout

GridLayout

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

Количество строк или столбцов может быть задано равным 0 – тогда этот параметр рассчитывается по количеству компонент и второму параметру. Оба в 0 выставлены быть не могут – конструктор не позволит. Если же оба параметры ненулевые – количество строк имеет преимущество, количество столбцов рассчитывается.

Тот факт, что все столбцы должны быть одной ширины, приводит к одному любопытному эффекту. При изменении ширины контейнера раскладка меняется скачкообразно. Скажем, если у нас есть 4 столбца, для изменения раскладки нужно изменить ширину на 4, 8 и т.д. точек, тогда каждый столбец получит по одной точке дополнительной ширины. Со строками то же самое. Чем больше столбцов/строк – тем явственней это проявляется.

Ориентация текста – ComponentOrientation – учитывается при раскладке. Предпочтительные размеры компонент – наоборот. Разумеется, как и любой менеджер раскладки GridLayout умеет рассчитывать предпочтительный размер контейнера, причем за размеры ячейки берется максимальные ширина и высота из всех предпочтительных размеров всех компонент контейнера. Но при раскладке все компоненты полностью занимают предоставленные им ячейки.

Небольшой пример, иллюстрирующий использование GridLayout, приведен ниже.

package test;

import javax.swing.*;
import javax.swing.event.ChangeListener;
import javax.swing.event.ChangeEvent;
import java.awt.*;

/**
 * GridLayoutTest
 *
 * @author Eugene Matyushkin
 */
public class GridLayoutTest extends JFrame{

    public GridLayoutTest(){
        super("GridLayout");
        final JPanel content = new JPanel(new GridLayout(2,2,5,5));
        for(int i=0; i<8; i++){
            content.add(createComponent(i));
        }
        final JCheckBox chkOrientation = new JCheckBox("Right-to-left orientation");
        chkOrientation.addChangeListener(new ChangeListener(){
            public void stateChanged(ChangeEvent e){
                content.setComponentOrientation( chkOrientation.isSelected() ?
                                                 ComponentOrientation.RIGHT_TO_LEFT :
                                                 ComponentOrientation.LEFT_TO_RIGHT);
                content.doLayout();
            }
        });
        content.setBorder(BorderFactory.createLineBorder(Color.red));
        getContentPane().add(content, BorderLayout.CENTER);
        getContentPane().add(chkOrientation, BorderLayout.SOUTH);
        setSize(410, 220);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setLocationRelativeTo(null);

    }

    private JComponent createComponent(int number){
        JLabel lbl = new JLabel("Label "+number);
        lbl.setPreferredSize(new Dimension(100, 50));
        lbl.setHorizontalAlignment(JLabel.CENTER);
        lbl.setBorder(BorderFactory.createLineBorder(Color.blue, 3));
        return lbl;
    }

    public static void main(String[] args){
        JFrame.setDefaultLookAndFeelDecorated(true);
        new GridLayoutTest().setVisible(true);
    }
}

Кто следующий? О, тут есть о чем поговорить! Следующий у нас –

java.awt.GridBagLayout

GridBagLayout

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

Каждой компоненте ставится в соответствие набор ограничений, задаваемых с помощью специального класса – GridBagConstraints. Дальше мы пройдемся по всем его параметрам подробно. Менеджер GridBagLayout, естественно, с состоянием. Помимо соответствия ограничений компонентам менеджер хранит и результаты рассчетов раскладки, ибо алгоритмы весьма непростые и пересчет занимает достаточно много времени.

Итак, что можно указать в качестке ограничений. Ну, во-первых, положение в сетке. Координаты верхнего левого угла. Во-вторых – количество занимаемых строк/столбцов. Заполняет ли компонента ячейку, в каком направлении. Куда прижимается – к какой-либо стороне, в угол, по центру. Пустое пространство вокруг компоненты. Распределение свободного пространства. И вот это вот обилие настроек играет злую шутку – человек, встретившийся с ними впервые, просто теряется. Между тем, достаточно разобраться с ними всего лишь один раз. Этим мы сейчас и займемся. Пока же я приведу исходный код того окна, которое используется в качестве иллюстрации.

package test;

import java.awt.*;
import javax.swing.*;
import javax.swing.event.ChangeListener;
import javax.swing.event.ChangeEvent;

/**
 * GridBagLayoutTest
 *
 * @author Eugene Matyushkin
 */
public class GridBagLayoutTest extends JFrame {

    public GridBagLayoutTest() {
        super("GridBagLayout");
        final JPanel content = new JPanel(new GridBagLayout());
        JLabel lblImage = new JLabel(new ImageIcon(getClass().getResource("/image.png")));
        content.add(lblImage, new GridBagConstraints(0, 0, 1, 2, 0, 0, GridBagConstraints.NORTH,
            GridBagConstraints.NONE, new Insets(5, 5, 5, 5), 0, 0));
        content.add(new JLabel("First name:"), new GridBagConstraints(1, 0, 1, 1, 0, 0,
            GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(5, 0, 5, 0), 0, 0));
        content.add(new JTextField("<enter first name here>", 20), new GridBagConstraints(2, 0, 2, 1, 1, 0,
            GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(5, 0, 5, 5), 0, 0));
        content.add(new JLabel("Last name:"), new GridBagConstraints(1, 1, 1, 1, 0, 1,
            GridBagConstraints.NORTH, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 5, 0), 0, 0));
        content.add(new JTextField("<enter last name here>", 20), new GridBagConstraints(2, 1, 2, 1, 1, 1,
            GridBagConstraints.NORTH, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 5, 5), 0, 0));
        JButton btnOk = new JButton("Ok");
        JButton btnCancel = new JButton("Cancel");
        btnOk.setPreferredSize(btnCancel.getPreferredSize());
        btnOk.setMinimumSize(btnOk.getPreferredSize());
        content.add(btnOk, new GridBagConstraints(2, 2, 1, 1, 1, 0, GridBagConstraints.LINE_END,
            GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
        content.add(btnCancel, new GridBagConstraints(3, 2, 1, 1, 0, 0, GridBagConstraints.CENTER,
            GridBagConstraints.NONE, new Insets(0, 0, 5, 5), 0, 0));
        final JCheckBox chkOrientation = new JCheckBox("Right-to-left orientation");
        chkOrientation.addChangeListener(new ChangeListener(){
            public void stateChanged(ChangeEvent e){
                content.setComponentOrientation( chkOrientation.isSelected() ?
                                                 ComponentOrientation.RIGHT_TO_LEFT :
                                                 ComponentOrientation.LEFT_TO_RIGHT);
                content.doLayout();
            }
        });
        content.setBorder(BorderFactory.createLineBorder(Color.red));
        getContentPane().add(content, BorderLayout.CENTER);
        getContentPane().add(chkOrientation, BorderLayout.SOUTH);
        setSize(410, 220);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {
        JFrame.setDefaultLookAndFeelDecorated(true);
        new GridBagLayoutTest().setVisible(true);
    }
}

Этот код дан скорее для общего представления об использовании GridBagLayout. А рассмотрение ограничений мы начнем с weightx/weighty.

Параметры weightx/weighty

GridBagLayout – weightx test

Эти два параметра вызывают больше всего вопросов. Мы рассмотрим использование weightx, второй параметр аналогичен.

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

Ответом на этот вопрос служат параметры weightx, указаные у каждой из компонент. Если оба параметра выставлены в 0 – свободное пространство остается снаружи компонент, они висят в центре контейнера. Это как раз тот случай, которые показан на снимке.

Пусть теперь хотя бы у какой-нибудь из компонент ненулевой weightx. Как рассчитывается распределение? Пусть у первой компоненты для определенности выставлено значение weightx=A, у второй – weightx=B. Ширина свободного пространства – W. Тогда первая компонента получит в свое пользование W * A / (A + B) пространства, вторая – W * B / (A + B). Если мы сложим эти две величины, то увидим, что в сумме они дадут W, т.е. пространство распределено полностью.

Когда я говорю, что пространство отдано компоненте – я имею в виду столбец, в котором она находится. Его ширина принимается равной предпочтительной ширине компоненты плюс выделенное свободное пространство. Т.е. при соотношении weightx двух компонент, равном 1:1, ширины столбцов, которые они занимают, вовсе необязательно будут равны. Лишнее пространство – оно да, распределится поровну. А остальное зависит от предпочтительных размеров самих компонент. Вот это очень часто служит камнем преткновения.

Код этого примера приведен ниже.

package test;

import java.awt.*;
import java.awt.event.ItemListener;
import java.awt.event.ItemEvent;
import javax.swing.*;

/**
 * GBLWeightTest
 *
 * @author Eugene Matyushkin
 */
public class GBLWeightTest extends JFrame {

    private static final Integer[] weights = new Integer[]{0, 1, 2, 5, 10};
    private GridBagConstraints leftGBC;
    private GridBagConstraints rightGBC;
    private GridBagLayout layout;

    public GBLWeightTest() {
        super("GridBagLayout – weightx test");
        layout = new GridBagLayout();
        final JPanel content = new JPanel(layout);
        final JComboBox cbxLeft = new JComboBox(weights);
        cbxLeft.setSelectedIndex(0);
        final JComboBox cbxRight = new JComboBox(weights);
        cbxRight.setSelectedIndex(0);
        //cbxRight.setPreferredSize(new Dimension(150, cbxRight.getPreferredSize().height));
        leftGBC = new GridBagConstraints(0, 0, 1, 1, 0, 0,
            GridBagConstraints.CENTER, GridBagConstraints.BOTH,
            new Insets(5, 5, 5, 5), 0, 0);
        rightGBC = new GridBagConstraints(1, 0, 1, 1, 0, 0,
            GridBagConstraints.CENTER, GridBagConstraints.BOTH,
            new Insets(5, 5, 5, 5), 0, 0);
        layout.setConstraints(cbxLeft, leftGBC);
        layout.setConstraints(cbxRight, rightGBC);
        content.add(cbxLeft);
        content.add(cbxRight);
        cbxLeft.addItemListener(new ItemListener(){
            public void itemStateChanged(ItemEvent e) {
                if (e.getStateChange() == ItemEvent.SELECTED){
                    leftGBC.weightx = (Integer)cbxLeft.getSelectedItem();
                    layout.setConstraints(cbxLeft, leftGBC);
                    content.doLayout();
                    cbxLeft.doLayout();
                    cbxRight.doLayout();
                }
            }
        });
        cbxRight.addItemListener(new ItemListener(){
            public void itemStateChanged(ItemEvent e) {
                if (e.getStateChange() == ItemEvent.SELECTED){
                    rightGBC.weightx = (Integer)cbxRight.getSelectedItem();
                    layout.setConstraints(cbxRight, rightGBC);
                    content.doLayout();
                    cbxLeft.doLayout();
                    cbxRight.doLayout();
                }
            }
        });
        setSize(410, 80);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        getContentPane().add(content, BorderLayout.CENTER);
    }

    public static void main(String[] args) {
        JFrame.setDefaultLookAndFeelDecorated(true);
        new GBLWeightTest().setVisible(true);
    }
}

Мой вам совет – поиграйтесь с этим тестом. Посмотрите, как распределяется пространство. Можете раскомментировать строку, в которой выставляется другой предпочтительный размер правому выпадающему списку, и посмотреть, как распределяется пространство при равных weightx. Довольно простой код, а понимание использования weightx дает хорошее.

На самом деле и тут есть свои тонкости. Прежде всего – несовместимые weightx. Допустим, у нас две строки, по две компоненты в каждой. В первой weightx установлены в 1 и 2, во второй – в 2 и 1. Вот и вопрос – что будет. Не, тут-то все просто, возьмутся максимальные значения. Но существует множество ситуаций, когда распределение пространства, выполняемое GridBagLayout-ом, неочевидно. Хотя чаще всего все свободное пространство отдается одному столбцу, так что с проблемами я, например, в жизни не сталкивался ни разу.

Точно так же работает и weighty, алгоритмы написаны под копирку.

Перейдем теперь к положению в сетке –

Параметры gridx/gridy/gridwidth/gridheight

GridBagLayout – positions

На первый взгляд тут все просто. gridx/gridy определяют координаты в сетке ячейки, в которой находится компонента. Ну или левой верхней ячейки, если компонента занимает несколько. В приведенном примере две ячейки по вертикали занимает изображение, две ячейки по горизонтали – поля ввода.

В принципе, этих знаний уже достаточно, чтобы разложить компоненты в сетке. Есть, однако, пара нюансов – константы GridBagConstraints.RELATIVE и GridBagConstraints.REMAINDER. Первая из них является допустимым значением для всех четырех параметров, вторая – для gridwidth/gridheight. Особенность этих констант в том, что они относительные.

Сначала поговорим о gridx/gridy. Дообавление в контейнер компоненты с ограничением gridx=GridBagConstraints.RELATIVE приведет к тому, что эта компонента будет расположена непосредственно за компонентой, добавленной в контейнер перед ней. Аналогично с gridy – добавляемая компонента расположится под компонентой, добавленной перед ней.

Теперь о gridwidth/gridheight. Для них GridBagConstraints.RELATIVE означает, что компонента будет расположена за последней из уже добавленных в контейнер, находящейся в той же самой строке (в том же самом столбце), что и добавляемая. Константа же GridBagConstraints.REMAINDER указывает на то, что компонента будет расположена самой последней в строке или столбце соответственно.

Преимущество такого подхода – можно менять расположение компонент, изменяя порядок их добавления в контейнер. Недостаток очевиден – относительные инструкции могут быть противоречивы. Скажем, если у компоненты при добавлении выставить одновременно gridx=gridy=GridBagConstraints.RELATIVE – она должна быть смещена относительно последней добавленной по двум перпендикулярным осям одновременно. В документации рекомендуется использовать абсолютные позиции. Вероятно поэтому во многих визуальных дизайнерах отсутствует возможность выставлять относительные значения в явном – текстовом – виде. Использовать значения констант при этом не запрещается. Я сам когда-то начинал именно с относительных координат, однако в последнее время склоняюсь к абсолютным ввиду большей предсказуемости. По большому счету – дело вкуса.

Следующий интересный аспект –

Параметры anchor/fill

GridBagLayout – anchor

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

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

Для начала объясню, почему эти два параметра, anchor и fill, я поместил в одном разделе. Казалось бы, где Рим, а где Крым. Однако эти параметры неявным образом связаны. Вернее, так – установка параметра fill может нейтрализовать установку anchor. Если компонента, допустим, растянута по горизонтали, то я могу прижать ее к правой стороне занимаемой области, к левой, вообще оставить в центре – на расположение это никак не повлияет. Существенно только вертикальное расположение. То же самое получится при вертикальном растяжении. А если компонента занимает всю предоставленную область (т.е. растянута по обеим осям) – значение anchor не играет вообще никакой роли.

Кстати, именно поэтому в JFormDesigner и IntelliJ IDEA в визуальных дизайнерах эти два параметра вообще совмещены. Вместо них используются настройки Vertical align/Horizontal alignIDEA) и h align/v alignJFormDesigner). Логику я понимаю, но не одобряю.

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

Как и в случае с java.awt.BorderLayout есть относительные и абсолютные константы. Абсолютные опираются на стороны света (сверху, по часовой стрелке):

  • NORTH
  • NORTHEAST
  • EAST
  • SOUTHEAST
  • SOUTH
  • SOUTHWEST
  • WEST
  • NORTHWEST

Относительные констаны используют те же логические понятия – строка, страница, первый, последний, начало, конец:

  • PAGE_START
  • FIRST_LINE_END
  • LINE_END
  • LAST_LINE_END
  • PAGE_END
  • LAST_LINE_START
  • LINE_START
  • FIRST_LINE_STAT

И особняком стоят константы, появившиеся в версии Java 6:

  • BASELINE
  • BASELINE_LEADING
  • BASELINE_TRAILING
  • ABOVE_BASELINE
  • ABOVE_BASELINE_LEADING
  • ABOVE_BASELINE_TRAILING
  • BELOW_BASELINE
  • BELOW_BASELINE_LEADING
  • BELOW_BASELINE_TRAILING

Ну и, естественно, CENTER, куда без нее?

С первыми двумя блоками, думаю, понятно. Остановимся на третьем. Он опять-таки завязан на понятие базовой линии. Легко заметить, что параметры только относительные (что неудивительно, на мой взгляд, ибо предназначены они в первую очеред для создания UI, ориентированного на текст). Оставив в стороне всё с приставками _LEADING/_TRAILING, мы получим три варианта – BASELINE, ABOVE_BASELINE и BELOW_BASELINE.

Работают они так. BASELINE выравнивает компоненту по базовой линии первой строки. Поясню, что это. На приведенном примере можно увидеть две текстовых метки. Кнопка же (вернее, область, где она находится) занимает по вертикали две строки. Так вот, берутся компоненты только из первой строки, в данном случае это метка Label И сама кнопка! Причем обратите внимание – хотя бы у одной компоненты в ограничениях должен стоять тип выравнивания BASELINE (допустимы BASELINE_LEADING/BASELINE_TRAILING), иначе считается, что базовой линии нет.

Когда базовая линия определена, рассчитывается положение компоненты. BASELINE означает, что базовая линия компоненты выровнена по базовой линии строки. ABOVE_BASELINE – что нижний край компоненты выровнен по базовой линии строки. Соответственно, BELOW_BASELINE выравнивает верхний край компоненты по базовой линии. Если же базовая линия не определена – компонента центрируется по вертикали.

Ну и, естественно, иллюстративный пример:

package test;

import java.awt.*;
import java.awt.event.ItemListener;
import java.awt.event.ItemEvent;
import javax.swing.*;
import javax.swing.event.ChangeListener;
import javax.swing.event.ChangeEvent;

/**
 * GBLAnchorTest
 *
 * @author Eugene Matyushkin
 */
public class GBLAnchorTest extends JFrame {

    private GridBagLayout layout;
    private JComponent component;
    private GridBagConstraints constraints;

    private static final Alignment[] ALIGNMENTS = new Alignment[]{
        new Alignment(GridBagConstraints.CENTER, "GridBagConstraints.CENTER"),
        new Alignment(GridBagConstraints.NORTHWEST, "GridBagConstraints.NORTHWEST"),
        new Alignment(GridBagConstraints.NORTH, "GridBagConstraints.NORTH"),
        new Alignment(GridBagConstraints.NORTHEAST, "GridBagConstraints.NORTHEAST"),
        new Alignment(GridBagConstraints.EAST, "GridBagConstraints.EAST"),
        new Alignment(GridBagConstraints.SOUTHEAST, "GridBagConstraints.SOUTHEAST"),
        new Alignment(GridBagConstraints.SOUTH, "GridBagConstraints.SOUTH"),
        new Alignment(GridBagConstraints.SOUTHWEST, "GridBagConstraints.SOUTHWEST"),
        new Alignment(GridBagConstraints.WEST, "GridBagConstraints.WEST"),
        new Alignment(GridBagConstraints.FIRST_LINE_START, "GridBagConstraints.FIRST_LINE_START"),
        new Alignment(GridBagConstraints.PAGE_START, "GridBagConstraints.PAGE_START"),
        new Alignment(GridBagConstraints.FIRST_LINE_END, "GridBagConstraints.FIRST_LINE_END"),
        new Alignment(GridBagConstraints.LINE_END, "GridBagConstraints.LINE_END"),
        new Alignment(GridBagConstraints.LAST_LINE_END, "GridBagConstraints.LAST_LINE_END"),
        new Alignment(GridBagConstraints.PAGE_END, "GridBagConstraints.PAGE_END"),
        new Alignment(GridBagConstraints.LAST_LINE_START, "GridBagConstraints.LAST_LINE_START"),
        new Alignment(GridBagConstraints.LINE_START, "GridBagConstraints.LINE_START"),
        new Alignment(GridBagConstraints.BASELINE, "GridBagConstraints.BASELINE"),
        new Alignment(GridBagConstraints.BASELINE_LEADING, "GridBagConstraints.BASELINE_LEADING"),
        new Alignment(GridBagConstraints.BASELINE_TRAILING, "GridBagConstraints.BASELINE_TRAILING"),
        new Alignment(GridBagConstraints.ABOVE_BASELINE, "GridBagConstraints.ABOVE_BASELINE"),
        new Alignment(GridBagConstraints.ABOVE_BASELINE_LEADING, "GridBagConstraints.ABOVE_BASELINE_LEADING"),
        new Alignment(GridBagConstraints.ABOVE_BASELINE_TRAILING, "GridBagConstraints.ABOVE_BASELINE_TRAILING"),
        new Alignment(GridBagConstraints.BELOW_BASELINE, "GridBagConstraints.BELOW_BASELINE"),
        new Alignment(GridBagConstraints.BELOW_BASELINE_LEADING, "GridBagConstraints.BELOW_BASELINE_LEADING"),
        new Alignment(GridBagConstraints.BELOW_BASELINE_TRAILING, "GridBagConstraints.BELOW_BASELINE_TRAILING"),
    };


    public GBLAnchorTest(){
        super("GridBagLayout – anchor test");
        layout = new GridBagLayout();
        final JPanel content = new JPanel(layout);

        JLabel lblFirst = new JLabel("Label");
        lblFirst.setFont(lblFirst.getFont().deriveFont(30f));
        content.add(lblFirst, new GridBagConstraints(0,0,1,1,0,1,
            GridBagConstraints.BASELINE, GridBagConstraints.HORIZONTAL,
            new Insets(5,5,5,5),0,0));
        component = new JButton("Button");
        constraints = new GridBagConstraints(1,0,1,2,1,1,
            GridBagConstraints.CENTER, GridBagConstraints.NONE,
            new Insets(5,5,5,5),0,0);
        content.add(component, constraints);
        content.setBorder(BorderFactory.createLineBorder(Color.red));
        content.add(new JLabel("Second label"), new GridBagConstraints(0,1,1,1,0,1,
            GridBagConstraints.BASELINE, GridBagConstraints.HORIZONTAL,
            new Insets(5,5,5,5),0,0));
        final JCheckBox chkOrientation = new JCheckBox("Right-to-left orientation");
        final JComboBox cbxAlignments = new JComboBox(ALIGNMENTS);
        cbxAlignments.setSelectedIndex(0);
        cbxAlignments.setBorder(BorderFactory.createEmptyBorder(0,5,0,5));
        JPanel controls = new JPanel(new GridLayout(3,1));
        JLabel lblAlignment = new JLabel("Alignment:");
        lblAlignment.setVerticalAlignment(JLabel.BOTTOM);
        lblAlignment.setBorder(BorderFactory.createEmptyBorder(0,5,0,5));
        controls.add(lblAlignment);
        controls.add(cbxAlignments);
        controls.add(chkOrientation);
        controls.setBorder(BorderFactory.createLineBorder(Color.orange));
        getContentPane().add(content, BorderLayout.CENTER);
        getContentPane().add(controls, BorderLayout.SOUTH);
        chkOrientation.addChangeListener(new ChangeListener(){
            public void stateChanged(ChangeEvent e){
                content.setComponentOrientation( chkOrientation.isSelected() ?
                                                 ComponentOrientation.RIGHT_TO_LEFT :
                                                 ComponentOrientation.LEFT_TO_RIGHT );
                content.doLayout();
            }
        });
        cbxAlignments.addItemListener(new ItemListener(){
            public void itemStateChanged(ItemEvent e){
                if (e.getStateChange() == ItemEvent.SELECTED ){
                    constraints.anchor = ((Alignment)cbxAlignments.getSelectedItem()).alignment;
                    layout.setConstraints(component, constraints);
                    content.doLayout();
                }
            }
        });
        setSize(410, 220);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
    }

    public static void main(String[] args){
        JFrame.setDefaultLookAndFeelDecorated(true);
        new GBLAnchorTest().setVisible(true);
    }

    private static class Alignment{

        private int alignment;
        private String description;

        Alignment(int alignment, String description){
            this.alignment = alignment;
            this.description = description;
        }

        @Override
        public String toString(){
            return description;
        }
    }
}

Обратите внимание на положение кнопки при выравнивании по базовой линии. Замените выравнивание для метки Label – вместо BASELINE поставьте CENTER. И посмотрите, что получается в режимах ABOVE_BASELINE/BELOW_BASELINE. Поскольку базовая линия не определяется – компонента центрируется по вертикали.

Ну вот, с этим вроде тоже разобрались. Осталось немного.

Параметры insets и ipadx/ipady

В отличие от рассматриваемых ранее параметров, определявших логическое размещение компонент, insets, ipadx и ipady служат для абсолютной корректировки. insets описывает прозрачную рамку вокруг компоненты. Это очень полезно для того, чтобы оставлять между компонентами небольшие расстояния. ipadx и ipady наоборот – добавляются к размерам самой компоненты. Причем обратите внимание – значение параметра добавляется с каждой стороны. Т.е. размер компоненты увеличивается на 2*ipadx по горизонтали и 2*ipady по вертикали. Честно сказать, эти параметры я не использую.

Ну, вот, вроде и все про GridBagLayout. Информации много, но повторюсь – если ее осознать, использовать этот менеджер просто. Основное, что надо сделать при использовании GridBagLayout:

  1. Расчертить сетку и определить положение компонент. За это отвечают gridx/gridy/gridwidth/gridheight.
  2. Определить, как распределять свободное пространство. Это weightx/weighty.
  3. Определить расположение компонент в предоставляемых им областях. Это параметры anchor и fill.
  4. Определить промежутки между компонентами и (если необходимо) "прибавки" к размерам – insets и ipadx/ipady.

Именно в перечисленном порядке и указываются эти параметры в конструкторе GridBagConstraints. Хочу обратить внимание еще и на то, что совсем необязательно каждый раз создавать новый экземпляр. Можно создать один и устанавливать ему нужные параметры (они все public) – при установке ограничений переданный экземпляр GridBagConstraints клонируется!

* * *

Уф... можно пересести дух. Мы закончили рассматривать менеджеры, существующие в AWT еще с Java 1.0. Теперь перейдем к тому, что появилось в Swing.

Здесь я хочу сделать одно разъяснение. Первоначально я намеревался плотно углубиться во все имеющиеся в Swing менеджеры раскладки. Однако по мере знакомства с ними у меня все больше возникало ощущение, что, собственно, углубляться-то не стоит. Внимания заслуживает только BoxLayout, и то по большей части обзорного. У GroupLayout и SpringLayout достаточно специфическое предназначение, если верить документации. Предназначения же OverlayLayout я, честно сказать, просто не понимаю.

Как бы то ни было – менеджеры раскладки в Swing тоже существуют. И мы на них посмотрим. Первым будет...

javax.swing.BoxLayout

BoxLayout

Представьте себе коробку. Длинную и узкую. Что бы вы ни клали туда – все будет собираться в стопку, даже если теоретически есть возможность положить рядом. Схематически это и есть поведение BoxLayout. Этот менеджер раскладывает компоненты в том порядке, в каком они были добавлены в контейнер, друг за дружкой, вплотную.

Единственный конструктор BoxLayout-а требует указания контейнера, на объектах которого менеджер и будет ставить свои опыты. В смысле, выполнять раскладки. Уже хотя бы по этому можно заключить, что менеджер – с состоянием.

Кроме указания контейнера конструктор требует указать ось, вдоль которой он будет осуществлять раскладку. Вариантов тут предусмотрено четыре: X_AXIS, Y_AXIS, LINE_AXIS, PAGE_AXIS. Как нетрудно догадаться, первые два из них – абсолютные, следующие два зависят от ориентации текста.

Как, собственно, менеджер осуществляет раскладку. По выбранной оси понятно – он располагает компоненты вплотную друг к другу. А что происходит по другой оси?

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

У каждой компоненты (здесь я имею в виду любого наследника java.awt.Component) есть такие методы как getAlignmentX и getAlignmentY, возвращающие значения типа float в промежутке [0;1]. Это значение определяет положение некой воображаемой оси (от левой границы и верха соответственно). Так вот, компоненты располагаются в контейнере так, чтобы эти воображаемые оси были на одной линии. На сайте Sun есть замечательный иллюстративный пример на эту тему: http://java.sun.com/docs/books/tutorialJWS/uiswing/layout/ex6/BoxLayoutDemo2.jnlp (если JavaWebStart у вас не настроен – можно просто скопировать ссылку и в командной строке набрать javaws <ссылка>). Там прорисованы три прямоугольника, по которым можно кликать и выставлять таким образом новое значение alignmentX.

В выбор положения оси, по которой выравниваются все компоненты, я углубляться не буду. Желающие могут найти этот алгоритм в исходниках класса SizeRequirements, метод getAlignedSizeRequirements.

Весьма полезным при использовании BoxLayout будет класс javax.swing.Box. Он представляет собой JComponent с уже установленным BoxLayout. Интереснее, однако, его статические методы: createHorizontalGlue, createVerticalGlue, createHorizontalStrut, createVerticalStrut. Первые два из них позволяют создать невидимую компоненту, которая будет забирать под себя все свободное пространство соответственно по горизонтали/вертикали. Вторые два позволяют создать невидимую компоненту с указанной соответственно шириной/высотой и нулевым вторым размером. Это может быть полезно для создания зазоров между компонентами либо для обеспечения ширины/высоты контейнера (если использовать их в вертикальной/горизонтальной раскладке соответственно). Для той же цели служит и метод createRigidArea, с той только разницей, что он создает невидимую компоненту с обоими фиксированными размерами.

Код примера приведен ниже:

package test;

import javax.swing.*;
import javax.swing.event.ChangeListener;
import javax.swing.event.ChangeEvent;
import java.awt.*;

/**
 * BoxLayoutTest
 *
 * @author Eugene Matyushkin
 */
public class BoxLayoutTest extends JFrame{

    public BoxLayoutTest(){
        super("BoxLayout");
        final JPanel content = new JPanel();
        content.setBorder(BorderFactory.createLineBorder(Color.red));
        content.setLayout(new BoxLayout(content, BoxLayout.LINE_AXIS));
        JLabel lbl = new JLabel("Label");
        lbl.setBorder(BorderFactory.createLineBorder(Color.blue));
        lbl.setFont(lbl.getFont().deriveFont(30f));
        //lbl.setAlignmentY(1);
        JButton btn = new JButton("Button");
        //btn.setAlignmentY(0);
        JTextField tf = new JTextField("text",5);
        tf.setMaximumSize(new Dimension(150, Integer.MAX_VALUE));
        JLabel lbl2 = new JLabel("Label 2");
        lbl2.setBorder(BorderFactory.createLineBorder(Color.green));
        lbl2.setFont(lbl2.getFont().deriveFont(20f));
        content.add(lbl);
        content.add(Box.createHorizontalStrut(5));
        content.add(btn);
        content.add(Box.createHorizontalStrut(5));
        content.add(tf);
        content.add(Box.createHorizontalGlue());
        content.add(lbl2);
        final JCheckBox chkOrientation = new JCheckBox("Right-to-left orientation");
        chkOrientation.addChangeListener(new ChangeListener(){
            public void stateChanged(ChangeEvent e){
                content.setComponentOrientation( chkOrientation.isSelected() ?
                                                 ComponentOrientation.RIGHT_TO_LEFT :
                                                 ComponentOrientation.LEFT_TO_RIGHT);
                content.doLayout();
            }
        });
        getContentPane().add(content, BorderLayout.CENTER);
        getContentPane().add(chkOrientation, BorderLayout.SOUTH);
        setSize(410,220);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
    }

    public static void main(String[] args){
        JFrame.setDefaultLookAndFeelDecorated(true);
        new BoxLayoutTest().setVisible(true);
    }
}

Можете раскомментировать строчки с выставлением alignmentY и посмотреть, как при этом изменится раскладка.

Собственно, про BoxLayout сказать больше нечего. Самый наглядный пример его применения – строка меню (javax.swing.JMenuBar).

Пойдем дальше. Следующий менеджер –

javax.swing.GroupLayout

Этот менеджер раскладки появился только в Java 6. Как сказано в документации:

GroupLayout is intended for use by builders, but may be hand-coded as well.

Собственно говоря, вот это "is intended for use by" меня и остановило. Подробнее на эту тему я поговорю в разделе, посвященном SpringLayout. Пока же краткая характеристика.

GroupLayout раскладывает компоненты по группам. Группы имеют направление по оси и могут быть параллельными и последовательными. В последовательной группе у каждой следующей компоненты координата вдоль оси на единицу больше (имеется в виду координата в сетке), в параллельной – компоненты имеют одну и ту же координату. Собственно, все это описано в API: http://java.sun.com/javase/6/docs/api/javax/swing/GroupLayout.html. Там же приведены и варинты использования.

На мой взгляд менеджер достаточно навороченный и сложный. Дает ли он какие-нибудь преимущества – я понять пока не успел. Внутри себя информацию о компонентах держит – он с состоянием.

Больше о GroupLayout я говорить не буду, по крайней мере пока. В Sun Java Tutorial он описан достаточно хорошо: http://java.sun.com/docs/books/tutorial/uiswing/layout/group.html. А мы пойдем дальше. Следующий менеджер –

javax.swing.OverlayLayout

Этот менеджер для меня – самая большая загадка. Он располагает компоненты одну над другой. Причем первая компонента располагается ближе всех, последняя – дальше всех. Как это выглядит для трех меток – вы можете увидеть на снимке окна. Я специально сделал разные цвета, чтобы было лучше видно. Исходный текст тут.

Зачем нужен такой менеджер раскладки – я не могу придумать при всем желании. Кстати, в tutorial от Sun он даже не упоминается.

Ну и последний менеджер –

javax.swing.SpringLayout

Он появился в Java в версии 1.4. Принцип действия – связывание границ компонент. Т.е. граница компоненты привязывается к границе другой компоненты или к границе контейнера. Можно задать фиксированное расстояние, можно использовать специальный класс javax.swing.Spring, который держит в себе минимальное, предпочтительное и максимальное значение для длины связи.

Как сказано в документации (вернее, в tutorial):

The SpringLayout class was added in JDK version 1.4 to support layout in GUI builders.

Вам не кажется, что я только что цитировал что-то подобное?

Как я уже говорил, SpringLayout появился в версии 1.4, вышедшей, если я правильно помню, в 2002-м году. Ну никак не позже 2003-го, ибо в декабре 2003-го я уже смотрел на Java 5 beta. Так вот, как я упоминал в статье о визуальных дизайнерах, SpringLayout до сих пор не поддерживается ни одним из визуальных дизайнеров. По причине того, что он сложен для реализации и практически никем не используется.

Философское отступление

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

Классический пример – EJB, entity-beans (до 3.0). Этот вариант ORM был разработан и стандартизирован ДО того, как его начали использовать. И по прошествии нескольких лет мучений в версии 3.0 это творение было заменено на то, что уже давно было известно как best practice, а именно – Hibernate.

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

То же самое произошло и со SpringLayout-ом. Он написан специально для использования в визуальных редакторах – и является единственным менеджером, который не поддерживает ни один визуальный редактор.

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

Ввиду вышеуказанного отступления я счел нецелесообразным тратить время на исследование SpringLayout. К тому же он достаточно хорошо описан в Sun Java Tutorial – http://java.sun.com/docs/books/tutorial/uiswing/layout/spring.html. Ровно по той же причине я пока не стал тратить время на GroupLayout. Посмотрим, насколько он приживется. В данный момент он не поддерживается ни в IntelliJ IDEA 7, ни в NetBeans 6 (по крайней мере в beta1 я его не видел).

Ну, вот мы и добрались до последней темы.

Пишем собственный менеджер – CircleLayout

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

Итак, чего мы хотим от менеджера. Первое – возможность раскладывать компоненты по часовой стрелке или против нее. Второе – вписывать компоненты в контейнер по одной или обоим осям. Или не вписывать. Третье – задать стартовый угол, с которого начнется раскладка.

Для упрощения задачи будем писать менеджер без состояния. Таким образом, из всех методов java.awt.LayoutManager2 нам надо реализовать только лишь layoutContainer, minimumLayoutSize, preferredLayoutSize и maximumLayoutSize. Причем реализация последнего тривиальна:

public Dimension maximumLayoutSize(Container target) {
    return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);
}

Опять-таки для упрощения задачи будем всегда полагаться на предпочтительный размер компонент. Таким образом minimumLayoutSize всегда будет тем же самым, что и preferredLayoutSize.

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

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

Полный исходный код вместе с примером использования находится вот тут: circle-layout.zip. Как всегда в архиве присутствует и build-файл для ant.

* * *

Ну, вот и всё. Я рассказал о менеджерах раскладки все, что хотел. Надеюсь, было познавательно!

Всем спасибо за внимание.


1. Более того, как я выяснил недавно, версия GTK для Java 6, собранная три года назад из исходников, идущих с Java SDK, не совместима с Java SDK последних версий. В связи с этим выкладываю командный файл ant, который поможет собрать версию для вашего текущего Java SDK: gtk.6.build.xml. Пользоваться им так: распаковываете из src.zip, идущего с Java SDK, дерево com/sun/java/swing/plaf/gtk, кладете его в директорию src. Рядом с src размещаете gtk.6.build.xml и выполняете в этой директорией команду ant -f gtk.6.build.xml. В данный момент (06.10.2010) выложена версия, собранная под Java SDK 1.6.0_21.