Java Swing 自定义组件库分享(十二)

Java Swing 自定义组件库分享(十二):栅格布局 --- ElRow、ElCol

一、背景

Swing 原生布局管理器在实现复杂表单时存在明显痛点:

FlowLayout:无法控制组件宽度,一行排不下就换行

BorderLayout:只有五个区域,无法实现多列布局

GridLayout:所有列等宽,无法单独指定某列宽度

GridBagLayout:功能强大但使用复杂,需要理解 GridBagConstraints 的十几个属性

参考 Web 端流行的栅格布局思想(24 列栅格系统),ElRow 和 ElCol 的作用是:提供一种简单直观的布局方式,通过 span(占据列数)和 offset(左侧偏移)快速实现响应式布局。

二、核心设计

栅格系统规则:

每行分为 24 列

ElRow:行容器,负责管理多个 ElCol 的布局

ElCol:列容器,通过 span 属性设置宽度(1-24),通过 offset 设置左侧偏移

同一行内的多个 ElCol 宽度之和不超过 24

布局计算原理:

第一遍遍历:计算每行的高度(取该行所有列的最大高度)

第二遍遍历:根据 span 计算每列实际宽度,根据 offset 计算偏移量,垂直居中对齐

三、ElCol 源码

java 复制代码
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;

/**
 * 栅格列组件
 * 配合 ElRow 使用,实现 24 栅格布局
 *
 * 使用示例:
 * ElCol col = new ElCol(12);        // 占 12 列(半行)
 * ElCol col = new ElCol(8, 4);      // 占 8 列,左侧偏移 4 列
 */
public class ElCol extends JPanel {
    /** 占据列数(1-24),默认占满整行 */
    public int span = 24;
    /** 左侧偏移列数,默认无偏移 */
    public int offset = 0;
    /** 所在行号(由 ElRow 自动设置) */
    public int row = 0;
    /** 行间距(由 ElRow 自动设置) */
    public int rowSpacing = 0;
    /** 水平对齐常量(使用 BorderLayout 的常量) */
    public static final String CENTER = BorderLayout.CENTER;
    public static final String LEFT = BorderLayout.WEST;
    public static final String RIGHT = BorderLayout.EAST;

    /**
     * 默认构造函数,span=24 占满整行
     */
    public ElCol() {
        setBorder(new EmptyBorder(0, 0, 0, 0));
        setLayout(new BorderLayout());
        setOpaque(false);
    }

    /**
     * 指定占据列数
     * @param span 占据列数(1-24)
     */
    public ElCol(int span) {
        this();
        this.span = Math.min(Math.max(span, 1), 24);
    }

    /**
     * 指定占据列数和左侧偏移
     * @param span 占据列数(1-24)
     * @param offset 左侧偏移列数(0 到 24-span)
     */
    public ElCol(int span, int offset) {
        this();
        this.span = Math.min(Math.max(span, 1), 24);
        this.offset = Math.min(Math.max(offset, 0), 24 - this.span);
    }
}

四、ElRow 源码

java 复制代码
import javax.swing.*;
import java.awt.*;
import java.util.HashMap;
import java.util.Map;

/**
 * 栅格行组件
 * 配合 ElCol 使用,实现 24 栅格布局
 *
 * 使用示例:
 * ElRow row = new ElRow();
 * row.add(new JLabel("用户名:"), 4);      // 标签占4列
 * row.add(new JTextField(), 16);          // 输入框占16列
 * row.add(new JButton("查询"), 4);        // 按钮占4列
 */
public class ElRow extends JPanel {
    /** 行号(用于区分不同行) */
    public int row = 0;
    /** 行间距 */
    public int rowSpacing = 0;

    public ElRow() {
        setLayout(new RowLayout());
        setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
        setOpaque(false);
    }

    /**
     * 添加 ElCol 组件
     * @param elCol 列组件
     */
    public void add(ElCol elCol) {
        elCol.row = row;
        elCol.rowSpacing = Math.max(0, rowSpacing);
        super.add(elCol);
    }

    /**
     * 批量添加 ElCol 组件
     * @param elCol 列组件数组
     */
    public void add(ElCol... elCol) {
        for (ElCol col : elCol) {
            col.row = row;
            col.rowSpacing = Math.max(0, rowSpacing);
            super.add(col);
        }
    }

