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

TablePanel 完整方案

一、背景

前面两篇分别介绍了 CusTable 和表格的渲染器/编辑器机制,但实际项目中,表格通常不是孤立存在的,还需要配合:

  • 查询条件面板(搜索框、下拉框、日期选择器等)
  • 工具栏(新增、删除、导出等操作按钮)
  • 分页组件

每次写一个表格页面都要重复搭建这些组件,代码冗余且容易出错。

TablePanel 的作用就是:将查询面板、工具栏、表格、分页整合为一体,提供开箱即用的完整表格页面解决方案。

二、整体架构

css 复制代码
┌──────────────────────────────────────────────────────┐
│                    TablePanel                        │
│  ┌────────────────────────────────────────────────┐  │
│  │              查询面板(QueryPanel)             │  │
│  │  条件1  [输入框]  条件2  [下拉框]  [查询] [重置] │  │
│  └────────────────────────────────────────────────┘  │
│  ┌────────────────────────────────────────────────┐  │
│  │              工具栏(ToolBar)                  │  │
│  │  [新增] [修改] [删除] [导出]                    │  │
│  └────────────────────────────────────────────────┘  │
│  ┌────────────────────────────────────────────────┐  │
│  │              表格(CusTable)                   │  │
│  │  ┌────┬──────┬──────┬──────┬──────────────┐   │  │
│  │  │ ☑  │ ID   │ 姓名  │ 状态  │   操作       │   │  │
│  │  │ ☐  │ 1    │ 张三  │ ON   │ [编辑][删除] │   │  │
│  │  │ ☐  │ 2    │ 李四  │ OFF  │ [编辑][删除] │   │  │
│  │  └────┴──────┴──────┴──────┴──────────────┘   │  │
│  └────────────────────────────────────────────────┘  │
│  ┌────────────────────────────────────────────────┐  │
│  │              分页工具栏                         │  │
│  │  共100条,第1页  [上一页] [1] [下一页] 共5页    │  │
│  └────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────┘

三、依赖组件

TablePanel 依赖以下组件(均已在前序篇章介绍):

组件 来源篇章
CusTable 第13篇
ButtonUtils 第4篇
CusScrollPane 第11篇
DatePickerUtils 第5篇
ComponentUtils 第3篇
CallbackProcessor 第2篇

四、TablePanel 用到的按钮类

TablePanel 中使用了三个按钮配置类,用于统一管理不同场景下的按钮配置:

4.1 QueryBtn --- 查询按钮类

用于查询面板中的按钮(搜索、重置、导出等自定义按钮)

java 复制代码
import lombok.Getter;

import javax.swing.*;
import java.io.Serializable;
import java.util.function.Consumer;

/**
 * 查询按钮类
 * 用于查询面板中的按钮配置
 */
@Getter
public class QueryBtn implements Serializable {
    private static final long serialVersionUID = 5063820073104962669L;
    private final String text;
    private final ImageIcon imageIcon;
    private final String bgColorHex;
    private final String fontColorHex;
    private final Runnable mouseClick;
    private final Consumer<JComponent> mouseEntered;
    private final Consumer<JComponent> mouseExited;

    public QueryBtn(String text, ImageIcon imageIcon, Runnable mouseClick) {
        this(text, imageIcon, null, null, mouseClick, null, null);
    }

    public QueryBtn(String text, ImageIcon imageIcon, String bgColorHex, String fontColorHex, Runnable mouseClick) {
        this(text, imageIcon, bgColorHex, fontColorHex, mouseClick, null, null);
    }

    public QueryBtn(String text, ImageIcon imageIcon, Runnable mouseClick,
                    Consumer<JComponent> mouseEntered, Consumer<JComponent> mouseExited) {
        this(text, imageIcon, null, null, mouseClick, mouseEntered, mouseExited);
    }

    public QueryBtn(String text, ImageIcon imageIcon, String bgColorHex, String fontColorHex,
                    Runnable mouseClick, Consumer<JComponent> mouseEntered, Consumer<JComponent> mouseExited) {
        this.text = text;
        this.imageIcon = imageIcon;
        this.bgColorHex = bgColorHex;
        this.fontColorHex = fontColorHex;
        this.mouseClick = mouseClick;
        this.mouseEntered = mouseEntered;
        this.mouseExited = mouseExited;
    }
}

4.2 ToolBarBtn --- 工具栏按钮类

用于表格上方工具栏中的按钮配置,扩展了 name 字段用于标识

java 复制代码
import lombok.Getter;

