Гибкая сетка для компонентов: GridBagLayout

November 20, 2011
java swing GridBagLayout GridBagConstraints LayoutManager

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

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

Управление расположением компонентов в GridBagLayout осуществляется отдельным объектом под названием GridBagConstraints, который полностью описывает расположение компонента в сетке и его особенности. Настроив его, вы передаете его в метод add() вместе с добавляемым в контейнер компонентом. К сожалению, именно этот объект делает GridBagLayout такой удобной целью для нападок, а код с его применением напоминает прекрасно перемешанное спагетти. Дело в том, что все настройки хранятся в нем в виде полей (что противоречит концепциям объектно-ориентированного программирования), имена этих полей иногда не соответствуют производимому им эффекту, а количество «волшебных цифр» и избыточной информации поражает воображение и лишает код читаемости.

Давайте начнем с положения компонента в таблице. Его позиция определяется номером ячейки по вертикали и горизонтали:

Координаты в таблице, таким образом, хранятся в полях gridx и gridy. Так как указывать каждый раз новые координаты несколько утомительно, GridBagConstraints любезно предоставляет нам константу RELATIVE, согласно которой компонент будет добавлен в тот же ряд и следующий столбец за последним добавленным компонентов. Такое поведение напомнит нам последовательное расположение FlowLayout, тем более что по умолчанию компоненту придается предпочтительный размер. По умолчанию, если вы не указываете значение полей gridx и gridy, как раз RELATIVE и применяется, располагая компоненты в один ряд.

Так как сетка гибкая, мы можем указать, какое количество ячеек занимает компонент по вертикали или горизонтали.

Очень часто компонент должен занять весь ряд или столбец, независимо от того, сколько там ячеек. Для этих случаев предусмотрено значение GridBagConstraints.REMINDER, которое и говорит, что компонент займет все оставшиеся ячейки, по одной из осей или даже по обеим их них. Это тем более важно, что компонент в этом случае не зависим от появления большего количества ячеек в каком-то из рядом или столбцов, и все равно займет все ячейки.

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

Итак, согласно иллюстрации мы получаем следующее:

Итак, основная идея GridBagLayout кажется понятной, и ячейки различных размеров, с полным контролем над их содержимым должны позволять создавать любые интерфейсы. Давайте рассмотрим простой пример:

// GridBagStart.java
// Первые опыты с расположением GridBagLayout
import java.awt.*;
import javax.swing.*;

public class GridBagStart extends JFrame {
  public GridBagStart() {
    super("GridBagStart");
    // выход при закрытии окна
    setDefaultCloseOperation(EXIT_ON_CLOSE);
    // устанавливаем расположение компонентов
    setLayout(new GridBagLayout());
    // добавляем две кнопки, ячейки по умолчанию
    add(new JButton("Привет"));
    add(new JButton("Отмена"));
    // настройка ячейки для текстового поля
    GridBagConstraints textFieldConstraints =
        new GridBagConstraints();
    // заполнение ячейки по горизонтали
    textFieldConstraints.fill = GridBagConstraints.HORIZONTAL;
    // просим занять все оставшиеся ячейки
    textFieldConstraints.gridwidth =
        GridBagConstraints.REMAINDER;
    textFieldConstraints.weightx = 1.0f;
    add(new JTextField(10), textFieldConstraints);
    // выведем окно на экран
    setSize(400, 200);
    setVisible(true);
  }
  public static void main(String[] args) {
    SwingUtilities.invokeLater(
        new Runnable() {
          public void run() { new GridBagStart(); } });
  }
}

Здесь мы устанавливаем в качестве менеджера расположения GridBagLayout, добавляем две кнопки, причем, не указывая им явно объекта GridBagConstraints. Как мы помним, это означает, что они будут добавляться в ряд, друг за другом, и иметь предпочтительный размер (именно так по умолчанию настроены поля gridx, gridy и fill). Далее мы добавим текстовое поле, и хотим чтобы оно заняло все оставшееся место контейнера. Для этого мы указываем в объекте GridBagConstraints что текстовое поле занимает все оставшиеся в ряду ячейки и заполняет ячейку по горизонтали. На первый взгляд, все просто и должно дать нужный результат. Запустив пример, мы увидим следующее:

Кнопки расположены именно так, как мы и ожидали, а вот с текстовым полем возникла некоторая проблема. Оно не растянуто на всю оставшуюся ширину окна, несмотря на то, что мы подходящим образом задали поля gridwidth и fill, и все равно осталось предпочтительного размера (длиной в 10 символов ввода, что мы задали в конструкторе для поля JTextField). Более того, если вы сделаете окно меньше, вы увидите, что при нехватке пространства поле сжимается до минимального размера «прыжком», не уменьшая свои размеры плавно. Пример показывает нам, что по умолчанию менеджер GridBagLayout ведет себя следующим образом:

Как же заставить GridBagLayout делать компоненты больше их предпочтительного размера? Для этого есть пара несколько загадочных полей, определяющих так называемый «вес» ячейки.

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

textFieldConstraints.weightx = 1.0f;

А результат будет таков:

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