    /**
     * 添加普通组件(自动包装为 ElCol,span=24)
     * @param component 普通组件
     */
    public void add(JComponent component) {
        ElCol col = new ElCol();
        col.row = row;
        col.rowSpacing = Math.max(0, rowSpacing);
        col.add(component);
        super.add(col);
    }

    /**
     * 添加普通组件,指定占据列数
     * @param component 普通组件
     * @param span 占据列数
     */
    public void add(JComponent component, int span) {
        ElCol col = new ElCol(span);
        col.row = row;
        col.rowSpacing = Math.max(0, rowSpacing);
        col.add(component);
        super.add(col);
    }

    /**
     * 添加普通组件,指定占据列数和偏移量
     * @param component 普通组件
     * @param span 占据列数
     * @param offset 左侧偏移列数
     */
    public void add(JComponent component, int span, int offset) {
        ElCol col = new ElCol(span, offset);
        col.row = row;
        col.rowSpacing = Math.max(0, rowSpacing);
        col.add(component);
        super.add(col);
    }

    /**
     * 添加多个普通组件(自动等分宽度)
     * @param components 组件数组
     */
    public void add(JComponent... components) {
        int colSpan = 24 / components.length;
        for (JComponent component : components) {
            ElCol col = new ElCol(colSpan);
            col.row = row;
            col.rowSpacing = Math.max(0, rowSpacing);
            col.add(component);
            super.add(col);
        }
    }

    /**
     * 自定义布局管理器,实现 24 栅格布局
     */
    private static class RowLayout implements LayoutManager {
        @Override
        public void addLayoutComponent(String name, Component comp) {}

        @Override
        public void removeLayoutComponent(Component comp) {}

        @Override
        public Dimension preferredLayoutSize(Container parent) {
            return calculateLayoutSize(parent);
        }

        @Override
        public Dimension minimumLayoutSize(Container parent) {
            return calculateLayoutSize(parent);
        }

        @Override
        public void layoutContainer(Container parent) {
            synchronized (parent.getTreeLock()) {
                Insets insets = parent.getInsets();
                int maxWidth = parent.getWidth() - insets.left - insets.right;
                int x = insets.left;
                int y = insets.top;
                int currentRow = -1;

                // 第一遍:计算每行的最大高度
                Map<Integer, Integer> rowHeights = new HashMap<>();
                for (Component comp : parent.getComponents()) {
                    if (!(comp instanceof ElCol)) continue;
                    ElCol elCol = (ElCol) comp;
                    int row = elCol.row;
                    int height = comp.getPreferredSize().height;
                    if (!rowHeights.containsKey(row) || height > rowHeights.get(row)) {
                        rowHeights.put(row, height);
                    }
                }

                // 第二遍:布局组件
                for (Component comp : parent.getComponents()) {
                    if (!(comp instanceof ElCol)) continue;
                    ElCol elCol = (ElCol) comp;
                    // 遇到新行,重置位置
                    if (elCol.row != currentRow) {
                        if (currentRow != -1) {
                            y += rowHeights.get(currentRow) + elCol.rowSpacing;
                        }
                        x = insets.left;
                        currentRow = elCol.row;
                    }
                    // 计算列宽和偏移
                    int colWidth = (maxWidth * elCol.span) / 24;
                    int colOffset = (maxWidth * elCol.offset) / 24;
                    x += colOffset;
                    // 设置组件位置(垂直居中)
                    int rowHeight = rowHeights.get(currentRow);
                    int compHeight = comp.getPreferredSize().height;
                    int yOffset = (rowHeight - compHeight) / 2;
                    comp.setBounds(x, y + yOffset, colWidth, compHeight);
                    x += colWidth;
                }
            }
        }

        /**
         * 计算布局所需尺寸
         * @param parent 父容器
         * @return 计算后的尺寸
         */
        private Dimension calculateLayoutSize(Container parent) {
            synchronized (parent.getTreeLock()) {
                Insets insets = parent.getInsets();
                int maxWidth = parent.getWidth() - insets.left - insets.right;
                if (maxWidth <= 0) {
                    maxWidth = 600;  // 默认宽度
                }
                int width = 0;
                int height = insets.top + insets.bottom;
                int x = insets.left;
                int rowHeight = 0;
                int currentRow = -1;

                for (Component comp : parent.getComponents()) {
                    if (!(comp instanceof ElCol)) continue;
                    ElCol elCol = (ElCol) comp;
                    // 遇到新行,累加高度
                    if (elCol.row != currentRow) {
                        if (currentRow != -1) {
                            height += rowHeight + elCol.rowSpacing;
                        }
                        x = insets.left;
                        rowHeight = 0;
                        currentRow = elCol.row;
                    }
                    // 计算列宽
                    int colWidth = (maxWidth * elCol.span) / 24;
                    int colOffset = (maxWidth * elCol.offset) / 24;
                    x += colOffset;
                    Dimension compSize = comp.getPreferredSize();
                    rowHeight = Math.max(rowHeight, compSize.height);
                    x += colWidth;
                    width = Math.max(width, x);
                }
                height += rowHeight;
                return new Dimension(width, height);
            }
        }
    }
}

