Eine Frage des Layouts


Wer eine ordentliche Swing-Oberfläche schreiben will, kommt um Layoutmanager nicht herum. Heute will ich einmal von Scala- und einmal von Java-Seite ein wenig dazu schreiben.

Scala-Swing drückt sich ein wenig um das Problem Layoutmanager, indem es die Panel gleich fix und fertig mit Layout liefert: FlowPanel, GridPanel, Borderpanel und so weiter. Ich weiß nicht so richtig, ob das eine gute Idee ist, schließlich entdecken immer mehr Programmierer, dass jenseits des schröcklichen GridBagMonsters auch Layoutmanager gibt, die sich recht gut bedienen lassen, obwohl sie komplexe Aufgaben erfüllen können, etwa FormLayout, MigLayout oder TableLayout. Um der Philosophie von Scala-Swing zu folgen, hieße das für jede dieser Implementierungen, einen eigenen Panel abzuleiten. Da eine ähnliche Frage im Scala Forum aufgetaucht ist, habe ich so einen speziellen Panel geschrieben, nämlich den für das NullLayout: In Swing kann man nämlich auch null als Layoutmanager übergeben, was signalisiert, dass man die Positionen aller Kinder selbst festlegen will. Hier ist der Code, der allerdings nur oberflächlich getestet ist:

import java.awt.Rectangle
 
import scala.swing.Button
import scala.swing.Component
import scala.swing.Frame
import scala.swing.LayoutContainer
import scala.swing.Panel
 
class NullPanel extends Panel with LayoutContainer {
  override lazy val peer = new javax.swing.JPanel(null) with SuperMixin
  type Constraints = Rectangle
  protected def areValid(c: Constraints): (Boolean, String) = (true, "")
  protected def constraintsFor(comp: Component) = comp.bounds
  def add(c: Component, b: Constraints) {
    if(b != null) {
      c.bounds.x = b.x
      c.bounds.y = b.y
      c.bounds.width = b.width
      c.bounds.height = b.height
      c.peer.setBounds(b)
    }
    peer.add(c.peer)
  }
}

Dabei habe ich die für die Komponenten zu setzenden Bounds als „Constraints“ getarnt. Eine Komponente kann man so hinzufügen:

panel.add(new Button("Test1"), new Rectangle(10,10,100,30))

Das soll es schon für Scala gewesen sein. Kommen wir zu Java:

Wie bereits erwähnt, gibt es viele gute, freie Layoutmanager für komplexere Layouts. Aber was ich bisher vermißt habe ist ein Layout, das sich wie GridLayout verhält, nur ein klein wenig flexibler mit den Zeilen und Spalten umgehen kann. Also habe ich einfach eins geschrieben. Hier kommt SimpleLayout:

package simplelayout;
 
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.LayoutManager2;
import java.util.HashMap;
import java.util.Map;
 
public class SimpleLayout implements LayoutManager2 {
    
    private final int outerGap;
    private final int innerGap;
    private final Policy[] rows;
    private final Policy[] columns;
    private final Component grid[][];
    private final Map<String, CellConstraint> consMap = new HashMap<String, CellConstraint>();
    
    private enum Fill {FIRST, CENTER, LAST, ALL };
    
    public SimpleLayout(Policy[] columns, Policy[] rows, int outerGap, int innerGap) {
        this.rows = rows;
        this.columns = columns;
        this.outerGap = outerGap;
        this.innerGap = innerGap;
        this.grid = new Component[columns.length][rows.length];
    }
    public SimpleLayout(Policy[] columns, Policy[] rows) {
        this(columns, rows, 4, 2);
    }
 
    @Override
    public void addLayoutComponent(String name, Component comp) {
        //no effect
    }
 
