恢宏  

封装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;
    }
}
Main.java

image

 

第二,SimpleGridLayout使用说明

SimpleGridLayout是由GridBagLayout封装得到的基于网格的布局管理器,通过字符串约束控制布局,使用分为四个步骤:

  1. 确定网格的行列大小,通过grid约束控制网格轨道的对齐方式,一共有七种模式:start、center、end、between、around、evenly、stretch(默认)
  2. 确定组件的放置位置以及单元格形状,通过x/y/span约束完成控制
  3. 确定组件在单元格内的对齐方式,通过align约束控制,一共有四种对齐方式:start、center、end、fill,例如align-x-center、align-start
  4. 确定组件和单元格的边距,通过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));
        }
    }
}
JGrid.java
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;
        }
    }
}
SimpleGridLayout.java
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));
    }
}
PatternUtils.java

 

posted on 2026-08-20 19:27  恢宏  阅读(9)  评论(0)    收藏  举报