import javax.swing.*;
import java.util.function.Consumer;

/**
 * 表格工具栏按钮类
 * 用于工具栏中的按钮配置,扩展了 name 字段
 */
public class ToolBarBtn extends QueryBtn {
    private static final long serialVersionUID = -3322742242512220910L;
    @Getter
    private final String name;

    public ToolBarBtn(String text, ImageIcon imageIcon, Runnable mouseClick) {
        super(text, imageIcon, null, null, mouseClick, null, null);
        this.name = "";
    }

    public ToolBarBtn(String text, ImageIcon imageIcon, String name, Runnable mouseClick) {
        super(text, imageIcon, null, null, mouseClick, null, null);
        this.name = name;
    }

    public ToolBarBtn(String text, ImageIcon imageIcon, String bgColorHex, String fontColorHex, Runnable mouseClick) {
        super(text, imageIcon, bgColorHex, fontColorHex, mouseClick, null, null);
        this.name = "";
    }

    public ToolBarBtn(String text, ImageIcon imageIcon, String bgColorHex, String fontColorHex, String name, Runnable mouseClick) {
        super(text, imageIcon, bgColorHex, fontColorHex, mouseClick, null, null);
        this.name = name;
    }

    public ToolBarBtn(String text, ImageIcon imageIcon, Runnable mouseClick,
                      Consumer<JComponent> mouseEntered, Consumer<JComponent> mouseExited) {
        super(text, imageIcon, null, null, mouseClick, mouseEntered, mouseExited);
        this.name = "";
    }

    public ToolBarBtn(String text, ImageIcon imageIcon, String name, Runnable mouseClick,
                      Consumer<JComponent> mouseEntered, Consumer<JComponent> mouseExited) {
        super(text, imageIcon, null, null, mouseClick, mouseEntered, mouseExited);
        this.name = name;
    }

    public ToolBarBtn(String text, ImageIcon imageIcon, String bgColorHex, String fontColorHex,
                      Runnable mouseClick, Consumer<JComponent> mouseEntered, Consumer<JComponent> mouseExited) {
        super(text, imageIcon, bgColorHex, fontColorHex, mouseClick, mouseEntered, mouseExited);
        this.name = "";
    }

    public ToolBarBtn(String text, ImageIcon imageIcon, String bgColorHex, String fontColorHex, String name,
                      Runnable mouseClick, Consumer<JComponent> mouseEntered, Consumer<JComponent> mouseExited) {
        super(text, imageIcon, bgColorHex, fontColorHex, mouseClick, mouseEntered, mouseExited);
        this.name = name;
    }
}

4.3 OperateBtn --- 操作列按钮类

用于表格操作列中的按钮配置,扩展了 width 字段控制按钮宽度

java 复制代码
import lombok.Getter;

import javax.swing.*;

/**
 * 操作列按钮类
 * 用于表格操作列中的按钮配置,扩展了 width 字段
 */
public class OperateBtn extends QueryBtn {
    private static final long serialVersionUID = 7964671454622940429L;
    @Getter
    private int width = 60;

    public OperateBtn(String text, Runnable mouseClick) {
        super(text, null, mouseClick);
    }

    public OperateBtn(String text, int width, Runnable mouseClick) {
        super(text, null, mouseClick);
        this.width = width;
    }

    public OperateBtn(String text, String bgColorHex, String fontColorHex, Runnable mouseClick) {
        super(text, null, bgColorHex, fontColorHex, mouseClick);
    }

    public OperateBtn(String text, int width, String bgColorHex, String fontColorHex, Runnable mouseClick) {
        super(text, null, bgColorHex, fontColorHex, mouseClick);
        this.width = width;
    }

    public OperateBtn(String text, ImageIcon imageIcon, Runnable mouseClick) {
        super(text, imageIcon, mouseClick);
    }

    public OperateBtn(String text, int width, ImageIcon imageIcon, Runnable mouseClick) {
        super(text, imageIcon, mouseClick);
        this.width = width;
    }

    public OperateBtn(String text, ImageIcon imageIcon, String bgColorHex, String fontColorHex, Runnable mouseClick) {
        super(text, imageIcon, bgColorHex, fontColorHex, mouseClick);
    }

    public OperateBtn(String text, int width, ImageIcon imageIcon, String bgColorHex, String fontColorHex, Runnable mouseClick) {
        super(text, imageIcon, bgColorHex, fontColorHex, mouseClick);
        this.width = width;
    }
}

4.4 三个按钮类的职责划分