    @Override
    public void removeLayoutComponent(Component comp) {
        for(int x = 0; x < grid.length; x++) {
            for(int y = 0; y < grid[x].length; y++) {
                if (comp.equals(grid[x][y])) {
                    grid[x][y] = null;
                    consMap.remove(x + ":" + y);
                    return;
                }
            }
        }
    }
 
    @Override
    public Dimension preferredLayoutSize(Container parent) {
        int width = 2*outerGap + (grid.length-1)*innerGap;
        for(int x = 0; x < grid.length; x++) {
            width += getColWidth(x, false);
        }
        int height = 2*outerGap + (grid[0].length-1)*innerGap;
        for(int y = 0; y < grid[0].length; y++) {
            width += getRowHeight(y, false);
        }
        return new Dimension(width, height);
    }
    
    private int getColWidth(int column, boolean isMinimum) {
        Policy pol = columns[column];
        if (! pol.isFixed()) {
            int max = 0;
            for(int y = 0; y < grid[column].length; y++) {
                Component c = grid[column][y];
                if(c != null) {
                    max = Math.max(max, isMinimum 
                            ? c.getMinimumSize().width
                            : c.getPreferredSize().width);
                } 
            }
            return max;
        } else {
            return pol.fixed;
        }
    }
 
    private int getRowHeight(int row, boolean isMinimum) {
        Policy pol = rows[row];
        if (! pol.isFixed()) {
            int max = 0;
            for(int x = 0; x < grid.length; x++) {
                Component c = grid[x][row];
                if(c != null) {
                    max = Math.max(max, isMinimum 
                            ? c.getMinimumSize().height
                            : c.getPreferredSize().height);
                } 
            }
            return max;
        } else {
            return pol.fixed;
        }
    }
 
    @Override
    public Dimension minimumLayoutSize(Container parent) {
        int width = 2*outerGap + (grid.length-1)*innerGap;
        for(int x = 0; x < grid.length; x++) {
            width += getColWidth(x, true);
        }
        int height = 2*outerGap + (grid[0].length-1)*innerGap;
        for(int y = 0; y < grid[0].length; y++) {
            width += getRowHeight(y, true);
        }
        return new Dimension(width, height);
    }
 
    @Override
    public void layoutContainer(Container parent) {
        int w = parent.getWidth();
        int h = parent.getHeight();
        int[] widths = widths(w);
        int[] xStart = new int[widths.length];
        for(int x = 0; x < widths.length; x++) {
            xStart[x] = x == 0 ? outerGap : xStart[x-1] + innerGap + widths[x-1];
        }
        
        int[] heights = heights(h);
        int[] yStart = new int[heights.length];
        for(int y = 0; y < heights.length; y++) {
            yStart[y] = y == 0 ? outerGap : yStart[y-1] + innerGap + heights[y-1];
        }
        
        for(int x = 0; x < grid.length; x++) {
            for(int y = 0; y < grid[0].length; y++) {
                Component comp = grid[x][y];
                if(comp != null) {
                    CellConstraint constr = getConstr(x,y);
                    int width = constr.hFill == Fill.ALL  ? widths[x]
                            : Math.min(widths[x], comp.getPreferredSize().width);
                    int xk = xStart[x];
                    if(constr.hFill == Fill.LAST) {
                       xk +=  widths[x] - width;                         
                    } else if(constr.hFill == Fill.CENTER) {
                       xk +=  (widths[x] - width)/2;                                                 
                    }
                    int height = constr.vFill == Fill.ALL  ? heights[y]
                            : Math.min(heights[y], comp.getPreferredSize().height);
                    int yk = yStart[y];
                    if(constr.vFill == Fill.LAST) {
                       yk +=  heights[x] - height;                         
                    } else if(constr.vFill == Fill.CENTER) {
                       yk +=  (heights[x] - height)/2;                                                 
                    }
                    comp.setBounds(xk, yk, width, height);
                }
            }
        }
    }
    
