当前位置:

首页 > 编程开发 > JavaSwing实现自定义TablePanel组件(附完整代码)

JavaSwing实现自定义TablePanel组件(附完整代码)

一、背景 前面两篇分别介绍了 CusTable 和表格的渲染器/编辑器机制,但实际项目中,表格通常不是孤立存在的,还需要配合: 查询条件面板(搜索框、下拉框、日期选择器等) 工具栏(新增、删除、导出等操作按钮) 分页组件 每次写一个表格页面都要重复搭建这些组件,代码冗余且容易出错。 TablePan

一、背景

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

Ja vaSwing实现自定义TablePanel组件(附完整代码)

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

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

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

二、整体架构

┌──────────────────────────────────────────────────────┐

│ 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 中使用了三个按钮配置类,用于统一管理不同场景下的按钮配置:

QueryBtn — 查询按钮类

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

import lombok.Getter;

import ja vax.swing.*;
import ja va.io.Serializable;
import ja va.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 mouseEntered;
    private final Consumer 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 mouseEntered, Consumer mouseExited) {
        this(text, imageIcon, null, null, mouseClick, mouseEntered, mouseExited);
    }

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

ToolBarBtn — 工具栏按钮类

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

import lombok.Getter;

import ja vax.swing.*;
import ja va.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 mouseEntered, Consumer mouseExited) {
        super(text, imageIcon, null, null, mouseClick, mouseEntered, mouseExited);
        this.name = "";
    }

    public ToolBarBtn(String text, ImageIcon imageIcon, String name, Runnable mouseClick,
                      Consumer mouseEntered, Consumer 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 mouseEntered, Consumer 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 mouseEntered, Consumer mouseExited) {
        super(text, imageIcon, bgColorHex, fontColorHex, mouseClick, mouseEntered, mouseExited);
        this.name = name;
    }
}

OperateBtn — 操作列按钮类

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

import lombok.Getter;

import ja vax.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;
    }
}

三个按钮类的职责划分

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

五、TablePanel 源码

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 ja vax.swing.*;
import ja vax.swing.table.DefaultTableCellRenderer;
import ja vax.swing.table.TableCellRenderer;
import ja vax.swing.table.TableColumn;
import ja vax.swing.text.JTextComponent;
import ja va.awt.*;
import ja va.awt.event.MouseAdapter;
import ja va.awt.event.MouseEvent;
import ja va.awt.event.MouseMotionAdapter;
import ja va.time.LocalDate;
import ja va.util.*;
import ja va.util.List;
import ja va.util.function.BiFunction;
import ja va.util.function.Function;
import ja va.util.function.Supplier;

/**
 * 完整表格面板
 * 整合查询面板、工具栏、表格、分页于一体
 *
 * 使用示例:
 * TablePanel 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 rowBase = user -> new Object[]{user.getId(), user.getName(), user.getAge(), user.isActive()};
 *
 * // 初始化
 * panel.initialize(queries, columns, rowBase, this::loadData, toolBarBtns);
 */