Идея GridBagLayout не так и сложна, и большинство расположений довольно легко поддаются ему. Однако основной проблемой является объект для описания ячеек GridBagConstraints. Названия его полей не всегда ясны (к примеру, gridwidth и weight довольно двусмысленны), а значения для них подстегивают к добавлению к коду цифр (уже упомянутые поля, плюс поле insets), и мы сталкиваемся с проблемой «волшебных чисел», когда цифры в коде не несут очевидной смысловой нагрузки. Огромное количество объектов GridBagConstraints и кода для их настройки, щедро перемешанные с инициализацией компонентов и деловой логикой программы, становятся настоящей головной болью. Поэтому, если есть возможность, всегда стоит отказываться от чистого, «без примесей» расположения GridBagLayout, в пользу описанного нами выше подхода из нескольких вложенных панелей с блочным расположением BoxLayout. Кода будет не больше, а его смысл и простота его чтения намного лучше.

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

Основная проблема состоит в том, что уровень объекта GridBagConstraints слишком низок, и вместо очевидных операций (растянуть компонент, добавить промежуток между компонентами, выровнять его по левому краю), мы манипулируем полями, и эффект от этих действий не «прочитать» напрямую. Но можно создать вспомогательный объект для работы с GridBagConstraints, и вызывая его методы, намного улучшить и упростить нашу работу с менеджером GridBagLayout. Давайте попробуем:

// com/porty/swing/GridBagHelper.java
// Вспомогательный класс, позволяющий писать
// качественный код для расположения GridBagLayout
package com.porty.swing;

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

public class GridBagHelper {
  // координаты текущей ячейки
  private int gridx, gridy;
  // настраиваемый объект GridBagConstraints
  private GridBagConstraints constraints;

  // возвращает настроенный объект GridBagConstraints
  public GridBagConstraints get() {
    return constraints;
  }
  // двигается на следующую ячейку
  public GridBagHelper nextCell() {
    constraints = new GridBagConstraints();
    constraints.gridx = gridx++;
    constraints.gridy = gridy;
    // для удобства возвращаем себя
    return this;
  }
  // двигается на следующий ряд
  public GridBagHelper nextRow() {
    gridy++;
    gridx = 0;
    constraints.gridx = 0;
    constraints.gridy = gridy;
    return this;
  }
  // раздвигает ячейку до конца строки
  public GridBagHelper span() {
    constraints.gridwidth = GridBagConstraints.REMAINDER;
    return this;
  }
  // заполняет ячейку по горизонтали
  public GridBagHelper fillHorizontally() {
    constraints.fill = GridBagConstraints.HORIZONTAL;
    return this;
  }
  // вставляет распорку справа
  public GridBagHelper gap(int size) {
    constraints.insets.right = size;
    return this;
  }

  public GridBagHelper spanY() {
    constraints.gridheight = GridBagConstraints.REMAINDER;
    return this;
  }


  public GridBagHelper fillBoth() {
    constraints.fill = GridBagConstraints.BOTH;
    return this;
  }

  public GridBagHelper alignLeft() {
    constraints.anchor = GridBagConstraints.LINE_START;
    return this;
  }

  public GridBagHelper alignRight() {
    constraints.anchor = GridBagConstraints.LINE_END;
    return this;
  }

  public GridBagHelper setInsets(int left, int top, int right, int bottom) {
    Insets i = new Insets(top, left, bottom, right);
    constraints.insets = i;
    return this;
  }

  public GridBagHelper setWeights(float horizontal, float vertical) {
    constraints.weightx = horizontal;
    constraints.weighty = vertical;
    return this;
  }

  public void insertEmptyRow(Container c, int height) {
    Component comp = Box.createVerticalStrut(height);
    nextCell().nextRow().fillHorizontally().span();
    c.add(comp, get());
    nextRow();
  }

  public void insertEmptyFiller(Container c) {
    Component comp = Box.createGlue();
    nextCell().nextRow().fillBoth().span().spanY().setWeights(1.0f, 1.0f);
    c.add(comp, get());
    nextRow();
  }
}

Класс GridBagHelper весьма прост. Он работает с объектом GridBagConstraints, настраивая его поля и скрывая некоторые не очень очевидные цифры и манипуляции с полями, вместо этого предоставляя нам весьма понятные методы, к примеру, nextRow() переходит на первую ячейку следующей строки таблицы, а gap() вставляет распорку справа от компонента. Чтобы работать с этим объектом, нужно создать его экземпляр, для каждой новой ячейки вызывать метод nextCell() (именно там создается новый объект GridBagConstraints), а в конце, когда настройка компонента проведена, получить результат методом get(). Чтобы вызывать методы было удобнее, каждый из них возвращает ссылку на объект GridBagHelper, так что можно вызывать методы «в ряд».

В качестве маленького примера можно привести надпись и текстовое поле, с распоркой между ними:

GridBagHelper helper = new GridBagHelper();
helper.nextCell().gap(5);
f.add(new JLabel("Имя:"), helper.get());
helper.nextCell().span();
f.add(new JTextField(20));

При запуске мы получим такой результат:

Мы получим и распорку, не работая со всеми четырьмя размерностями объекта Insets, и скажем полю занять все оставшиеся ячейки более понятным методом span(), и прозрачный способ перехода по ячейкам. В полной версии класса GridBagHelper, которую вы можете найти в исходных текстах программ книги на Github, есть и много других полезных методов с простыми названиями. Такой код не только во много раз быстрее писать, но и легче читать и обновлять. В будущем, если вы решите применить на практике GridBagLayout, класс GridBagHelper или его эквиваленты будут весьма кстати, а применять напрямую запутанный класс GridBagConstraints вряд ли можно рекомендовать.