    private CellConstraint getConstr(int x, int y) {
        CellConstraint cons = consMap.get(x + ":" + y);
        if (cons == null) {
            cons = columns[x].defaultConstraint;
        }
        if (cons == null) {
            cons = rows[y].defaultConstraint;
        }
        if (cons == null) {
            cons = cell(x, y);
        }
        return cons;
    }
        
    private int[] widths(int w) {    
        int gaps = 2*outerGap + (grid.length-1)*innerGap;
        int wm = gaps;
        int wp = gaps;
        int[] wms = new int[grid.length];
        int[] wps = new int[grid.length];
        for(int x = 0; x < grid.length; x++) {
            wms[x] = getColWidth(x, true);
            wps[x] = getColWidth(x, false);
            wm += wms[x];
            wp += wps[x];
        }
        int[] widths = null; 
        if(wm >= w) { //we won't layout anything smaller than minimum sizes
            widths = wms;
        } else if (wp > w) {
            float ratio = (float)(w - wm) / (wp - wm);
            widths = new int[grid.length];
            for(int x = 0; x < grid.length; x++) {
                widths[x] = wms[x] + (int)((wps[x] - wms[x]) * ratio);
            }
        } else {
            int delta = w - wp;
            widths = new int[grid.length];
            int props = 0;
            for(int x = 0; x < grid.length; x++) {
                if (columns[x].isProp()) {
                    props ++;
                }
            }
            for (int x = 0; x < grid.length; x++) {
               widths[x] = wps[x] + (columns[x].isProp() ? delta / props : 0);
            }
        }
        return widths;
    }
    
    private int[] heights(int h) {    
        int gaps = 2*outerGap + (grid[0].length-1)*innerGap;
        int hm = gaps;
        int hp = gaps;
        int[] hms = new int[grid[0].length];
        int[] hps = new int[grid[0].length];
        for(int y = 0; y < grid[0].length; y++) {
            hms[y] = getRowHeight(y, true);
            hps[y] = getRowHeight(y, false);
            hm += hms[y];
            hp += hps[y];
        }
        int[] heights = null; 
        if(hm >= h) { //we won't layout anything smaller than minimum sizes
            heights = hms;
        } else if (hp > h) {
            float ratio = (float)(h - hm) / (hp - hm);
            heights = new int[grid[0].length];
            for(int y = 0; y < grid[0].length; y++) {
                heights[y] = hms[y] + (int)((hps[y] - hms[y]) * ratio);
            }
        } else {
            int delta = h - hp;
            heights = new int[grid[0].length];
            int props = 0;
            for(int y = 0; y < grid[0].length; y++) {
                if (rows[y].isProp()) {
                    props ++;
                }
            }
            for (int y = 0; y < grid[0].length; y++) {
               heights[y] = hps[y] + (rows[y].isProp() ? delta / props : 0);
            }
        }
        return heights;
    }
    
    @Override
    public void addLayoutComponent(Component comp, Object constraints) {
        CellConstraint cons = null;
        if(constraints instanceof CellConstraint) {
           cons = (CellConstraint) constraints; 
        } else if (constraints == null || cons.x == -1 || cons.y == -1) {
            outer:
            for(int y = 0; y < grid[0].length; y++) {
                for(int x = 0; x < grid.length; x++) {
                    if (grid[x][y] == null) {
                        cons = (cons == null ? getConstr(x,y) : cons).moveTo(x, y);
                        break outer;
                    }
                }
            }
        } else {
            throw new IllegalArgumentException("Unknown contstraint type: " + constraints.getClass().getName());
        }
        grid[cons.x][cons.y] = comp;
        consMap.put(cons.x + ":" + cons.y, cons);
    }
 
