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);
八、注意事项
- 泛型一致性 :
TablePanel<T>、CusTable<T>、JTableRowBase<T>的泛型类型需保持一致 - 数据加载 :
loadData方法需返回Page<T>对象,包含数据和分页信息 - 列索引:启用复选框后,数据列从索引1开始;开关列、操作列需根据实际列索引设置
- 操作列按钮 :每个
OperateBtn的width控制按钮宽度,默认 60px - 查询条件重置 :
reloadData()会清空所有查询条件并重新查询 - 提示文本:内容宽度超过列宽时自动显示 ToolTip
九、小结
TablePanel 是一个完整的表格页面解决方案,将查询、表格、分页三大模块整合为一体:
- 查询面板 → 条件筛选
- 工具栏 → 批量操作
- 表格 → 数据展示 + 交互(复选框/开关/操作列)
- 分页 → 大数据量分页加载
使用 TablePanel 可以大幅减少表格页面的重复代码,一套代码适配多种场景。