一、项目背景详细介绍

在企业级桌面应用中,表格(JTable)是展示结构化数据的核心组件,用于显示数据库查询结果、配置参数表、报表统计等。通常开发者需要根据业务场景动态地设置表格列的名称及每列宽度,以便优化可视化效果,提升用户操作体验。

虽然 JTable 本身支持通过 TableModel 获取列名,也可通过 TableColumnModel 调整列宽,但在实际开发中,列名和列宽往往来自配置文件、用户偏好或上层业务逻辑,需要以数组或集合方式批量初始化。手动逐列设置既繁琐又易错,且不利于后续维护与扩展。

本项目旨在通过 Java Swing 的标准 API,演示如何使用两个并行数组——一个存放列名(String[] columnNames),一个存放列宽(int[] columnWidths)——来一次性初始化 JTable 的表头及宽度,并提供简洁易用的封装类,以便在各种桌面应用中快速复用。


二、项目需求详细介绍

功能需求

  1. 动态设置表头列名

    • 接收一个 String[] columnNames,长度与列数一致,用于设置 JTable 的每列标题。

  2. 动态设置列宽

    • 接收一个 int[] columnWidths,长度与列数一致,单位为像素,用于设置每列的首选宽度。

  3. 数据模型兼容

    • 支持任意 TableModel 实例,如 DefaultTableModel 或自定义 AbstractTableModel,在构造表格时注入。

  4. 封装组件

    • 提供一个 TableUtil 工具类或 CustomTablePanel 组件,用户只需传入列名数组、列宽数组和数据模型,即可完成初始化。

  5. 异常与边界处理

    • 当列名数组与列宽数组长度不一致时,抛出自定义 InvalidTableConfigException 并提示原因。

    • 当数组长度与模型列数不匹配时,同样抛出异常并指出预期与实际列数。

  6. 可视化示例

    • 在一个示例 JFrame 中展示配置好的 JTable,并支持修改列宽后保存用户偏好。

非功能需求

  1. 可维护性

    • 代码清晰、注释完整,对外暴露的 API 简洁易懂。

  2. 可扩展性

    • 后期可在 TableUtil 中加入列对齐、排序器、渲染器等更多功能。

  3. 性能要求

    • 即使列数较多(>50),初始化速度应在 100ms 内完成。

  4. 易用性与美观性

    • 默认采用表格网格线和可调节列宽功能;支持 Look and Feel 切换。

  5. 测试覆盖率

    • 包含 JUnit 单元测试,验证异常场景与正确配置。


三、相关技术详细介绍

  1. Swing 核心组件

    • JTable:核心表格控件;

    • TableModelDefaultTableModel:表格数据模型;

    • JScrollPane:为表格提供滚动支持;

    • TableColumnTableColumnModel:管理单列的属性(如宽度、排序器、渲染器)。

  2. 异常处理

    • 自定义异常 InvalidTableConfigException(继承 RuntimeException),用于列配置错误提示。

  3. 布局管理器

    • BorderLayout:将表格置于中心、控制按钮置于南部;

    • 可选 BoxLayoutFlowLayout:用于放置示例按钮。

  4. MVC 设计模式

    • Model:表格数据通过 TableModel 提供;

    • ViewCustomTablePanel 封装 JTable 与滚动面板;

    • Controller/UtilTableUtil 提供静态方法完成列名与列宽的批量设置。

  5. 测试框架

    • JUnit 5:编写单元测试;

    • 可以结合 AssertJ Swing 进行简单 UI 自动化测试。

  6. 构建管理

    • Maven:依赖与构建;

    • Surefire 插件:运行单元测试。


四、实现思路详细介绍

  1. 配置校验

    • 在工具类入口方法(如 TableUtil.setupTable(JTable table, String[] names, int[] widths))中,首先比较 names.lengthwidths.lengthtable.getColumnCount(),不匹配则抛出 InvalidTableConfigException

  2. 表头设置

    • 调用 TableColumnModel colModel = table.getColumnModel()

    • 遍历每一列索引 i

colModel.getColumn(i).setHeaderValue(names[i]);