使用场景 特有字段 说明
QueryBtn 查询面板 用于搜索、重置、导出等查询面板中的按钮
ToolBarBtn 工具栏 name 用于表格上方工具栏中的操作按钮
OperateBtn 操作列 width 用于表格每行的操作列按钮,支持自定义宽度

五、TablePanel 源码

java 复制代码
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.github.lgooddatepicker.components.DatePicker;
import lombok.Getter;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.swing.*;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.text.JTextComponent;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.time.LocalDate;
import java.util.*;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * 完整表格面板
 * 整合查询面板、工具栏、表格、分页于一体
 *
 * 使用示例:
 * TablePanel<User> panel = new TablePanel<>();
 *
 * // 查询条件
 * TablePanel.Query[] queries = {
 *     new TablePanel.Query("姓名:", new JTextField(10)),
 *     new TablePanel.Query("状态:", new JComboBox<>(new String[]{"全部", "启用", "禁用"}))
 * };
 *
 * // 工具栏按钮
 * ToolBarBtn[] toolBarBtns = {
 *     new ToolBarBtn("新增", addIcon, this::handleAdd),
 *     new ToolBarBtn("删除", delIcon, this::handleDelete)
 * };
 *
 * // 配置表格
 * String[] columns = {"ID", "姓名", "年龄", "状态"};
 * JTableRowBase<User> rowBase = user -> new Object[]{user.getId(), user.getName(), user.getAge(), user.isActive()};
 *
 * // 初始化
 * panel.initialize(queries, columns, rowBase, this::loadData, toolBarBtns);
 */
public class TablePanel<T> extends JPanel {
    private static final Logger log = LoggerFactory.getLogger(TablePanel.class);
    private static final int DEFAULT_OPERATE_COLUMN_WIDTH = 200;
    private static final int COLUMN_PADDING = 10;
    private static final int CHECK_BOX_WIDTH = 40;
    private static final int BUTTON_GAP = 10;

    public DatePicker startDatePicker = DatePickerUtils.datePicker();
    public DatePicker endDatePicker = DatePickerUtils.datePicker();

    @Getter
    private JPanel queryPanel;
    private Query[] queryList;
    private Runnable loadData;
    @Getter
    private CusTable<T> cusTable;
    @Getter
    private PageToolBar pageToolBar;
    @Setter
    private int pageSize = 20;
    @Getter
    private Page<T> page;
    private boolean withCb = false;
    @Getter
    private JPanel outerPanel;
    private List<Integer> hideTipColumns = new ArrayList<>();
    private Integer switchColumn;
    private final Map<Integer, BiFunction<Integer, Boolean, Boolean>> switchActions = new HashMap<>();
    private Integer operateColumn;
    private Function<T, OperateBtn[]> actionProvider;

    public TablePanel() {
        setLayout(new BorderLayout());
    }

    // ==================== 查询面板 ====================