public class TablePanel 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 cusTable;
    @Getter
    private PageToolBar pageToolBar;
    @Setter
    private int pageSize = 20;
    @Getter
    private Page page;
    private boolean withCb = false;
    @Getter
    private JPanel outerPanel;
    private List hideTipColumns = new ArrayList<>();
    private Integer switchColumn;
    private final Map> switchActions = new HashMap<>();
    private Integer operateColumn;
    private Function 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 tableRowBase) {
        cusTable = new CusTable(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> loadData) {
        pageToolBar = new PageToolBar(params -> {
            page.setCurrent(params[0]);
            Page 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 tableRowBase,
                           Supplier> 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 tableRowBase,
                           Supplier> 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 getSelectedData() {
        if (!withCb) {
            log.warn("表格未启用复选框功能");
            return new ArrayList<>();
        }
        List 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 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 switchAction = switchActions.get(column);
        tableColumn.setCellEditor(new SwitchCellEditor(cusTable, switchAction));
    }

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

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

    private void renderOperateColumn(int column, Function 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 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 processTableRowBase(JTableRowBase 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)
  • 支持序号列

分页:

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

七、使用示例

基本用法

TablePanel 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 rowBase = user -> new Object[]{
    user.getId(), user.getName(), user.getAge(), user.isActive()
};

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

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

启用复选框

panel.initialize(true, queries, columns, rowBase, loadData, toolBarBtns);
List selected = panel.getSelectedData();

启用开关列

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

启用操作列

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

显示序号

panel.showIndexCell();

设置列宽

panel.setColumnWidth(0, 60);
panel.setColumnWidth(1, 120);

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

// 导出按钮(带悬停效果)
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);

八、注意事项

泛型一致性:TablePanel、CusTable、JTableRowBase 的泛型类型需保持一致

数据加载:loadData 方法需返回 Page 对象,包含数据和分页信息

列索引:启用复选框后,数据列从索引1开始;开关列、操作列需根据实际列索引设置

操作列按钮:每个 OperateBtn 的 width 控制按钮宽度,默认 60px

查询条件重置:reloadData() 会清空所有查询条件并重新查询

提示文本:内容宽度超过列宽时自动显示 ToolTip

九、小结

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

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

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

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
作者最新文章
编程开发
相关文章 更多
Java Swing家庭收支管理
Java Swing家庭收支管理

基于JavaSwing和MySQL构建的家庭收支管理工具,功能简洁清晰,支持收入与支出记录及类别管理。通过Eclipse导入项目,配置JDBC连接参数,导入SQL文件建立数据库,运行后即可登录使用。适合Java初学者练习桌面应用开发与数据库交互。

如何确保 Swing 中 JPanel 的自绘图形完整可见
如何确保 Swing 中 JPanel 的自绘图形完整可见

在Swing中,JPanel自绘图形被裁剪的根本原因是布局管理器依据getPreferredSize()分配空间,而默认尺寸过小,通常为零。解决方法是重写getPreferredSize()返回所需尺寸,并同步调整getMinimumSize(),确保自定义绘制内容完整可见。此外,还需注意在paintComponent中正确绘制。

JavaSwing实现自定义可搜索下拉框(附完整代码)
JavaSwing实现自定义可搜索下拉框(附完整代码)

一种基于JavaSwing的可搜索下拉框CusComboBox,通过继承JComboBox并启用可编辑模式,实现输入关键字动态过滤选项、键盘导航(上下键、回车、ESC)及无匹配回调,同时加入200ms防抖延迟和中文输入状态处理,优化搜索性能与用户体验。

Java Swing自定义复选框CusCheckBox组件(附源码)
Java Swing自定义复选框CusCheckBox组件(附源码)

Swing原生复选框样式受系统限制,难以统一自定义。CusCheckBox组件通过实现Icon接口,将绘制逻辑完全掌控。核心由CheckBoxIcon和CusCheckBox两个类构成,前者负责绘制图标,后者包装标准组件并提供样式设置方法,实现了圆角、颜色、状态等外观的灵活定制。

如何在Notepad++中配置Java Swing界面的运行环境
如何在Notepad++中配置Java Swing界面的运行环境

如何在Notepad++中配置Ja va Swing界面的运行环境 先说一个核心事实:Notepad++本身只是个出色的文本编辑器,它并不提供Ja va的编译与运行环境。想在里面跑起Swing程序,本质上还是得依赖系统里已经装好的JDK,然后通过外部工具(比如nppexec插件)去调用ja vac和

提升Java GUI技能:掌握高级技术,打造卓越的界面
提升Java GUI技能:掌握高级技术,打造卓越的界面

1.深入定制组件外观1.1自定义组件外观使用Swing的“外观”功能,可以轻松地更改组件的外观,而无需重新编码。提供了多种预定义的外观,如Nimbus、Metal、Motif等等,只需几行代码即可应用外观。如果需要更高级的定制,可以通过创建自定义外观来实现。1.2调整组件大小和布局通过调整组件的大小和布局,可以创建更具吸引力和易用性的GUI。可以使用BorderLayout、GridLayout、FlowLayout等布局管理器来安排组件,也可以使用自定义布局来实现更复杂的布局。1.3创建自定义组件如果现

swing布局嵌套的例子
swing布局嵌套的例子

嵌套的Swing布局示例Swing是一个非常流行的Java界面开发工具包,它提供了一系列的布局管理器,用于在图形用户界面中设计和构建组件的布局。本文将介绍一个使用Swing布局管理器进行嵌套布局的示例。在Swing中,可以使用多种布局管理器来实现不同的布局效果,例如BorderLayout、FlowLayout、GridLayout等。

怎么使用java+io+swing实现学生信息管理系统
怎么使用java+io+swing实现学生信息管理系统

说明:1.开发环境基于eclipse外加windowbuilder插件。2.采用io流持续储存文件到本地磁盘。3.Arrylist对文件信息进行操作。一、界面展示本地储存学生信息的txt文件,可在此对学生信息增删改查但需要注意格式。二、项目构架三、相关代码及介绍1.IO部分学生实体类packagestuManager;publicclassStuInfo{privateintstuId;//学号privateStringstuName;//姓名privateintstuAge;//年龄privateStr

java swing怎么实现简单计算器界面
java swing怎么实现简单计算器界面

运用到的知识:1.常用顶层容器JFrame类的使用2.常用中间容器JPanel类的使用3.单行文本框的实现类JTextField类的使用4.按钮的实现类JButton类的使用5.边框布局管理器的使用6.网格布局管理器的使用基本思路:1.创建1个窗口f12.创建1个文本框t13.创建内层面板p3,保存文本框t14.创建16个按钮5.创建内层面板p2,设置为网格布局,用于保存16个按钮6.创建外层面板p1,设置为边框布局,北部保存p3,中部保存p2下面是是具体的代码实现,可以直接运行importjavax.s

怎么用Java+swing实现抖音上的表白程序
怎么用Java+swing实现抖音上的表白程序

1.准备工作a.需要下载一个带着swing插件的eclipseb.需要配置好JDKc.创建一个JFrame的项目(如下图所示的步骤)d.把资源文件放入与src所在的那个目录步骤如下:1.先复制资源文件2.粘贴文件3.把jar文件放入ReferencedLibraries文件夹下这第3步的具体操作如何所示那么如何判断添加是否成功呢?解答:看ReferencedLibraries下面是否出现了刚刚buildpath的两个文件,若出现了,则代表添加成功(成功的视图如下所示:)e.design界面和source界

查看更多
精品专题 更多
装机必备
装机必备

正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

Windows
Windows

正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

PDF教程
PDF教程

正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

Mac软件 更多
Adobe Acrobat Pro macOS版
Adobe Acrobat Pro macOS版
Mac

Adobe Acrobat Pro 是面向办公、法务、财务、教育及出版用户的专业PDF处理软件,可在Mac上创建、编辑、转换、合并和保护文档,并提供OCR文字识别、表单、批注、内容比较、敏感信息遮盖及电子签名功能。

Blender
Blender
Windows、macOS 和 Linux

Blender 是一款免费开源、跨平台的专业 3D 创作软件,集建模、动画、渲染、视频编辑与视觉合成等功能于一体,广泛应用于影视动画、游戏设计和建筑可视化等领域。软件支持 Cycles 物理渲染器与 Eevee 实时渲染引擎,并提供多边形建模、骨骼绑定、物理模拟等专业工具。Blender 兼容 Windows、macOS 和 Linux 系统,安装包轻巧、运行流畅,依托活跃的全球开发者社区持续更新,是从初学者到专业创作者都值得选择的正版 3D 创作工具。

Shapr3D macOS版
Shapr3D macOS版
Mac

Shapr3D是一款面向工业设计、机械工程、建筑概念和三维打印工作流的CAD软件。Mac版采用Parasolid建模内核,支持草图约束、实体建模、工程图、可视化渲染及常见CAD格式交换,并可通过账户在多台设备之间同步项目。

WINDOWS 更多
3ds max(3dmax)
3ds max(3dmax)
Windows

Autodesk 3ds Max 是一款专业的三维建模、动画与渲染软件,广泛应用于建筑可视化、游戏开发、影视动画、广告设计和产品展示等领域。

PixTV 网页版
PixTV 网页版
Windows

PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。

AutoCAD Windows版
AutoCAD Windows版
Windows

AutoCAD 是 Autodesk 推出的专业计算机辅助设计软件,Windows 版支持精确二维绘图、三维实体与曲面建模、图纸标注、布局发布和任务自动化,并包含多套行业专业化工具组合,适用于建筑、机械、电气、工程建设及制造设计人员。