列宽设置

  • 在同一循环中,继续调用:

TableColumn col = colModel.getColumn(i);
col.setPreferredWidth(widths[i]);
col.setMinWidth( Math.max(50, widths[i]/2) );
col.setMaxWidth(widths[i]*2);
  • 刷新表头

    • 完成后调用 table.getTableHeader().repaint(),使新表头立即生效。

  • 封装方法

    • 将上述逻辑封装在 TableUtil 的静态方法里,并针对常见场景提供重载,如:

public static void setupTable(JTable table, Object[][] data, String[] names, int[] widths)
    • 一次性创建模型并设置。

  • 示例面板

    • 实现 CustomTablePanel:构造时传入 datanameswidths,内部完成模型构建与工具调用;并提供方法 getTable() 以供业务调用。

五、完整实现代码

// ========== File: InvalidTableConfigException.java ==========
package com.example.tableutil.exception;

/**
 * 自定义异常:表格配置无效
 */
public class InvalidTableConfigException extends RuntimeException {
    public InvalidTableConfigException(String message) {
        super(message);
    }
}

// ========== File: TableUtil.java ==========
package com.example.tableutil.core;

import com.example.tableutil.exception.InvalidTableConfigException;

import javax.swing.*;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import javax.swing.table.DefaultTableModel;

/**
 * 工具类:使用数组批量设置 JTable 的列名与列宽
 */
public class TableUtil {

    /**
     * 根据已有 JTable 与其模型,使用数组批量设置列名与列宽
     * @param table        目标 JTable
     * @param columnNames  列名数组,长度必须等于 table.getColumnCount()
     * @param columnWidths 列宽数组,长度必须等于 table.getColumnCount()
     */
    public static void setupTable(JTable table, String[] columnNames, int[] columnWidths) {
        int colCount = table.getColumnCount();
        if (columnNames == null || columnWidths == null) {
            throw new InvalidTableConfigException("列名数组或列宽数组不能为空");
        }
        if (columnNames.length != colCount || columnWidths.length != colCount) {
            throw new InvalidTableConfigException(
                String.format("配置数组长度与列数不匹配:期望 %d,列名数组 %d,列宽数组 %d",
                              colCount, columnNames.length, columnWidths.length));
        }
        TableColumnModel colModel = table.getColumnModel();
        for (int i = 0; i < colCount; i++) {
            TableColumn column = colModel.getColumn(i);
            // 设置列名
            column.setHeaderValue(columnNames[i]);
            // 设置首选宽度及最小/最大宽度
            column.setPreferredWidth(columnWidths[i]);
            column.setMinWidth(Math.max(50, columnWidths[i] / 2));
            column.setMaxWidth(columnWidths[i] * 2);
        }
        // 刷新表头
        table.getTableHeader().repaint();
    }

    /**
     * 直接根据数据数组与配置数组创建 DefaultTableModel 并初始化 JTable
     * @param table        目标 JTable
     * @param rowData      2D 数据数组
     * @param columnNames  列名数组
     * @param columnWidths 列宽数组
     */
    public static void initTable(JTable table, Object[][] rowData, String[] columnNames, int[] columnWidths) {
        if (rowData == null) rowData = new Object[0][columnNames.length];
        DefaultTableModel model = new DefaultTableModel(rowData, columnNames);
        table.setModel(model);
        setupTable(table, columnNames, columnWidths);
    }
}

// ========== File: CustomTablePanel.java ==========
package com.example.tableutil.ui;

import com.example.tableutil.core.TableUtil;
import com.example.tableutil.exception.InvalidTableConfigException;

import javax.swing.*;
import java.awt.*;

/**
 * 封装面板:接收列名、列宽与数据,自动构建可滚动的 JTable
 */
public class CustomTablePanel extends JPanel {
    private final JTable table;

