Ein Swing-Layout-Manager – OneRowSameSizeLayoutManager

Jeder Java-Entwickler, der schon mal seriös mit Swing eine Oberfläche gebaut hat, hat schon einmal mit dem Layout-Manager seiner Wahl gekämpft. Die standardmäßig im JDK ausgelieferten Layout-Manager sind eher gewöhnungsbedürftig bis leistungsschwach, mindestens aber umständlich. GroupLayout für komplexe Dinge und BorderLayout für Basislayouts sind gerade noch so verwendbar. Beliebte 3rd-party-Layout-Manager sind sicherlich MigLayout (oder MiG Layout? Man kann sich nicht entscheiden…), FormLayout, TableLayout und DesignGridLayout (Jahre nach dem Ableben von java.net wohl nur noch über Maven Central beziehbar).

Für besondere Bedürfnisse ist es manchmal eine gute Idee, einen einfachen LayoutManager selbst zu bauen. Ich habe das mal beispielhaft getan für den Anwendungsfall “Layout einer typischen Reihe von Buttons unten in einem Dialog”. Im Gegensatz zum merkwürdigen Beispiel im Swing-Tutorial von Oracle namens “DiagonalLayout” hoffe ich jedenfalls, dass mein Beispiel etwas mehr Nützlichkeit ausstrahlt. Der Quellcode ist mehr auf Lesbarkeit und Nachvollziehbarkeit denn auf Eleganz getrimmt. An einem schmissigeren Namen arbeite ich noch…

Irgendwann werde ich noch mal den “Packer”, einen Layout-Manager von Tcl/Tk, für Swing nachbauen. M.E. war der “Packer” in seinem Verhalten und der Vorhersagbarkeit des programmierten Resultats absolut untadelig, etwas was ich von den meisten Swing-LayoutManagern nicht sagen kann. Aber vielleicht ist meine Erinnerung an den Packer auch rosarot verklärt – ist schon über zwei Jahrzehnte her seit meinem ersten und letzten Kontakt.

/*
 * (c) hubersn Software
 * www.hubersn.com
 */
package com.hubersn.playground.swing.layout;

import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.LayoutManager;

import javax.swing.SwingConstants;

/**
 * A layout manager supporting a row of components kept at the same width and height with specifiable alignment and gap size between the
 * components. It has its own insets to avoid having to work with empty borders.
 */
public class OneRowSameSizeLayout implements LayoutManager {

  /** Constant for default left alignment (identical to SwingConstants.LEFT). */
  public static final int ALIGN_LEFT = SwingConstants.LEFT;
  /** Constant for default centre alignment (identical to SwingConstants.CENTER). */
  public static final int ALIGN_CENTER = SwingConstants.CENTER;
  /** Constant for default right alignment (identical to SwingConstants.RIGHT). */
  public static final int ALIGN_RIGHT = SwingConstants.RIGHT;

  // calculated overall minimum/preferred size
  private int minWidth = 0;
  private int minHeight = 0;
  private int preferredWidth = 0;
  private int preferredHeight = 0;
  // calculated maximum component preferred sizes
  private int maxPreferredWidth = 0;
  private int maxPreferredHeight = 0;

  private Insets insets;

  private int componentGap = 5;

  private int alignment;

  /**
   * Creates a new layout with right alignment, 4 pixels gap and 8 pixels insets all around.
   */
  public OneRowSameSizeLayout() {
    this(ALIGN_RIGHT, 4, new Insets(8, 8, 8, 8));
  }

  /**
   * Creates a new layout with given alignment, gap between components and insets.
   * 
   * @param alignment component alignment - ALIGN_LEFT, ALIGN_CENTER or ALIGN_RIGHT.
   * @param componentGap gap in pixels between components.
   * @param insets insets to use additional to possible container border - if null, all insets are 0.
   */
  public OneRowSameSizeLayout(final int alignment, final int componentGap, final Insets insets) {
    setAlignment(alignment);
    this.componentGap = componentGap;
    this.insets = insets;
    if (insets == null) {
      this.insets = new Insets(0, 0, 0, 0);
    }
  }

  /**
   * Sets the component alignment for this layout.
   * 
   * @param alignment component alignment - ALIGN_LEFT, ALIGN_CENTER or ALIGN_RIGHT.
   */
  public void setAlignment(final int alignment) {
    this.alignment = alignment;
  }

  @Override
  public void addLayoutComponent(String name, Component comp) {
    // nothing to do - we are not interested in user-given layout constraints.
  }

  @Override
  public void removeLayoutComponent(Component comp) {
    // nothing to do.
  }

  private void calculateSizes(Container parent) {
    final int numberOfComponents = parent.getComponentCount();

    this.preferredWidth = this.insets.left + this.insets.right;
    this.preferredHeight = this.insets.top + this.insets.bottom;
    this.minWidth = 0;
    this.minHeight = 0;

    final int numberOfVisibleComponents = getNumberOfVisibleComponents(parent);
    for (int i = 0; i < numberOfComponents; i++) {
      final Component c = parent.getComponent(i);
      if (c.isVisible()) {
        final Dimension d = c.getPreferredSize();
        this.maxPreferredWidth = Math.max(this.maxPreferredWidth, d.width);
        this.maxPreferredHeight = Math.max(this.maxPreferredHeight, d.height);
        this.minWidth += c.getMinimumSize().width;
      }
    }

    this.preferredHeight += this.maxPreferredHeight;
    this.minHeight = this.maxPreferredHeight;

    if (numberOfVisibleComponents > 0) {
      this.preferredWidth += (this.maxPreferredWidth * numberOfVisibleComponents);
      this.preferredWidth += this.componentGap * (numberOfVisibleComponents - 1);
    }
  }