    /**
     * 创建查询条件面板
     * @param queryList 查询条件列表
     * @param search 查询方法
     * @param actionBtns 除搜索、重置以外的按钮
     * @return 查询面板
     */
    public JPanel createQuery(Query[] queryList, Runnable search, QueryBtn... actionBtns) {
        this.queryList = queryList;
        this.loadData = search;
        JPanel queryPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 10));
        queryPanel.putClientProperty("FlatLaf.style", "arc: 30;");
        queryPanel.setBackground(Color.WHITE);
        queryPanel.setBorder(BorderFactory.createEmptyBorder(5, 50, 5, 10));

        for (Query query : queryList) {
            JLabel label = new JLabel(query.getLabel());
            label.setFont(new Font(MetConstants.FONT_NAME, Font.BOLD, 16));
            queryPanel.add(label);
            queryPanel.add(query.getComponent());
        }

        ButtonUtils.createSearchBtn(queryPanel, this::loadAndRender);
        ButtonUtils.createResetBtn(queryPanel, this::reloadData);

        for (QueryBtn btn : actionBtns) {
            ButtonUtils.createActionBtn(queryPanel, btn.getText(), btn.getImageIcon(),
                    btn.getBgColorHex(), btn.getFontColorHex(),
                    btn.getMouseClick(), btn.getMouseEntered(), btn.getMouseExited());
        }
        return queryPanel;
    }

    // ==================== 工具栏 ====================

    /**
     * 创建工具栏
     * @param toolBarBtns 工具栏按钮数组
     * @return 工具栏
     */
    public JToolBar createToolBar(ToolBarBtn[] toolBarBtns) {
        if (null == toolBarBtns || toolBarBtns.length == 0) {
            return null;
        }
        JToolBar toolBar = new JToolBar("操作工具栏", SwingConstants.HORIZONTAL);
        toolBar.addSeparator();
        toolBar.setOpaque(false);
        toolBar.setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 10));

        for (ToolBarBtn btn : toolBarBtns) {
            ButtonUtils.createActionBtn(toolBar, btn.getText(), btn.getImageIcon(),
                    btn.getBgColorHex(), btn.getFontColorHex(),
                    btn.getMouseClick(), btn.getMouseEntered(), btn.getMouseExited());
        }
        return toolBar;
    }

    // ==================== 表格 ====================

    /**
     * 创建表格
     * @param columns 列名数组
     * @param tableRowBase 行数据转换器
     * @return 滚动面板
     */
    public JScrollPane createTable(String[] columns, JTableRowBase<T> tableRowBase) {
        cusTable = new CusTable<T>(columns, tableRowBase) {
            @Override
            public boolean isCellEditable(int row, int column) {
                if (switchActions.containsKey(column)) {
                    Object value = getValueAt(row, column);
                    return value instanceof Boolean;
                }
                return super.isCellEditable(row, column);
            }

            @Override
            public Class<?> getColumnClass(int column) {
                if (withCb && column == 0) {
                    return Boolean.class;
                }
                return super.getColumnClass(column);
            }

            @Override
            public String getToolTipText(MouseEvent e) {
                int row = rowAtPoint(e.getPoint());
                int col = columnAtPoint(e.getPoint());
                if (row >= 0 && col >= 0 && !hideTipColumns.contains(col)) {
                    Object value = getValueAt(row, col);
                    if (null != value) {
                        String text = value.toString();
                        int colWidth = getColumnModel().getColumn(col).getWidth();
                        FontMetrics fm = getFontMetrics(getFont());
                        int textWidth = ComponentUtils.calculateTextWidth(text, fm);
                        if (textWidth > colWidth - COLUMN_PADDING) {
                            return text;
                        }
                    }
                }
                return null;
            }
        };

        cusTable.addMouseMotionListener(new MouseMotionAdapter() {
            @Override
            public void mouseMoved(MouseEvent e) {
                handleOperateMoved(e);
            }
        });
        cusTable.addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                handleCheckboxPressed(e);
            }
            @Override
            public void mouseClicked(MouseEvent e) {
                handleOperateClicked(e);
            }
        });

        if (withCb) {
            configureCheckboxColumn();
        }

        cusTable.setRowHeight(40);
        cusTable.getTableHeader().setFont(new Font(MetConstants.FONT_NAME, Font.BOLD, 16));
        cusTable.setFont(new Font(MetConstants.FONT_NAME, Font.PLAIN, 16));
        cusTable.setSelectionBackground(new Color(cusTable.getBackground().getRGB()));
        cusTable.setSelectionForeground(new Color(cusTable.getSelectionForeground().getRGB()));

        CusScrollPane scrollPane = new CusScrollPane(cusTable);
        scrollPane.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 10));
        return scrollPane;
    }

    // ==================== 分页 ====================

    /**
     * 创建分页工具栏
     * @param loadData 加载数据的方法,返回Page
     */
    public void createPageToolBar(Supplier<Page<T>> loadData) {
        pageToolBar = new PageToolBar(params -> {
            page.setCurrent(params[0]);
            Page<T> pageData = loadData.get();
            return new int[]{Math.toIntExact(pageData.getCurrent()), Math.toIntExact(pageData.getTotal())};
        });
        pageToolBar.setPageSize(pageSize);
        pageToolBar.setOpaque(false);
        page = new Page<>(pageToolBar.getPageIdx(), pageSize);
    }

    // ==================== 初始化 ====================

    /**
     * 初始化表格面板
     */
    public void initialize(JPanel queryPanel, JToolBar toolBar, JScrollPane tablePanel) {
        if (null != queryPanel) {
            add(queryPanel, BorderLayout.NORTH);
        }

        JPanel centerPanel = new JPanel(new BorderLayout());
        centerPanel.setBorder(BorderFactory.createEmptyBorder(10, 40, 0, 40));
        centerPanel.putClientProperty("FlatLaf.style", "arc: 30;");
        centerPanel.setBackground(Color.WHITE);

        if (null != toolBar) {
            centerPanel.add(toolBar, BorderLayout.NORTH);
        }
        centerPanel.add(tablePanel, BorderLayout.CENTER);
        centerPanel.add(pageToolBar, BorderLayout.SOUTH);

        outerPanel = new JPanel(new BorderLayout());
        outerPanel.setBorder(BorderFactory.createEmptyBorder(10, 0, 10, 0));
        outerPanel.add(centerPanel, BorderLayout.CENTER);
        add(outerPanel, BorderLayout.CENTER);
    }

    /**
     * 初始化表格面板(完整参数)
     */
    public void initialize(Query[] queryList, String[] columns, JTableRowBase<T> tableRowBase,
                           Supplier<Page<T>> loadData, ToolBarBtn[] toolBarBtns, QueryBtn... actionBtns) {
        this.loadData = loadData::get;
        queryPanel = null != queryList ? createQuery(queryList, this.loadData, actionBtns) : null;
        JToolBar toolBar = createToolBar(toolBarBtns);
        JScrollPane tablePanel = createTable(columns, tableRowBase);
        createPageToolBar(loadData);
        initialize(queryPanel, toolBar, tablePanel);
    }

    /**
     * 初始化表格面板(带复选框)
     */
    public void initialize(boolean withCb, Query[] queryList, String[] columns, JTableRowBase<T> tableRowBase,
                           Supplier<Page<T>> loadData, ToolBarBtn[] toolBarBtns, QueryBtn... actionBtns) {
        this.withCb = withCb;
        initialize(queryList, processColumns(columns), processTableRowBase(tableRowBase), loadData, toolBarBtns, actionBtns);
        if (withCb) {
            TableColumn tableColumn = cusTable.getColumnModel().getColumn(0);
            tableColumn.setCellRenderer(cusTable.getDefaultRenderer(Boolean.class));
            tableColumn.setCellEditor(cusTable.getDefaultEditor(Boolean.class));
            setColumnWidth(0, CHECK_BOX_WIDTH);
        }
    }

    // ==================== 数据加载 ====================

    private void loadAndRender() {
        CallbackProcessor.run(loadData);
        renderColumnsAfterLoad();
    }

    /**
     * 重新加载数据(重置查询条件)
     */
    public void reloadData() {
        pageToolBar.setPageIdx(1);
        page.setCurrent(1);
        if (null != this.queryList) {
            for (Query query : this.queryList) {
                if (query.getComponent() instanceof JTextComponent) {
                    ((JTextComponent) query.getComponent()).setText("");
                }
                if (query.getComponent() instanceof JComboBox) {
                    ((JComboBox<?>) query.getComponent()).setSelectedIndex(-1);
                }
                if (query.getComponent() instanceof DatePickerImpl) {
                    ((DatePickerImpl) query.getComponent()).getFormattedTextField().setText(null);
                }
                if (query.getComponent() instanceof DatePicker) {
                    ((DatePicker) query.getComponent()).setDate(null);
                    ((DatePicker) query.getComponent()).getSettings().setDateRangeLimits(null, null);
                }
            }
        }
        loadAndRender();
    }

    private void renderColumnsAfterLoad() {
        if (null == cusTable) return;
        if (withCb) {
            renderCheckboxColumn();
        }
        if (null != switchColumn && cusTable.getColumnCount() > switchColumn) {
            renderSwitchColumn(switchColumn);
        }
        if (null != operateColumn && null != actionProvider && cusTable.getColumnCount() > operateColumn) {
            renderOperateColumn(operateColumn, actionProvider);
        }
    }

    // ==================== 复选框支持 ====================

    public List<T> getSelectedData() {
        if (!withCb) {
            log.warn("表格未启用复选框功能");
            return new ArrayList<>();
        }
        List<T> selected = new ArrayList<>();
        for (int i = 0; i < cusTable.getRowCount(); i++) {
            Boolean isSelected = (Boolean) cusTable.getValueAt(i, 0);
            if (null != isSelected && isSelected) {
                selected.add(cusTable.getData(i));
            }
        }
        return selected;
    }

    private void renderCheckboxColumn() {
        TableColumn checkboxColumn = cusTable.getColumnModel().getColumn(0);
        cusTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
        checkboxColumn.setCellRenderer(new CheckboxRenderer());
        checkboxColumn.setCellEditor(new CheckboxEditor());
        setColumnWidth(0, CHECK_BOX_WIDTH);
    }

    private void handleCheckboxPressed(MouseEvent e) {
        if (withCb && null != cusTable) {
            int row = cusTable.rowAtPoint(e.getPoint());
            int col = cusTable.columnAtPoint(e.getPoint());
            if (row >= 0 && col == 0) {
                if (cusTable.isEditing()) {
                    cusTable.getCellEditor().stopCellEditing();
                }
                Boolean current = (Boolean) cusTable.getValueAt(row, col);
                cusTable.setValueAt(null == current || !current, row, col);
            }
        }
    }

    private void configureCheckboxColumn() {
        TableColumn checkboxColumn = cusTable.getColumnModel().getColumn(0);
        cusTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
        checkboxColumn.setCellRenderer(new CheckboxRenderer());
        checkboxColumn.setCellEditor(new CheckboxEditor());
    }

    // ==================== 开关列支持 ====================

    public void enableSwitchClick(int column, BiFunction<Integer, Boolean, Boolean> switchAction) {
        switchColumn = column;
        switchActions.put(column, switchAction);
        if (null != cusTable && cusTable.getColumnCount() > column) {
            renderSwitchColumn(column);
        }
    }

    private void renderSwitchColumn(int column) {
        TableCellRenderer defaultRenderer = cusTable.getDefaultRenderer(Object.class);
        TableColumn tableColumn = cusTable.getColumnModel().getColumn(column);
        tableColumn.setCellRenderer(new SwitchCellRenderer(defaultRenderer));
        BiFunction<Integer, Boolean, Boolean> switchAction = switchActions.get(column);
        tableColumn.setCellEditor(new SwitchCellEditor(cusTable, switchAction));
    }

    // ==================== 操作列支持 ====================

    public void handleOperateColumn(int column, Function<T, OperateBtn[]> actionProvider) {
        this.operateColumn = column;
        this.actionProvider = actionProvider;
        if (null != cusTable && cusTable.getColumnCount() > column) {
            renderOperateColumn(column, actionProvider);
        }
    }

    private void renderOperateColumn(int column, Function<T, OperateBtn[]> actionProvider) {
        TableColumn col = cusTable.getColumnModel().getColumn(column);
        col.setCellRenderer(new OperateColumnRenderer<>(actionProvider::apply, cusTable));
        col.setPreferredWidth(DEFAULT_OPERATE_COLUMN_WIDTH);
    }

    private void handleOperateMoved(MouseEvent e) {
        OperateBtn cursorBtn = getButtonAtPoint(actionProvider, e.getPoint());
        cusTable.setCursor(null != cursorBtn ? Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) : Cursor.getDefaultCursor());
    }

    private void handleOperateClicked(MouseEvent e) {
        OperateBtn btn = getButtonAtPoint(actionProvider, e.getPoint());
        if (null != btn) {
            Runnable action = btn.getMouseClick();
            if (null != action) {
                action.run();
            }
            e.consume();
        }
    }

    private OperateBtn getButtonAtPoint(Function<T, OperateBtn[]> actionProvider, Point p) {
        if (null == operateColumn || null == actionProvider || null == cusTable) {
            return null;
        }
        int rowView = cusTable.rowAtPoint(p);
        int colIndex = cusTable.columnAtPoint(p);
        if (rowView >= 0 && colIndex == operateColumn) {
            int rowModel = cusTable.convertRowIndexToModel(rowView);
            T rowData = cusTable.getData(rowModel);
            OperateBtn[] btns = actionProvider.apply(rowData);
            if (null != btns && btns.length > 0) {
                Rectangle cellRect = cusTable.getCellRect(rowView, colIndex, false);
                int currentX = calCurrentX(btns, cellRect);
                for (OperateBtn btn : btns) {
                    int btnWidth = null == btn ? MetConstants.BUTTON_WIDTH : Math.min(btn.getWidth(), cellRect.width / btns.length);
                    Rectangle btnRect = new Rectangle(currentX, cellRect.y, btnWidth, cusTable.getRowHeight());
                    if (null != btn && btnRect.contains(p)) {
                        return btn;
                    }
                    currentX += btnWidth + BUTTON_GAP;
                }
            }
        }
        return null;
    }

    private static int calCurrentX(OperateBtn[] btns, Rectangle cellRect) {
        int totalBtnWidth = 0;
        for (int i = 0; i < btns.length; i++) {
            int btnWidth = null == btns[i] ? MetConstants.BUTTON_WIDTH : Math.min(btns[i].getWidth(), cellRect.width / btns.length);
            totalBtnWidth += btnWidth;
            if (i < btns.length - 1) {
                totalBtnWidth += BUTTON_GAP;
            }
        }
        return cellRect.x + (cellRect.width - totalBtnWidth) / 2;
    }

    // ==================== 辅助方法 ====================

    private String[] processColumns(String[] columns) {
        if (!withCb) {
            return columns;
        }
        String[] newColumns = new String[columns.length + 1];
        newColumns[0] = "";
        System.arraycopy(columns, 0, newColumns, 1, columns.length);
        return newColumns;
    }

    private JTableRowBase<T> processTableRowBase(JTableRowBase<T> tableRowBase) {
        if (!withCb) {
            return tableRowBase;
        }
        return item -> {
            Object[] originalData = tableRowBase.getRow(item);
            Object[] newData = new Object[originalData.length + 1];
            newData[0] = false;
            System.arraycopy(originalData, 0, newData, 1, originalData.length);
            return newData;
        };
    }

    public void setColumnWidth(int columnIndex, int width) {
        if (columnIndex < 0 || columnIndex >= cusTable.getColumnCount() || width < 0) {
            log.warn("列索引[{}]越界或者宽度[{}]小于0", columnIndex, width);
            return;
        }
        TableColumn column = cusTable.getColumnModel().getColumn(columnIndex);
        column.setMinWidth(width);
        column.setMaxWidth(width);
        column.setPreferredWidth(width);
        column.setWidth(width);
        column.setResizable(false);
    }

    public void setColumnWidth(int width, int... columns) {
        if (null == columns) return;
        for (int columnIndex : columns) {
            setColumnWidth(columnIndex, width);
        }
    }

    public void showIndexCell() {
        int colIndex = withCb ? 1 : 0;
        TableColumn column = cusTable.getColumnModel().getColumn(colIndex);
        column.setMinWidth(80);
        column.setMaxWidth(80);
        column.setPreferredWidth(80);
        column.setWidth(80);
        column.setCellRenderer(new DefaultTableCellRenderer() {
            @Override
            public Component getTableCellRendererComponent(JTable table, Object value,
                                                           boolean isSelected, boolean hasFocus, int row, int column) {
                value = row + 1;
                Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
                setHorizontalAlignment(SwingConstants.CENTER);
                return c;
            }
        });
        column.setResizable(false);
    }

    public void disableBgColor(int... columns) {
        TableCellRenderer defaultRenderer = cusTable.getDefaultRenderer(Object.class);
        for (int col : columns) {
            TableColumn column = cusTable.getColumnModel().getColumn(col);
            column.setCellRenderer(new DisableBgCellRenderer(defaultRenderer, col));
        }
    }

    public void hideToolTipColumns(Integer... columns) {
        hideTipColumns = Arrays.asList(columns);
    }

    public void dateChangeListener() {
        startDatePicker.addDateChangeListener(e -> {
            LocalDate start = e.getNewDate();
            if (null != start) {
                endDatePicker.getSettings().setDateRangeLimits(start, null);
                if (null != endDatePicker.getDate() && endDatePicker.getDate().isBefore(start)) {
                    endDatePicker.setDate(null);
                }
            }
        });
        endDatePicker.addDateChangeListener(e -> {
            LocalDate end = e.getNewDate();
            if (null != end) {
                startDatePicker.getSettings().setDateRangeLimits(null, end);
                if (null != startDatePicker.getDate() && startDatePicker.getDate().isAfter(end)) {
                    startDatePicker.setDate(null);
                }
            }
        });
    }

    // ==================== 查询条件内部类 ====================

    @Getter
    public static class Query {
        private final String label;
        private final JComponent component;

        public Query(String label, JComponent component) {
            this.label = label;
            this.component = component;
        }
    }
}

