封装GridBagLayout得到一个简单的网格布局管理器,GridBagLayout->SimpleGridLayout->JGrid
第一,简单案例和效果
import javax.swing.*; import javax.swing.table.DefaultTableModel; import static xh.layout.SimpleGridLayout.AlignMode.*; public class Main { public static void main(String[] args) { SwingUtilities.invokeLater(() -> { Main main = new Main(); main.initialize().setVisible(true); }); } public enum Kind { INCOME, EXPENSE } private JFrame initialize() { JFrame frame = new JFrame("Finance"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(800, 650); frame.setResizable(false); frame.setLocationRelativeTo(null); JGrid.layout(frame, grid -> { grid.addMiddleware(new JGrid.JGridMiddleware() { @Override public void beforeEachRow(JGrid.JGridRow row) { row.addEmptyCell(); } @Override public void afterEachRow(JGrid.JGridRow row) { row.addEmptyCell(); } }); grid.addEmptyRow(); grid.addRow(row -> { row.addCell(new JLabel("Date:"), cell -> cell.alignX(START)); row.addCell(new JTextField(15), cell -> cell.colspan(2).align(FILL).margin(0, 2)); row.addCell(new JLabel("Category:"), cell -> cell.alignX(START).margin(m -> m.left(10))); row.addCell(new JTextField("", 20), cell -> cell.colspan(2).align(FILL).margin(0, 2)); }); grid.addRow(row -> { JComboBox<Kind> kindComboBox = new JComboBox<>(Kind.values()); kindComboBox.setSelectedItem(Kind.EXPENSE); row.addCell(new JLabel("Amount:"), cell -> cell.alignX(START)); row.addCell(new JTextField("", 20), cell -> cell.colspan(2).align(FILL).margin(0, 2)); row.addCell(new JLabel("Kind:"), cell -> cell.alignX(START).margin(m -> m.left(10))); row.addCell(kindComboBox, cell -> cell.colspan(2).alignX(START).margin(0, 2)); }); grid.addRow(row -> { JTextArea textArea = new JTextArea(); textArea.setLineWrap(true); textArea.setWrapStyleWord(true); row.addCell(new JLabel("Description:"), cell -> cell.alignX(START).alignY(START)); row.addCell(new JScrollPane(textArea), cell -> cell.colspan(5).rowspan(3).align(FILL)); }); grid.addEmptyRows(2); grid.addRow(row -> { row.addEmptyCells(4); row.addCell(new JButton("Add")); row.addCell(new JButton("Delete"), cell -> cell.alignX(END)); }); grid.addRow(row -> { String[] columns = {"ID", "Date", "Category", "Description", "Amount", "Kind"}; DefaultTableModel tableModel = new DefaultTableModel(columns, 0) { @Override public boolean isCellEditable(int row, int column) { return false; } }; JTable table = new JTable(tableModel); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.setFillsViewportHeight(true); table.setRowHeight(24); JScrollPane scrollPane = new JScrollPane(table); row.addCell(scrollPane, cell -> cell.colspan(6).rowspan(10).align(FILL)); }); grid.addEmptyRows(9); grid.addRow(row -> { row.addCell(new JLabel("Balance: $0.0"), cell -> cell.alignX(START)); }); grid.addEmptyRow(); }); return frame; } }

第二,SimpleGridLayout使用说明
SimpleGridLayout是由GridBagLayout封装得到的基于网格的布局管理器,通过字符串约束控制布局,使用分为四个步骤:
- 确定网格的行列大小,通过grid约束控制网格轨道的对齐方式,一共有七种模式:start、center、end、between、around、evenly、stretch(默认)
- 确定组件的放置位置以及单元格形状,通过x/y/span约束完成控制
- 确定组件在单元格内的对齐方式,通过align约束控制,一共有四种对齐方式:start、center、end、fill,例如align-x-center、align-start
- 确定组件和单元格的边距,通过margin约束控制,比如m-2、m-x-2、m-top-3
简单案例:
JPanel panel = new JPanel(new SimpleGridLayout(3, 3, "grid-center"));
panel.add(new JButton("OK"), "x-0 y-0 span-x-2 align-fill m-4");
panel.add(new JButton("Cancel"), "x-2 y-0 align-end m-4");
第三,JGrid和SimpleGridLayout源代码
import javax.swing.*; import java.awt.*; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.function.Consumer; public class JGrid { private boolean dense = true; private SimpleGridLayout.GridMode gridX, gridY; private int currentRow = -1; private int nextCol = 0; private final List<Map.Entry<JComponent, String>> entries = new ArrayList<>(); private final Map<Integer, List<Boolean>> occupied = new java.util.HashMap<>(); private int maxRowUsed = -1; private int maxColUsed = -1; private final List<JGridMiddleware> middlewares = new ArrayList<>(); private JGrid() { } public static void layout(Container container, Consumer<JGrid> config) { JGrid grid = new JGrid(); config.accept(grid); SimpleGridLayout layout = grid.getLayout(); container.setLayout(layout); for (var entry : grid.entries) { container.add(entry.getKey(), entry.getValue()); } } private SimpleGridLayout getLayout() { int rows = Math.max(currentRow + 1, maxRowUsed + 1); int cols = Math.max(maxColUsed + 1, 1); SimpleGridLayout.GridAlignment alignment = SimpleGridLayout.GridAlignment.create(); if (gridX != null) { alignment = alignment.setX(gridX); } if (gridY != null) { alignment = alignment.setY(gridY); } return new SimpleGridLayout(rows, cols, alignment); } public JGrid setDense(boolean value) { this.dense = value; return this; } public JGrid setGridX(SimpleGridLayout.GridMode gridX) { this.gridX = gridX; return this; } public JGrid setGridY(SimpleGridLayout.GridMode gridY) { this.gridY = gridY; return this; } public void addMiddleware(JGridMiddleware middleware) { this.middlewares.add(middleware); } public void addRow(Consumer<JGridRow> config) { currentRow++; nextCol = 0; JGridRow row = new JGridRow(this); middlewares.forEach(m -> m.beforeEachRow(row)); config.accept(row); middlewares.reversed().forEach(m -> m.afterEachRow(row)); } public void addEmptyRow() { addEmptyRows(1); } public void addEmptyRows(int value) { if (value < 0) throw new IllegalArgumentException("value 必须 >= 0"); currentRow += value; nextCol = 0; } // ---- 内部方法 ---- private List<Boolean> getRowOccupied(int row) { return occupied.computeIfAbsent(row, k -> new ArrayList<>()); } private boolean isOccupied(int row, int col) { List<Boolean> list = occupied.get(row); if (list == null || col >= list.size()) return false; return list.get(col); } private void setOccupied(int row, int col) { List<Boolean> list = getRowOccupied(row); while (list.size() <= col) { list.add(false); } list.set(col, true); } private int findStartCol(int row, int spanX) { getRowOccupied(row); int col = dense ? 0 : nextCol; while (true) { boolean canPlace = true; for (int i = 0; i < spanX; i++) { if (isOccupied(row, col + i)) { canPlace = false; break; } } if (canPlace) { return col; } col++; } } void addCellInternal(JGridCell cell) { int spanX = cell.spanX; int spanY = cell.spanY; int startRow = currentRow; int startCol = findStartCol(startRow, spanX); // 标记占用 for (int r = startRow; r < startRow + spanY; r++) { for (int c = startCol; c < startCol + spanX; c++) { setOccupied(r, c); } } // 更新游标 nextCol = startCol + spanX; // 更新最大行列 maxRowUsed = Math.max(maxRowUsed, startRow + spanY - 1); maxColUsed = Math.max(maxColUsed, startCol + spanX - 1); // 记录条目 if (cell.component() != null) { String constraints = SimpleGridLayout.constraints() .setLocation(startCol, startRow) .setSpan(spanX, spanY) .setAlignX(cell.alignX).setAlignY(cell.alignY) .setMarginTop(cell.top).setMarginLeft(cell.left).setMarginBottom(cell.bottom).setMarginRight(cell.right) .build(); entries.add(Map.entry(cell.component(), constraints)); } } // ---- 内部类 ---- public interface JGridMiddleware { default void beforeEachRow(JGridRow row) { } default void afterEachRow(JGridRow row) { } default void beforeEachCell(JGridCell cell) { } default void afterEachCell(JGridCell cell) { } } public record JGridMargin(JGridCell cell) { public JGridMargin left(int value) { cell.left = value; return this; } public JGridMargin right(int value) { cell.right = value; return this; } public JGridMargin top(int value) { cell.top = value; return this; } public JGridMargin bottom(int value) { cell.bottom = value; return this; } public JGridMargin all(int value) { return left(value).right(value).top(value).bottom(value); } public JGridMargin all(int x, int y) { return left(x).right(x).top(y).bottom(y); } public JGridMargin all(int top, int left, int bottom, int right) { return left(left).right(right).top(top).bottom(bottom); } } public static class JGridCell { private final JComponent component; private int spanX = 1; private int spanY = 1; private SimpleGridLayout.AlignMode alignX = SimpleGridLayout.AlignMode.CENTER; private SimpleGridLayout.AlignMode alignY = SimpleGridLayout.AlignMode.CENTER; private int top, left, bottom, right; private JGridCell(JComponent component) { this.component = component; } public JComponent component() { return component; } public JGridCell colspan(int value) { if (value < 1) throw new IllegalArgumentException("colspan 必须 >= 1"); this.spanX = value; return this; } public JGridCell rowspan(int value) { if (value < 1) throw new IllegalArgumentException("rowspan 必须 >= 1"); this.spanY = value; return this; } public JGridCell alignX(SimpleGridLayout.AlignMode mode) { this.alignX = mode; return this; } public JGridCell alignY(SimpleGridLayout.AlignMode mode) { this.alignY = mode; return this; } public JGridCell align(SimpleGridLayout.AlignMode mode) { return alignX(mode).alignY(mode); } public JGridCell margin(Consumer<JGridMargin> config) { config.accept(new JGridMargin(this)); return this; } public JGridCell margin(int top, int left, int bottom, int right) { return margin(m -> m.all(top, left, bottom, right)); } public JGridCell margin(int x, int y) { return margin(m -> m.all(x, y)); } public JGridCell margin(int value) { return margin(m -> m.all(value)); } } public static class JGridRow { private final JGrid grid; JGridRow(JGrid grid) { this.grid = grid; } public int index() { return grid.currentRow; } public void addCell(JComponent component) { addCell(component, null); } public void addCell(JComponent component, Consumer<JGridCell> config) { JGridCell cell = new JGridCell(component); grid.middlewares.forEach(g -> g.beforeEachCell(cell)); if (config != null) { config.accept(cell); } grid.middlewares.reversed().forEach(g -> g.afterEachCell(cell)); grid.addCellInternal(cell); } public void addEmptyCell() { addEmptyCells(1); } public void addEmptyCells(int value) { addCell(null, item -> item.colspan(value)); } } }
import java.awt.*; import java.util.*; import java.util.List; import java.util.function.BiConsumer; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * 基于 {@link GridBagLayout} 实现的简化网格布局管理器。 * <p> * 使用受 CSS Grid 启发的 DSL 定义固定网格并放置组件。 * 每个组件必须显式指定单元格位置,不再支持自动流式布局。 * </p> * * <h3>全局网格对齐</h3> * <p> * 控制整个网格在容器中的对齐方式。语法: * {@code grid-x-<对齐>} 和 {@code grid-y-<对齐>},或简写 {@code grid-<对齐>} 同时作用于两个轴。 * 支持的值:{@code start | center | end | between | around | evenly | stretch}。 * 默认对齐方式为 {@code stretch}。 * </p> * * <h3>组件约束</h3> * <p> * 以空格分隔的字符串形式传递给 {@link #addLayoutComponent(Component, Object)}。可用的标记: * </p> * <ul> * <li><b>位置</b>(必填) – {@code x-<整数> y-<整数>}</li> * <li><b>跨度</b>(可选,默认 1) – {@code span-x-<整数> span-y-<整数>}</li> * <li><b>对齐</b>(可选,默认 center) – * {@code align-x-<start|center|end|fill>}, * {@code align-y-<start|center|end|fill>}, * 或简写 {@code align-<start|center|end|fill>}</li> * <li><b>外边距</b>(可选,默认 0) – * {@code m-<整数>}(四边统一), * {@code m-x-<整数> m-y-<整数>}(水平/垂直), * 或单独指定 {@code m-left-<整数> m-right-<整数> m-top-<整数> m-bottom-<整数>}</li> * </ul> * * <h3>使用示例</h3> * <pre>{@code * JPanel panel = new JPanel(new SimpleGridLayout(3, 3, "grid-center")); * panel.add(new JButton("OK"), "x-0 y-0 span-x-2 align-fill m-4"); * panel.add(new JButton("Cancel"), "x-2 y-0 align-end m-4"); * }</pre> * * @since 2.0 */ public class SimpleGridLayout extends GridBagLayout { // ---- 静态模式缓存,用于解析组件约束字符串 ---- private static final Map<String, Pattern> ATOMIC = new HashMap<>(); static { // 位置 ATOMIC.put("x", PatternUtils.compile("^x-(\\d+)$")); ATOMIC.put("y", PatternUtils.compile("^y-(\\d+)$")); // 跨度 ATOMIC.put("span-x", PatternUtils.compile("^span-x-(\\d+)$")); ATOMIC.put("span-y", PatternUtils.compile("^span-y-(\\d+)$")); // 对齐 ATOMIC.put("align-x", PatternUtils.compile("^align-x-(start|center|end|fill)$")); ATOMIC.put("align-y", PatternUtils.compile("^align-y-(start|center|end|fill)$")); // 外边距 ATOMIC.put("m-top", PatternUtils.compile("^m-top-(\\d+)$")); ATOMIC.put("m-left", PatternUtils.compile("^m-left-(\\d+)$")); ATOMIC.put("m-bottom", PatternUtils.compile("^m-bottom-(\\d+)$")); ATOMIC.put("m-right", PatternUtils.compile("^m-right-(\\d+)$")); } // ---- 实例字段 ---- /** * 默认的约束模板,每个组件都会基于此克隆 */ private final GridBagConstraints defaultGbc; /** * 将组件的逻辑列索引映射到包含对齐间隙的物理列索引。 * 数组索引为逻辑列号,值为物理列号。 */ private final int[] columnMapping; /** * 逻辑行索引到物理行索引的映射(同上) */ private final int[] rowMapping; // ---- 构造函数 ---- /** * 创建组件约束构建器 */ public static ConstraintsBuilder constraints() { return new ConstraintsBuilder(); } /** * 创建指定行列数的网格布局,两个轴默认使用 {@code stretch} 对齐。 * * @param rows 行数(必须 > 0) * @param columns 列数(必须 > 0) * @throws IllegalArgumentException 如果 rows 或 columns < 1 */ public SimpleGridLayout(int rows, int columns) { this(rows, columns, GridAlignment.create()); } /** * 创建指定维度和全局网格对齐方式的布局。 * * @param rows 行数(必须 > 0) * @param columns 列数(必须 > 0) * @param gridConstraints 以空格分隔的对齐标记(例如 {@code "grid-center"} 或 {@code "grid-x-start grid-y-end"})。 * 可以为 {@code null} 或空字符串,此时两轴均使用 {@code stretch}。 * @throws IllegalArgumentException 如果 rows 或 columns < 1,或遇到无法识别的约束标记 */ public SimpleGridLayout(int rows, int columns, String gridConstraints) { this(rows, columns, parseGlobalAlignment(gridConstraints)); } public SimpleGridLayout(int rows, int columns, GridAlignment alignment) { if (rows < 1 || columns < 1) { throw new IllegalArgumentException("网格维度必须 >= 1,实际 rows=" + rows + ", columns=" + columns); } this.defaultGbc = new GridBagConstraints(); defaultGbc.fill = GridBagConstraints.NONE; defaultGbc.anchor = GridBagConstraints.CENTER; defaultGbc.insets = new Insets(0, 0, 0, 0); // 解析全局对齐方式 String gridXAlign = alignment.x; String gridYAlign = alignment.y; // 构建列/行轨道(包含间隙及索引映射) Track columnTrack = buildTrack(gridXAlign, columns); this.columnWidths = columnTrack.getWidths(); this.columnWeights = columnTrack.getWeights(); this.columnMapping = columnTrack.getMapping(); Track rowTrack = buildTrack(gridYAlign, rows); this.rowHeights = rowTrack.getWidths(); this.rowWeights = rowTrack.getWeights(); this.rowMapping = rowTrack.getMapping(); } // ---- 重写 GridBagLayout 方法 ---- /** * 添加组件到布局,并应用给定的约束。 * <p> * 约束参数支持以下形式: * <ul> * <li>{@link String} – 按类文档描述的 DSL 字符串</li> * <li>{@link ConstraintsBuilder} – 构建器实例(将转换为其字符串表示)</li> * <li>{@code null} – 视为空约束字符串,此时若未配置默认位置将导致抛出异常</li> * </ul> * * @param comp 要添加的组件 * @param constraints 约束,可以是 {@link String}、{@link ConstraintsBuilder} 或 {@code null} * @throws IllegalArgumentException 如果约束类型不合法,或解析后的字符串缺少必填的位置信息 */ @Override public void addLayoutComponent(Component comp, Object constraints) { switch (constraints) { case String spec -> setConstraints(comp, buildGbc(spec)); case ConstraintsBuilder builder -> setConstraints(comp, buildGbc(builder.build())); case null -> setConstraints(comp, buildGbc(null)); default -> throw new IllegalArgumentException("约束必须是 String、ConstraintsBuilder 或 null,但实际为: " + constraints.getClass().getName()); } } /** * 从布局中移除指定组件。 * * @param comp 要移除的组件 */ @Override public void removeLayoutComponent(Component comp) { super.removeLayoutComponent(comp); } // ---- 约束解析(私有) ---- /** * 根据组件级约束字符串构建 {@link GridBagConstraints}。 */ private GridBagConstraints buildGbc(String spec) { GridBagConstraints gbc = (GridBagConstraints) defaultGbc.clone(); Map<String, String> map = parseComponentConstraints(spec); // 必填的位置信息 if (!map.containsKey("x") || !map.containsKey("y")) { throw new IllegalArgumentException("组件必须同时指定 x-<整数> 和 y-<整数> 约束。当前约束: " + spec); } gbc.gridx = Integer.parseInt(map.get("x")); gbc.gridy = Integer.parseInt(map.get("y")); // 跨度(默认 1) gbc.gridwidth = Integer.parseInt(map.getOrDefault("span-x", "1")); gbc.gridheight = Integer.parseInt(map.getOrDefault("span-y", "1")); // 对齐与填充 applyAlignment(map, gbc); // 外边距 applyMargins(map, gbc); // 应用轨道映射(将逻辑坐标转换为包含间隙的物理坐标) applyTrackMapping(gbc); return gbc; } /** * 解析全局网格对齐约束字符串。 */ private static GridAlignment parseGlobalAlignment(String constraints) { Map<String, String> map = new HashMap<>(); boolean matched; if (constraints != null && !constraints.isBlank()) { for (String token : constraints.trim().split("\\s+")) { matched = PatternUtils.matches("grid-x-(?<mode>start|center|end|between|around|evenly|stretch)", token, (name, value) -> { map.put("grid-x", value); }); if (matched) continue; matched = PatternUtils.matches("grid-y-(?<mode>start|center|end|between|around|evenly|stretch)", token, (name, value) -> { map.put("grid-y", value); }); if (matched) continue; matched = PatternUtils.matches("grid-(?<mode>start|center|end|between|around|evenly|stretch)", token, (name, value) -> { map.put("grid-x", value); map.put("grid-y", value); }); if (matched) continue; throw new IllegalArgumentException("无效的全局网格约束: '" + token + "', 期望 [grid-<mode>, grid-x-<mode>, grid-y-<mode>]"); } } String text = GridMode.STRETCH.getText(); return new GridAlignment(map.getOrDefault("grid-x", text), map.getOrDefault("grid-y", text)); } /** * 将组件级约束字符串解析为键值对映射。 */ private Map<String, String> parseComponentConstraints(String spec) { Map<String, String> map = new HashMap<>(); if (spec == null || spec.isBlank()) { return map; } for (String token : spec.trim().split("\\s+")) { boolean matched = matchAtomic(token, map::put); if (matched) continue; // span-<num> matched = PatternUtils.matches("^span-(?<num>\\d+)$", token, (name, value) -> { map.put("span-x", value); map.put("span-y", value); }); if (matched) continue; // align-<start|center|end|fill> matched = PatternUtils.matches("^align-(?<mode>start|center|end|fill)$", token, (name, value) -> { map.put("align-x", value); map.put("align-y", value); }); if (matched) continue; // m-<num> matched = PatternUtils.matches("^m-(?<num>\\d+)$", token, (name, value) -> { map.put("m-top", value); map.put("m-right", value); map.put("m-bottom", value); map.put("m-left", value); }); if (matched) continue; // m-x-<num> matched = PatternUtils.matches("^m-x-(?<num>\\d+)$", token, (name, value) -> { map.put("m-left", value); map.put("m-right", value); }); if (matched) continue; // m-y-<num> matched = PatternUtils.matches("^m-y-(?<num>\\d+)$", token, (name, value) -> { map.put("m-top", value); map.put("m-bottom", value); }); if (matched) continue; throw new IllegalArgumentException("无效的组件约束: '" + token + "'"); } return map; } private boolean matchAtomic(String token, BiConsumer<String, String> action) { for (Map.Entry<String, Pattern> entry : ATOMIC.entrySet()) { String name = entry.getKey(); Pattern pattern = entry.getValue(); Matcher matcher = pattern.matcher(token); if (matcher.matches()) { action.accept(name, matcher.group(1)); return true; } } return false; } // ---- 对齐逻辑 ---- /** * 应用水平和垂直对齐及填充设置到约束对象。 */ private static void applyAlignment(Map<String, String> map, GridBagConstraints gbc) { String hAlign = map.getOrDefault("align-x", "center"); String vAlign = map.getOrDefault("align-y", "center"); int hConstraint = switch (hAlign) { case "start" -> GridBagConstraints.LINE_START; case "end" -> GridBagConstraints.LINE_END; case "fill" -> GridBagConstraints.HORIZONTAL; default -> GridBagConstraints.CENTER; // center }; int vConstraint = switch (vAlign) { case "start" -> GridBagConstraints.PAGE_START; case "end" -> GridBagConstraints.PAGE_END; case "fill" -> GridBagConstraints.VERTICAL; default -> GridBagConstraints.CENTER; }; // 填充模式:根据水平和垂直填充组合设置 fill 属性 if (hConstraint == GridBagConstraints.HORIZONTAL && vConstraint == GridBagConstraints.VERTICAL) { gbc.fill = GridBagConstraints.BOTH; } else if (hConstraint == GridBagConstraints.HORIZONTAL) { gbc.fill = GridBagConstraints.HORIZONTAL; } else if (vConstraint == GridBagConstraints.VERTICAL) { gbc.fill = GridBagConstraints.VERTICAL; } else { gbc.fill = GridBagConstraints.NONE; } // 锚点:根据水平和垂直起始/结束组合设置 anchor if (hConstraint == GridBagConstraints.LINE_START) { gbc.anchor = switch (vConstraint) { case GridBagConstraints.PAGE_START -> GridBagConstraints.NORTHWEST; case GridBagConstraints.PAGE_END -> GridBagConstraints.SOUTHWEST; default -> GridBagConstraints.WEST; }; } else if (hConstraint == GridBagConstraints.LINE_END) { gbc.anchor = switch (vConstraint) { case GridBagConstraints.PAGE_START -> GridBagConstraints.NORTHEAST; case GridBagConstraints.PAGE_END -> GridBagConstraints.SOUTHEAST; default -> GridBagConstraints.EAST; }; } else { gbc.anchor = switch (vConstraint) { case GridBagConstraints.PAGE_START -> GridBagConstraints.NORTH; case GridBagConstraints.PAGE_END -> GridBagConstraints.SOUTH; default -> GridBagConstraints.CENTER; }; } } // ---- 外边距逻辑 ---- /** * 解析外边距并应用到约束对象。 */ private static void applyMargins(Map<String, String> map, GridBagConstraints gbc) { int top = Integer.parseInt(map.getOrDefault("m-top", "0")); int right = Integer.parseInt(map.getOrDefault("m-right", "0")); int bottom = Integer.parseInt(map.getOrDefault("m-bottom", "0")); int left = Integer.parseInt(map.getOrDefault("m-left", "0")); gbc.insets = new Insets(top, left, bottom, right); } // ---- 轨道映射 ---- /** * 将组件的逻辑行列索引转换为包含对齐间隙的物理索引。 * 由于全局对齐会在网格前后或之间插入“间隙”列/行,所以逻辑网格位置需要映射到实际的 * GridBagLayout 列/行编号。 */ private void applyTrackMapping(GridBagConstraints gbc) { int endX = columnMapping[gbc.gridx + gbc.gridwidth - 1]; int beginX = columnMapping[gbc.gridx]; gbc.gridx = beginX; gbc.gridwidth = endX - beginX + 1; int endY = rowMapping[gbc.gridy + gbc.gridheight - 1]; int beginY = rowMapping[gbc.gridy]; gbc.gridy = beginY; gbc.gridheight = endY - beginY + 1; } /** * 根据对齐方式和轨道数量构建包含间隙的 {@link Track} 对象。 * 不同对齐方式会在内容轨道之间插入权重为 1 的间隙列/行,以控制剩余空间的分配。 */ private Track buildTrack(String alignment, int count) { Track track = new Track(); switch (alignment) { case "start": for (int i = 0; i < count; i++) track.track(); track.gap(); // 尾部间隙吸收剩余空间 break; case "center": track.gap(); for (int i = 0; i < count; i++) track.track(); track.gap(); break; case "end": track.gap(); for (int i = 0; i < count; i++) track.track(); break; case "between": track.track(); for (int i = 1; i < count; i++) { track.gap(); track.track(); } break; case "around": for (int i = 0; i < count; i++) { track.gap(); track.track(); track.gap(); } break; case "evenly": track.gap(); for (int i = 0; i < count; i++) { track.track(); track.gap(); } break; case "stretch": // stretch 模式:所有内容轨道权重为 1,平分空间,无单独间隙 for (int i = 0; i < count; i++) track.add(false, Short.MAX_VALUE, 1.0); break; default: throw new IllegalArgumentException("未知的网格对齐方式: " + alignment); } return track; } // ---- 内部类型 ---- /** * 全局 X/Y 对齐解析结果的值对象。 */ public static class GridAlignment { public static GridAlignment create() { return new GridAlignment(GridMode.STRETCH.getText(), GridMode.STRETCH.getText()); } private final String x, y; private GridAlignment(String x, String y) { this.x = x; this.y = y; } public GridAlignment setX(GridMode mode) { return new GridAlignment(mode.getText(), y); } public GridAlignment setY(GridMode mode) { return new GridAlignment(x, mode.getText()); } } /** * 全局网格对齐方式枚举 */ public enum GridMode { START("start"), CENTER("center"), END("end"), BETWEEN("between"), AROUND("around"), EVENLY("evenly"), STRETCH("stretch"); private final String text; GridMode(String text) { this.text = text; } public String getText() { return text; } } /** * 用于构建一系列内容轨道和间隙轨道的辅助类, * 最终为 GridBagLayout 提供列宽/行高、权重以及逻辑到物理索引的映射数组。 */ private static class Track { private final java.util.List<Boolean> isGap = new ArrayList<>(); private final java.util.List<Integer> widths = new ArrayList<>(); private final java.util.List<Double> weights = new ArrayList<>(); /** * 添加一个间隙轨道(宽度为 0,权重为 1 以吸收剩余空间) */ void gap() { add(true, 0, 1.0); } /** * 添加一个内容轨道(宽度为 0,权重为 0) */ void track() { add(false, 0, 0.0); } void add(boolean gap, int width, double weight) { isGap.add(gap); widths.add(width); weights.add(weight); } int[] getWidths() { return widths.stream().mapToInt(Integer::intValue).toArray(); } double[] getWeights() { return weights.stream().mapToDouble(Double::doubleValue).toArray(); } /** * 返回逻辑索引到物理索引的映射。 * 仅非间隙轨道的物理索引被收集,数组索引对应逻辑位置。 */ int[] getMapping() { List<Integer> mapping = new ArrayList<>(); int idx = 0; for (boolean b : isGap) { if (!b) mapping.add(idx); idx++; } return mapping.stream().mapToInt(Integer::intValue).toArray(); } } // ---- 便捷构建器入口 ---- /** * 组件级约束的流式构建器,最终生成符合 DSL 格式的字符串。 */ public static class ConstraintsBuilder { private final Map<String, String> constraints = new LinkedHashMap<>(); public ConstraintsBuilder setLocation(int x, int y) { constraints.put("x", "x-" + x); constraints.put("y", "y-" + y); return this; } public ConstraintsBuilder setSpan(int x, int y) { constraints.put("span-x", "span-x-" + x); constraints.put("span-y", "span-y-" + y); return this; } public ConstraintsBuilder setAlignX(AlignMode value) { constraints.put("align-x", "align-x-" + value.getText()); return this; } public ConstraintsBuilder setAlignY(AlignMode value) { constraints.put("align-y", "align-y-" + value.getText()); return this; } public ConstraintsBuilder setAlign(AlignMode value) { return setAlignX(value).setAlignY(value); } public ConstraintsBuilder setMarginTop(int value) { constraints.put("m-top", "m-top-" + value); return this; } public ConstraintsBuilder setMarginLeft(int value) { constraints.put("m-left", "m-left-" + value); return this; } public ConstraintsBuilder setMarginBottom(int value) { constraints.put("m-bottom", "m-bottom-" + value); return this; } public ConstraintsBuilder setMarginRight(int value) { constraints.put("m-right", "m-right-" + value); return this; } public ConstraintsBuilder setMarginX(int value) { return setMarginLeft(value).setMarginRight(value); } public ConstraintsBuilder setMarginY(int value) { return setMarginTop(value).setMarginBottom(value); } public ConstraintsBuilder setMargin(int value) { return setMarginX(value).setMarginY(value); } public ConstraintsBuilder copy() { ConstraintsBuilder builder = new ConstraintsBuilder(); builder.constraints.putAll(constraints); return builder; } public String build() { return String.join(" ", constraints.values()); } } /** * 组件对齐方式枚举 */ public enum AlignMode { START("start"), CENTER("center"), END("end"), FILL("fill"); private final String text; AlignMode(String text) { this.text = text; } public String getText() { return text; } } }
import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; public class PatternUtils { private static final int MAX_CACHE_SIZE = 128; private static final Map<String, Pattern> CACHE = Collections.synchronizedMap( new LinkedHashMap<>(16, 0.75f, true) { @Override protected boolean removeEldestEntry(Map.Entry<String, Pattern> eldest) { return size() > MAX_CACHE_SIZE; } } ); public static Pattern compile(String regex) { Pattern pattern = CACHE.get(regex); if (pattern != null) { return pattern; } synchronized (CACHE) { Pattern existing = CACHE.get(regex); if (existing != null) { return existing; } Pattern created = Pattern.compile(regex); CACHE.put(regex, created); return created; } } public static boolean matches(String regex, String input) { return compile(regex).matcher(input).matches(); } /** * 尝试匹配整个输入字符串(相当于 String.matches) * 如果匹配成功,提取所有命名分组并通过回调返回 * * @param regex 正则表达式 * @param input 输入字符串 * @param handler 回调函数,接收 (分组名, 分组值) * @return true 表示匹配成功,false 表示匹配失败 */ public static boolean matches(String regex, String input, BiConsumer<String, String> handler) { Pattern pattern = compile(regex); Matcher matcher = pattern.matcher(input); if (!matcher.matches()) return false; Map<String, Integer> namedGroups = pattern.namedGroups(); if (!namedGroups.isEmpty()) { namedGroups.forEach((name, index) -> { String value = matcher.group(index); handler.accept(name, value); }); } return true; } /** * 尝试匹配整个输入字符串(相当于 String.matches) * 如果匹配成功,提取所有命名分组并通过回调返回 * * @param regex 正则表达式 * @param input 输入字符串 * @param handler 回调函数,接收 (分组值) * @return true 表示匹配成功,false 表示匹配失败 */ public static boolean matches(String regex, String input, Consumer<String> handler) { return matches(regex, input, (name, value) -> handler.accept(value)); } }

浙公网安备 33010602011771号