    /**
     * 构造函数
     * @param rowData      表格数据
     * @param columnNames  列名数组
     * @param columnWidths 列宽数组
     */
    public CustomTablePanel(Object[][] rowData, String[] columnNames, int[] columnWidths) {
        if (columnNames == null || columnWidths == null) {
            throw new InvalidTableConfigException("列名或列宽数组不能为空");
        }
        if (columnNames.length != columnWidths.length) {
            throw new InvalidTableConfigException("列名数组与列宽数组长度不一致");
        }
        setLayout(new BorderLayout());
        table = new JTable();
        // 初始化模型与列宽
        TableUtil.initTable(table, rowData, columnNames, columnWidths);
        JScrollPane scroll = new JScrollPane(table);
        add(scroll, BorderLayout.CENTER);
    }

    public JTable getTable() {
        return table;
    }
}

// ========== File: MainFrame.java ==========
package com.example.tableutil.app;

import com.example.tableutil.ui.CustomTablePanel;

import javax.swing.*;
import java.awt.*;

/**
 * 示例程序:展示 CustomTablePanel 的使用
 */
public class MainFrame {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            // 示例数据
            String[] columnNames = {"ID", "姓名", "年龄", "邮箱", "注册时间"};
            int[] columnWidths = {50, 100, 50, 200, 150};
            Object[][] data = {
                {1, "张三", 28, "zhang3@example.com", "2025-01-10"},
                {2, "李四", 32, "li4@example.com", "2024-12-05"},
                {3, "王五", 45, "wang5@example.com", "2023-07-20"}
            };

            JFrame frame = new JFrame("JTable 列名与列宽示例");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setSize(600, 300);
            frame.setLayout(new BorderLayout());
            frame.add(new CustomTablePanel(data, columnNames, columnWidths), BorderLayout.CENTER);
            frame.setVisible(true);
        });
    }
}

// ========== File: TableUtilTest.java ==========
package com.example.tableutil.test;

import com.example.tableutil.core.TableUtil;
import com.example.tableutil.exception.InvalidTableConfigException;
import org.junit.jupiter.api.Test;

import javax.swing.JTable;
import javax.swing.table.DefaultTableModel;

import static org.junit.jupiter.api.Assertions.*;

/**
 * 单元测试:验证 TableUtil 的配置校验与正确性
 */
public class TableUtilTest {

    @Test
    void testSetupTableSuccess() {
        String[] names = {"A", "B", "C"};
        int[] widths = {60, 80, 100};
        Object[][] data = {{1,2,3},{4,5,6}};
        JTable table = new JTable(new DefaultTableModel(data, names));
        assertDoesNotThrow(() -> TableUtil.setupTable(table, names, widths));
        assertEquals("B", table.getColumnModel().getColumn(1).getHeaderValue());
        assertEquals(80, table.getColumnModel().getColumn(1).getPreferredWidth());
    }

    @Test
    void testSetupTableInvalidLength() {
        String[] names = {"A", "B"};
        int[] widths = {50, 50, 50};
        JTable table = new JTable(new DefaultTableModel(new Object[0][2], new String[]{"X","Y"}));
        assertThrows(InvalidTableConfigException.class, () -> TableUtil.setupTable(table, names, widths));
    }

    @Test
    void testInitTableSetsModelAndColumns() {
        String[] names = {"X","Y"};
        int[] widths = {70, 90};
        Object[][] data = {{10,20}};
        JTable table = new JTable();
        TableUtil.initTable(table, data, names, widths);
        assertEquals(2, table.getColumnCount());
        assertEquals(90, table.getColumnModel().getColumn(1).getPreferredWidth());
    }
}

六、代码详细解读

  1. InvalidTableConfigException

    • 继承自 RuntimeException,用于表格列名与列宽配置不匹配时的统一异常提示。

  2. TableUtil

    • setupTable(JTable, String[], int[]):核心方法,先校验数组长度与列数是否一致,抛出自定义异常;再通过 TableColumnModel 遍历每列,设置 headerValue 与宽度(首选、最小、最大),完成后重绘表头。

    • initTable(JTable, Object[][], String[], int[]):便捷重载,直接创建 DefaultTableModel 并调用 setupTable,简化一次性建表操作。

  3. CustomTablePanel

    • 封装了一个带滚动条的 JTable 面板,构造时接收数据数组、列名数组与列宽数组,内部调用 TableUtil 完成初始化,并通过 getTable() 提供访问。

  4. MainFrame

    • Swing 应用入口,演示如何构造 CustomTablePanel 并将其加入 JFrame。示例数据包括 ID、姓名、年龄、邮箱、注册时间五列,分别设置不同宽度。

  5. TableUtilTest

    • 使用 JUnit 5 对 TableUtil 进行测试:

      • 校验正常配置不抛异常,且列名与宽度正确生效;

      • 校验数组长度不匹配时抛 InvalidTableConfigException

      • 校验 initTable 方法能正确设置模型与列宽。


