知ってました? GroupLayout
Swingには、もう1つ、Swing専用のレイアウトマネージャがあります。「GroupLayout」というもので、これはJavaSE 6.0から新たに用意されたものです。GroupLayoutは、縦横に規則的にコンポーネントを配置したレイアウトを作成するものです。GridLayoutに似ていますが、GridLayoutのように等間隔で全項目にコンポーネントを組み込むのではなく、指定した場所にだけ必要なコンポーネントを組み込みレイアウトすることができます。
package jp.codezine; import java.awt.*; import javax.swing.*; import javax.swing.GroupLayout.Alignment; public class SampleApp extends JFrame { private static final long serialVersionUID = 1L; public SampleApp(){ this.setSize(new Dimension(300,200)); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); GroupLayout layout = new GroupLayout(this.getContentPane()); this.setLayout(layout); // コンポーネント保管用の配列準備 JLabel[] labels = new JLabel[5]; JTextField[] tfields = new JTextField[5]; JButton[] btns = new JButton[5]; // コンポーネント作成 for(int i = 0;i < 5;i++){ labels[i] = new JLabel("label" + i); tfields[i] = new JTextField(); btns[i] = new JButton("button" + i); } // 垂直SequentialGroupの作成 GroupLayout.SequentialGroup vgroup = layout.createSequentialGroup(); for(int i = 0;i < 5;i++){ GroupLayout.ParallelGroup group2 = layout.createParallelGroup(Alignment.BASELINE); group2.addComponent(labels[i]); group2.addComponent(tfields[i]); group2.addComponent(btns[i]); vgroup.addGroup(group2); } // 水平SequentialGroupの作成 GroupLayout.SequentialGroup hgroup = layout.createSequentialGroup(); GroupLayout.ParallelGroup groupLabel = layout.createParallelGroup(); GroupLayout.ParallelGroup groupTField = layout.createParallelGroup(); GroupLayout.ParallelGroup groupBtn = layout.createParallelGroup(); for(int i = 0;i < 5;i++){ groupLabel.addComponent(labels[i]); groupTField.addComponent(tfields[i]); groupBtn.addComponent(btns[i]); } hgroup.addGroup(groupLabel); hgroup.addGroup(groupTField); hgroup.addGroup(groupBtn); // 水平・垂直SequentialGroupの設定 layout.setVerticalGroup(vgroup); layout.setHorizontalGroup(hgroup); } public static void main(String[] args) { new SampleApp().setVisible(true); } }

これがGroupLayoutを使用した簡単なサンプルです。GroupLayoutは、使い方に少しばかりクセがあります。このレイアウトマネージャを使うためには、次のような手順を踏まなければいけません。
GroupLayoutを作成します。- 水平方向と垂直方向のグループをそれぞれ管理する
GroupLayout.SequentialGroupを作成します。これは組み込まれたグループを順番に保管し管理するものです。 - 用意した水平垂直用
GroupLayout.SequentialGroupに、GroupLayout.ParallelGroupを作成して組み込んでいきます。コンポーネント類は、このGroupLayout.ParallelGroupに組み込んでおきます。
GroupLayoutは、GroupLayout.SequentialGroupとGroupLayout.ParallelGroupの組み合わせによってコンポーネントを管理します。GroupLayout.SequentialGroupは複数のグループを管理するグループであり、GroupLayout.ParallelGroupは複数のコンポーネントを管理するグループです。
GroupLayoutは、垂直方向および水平方向にグループを使ってコンポーネントを管理するようになっています。例えば、ここでは3列×5行にコンポーネントを並べています。この場合、垂直方向のグループは5つのグループが組み込まれ、それぞれのグループに各行のコンポーネントが組み込まれます。同様に、水平方向のグループには3つのグループが用意され、それぞれの中に各列ごとのコンポーネントがまとめられるわけです。
こうして垂直水平方向にグループが用意され設定されると、これらのグループをもとにして、組み込まれている各コンポーネントが配置されていくわけです。このGroupLayoutは、基本的な考え方を飲み込めれば、縦横に配置するタイプのレイアウトを非常に柔軟に作成することができます。
独自に並べる最も単純な方法は?
レイアウトマネージャを使わず、もっと自由にコンポーネントをレイアウトしたい、という人も多いことでしょう。setLayout(null)にして、1つ1つのコンポーネントの位置と大きさを設定していけばそれは可能です。が、例えばウインドウサイズが変更されたりしたときなど、対応するのが面倒になることもあります。
こういうとき、どういうやり方をしますか? 「独自にレイアウトマネージャを作れ」という声が聞こえそうですが、実はもっと単純にレイアウトを調整する方法があります。それは、ContainerListenerとComponentListenerを用意する方法です。
コンテナに組み込まれたコンポーネントのレイアウトというのは、「コンポーネントが追加・削除されたとき」「コンテナがリサイズされたとき」に再レイアウトする必要が生じます。ということは、こうしたイベントが発生したときにコンポーネント類を並べなおすようにしていれば、レイアウトマネージャなど使わずともちゃんとコンポーネント類はレイアウトされることになるはずです。
package jp.codezine; import java.awt.*; import java.awt.event.*; import javax.swing.*; public class SampleApp extends JFrame implements ActionListener { private static final long serialVersionUID = 1L; public SampleApp(){ this.setSize(new Dimension(300,200)); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.setLayout(null); this.getContentPane().addComponentListener( new MyComponentAdapter(this)); this.getContentPane().addContainerListener( new MyContainerAdapter(this)); actionPerformed(null); } public void actionPerformed(ActionEvent e){ JButton btn = new JButton("button"); btn.setFont(new Font("Serif",Font.PLAIN,18)); btn.addActionListener(this); this.add(btn); } public void doLayout() { super.doLayout(); Component[] components = this.getContentPane().getComponents(); int insetSize = 20; int w = (this.getContentPane().getWidth() - insetSize * 2) / 2; int h = (this.getContentPane().getHeight() - insetSize * 2); int insetW = w / components.length; int insetH = h / components.length; int x = insetSize; int y = insetSize; for(Component c: components){ c.setBounds(new Rectangle(x,y,w,insetH)); x += insetW; y += insetH; } } public static void main(String[] args) { new SampleApp().setVisible(true); } } class MyContainerAdapter extends ContainerAdapter { private SampleApp sampleapp; public MyContainerAdapter(SampleApp app){ super(); this.sampleapp = app; } @Override public void componentAdded(ContainerEvent e) { sampleapp.doLayout(); } @Override public void componentRemoved(ContainerEvent e) { sampleapp.doLayout(); } } class MyComponentAdapter extends ComponentAdapter { private SampleApp sampleapp; public MyComponentAdapter(SampleApp app){ super(); sampleapp = app; } @Override public void componentResized(ComponentEvent e) { sampleapp.doLayout(); } }


これは、ごく単純なレイアウト調整の例です。ComponentListener/ContainerListenerをそれぞれ用意して組み込み、その中でコンポーネントやコンテナのイベントに応じてdoLayoutを実行して再レイアウトを行わせています。ここでは、コンポーネントを左上から右下にかけて斜めに配置させています。一応、再レイアウトされていることが分かるように、JFrameの横幅にあわせてコンポーネントの幅を調整するようにしておきました。
コンポーネントの追加と削除およびウインドウのリサイズ時に、doLayoutを呼び出して再レイアウトするようになっています。このように、再レイアウトのためのメソッドを用意しておき、イベントが起こった際にこれを呼び出してレイアウトし直せば、常に正しいレイアウト状態を保つことができます。
この「レイアウトマネージャを使わず、コンテナのイベントを使ってレイアウトする」という方法は、いわばAWT/Swingのレイアウトの根本を否定するようなやり方ですから、あまりほめられたものではありませんが、ちょっとしたテクニックとして覚えておいても損ではないでしょう。