    @Override
    public Dimension maximumLayoutSize(Container target) {
        return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);
    }
 
    @Override
    public float getLayoutAlignmentX(Container target) {
        return 0.5f;
    }
 
    @Override
    public float getLayoutAlignmentY(Container target) {
        return 0.5f;
    }
 
    @Override
    public void invalidateLayout(Container target) {
        //ignore
    }
 
    public static class Policy {
 
        private CellConstraint defaultConstraint = null;
        private final int fixed;
        
        private Policy(int fixed) {
            this.fixed = fixed;
        }
        
        public boolean isFixed() {
            return fixed > 0;
        }
        public boolean isComp() {
            return fixed == -1;
        }
        public boolean isProp() {
            return fixed == -2;
        }
        
        private CellConstraint constr() {
            if(defaultConstraint == null) {
                defaultConstraint = cell();
            }
            return defaultConstraint;
        }
 
        public Policy left() {
            defaultConstraint = constr().left();
            return this;
        }
        public Policy hCenter() {
            defaultConstraint = constr().hCenter();
            return this;
        }
        public Policy right() {
            defaultConstraint = constr().right();
            return this;
        }
        public Policy hFill() {
            defaultConstraint = constr().hFill();
            return this;
        }
        public Policy top() {
            defaultConstraint = constr().top();
            return this;
        }
        public Policy vCenter() {
            defaultConstraint = constr().vCenter();
            return this;
        }
        public Policy bottom() {
            defaultConstraint = constr().bottom();
            return this;
        }
        public Policy vFill() {
            defaultConstraint = constr().vFill();
            return this;
        }
        public Policy center() {
           defaultConstraint = constr().center();
            return this;
        }
        public Policy fill() {
            defaultConstraint = constr().fill();
            return this;
        }
    }
    
    public static Policy fixed(int fixed) {
        assert fixed > 0;
        return new Policy(fixed);
    }
    public static Policy comp() {
        return new Policy(-1);
    }
    public static Policy prop() {
        return new Policy(-2);
    }
    
    public static Policy[] pol(Policy ... policies) {
        return policies;
    }
    
    public static class CellConstraint {
        final int x;
        final int y;
        final Fill hFill;
        final Fill vFill;
        private CellConstraint(int x, int y, Fill hFill, Fill vFill) {
            this.x = x;
            this.y = y;
            this.hFill = hFill;
            this.vFill = vFill;
        }
        public CellConstraint left() {
            return new CellConstraint(x,y,Fill.FIRST,vFill);
        }
        public CellConstraint hCenter() {
            return new CellConstraint(x,y,Fill.CENTER,vFill);
        }
        public CellConstraint right() {
            return new CellConstraint(x,y,Fill.LAST,vFill);
        }
        public CellConstraint hFill() {
            return new CellConstraint(x,y,Fill.ALL,vFill);
        }
        public CellConstraint top() {
            return new CellConstraint(x,y,hFill, Fill.FIRST);
        }
        public CellConstraint vCenter() {
            return new CellConstraint(x,y,hFill,Fill.CENTER);
        }
        public CellConstraint bottom() {
            return new CellConstraint(x,y,hFill,Fill.LAST);
        }
        public CellConstraint vFill() {
            return new CellConstraint(x,y,hFill, Fill.ALL);
        }
        public CellConstraint center() {
            return new CellConstraint(x,y,Fill.CENTER,Fill.CENTER);
        }
        public CellConstraint fill() {
            return new CellConstraint(x,y,Fill.ALL,Fill.ALL);
        }
        public CellConstraint moveTo(int nx, int ny) {
            return new CellConstraint(nx, ny, hFill, vFill);
        }
    }
    
    public static CellConstraint cell(int x, int y) {
        return new CellConstraint(x, y, Fill.ALL, Fill.ALL);
    }
    public static CellConstraint cell() {
        return new CellConstraint(-1, -1, Fill.ALL, Fill.ALL);
    }
    
}

Vorsicht, das Ding ist noch sehr „beta“, und jegliche Fehlerbehandlung steckt hier noch in den Kinderschuhen. Trotzdem wollte ich den Code schonmal zeigen – vielleicht hat ja jemand Verbesserungsvorschläge. Ehe ich lange herumerkläre erst einmal ein einfaches Beispiel:

import javax.swing.JTextArea;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JFrame;
import static simplelayout.SimpleLayout.*;
 
public class SimpleLayoutTest {
   public static void main(String... args) {
       JFrame frame = new JFrame();
       frame.getContentPane().setLayout(new SimpleLayout(
          pol(fixed(50), prop(), comp()),
          pol(fixed(50), prop(), comp().top())
       ));
       frame.getContentPane().add(new BLabel("1"));
       frame.getContentPane().add(new JButton("2"));
       frame.getContentPane().add(new JTextArea("3"));
       frame.getContentPane().add(new JButton("4"));
       frame.getContentPane().add(new JTextArea("5"));
       frame.getContentPane().add(new BLabel("66666666666"));
       frame.getContentPane().add(new BLabel("7"));
       frame.getContentPane().add(new BLabel("9"), SimpleLayout.cell(2, 2).right());
       frame.getContentPane().add(new BLabel("888"));
       frame.setSize(400,400);
       frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
       frame.setVisible(true);
   }
   
   private static class BLabel extends JLabel {
       private BLabel(String s) {
          super(s);   
          setBorder(BorderFactory.createLineBorder(Color.red));
       }
    }
}

Die Konfiguration erfolgt mit statische Methoden. Der Konstruktor von SimpleLayout nimmt zwei Arrays von „Policies“ für die Spalten und Zeilen (die Hilfsmethode pol() macht bequemerweise aus VarArgs ein Array), und optional noch die Breite des äußeren Randes und der Abstand zwischen den Zellen. Es gibt drei Typen von Policies: fixed(w) setzt die Zeilenhöhe oder Spaltenbreite auf einen festen Wert w. Die Policy comp() respektiert die Werte, die in den Komponenten der jeweiligen Spalte oder Zeile gesetzt sind, die Werte sind je nach Platzangebot zwischen der minimalen und bevorzugten Größe der Elemente (natürlich orientiere ich mich an den jeweils größten Werten). Zeilen oder Spalten mit der Policy prop() beanspruchen übeschüssigen Platz. Zellen mit comp() oder prop() werden niemals kleiner als die Minimalgröße ihrer Komponente.

Komponenten werden normalerweise von links nach rechts und von oben nach unten hinzugefügt, es sei denn, man gibt mit der cell(x,y)-Constraint eine Position an. Dabei hat jede Zelle höchstens eine Komponente, und eine Komponente belegt nur eine Zelle. Man kann auch angeben, wie sich die Komponente bei überschüssigen Platz verhält, ob sie ihn z.B. ausfüllt, zentriert werden oder nach links oder rechts bzw. oben oder unten ausgesrichtet werden soll. Solche Constaints kann man auch an Policies hängen, um für diese ein Default-Verhalten festzulegen.

Ich werde SimpleLayout auf jeden Fall noch verbessern, aber schon jetzt finde ich, dass es seine Sache ganz gut macht.

Advertisements

3 Gedanken zu “Eine Frage des Layouts

  1. Hmm, also der Aufruf in der letzten Codebox sieht irgendwie aus wie ein Wrapper um GridBaglayout.
    Ich baue einstweilen meine GUIs weiter mit dem NetBeans GUI Builder. Finde ich deutlich produktiver. Nichtsdestotrotz sollte man natürlich verstehen, was unter der Motorhaube passiert.

  2. Ich lade demnächst eine verbesserte Version von SimpleLayout hoch, die deutlich intuitiver zu bedienen sein wird. Mit GridBagLayout soll es weder bezüglich Funktionalität noch Komplexität konkurrieren 😛

  3. Na dann bin ich gespannt. 🙂
    Zum Aufbauen dynamischer GUIs ist es vermutlich leichter/besser als GridBag.
    Viel Erfolg!

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s