  @Override
  public Dimension preferredLayoutSize(Container parent) {
    final Dimension dim = new Dimension(0, 0);

    calculateSizes(parent);

    final Insets parentInsets = parent.getInsets();
    dim.width = this.preferredWidth + parentInsets.left + parentInsets.right;
    dim.height = this.preferredHeight + parentInsets.top + parentInsets.bottom;

    return dim;
  }

  @Override
  public Dimension minimumLayoutSize(Container parent) {
    final Dimension dim = new Dimension(0, 0);

    final Insets parentInsets = parent.getInsets();
    dim.width = this.minWidth + parentInsets.left + parentInsets.right;
    dim.height = this.minHeight + parentInsets.top + parentInsets.bottom;

    return dim;
  }

  @Override
  public void layoutContainer(Container parent) {
    final int numberOfVisibleComponents = getNumberOfVisibleComponents(parent);
    if (numberOfVisibleComponents == 0) {
      return;
    }

    calculateSizes(parent);

    final Insets parentInsets = parent.getInsets();

    // will all fit into parent? Which sizes to reduce?
    // width sizing strategy:
    // first, shrink gap and insets to 0
    // then, shrink button width and height
    // height sizing strategy:
    // shrink insets to 0
    final int parentWidth = parent.getWidth() - (parentInsets.left + parentInsets.right);
    final int parentHeight = parent.getHeight() - (parentInsets.top + parentInsets.bottom);
    final int heightToUse = Math.min(parentHeight, this.maxPreferredHeight);
    // width shrinking - toUse values are used for final layout bounds setting step
    int widthToUse = this.maxPreferredWidth;
    int buttonGapToUse = this.componentGap;
    int leftInsetToUse = this.insets.left;
    int rightInsetToUse = this.insets.right;

    int amountToReduceWidth = this.preferredWidth - parentWidth;
    if (amountToReduceWidth > 0) {
      // we need to save some pixels...
      // check for left/right insets
      if (leftInsetToUse + rightInsetToUse >= amountToReduceWidth) {
        int reduceInsetBy = amountToReduceWidth / 2;
        leftInsetToUse -= reduceInsetBy;
        amountToReduceWidth = 0;
      } else {
        leftInsetToUse = 0;
        amountToReduceWidth -= (this.insets.left + this.insets.right);
      }
      if (amountToReduceWidth > 0) {
        // check for button gaps
        if (numberOfVisibleComponents > 1 && (numberOfVisibleComponents - 1) * buttonGapToUse >= amountToReduceWidth) {
          buttonGapToUse -= amountToReduceWidth / (numberOfVisibleComponents - 1);
          amountToReduceWidth = 0;
        } else {
          buttonGapToUse = 0;
          amountToReduceWidth -= (numberOfVisibleComponents - 1) * this.componentGap;
        }
        // now shrink the button size itself as a last resort
        if (amountToReduceWidth > 0) {
          widthToUse = parentWidth / numberOfVisibleComponents;
        }
      }
    }

    // height shrinking - toUse values are used for final layout bounds setting step
    int topInsetToUse = this.insets.top;
    int amountToReduceHeight = this.preferredHeight - parentHeight;
    if (amountToReduceHeight > 0) {
      // we need to save some pixels...
      // check for top/bottom insets
      if (topInsetToUse + this.insets.bottom >= amountToReduceHeight) {
        int reduceInsetBy = amountToReduceHeight / 2;
        topInsetToUse -= reduceInsetBy;
      } else {
        topInsetToUse = 0;
      }
    }

    // finally size and place the components
    int x = parentInsets.left + leftInsetToUse;
    // alignment is only relevant if we have enough space
    if (parentWidth > this.preferredWidth) {
      if (this.alignment == ALIGN_CENTER) {
        x += (parentWidth - this.preferredWidth) / 2;
      } else if (this.alignment == ALIGN_RIGHT) {
        x += parentWidth - this.preferredWidth;
      }
    }

    final int y = parentInsets.top + topInsetToUse;

    final int numberOfComponents = parent.getComponentCount();
    for (int i = 0; i < numberOfComponents; i++) {
      Component c = parent.getComponent(i);
      if (c.isVisible()) {
        c.setBounds(x, y, widthToUse, heightToUse);
        x += widthToUse + buttonGapToUse;
      }
    }
  }

  private static int getNumberOfVisibleComponents(final Container parent) {
    final int numberOfComponents = parent.getComponentCount();
    int numberOfVisibleComponents = 0;

    for (int i = 0; i < numberOfComponents; i++) {
      Component c = parent.getComponent(i);
      if (c.isVisible()) {
        numberOfVisibleComponents++;
      }
    }

    return numberOfVisibleComponents;
  }
}