六、核心功能说明

查询面板:

  • 支持多个查询条件(文本框、下拉框、日期选择器等)
  • 自动生成"查询"和"重置"按钮
  • 支持额外的自定义按钮(通过 QueryBtn

工具栏:

  • 水平工具栏,支持多个操作按钮
  • 按钮样式统一(由 ButtonUtils + ToolBarBtn 提供)

表格:

  • 基于 CusTable,支持泛型数据绑定
  • 支持复选框列(多选)
  • 支持开关列(点击切换状态)
  • 支持操作列(每行多个操作按钮,通过 OperateBtn 配置)
  • 支持提示文本(内容过长时显示 ToolTip)
  • 支持序号列

分页:

  • 内置分页工具栏(页码跳转、上一页/下一页)
  • 可配置每页大小

七、使用示例

7.1 基本用法

java 复制代码
TablePanel<User> panel = new TablePanel<>();

// 查询条件
TablePanel.Query[] queries = {
    new TablePanel.Query("姓名:", new JTextField(10)),
    new TablePanel.Query("状态:", new JComboBox<>(new String[]{"全部", "启用", "禁用"}))
};

// 工具栏按钮
ToolBarBtn[] toolBarBtns = {
    new ToolBarBtn("新增", addIcon, "add", this::handleAdd),
    new ToolBarBtn("删除", delIcon, "delete", this::handleDelete)
};

// 列定义
String[] columns = {"ID", "姓名", "年龄", "状态"};

// 行数据转换
JTableRowBase<User> rowBase = user -> new Object[]{
    user.getId(), user.getName(), user.getAge(), user.isActive()
};

// 数据加载方法
Supplier<Page<User>> loadData = () -> userService.queryPage(page);

// 初始化
panel.initialize(queries, columns, rowBase, loadData, toolBarBtns);
add(panel);

7.2 启用复选框

java 复制代码
panel.initialize(true, queries, columns, rowBase, loadData, toolBarBtns);
List<User> selected = panel.getSelectedData();

7.3 启用开关列

java 复制代码
panel.enableSwitchClick(3, (row, newState) -> {
    User user = panel.getCusTable().getData(row);
    user.setActive(newState);
    return userService.update(user) > 0;
});

7.4 启用操作列

java 复制代码
panel.handleOperateColumn(columns.length - 1, user -> new OperateBtn[]{
    new OperateBtn("编辑", 60, editIcon, "#409EFF", "#FFFFFF", () -> editUser(user)),
    new OperateBtn("删除", 60, delIcon, "#F56C6C", "#FFFFFF", () -> deleteUser(user))
});

7.5 显示序号

java 复制代码
panel.showIndexCell();

7.6 设置列宽

java 复制代码
panel.setColumnWidth(0, 60);
panel.setColumnWidth(1, 120);

7.7 自定义查询按钮(使用 QueryBtn)

java 复制代码
// 导出按钮(带悬停效果)
QueryBtn exportBtn = new QueryBtn("导出", exportIcon, "#E6A23C", "#FFFFFF", this::handleExport,
    comp -> ((JButton) comp).setBackground(Color.decode("#D4891B")),
    comp -> ((JButton) comp).setBackground(Color.decode("#E6A23C"))
);

// 打印按钮
QueryBtn printBtn = new QueryBtn("打印", printIcon, "#909399", "#FFFFFF", this::handlePrint,
    comp -> ((JButton) comp).setBackground(Color.decode("#7A7A7A")),
    comp -> ((JButton) comp).setBackground(Color.decode("#909399"))
);

panel.initialize(queries, columns, rowBase, loadData, toolBarBtns, exportBtn, printBtn);

八、注意事项

  1. 泛型一致性TablePanel<T>CusTable<T>JTableRowBase<T> 的泛型类型需保持一致
  2. 数据加载loadData 方法需返回 Page<T> 对象,包含数据和分页信息
  3. 列索引:启用复选框后,数据列从索引1开始;开关列、操作列需根据实际列索引设置
  4. 操作列按钮 :每个 OperateBtnwidth 控制按钮宽度,默认 60px
  5. 查询条件重置reloadData() 会清空所有查询条件并重新查询
  6. 提示文本:内容宽度超过列宽时自动显示 ToolTip

九、小结

TablePanel 是一个完整的表格页面解决方案,将查询、表格、分页三大模块整合为一体:

  • 查询面板 → 条件筛选
  • 工具栏 → 批量操作
  • 表格 → 数据展示 + 交互(复选框/开关/操作列)
  • 分页 → 大数据量分页加载

使用 TablePanel 可以大幅减少表格页面的重复代码,一套代码适配多种场景。

相关推荐
程序员麻辣烫1 小时前
Memory向量记忆系统4-文本向量化
后端·aigc
Java编程爱好者1 小时前
Java 并发锁体系全解:从 synchronized、Lock 到 AQS 底层原理与源码深度剖析
后端
程序员天天困1 小时前
Java 项目用 Single-flight 一招终结 AI 接口重复调用
redis·后端
farerboy1 小时前
23-Java 构造函数
java·后端
程序员麻辣烫1 小时前
Memory向量记忆系统3-数据选取
后端·aigc
程序员麻辣烫1 小时前
Memory向量记忆系统2-SQLite
后端·aigc
坚持的小马1 小时前
Rocketmq搭建操作步骤
java·rocketmq·java-rocketmq
阳光是sunny1 小时前
LangGraph高级教程:Multi Schema多状态管理详解
前端·人工智能·后端
砚底藏山河2 小时前
多家股票数据接口对比、企业级股票数据API
java·python·金融·maven