七、项目详细总结

本项目通过 TableUtil 工具类与 CustomTablePanel 封装组件,实现了基于数组批量配置 JTable 列名与列宽的高效方案,具备以下优势:

  1. 简洁易用:调用静态方法或封装面板即可,一行代码完成表头与宽度初始化。

  2. 健壮性:通过自定义异常与严格校验,避免数组长度不匹配导致的潜在错误。

  3. 可扩展性:后续可在 TableUtil 增加列对齐、排序器、渲染器、编辑器等功能。

  4. 复用性CustomTablePanel 可直接用于任何 Swing 应用场景,减少重复代码。

  5. 测试覆盖:核心方法通过单元测试验证,确保在重构或扩展时功能稳定。


八、项目常见问题及解答

  1. Q:如果只想设置部分列的宽度怎么办?
    A:可手动调用 table.getColumnModel().getColumn(i).setPreferredWidth(width),或对 columnWidths 数组填入原 table.getColumnModel().getColumn(i).getPreferredWidth() 以保留不变。

  2. Q:列名或列宽数组为 null 时如何处理?
    A:TableUtilCustomTablePanel 均在入口进行非空校验,并抛出 InvalidTableConfigException 提示调用方修正。

  3. Q:如何在运行时保存用户调整后的列宽?
    A:可在窗口关闭或某个保存按钮事件中,通过遍历 table.getColumnModel().getColumn(i).getWidth() 获取当前列宽,并将其保存到配置文件或数据库。

  4. Q:能否对表格列进行对齐或自定义渲染?
    A:可以在 TableUtil 中再遍历列调用 column.setCellRenderer(...)column.setCellEditor(...),注入自定义 DefaultTableCellRenderer 实现文本或图标对齐。

  5. Q:如果数据量很大,性能会受影响吗?
    A:列数通常远小于行数,本项目仅对列进行一次性设置,性能开销极小。若需要优化行渲染,可使用虚拟化技术或使用更轻量的渲染器。


九、扩展方向与性能优化

  1. 列对齐与复杂渲染

    • 增加对齐参数数组(如 int[] alignments),在 setupTable 中调用 column.setCellRenderer 统一对齐。

    • 支持通过数组传入渲染器实例(TableCellRenderer[])实现单列自定义渲染。

  2. 列排序与过滤

    • 集成 TableRowSorter,提供按列排序功能;可封装 initTable 重载以自动启用排序。

  3. 持久化用户配置

    • CustomTablePanel 增加保存与加载方法,将列宽、列顺序等写入 Properties 或 JSON 文件,实现用户偏好记忆。

  4. 国际化支持

    • 将列名抽离到 ResourceBundle,支持多语言切换;CustomTablePanel 构造时根据 Locale 自动加载。

  5. 响应式布局

    • 结合 JSplitPane 或自适应布局,当窗口宽度变化时,动态调整列宽比例,保证重要列始终可见。

  6. UI 美化与主题

    • 支持 UIManager 外观切换(如 Nimbus、Metal、Windows),并在 CustomTablePanel 中暴露主题接口,以便统一风格。

  7. 批量操作工具

    • TableUtil 增加列批量隐藏/显示、锁定/解锁等方法,进一步提升表格管理能力。

  8. 与 ORM 框架集成

    • 封装可以直接接入 Hibernate、MyBatis 查询结果的 TableModel,并自动更新列名与宽度配置。

  9. 性能监控

    • TableUtil 中加入日志记录,对初始化、重新设置列宽等操作计时,并提供日志输出,可用于性能分析与优化。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