Java tutorial
// Table layout manager, with the flexibility of GridBagLayout but the ease // of use of HTML table declarations. // See http://www.parallax.co.uk/~rolf/download/table.html // Copyright (C) Rolf Howarth 1997, 1998 (rolf@parallax.co.uk) // Permission to freely use, modify and distribute this code is given, // provided this notice remains attached. This code is provided for // educational use only and no warranty as to its suitability for any // other purpose is made. // Modification history // 0.1 01 Nov 96 First version // 1.0 17 Jan 97 Minor bug fix; added column weighting. // 1.1 08 Apr 98 Don't use methods deprecated in JDK1.1 // 1.2 16 Apr 98 Make own copy of Dimension objects as they're not immutable import java.awt.*; import java.util.*; // Private class to parse and store the options for a single table entry /** * Table layout manager, with the flexibity of GridBagLayout but the ease of * use of HTML table declarations. * * <p>use like: </br> * new TableLayout(cols) </br> * add(comp, new TableOption(..)) </br> * .. * </p> */ public class TableLayout implements LayoutManager, LayoutManager2 { private Hashtable options = new Hashtable(); private TableOption defaultOption; private int nrows = 0, ncols = 0; private int ncomponents = 0; private Component[][] components = null; private int MinWidth = 0, MinHeight = 0, PrefWidth = 0, PrefHeight = 0; private int[] minWidth = null, minHeight = null, prefWidth = null, prefHeight = null; private int[] weight = null, columnWidth = null; private int hgap = 0, vgap = 0; /** * Construct a new table layout manager. * @param cols Number of columns, used when adding components to tell when to go to the next row * @param defaultAlignment Default defaultAlignment for cells if not specified at the time of adding the component * @param hgap Horizontal gap between cells and at edge (in pixels) * @param vgap Vertical gap between cells and at edge (in pixels) **/ public TableLayout(int cols, String defaultAlignment, int hgap, int vgap) { this(cols, new TableOption(defaultAlignment), hgap, vgap); } public TableLayout(int cols, TableOption defaultAlignment, int hgap, int vgap) { this.ncols = cols; // the number of columns is specified this.nrows = 0; // the number of rows is calculated this.components = new Component[cols][]; this.defaultOption = defaultAlignment; this.hgap = hgap; this.vgap = vgap; } public TableLayout(int cols, String alignment) { this(cols, alignment, 0, 0); } public TableLayout(int cols) { this(cols, "", 0, 0); } public void addLayoutComponent(String alignment, Component comp) { options.put(comp, new TableOption(alignment)); } public void removeLayoutComponent(Component comp) { options.remove(comp); } // Iterate through the components, counting the number of rows taking into account // row and column spanning, then initialise the components[c][r] matrix so that // we can retrieve the component at a particular row,column position. private void loadComponents(Container parent) { ncomponents = parent.getComponentCount(); // If we haven't allocated the right sized array for each column yet, do so now. // Note that the number of columns is fixed, but the number of rows is not know // and could in the worst case be up the number of components. Unfortunately this // means we need to allocate quite big arrays, but the alternative would require // complex multiple passes as we try to work out the effect of row spanning. if (components[0] == null || components[0].length < ncomponents) { for (int i = 0; i < ncols; ++i) components[i] = new Component[ncomponents]; } // Nullify the array for (int i = 0; i < ncols; ++i) { for (int j = 0; j < components[i].length; ++j) components[i][j] = null; } // fill the matrix with components, taking row/column spanning into account int row = 0, col = 0; for (int i = 0; i < ncomponents; ++i) { // get the next component and its options Component comp = parent.getComponent(i); TableOption option = (TableOption) options.get(comp); if (option == null) option = defaultOption; // handle options to force us to column 0 or to skip columns if (option.forceColumn >= 0) { if (col > option.forceColumn) ++row; col = option.forceColumn; } col += option.skipColumns; if (col >= ncols) { ++row; col = 0; } // skip over any cells that are already occupied while (components[col][row] != null) { ++col; if (col >= ncols) { ++row; col = 0; } } // if using colspan, will we fit on this row? if (col + option.colSpan > ncols) { ++row; col = 0; } // for now, fill all the cells that are occupied by this component for (int c = 0; c < option.colSpan; ++c) for (int r = 0; r < option.rowSpan; ++r) components[col + c][row + r] = comp; // advance to the next cell, ready for the next component col += option.colSpan; if (col >= ncols) { ++row; col = 0; } } // now we know how many rows there are if (col == 0) nrows = row; else nrows = row + 1; // now we've positioned our components we can thin out the cells so // we only remember the top left corner of each component for (row = 0; row < nrows; ++row) { for (col = 0; col < ncols; ++col) { Component comp = components[col][row]; for (int r = row; r < nrows && components[col][r] == comp; ++r) { for (int c = col; c < ncols && components[c][r] == comp; ++c) { if (r > row || c > col) components[c][r] = null; } } } } } private void measureComponents(Container parent) { // set basic metrics such as ncomponents & nrows, and load the components // into the components[][] array. loadComponents(parent); // allocate new arrays to store row and column preferred and min sizes, but // only if the old arrays aren't big enough if (minWidth == null || minWidth.length < ncols) { minWidth = new int[ncols]; prefWidth = new int[ncols]; columnWidth = new int[ncols]; weight = new int[ncols]; } if (minHeight == null || minHeight.length < nrows) { minHeight = new int[nrows]; prefHeight = new int[nrows]; } int i; for (i = 0; i < ncols; ++i) { minWidth[i] = 0; prefWidth[i] = 0; } for (i = 0; i < nrows; ++i) { minHeight[i] = 0; prefHeight[i] = 0; } // measure the minimum and preferred size of each row and column for (int row = 0; row < nrows; ++row) { for (int col = 0; col < ncols; ++col) { Component comp = components[col][row]; if (comp != null) { TableOption option = (TableOption) options.get(comp); if (option == null) option = defaultOption; Dimension minSize = new Dimension(comp.getMinimumSize()); Dimension prefSize = new Dimension(comp.getPreferredSize()); // enforce prefSize>=minSize if (prefSize.width < minSize.width) prefSize.width = minSize.width; if (prefSize.height < minSize.height) prefSize.height = minSize.height; // divide size across all the rows or columns being spanned minSize.width /= option.colSpan; minSize.height /= option.rowSpan; prefSize.width = (prefSize.width - hgap * (option.colSpan - 1)) / option.colSpan; prefSize.height = (prefSize.height - vgap * (option.rowSpan - 1)) / option.rowSpan; for (int c = 0; c < option.colSpan; ++c) { if (minSize.width > minWidth[col + c]) minWidth[col + c] = minSize.width; if (prefSize.width > prefWidth[col + c]) prefWidth[col + c] = prefSize.width; } for (int r = 0; r < option.rowSpan; ++r) { if (minSize.height > minHeight[row + r]) minHeight[row + r] = minSize.height; if (prefSize.height > prefHeight[row + r]) prefHeight[row + r] = prefSize.height; } } } } // add rows and columns to give total min and preferred size of whole grid MinWidth = 0; MinHeight = 0; PrefWidth = hgap; PrefHeight = vgap; for (i = 0; i < ncols; ++i) { MinWidth += minWidth[i]; PrefWidth += prefWidth[i] + hgap; } for (i = 0; i < nrows; ++i) { MinHeight += minHeight[i]; PrefHeight += prefHeight[i] + vgap; } } public Dimension minimumLayoutSize(Container parent) { Insets insets = parent.getInsets(); measureComponents(parent); // System.out.println("Min Size: "+MinWidth+","+MinHeight); return new Dimension(insets.left + insets.right + MinWidth, insets.top + insets.bottom + MinHeight); } public Dimension preferredLayoutSize(Container parent) { Insets insets = parent.getInsets(); measureComponents(parent); // System.out.println("Pref Size: "+PrefWidth+","+PrefHeight); // System.out.println("+ insets LR "+insets.left+"+"+insets.right+", TB "+insets.top+"+"+insets.bottom); return new Dimension(insets.left + insets.right + PrefWidth, insets.top + insets.bottom + PrefHeight); } public void layoutContainer(Container parent) { Insets insets = parent.getInsets(); measureComponents(parent); int width = parent.getSize().width - (insets.left + insets.right); int height = parent.getSize().height - (insets.top + insets.bottom); // System.out.println("Resize "+width+","+height); // Decide whether to base our scaling on minimum or preferred sizes, or // a mixture of both, separately for width and height scaling. // This weighting also tells us how much of the hgap/vgap to use. double widthWeighting = 0.0; if (width >= PrefWidth || PrefWidth == MinWidth) widthWeighting = 1.0; else if (width <= MinWidth) { widthWeighting = 0.0; width = MinWidth; } else widthWeighting = (double) (width - MinWidth) / (double) (PrefWidth - MinWidth); double heightWeighting = 0.0; if (height >= PrefHeight || PrefHeight == MinHeight) heightWeighting = 1.0; else if (height <= MinHeight) { heightWeighting = 0.0; height = MinHeight; } else heightWeighting = (double) (height - MinHeight) / (double) (PrefHeight - MinHeight); // calculate scale factors to scale components to size of container, based // on weighted combination of minimum and preferred sizes double minWidthScale = (1.0 - widthWeighting) * width / MinWidth; //double prefWidthScale = widthWeighting * (width-hgap*(ncols+1))/(PrefWidth-hgap*(ncols+1)); double minHeightScale = (1.0 - heightWeighting) * height / MinHeight; double prefHeightScale = heightWeighting * (height - vgap * (nrows + 1)) / (PrefHeight - vgap * (nrows + 1)); // only get the full amount of gap if we're working to preferred size int vGap = (int) (vgap * heightWeighting); int hGap = (int) (hgap * widthWeighting); int y = insets.top + vGap; for (int c = 0; c < ncols; ++c) weight[c] = prefWidth[c]; for (int r = 0; r < nrows; ++r) { int x = insets.left + hGap; int rowHeight = (int) (minHeight[r] * minHeightScale + prefHeight[r] * prefHeightScale); // Column padding can vary from row to row, so we need several // passes through the columns for each row: // First, work out the weighting that deterimines how we distribute column padding for (int c = 0; c < ncols; ++c) { Component comp = components[c][r]; if (comp != null) { TableOption option = (TableOption) options.get(comp); if (option == null) option = defaultOption; if (option.weight >= 0) weight[c] = option.weight; else if (option.weight == -1) weight[c] = prefWidth[c]; } } int totalWeight = 0; for (int c = 0; c < ncols; ++c) totalWeight += weight[c]; int horizSurplus = width - hgap * (ncols + 1) - PrefWidth; // Then work out column sizes, essentially preferred size + share of padding for (int c = 0; c < ncols; ++c) { columnWidth[c] = (int) (minWidthScale * minWidth[c] + widthWeighting * prefWidth[c]); if (horizSurplus > 0 && totalWeight > 0) columnWidth[c] += (int) (widthWeighting * horizSurplus * weight[c] / totalWeight); } // Only now do we know enough to position all the columns within this row... for (int c = 0; c < ncols; ++c) { Component comp = components[c][r]; if (comp != null) { TableOption option = (TableOption) options.get(comp); if (option == null) option = defaultOption; // cell size may be bigger than row/column size due to spanning int cellHeight = rowHeight; int cellWidth = columnWidth[c]; for (int i = 1; i < option.colSpan; ++i) cellWidth += columnWidth[c + i]; for (int i = 1; i < option.rowSpan; ++i) cellHeight += (int) (minHeight[r + i] * minHeightScale + prefHeight[r + i] * prefHeightScale + vGap); Dimension d = new Dimension(comp.getPreferredSize()); if (d.width > cellWidth || option.horizontal == TableOption.FILL) d.width = cellWidth; if (d.height > cellHeight || option.vertical == TableOption.FILL) d.height = cellHeight; int yoff = 0; if (option.vertical == TableOption.BOTTOM) yoff = cellHeight - d.height; else if (option.vertical == TableOption.CENTRE) yoff = (cellHeight - d.height) / 2; int xoff = 0; if (option.horizontal == TableOption.RIGHT) xoff = cellWidth - d.width; else if (option.horizontal == TableOption.CENTRE) xoff = (cellWidth - d.width) / 2; // System.out.println(" "+comp.getClass().getName()+" at ("+x+"+"+xoff+","+y+"+"+yoff+"), size "+d.width+","+d.height); comp.setBounds(x + xoff, y + yoff, d.width, d.height); } x += columnWidth[c] + hGap; } y += rowHeight + vGap; } } public void addLayoutComponent(Component comp, Object constraints) { if (constraints instanceof TableOption) { options.put(comp, constraints); } else if (constraints == null) { options.put(comp, defaultOption); } else throw new IllegalArgumentException("not a valid constraints object=" + constraints); } /** * Returns the alignment along the x axis. This specifies how * the component would like to be aligned relative to other * components. The value should be a number between 0 and 1 * where 0 represents alignment along the origin, 1 is aligned * the furthest away from the origin, 0.5 is centered, etc. * <p> * @return the value <code>0.5f</code> to indicate centered */ public float getLayoutAlignmentX(Container parent) { return 0.5f; } /** * Returns the alignment along the y axis. This specifies how * the component would like to be aligned relative to other * components. The value should be a number between 0 and 1 * where 0 represents alignment along the origin, 1 is aligned * the furthest away from the origin, 0.5 is centered, etc. * <p> * @return the value <code>0.5f</code> to indicate centered */ public float getLayoutAlignmentY(Container parent) { return 0.5f; } /** * Invalidates the layout, indicating that if the layout manager * has cached information it should be discarded. */ public void invalidateLayout(Container target) { } /** * Returns the maximum dimensions for this layout given the components * in the specified target container. * @param target the container which needs to be laid out * @see Container * @see #minimumLayoutSize(Container) * @see #preferredLayoutSize(Container) * @return the maximum dimensions for this layout */ public Dimension maximumLayoutSize(Container target) { return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE); } } class TableOption { public static final int CENTRE = 1, FILL = 2, LEFT = 3, RIGHT = 4, TOP = 5, BOTTOM = 6; int horizontal = CENTRE; int vertical = CENTRE; int rowSpan = 1, colSpan = 1, skipColumns = 0, forceColumn = -1, weight = -2; /** * * @param horizontal one of CENTRE,FILL,LEFT,RIGHT,TOP,BOTTOM * @param vertical */ public TableOption(int horizontal, int vertical) { this.horizontal = horizontal; this.vertical = vertical; } public TableOption(int horizontal, int vertical, int rowSpan, int colSpan) { this.horizontal = horizontal; this.vertical = vertical; this.rowSpan = rowSpan; this.colSpan = colSpan; } public TableOption(int horizontal, int vertical, int rowSpan, int colSpan, int skipColumns, int forceColumn, int weight) { this.horizontal = horizontal; this.vertical = vertical; this.rowSpan = rowSpan; this.colSpan = colSpan; this.skipColumns = skipColumns; this.forceColumn = forceColumn; this.weight = weight; } TableOption(String alignment) { StringTokenizer tk = new StringTokenizer(alignment, ","); while (tk.hasMoreTokens()) { String token = tk.nextToken(); boolean ok = false; int delim = token.indexOf("="); if (token.equals("NW") || token.equals("W") || token.equals("SW")) { horizontal = LEFT; ok = true; } if (token.equals("NE") || token.equals("E") || token.equals("SE")) { horizontal = RIGHT; ok = true; } if (token.equals("N") || token.equals("C") || token.equals("F")) { horizontal = CENTRE; ok = true; } if (token.equals("F") || token.equals("FH")) { horizontal = FILL; ok = true; } if (token.equals("N") || token.equals("NW") || token.equals("NE")) { vertical = TOP; ok = true; } if (token.equals("S") || token.equals("SW") || token.equals("SE")) { vertical = BOTTOM; ok = true; } if (token.equals("W") || token.equals("C") || token.equals("E")) { vertical = CENTRE; ok = true; } if (token.equals("F") || token.equals("FV")) { vertical = FILL; ok = true; } if (delim > 0) { int val = Integer.parseInt(token.substring(delim + 1)); token = token.substring(0, delim); if (token.equals("CS") && val > 0) { colSpan = val; ok = true; } else if (token.equals("RS") && val > 0) { rowSpan = val; ok = true; } else if (token.equals("SKIP") && val > 0) { skipColumns = val; ok = true; } else if (token.equals("COL")) { forceColumn = val; ok = true; } else if (token.equals("WT")) { weight = val; ok = true; } } if (!ok) throw new IllegalArgumentException("TableOption " + token); } } }