五、核心功能说明

ElRow 核心方法:

add(ElCol):添加列组件

add(JComponent):自动包装为 ElCol,span=24

add(JComponent, int span):指定占据列数

add(JComponent, int span, int offset):指定列数和偏移

add(JComponent...):多个组件等分宽度(总宽度 / 组件个数)

ElCol 属性:

span:占据列数(1-24),默认 24

offset:左侧偏移列数(0 到 24-span),默认 0

对齐常量:CENTER、LEFT、RIGHT(配合 BorderLayout 使用)

RowLayout 布局计算:

两遍遍历:第一遍计算每行最大高度,第二遍执行布局

列宽 = 容器宽度 × span ÷ 24

偏移量 = 容器宽度 × offset ÷ 24

垂直居中对齐

六、使用示例

6.1 基本用法

java 复制代码
ElRow row = new ElRow();
row.add(new JLabel("用户名:"));
row.add(new JTextField());
panel.add(row);

6.2 指定列宽

java 复制代码
ElRow row = new ElRow();
row.add(new JLabel("姓名:"), 4);      // 标签占4列
row.add(new JTextField(), 16);        // 输入框占16列
row.add(new JButton("查询"), 4);      // 按钮占4列
panel.add(row);

6.3 带偏移

java 复制代码
ElRow row = new ElRow();
row.add(new JLabel("备注:"), 4, 2);   // 偏移2列,占4列
row.add(new JTextArea(3, 20), 16);    // 文本域占16列
panel.add(row);

6.4 多组件等分

java 复制代码
ElRow row = new ElRow();
row.add(new JButton("新增"), new JButton("修改"), new JButton("删除"));
// 三个按钮等分宽度,各占8列
panel.add(row);

6.5 复杂表单示例

java 复制代码
JPanel panel = new JPanel(new BorderLayout());

ElRow elRow = new ElRow();
// 行间距
elRow.rowSpacing = 25;
// 第一行:姓名(从0开始,默认是0,所以这里也可以不写)
elRow.row = 0;
elRow.add(new JLabel("姓名:"), 4);
elRow.add(new JTextField(), 16);
// 第二行:年龄
elRow.row = 1;
elRow.add(new JLabel("年龄:"), 4);
elRow.add(new JTextField(), 16);
// 第三行:性别
elRow.row = 2;
elRow.add(new JLabel("性别:"), 4);
JRadioButton male = new JRadioButton("男");
JRadioButton female = new JRadioButton("女");
ButtonGroup group = new ButtonGroup();
group.add(male);
group.add(female);
elRow.add(male, 10);
elRow.add(female, 10);

panel.add(gridPanel, BorderLayout.CENTER);

七、注意事项

  1. span 范围:1-24,超过范围会自动修正
  2. offset 范围:0 到 24-span,偏移后不能超出边界
  3. 多行支持:通过 ElRow 的 row 属性区分不同行,每个 ElRow 实例默认独立
  4. 行间距:可通过 rowSpacing 设置,默认 0
  5. 垂直对齐:当前实现为垂直居中对齐,如需顶部/底部对齐可修改 yOffset 计算逻辑
  6. 容器宽度:布局计算依赖父容器实际宽度,未显示时使用默认宽度 600px

八、小结

ElRow 和 ElCol 实现了类似 Web 端的栅格布局系统,核心要点:

  • 24 列栅格,通过 span 控制宽度,offset 控制偏移
  • 自定义 LayoutManager,两遍遍历实现行高自适应
  • 支持普通组件自动包装,无需手动创建 ElCol
  • 支持多行、行间距、垂直居中对齐

与原生 GridBagLayout 相比,栅格布局更直观、代码更简洁。