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);
七、注意事项
- span 范围:1-24,超过范围会自动修正
- offset 范围:0 到 24-span,偏移后不能超出边界
- 多行支持:通过 ElRow 的 row 属性区分不同行,每个 ElRow 实例默认独立
- 行间距:可通过 rowSpacing 设置,默认 0
- 垂直对齐:当前实现为垂直居中对齐,如需顶部/底部对齐可修改 yOffset 计算逻辑
- 容器宽度:布局计算依赖父容器实际宽度,未显示时使用默认宽度 600px
八、小结
ElRow 和 ElCol 实现了类似 Web 端的栅格布局系统,核心要点:
- 24 列栅格,通过 span 控制宽度,offset 控制偏移
- 自定义 LayoutManager,两遍遍历实现行高自适应
- 支持普通组件自动包装,无需手动创建 ElCol
- 支持多行、行间距、垂直居中对齐
与原生 GridBagLayout 相比,栅格布局更直观、代码更简洁。