☰
Java Swing+SQLite+JDBC实战:从零构建可视化本地记账本桌面应用
2026/9/26 6:54:11 网站建设 项目流程

很多Java初学者在掌握了基础语法后,常常会感到迷茫:下一步该做什么?如何将零散的知识点串联成一个完整的、能实际运行的应用?一个常见的痛点就是,虽然理解了类和对象,也学了数据库连接,但面对一个需要界面、数据存储和业务逻辑的完整项目时,却不知从何下手。

本文将以一个经典的练手项目——“可视化本地记账本”为例,带你从零开始,整合Java Swing(GUI界面)、SQLite(嵌入式数据库)和JDBC(数据库连接)三大技术栈。你将亲手搭建一个结构清晰、功能完整的桌面应用,不仅能巩固Java SE核心知识,更能掌握小型桌面应用从设计到实现的完整闭环。无论你是正在寻找课程设计项目的学生,还是希望夯实Java桌面开发基础的开发者,都能从本文获得可直接复用的代码和清晰的实现思路。

1. 项目概述与技术选型

在开始编码之前,我们首先需要明确我们要构建什么,以及为什么选择这些技术。

1.1 项目目标:一个什么样的记账本?

我们的目标是开发一个运行在本地的桌面记账软件,它需要具备以下核心功能:

  1. 记录流水:能够添加新的收入或支出记录,包含金额、类别、日期、备注等信息。
  2. 数据持久化:所有记录必须保存在本地,即使关闭程序,下次打开数据依然存在。
  3. 可视化查看:以清晰的表格形式展示所有历史记录,并支持按条件(如时间范围、类别)筛选。
  4. 数据统计:提供简单的统计功能,如总收入、总支出、结余,以及按类别的消费分析图表。
  5. 数据管理:支持对已有记录的修改和删除操作。

这是一个典型的CRUD(增删改查)应用,非常适合用来练习数据库操作和界面交互。

1.2 为什么选择 Java Swing + SQLite?

  • Java Swing: 它是Java标准库(JFC)的一部分,无需安装任何额外框架或依赖。对于学习GUI编程和理解事件驱动模型来说,Swing是一个绝佳的起点。虽然其外观可能不如现代UI库炫酷,但用于构建功能完备的桌面应用完全足够,且能让你更专注于业务逻辑而非界面美化。
  • SQLite: 它是一个轻量级的、无服务器的、自包含的SQL数据库引擎。整个数据库就是一个单一的.db或.sqlite文件,非常适合嵌入式或桌面应用。我们无需像MySQL或PostgreSQL那样安装和配置数据库服务,JDBC驱动加载后即可直接操作文件,极大简化了部署和分发。
  • JDBC: Java数据库连接的标准API。通过它,我们可以用统一的Java代码来操作SQLite数据库,执行SQL语句。学习JDBC是理解Java后端如何与数据库交互的基础。

这个技术组合零外部依赖(除了SQLite的JDBC驱动jar包),环境搭建简单,能让你快速聚焦于核心开发流程。

2. 环境准备与项目初始化

2.1 开发环境要求

  • 操作系统: Windows 10/11, macOS, Linux (本文示例以Windows环境为主,命令有差异时会说明)。
  • Java SDK: JDK 8 或更高版本 (推荐 JDK 11 或 17 LTS)。确保java和javac命令可以在命令行中运行。
  • 集成开发环境 (IDE): IntelliJ IDEA (社区版即可) 或 Eclipse。本文使用 IntelliJ IDEA 进行演示,因其对Java项目管理和依赖管理非常友好。
  • SQLite JDBC 驱动: 我们需要一个.jar文件来连接SQLite数据库。我们将从Maven中央仓库获取。

2.2 创建项目与导入驱动

  1. 创建新项目: 打开IntelliJ IDEA,选择New Project。左侧选择Java,不要选择Maven或Gradle(为了最简化),直接点击Next。输入项目名称,例如PersonalFinanceLedger,选择项目位置,点击Finish。

  2. 下载 SQLite JDBC 驱动:

    • 访问 Maven 仓库网站,搜索sqlite-jdbc。
    • 选择一个较新的稳定版本(例如3.45.1.0),点击进入详情页。
    • 找到JAR文件的下载链接,下载sqlite-jdbc-3.45.1.0.jar到本地。
  3. 将驱动 Jar 包添加到项目依赖:

    • 在IDEA的项目视图中,右键点击你的项目根目录 ->Open Module Settings(或按F4)。
    • 在弹出窗口左侧选择Libraries。
    • 点击+->Java,然后浏览并选择你刚刚下载的sqlite-jdbc-3.45.1.0.jar文件,点击OK。
    • 确保该库被添加到你的模块依赖中。

2.3 项目目录结构规划

一个清晰的结构有助于代码管理。在项目的src目录下,创建以下包和类:

src/ ├── com.yourname.finance/ │ ├── model/ │ │ └── Transaction.java // 交易数据模型 (Java Bean) │ ├── dao/ │ │ └── TransactionDAO.java // 数据访问对象,负责所有数据库操作 │ ├── util/ │ │ └── DBUtil.java // 数据库连接工具类 │ └── view/ │ ├── MainFrame.java // 程序主窗口 │ ├── AddEditDialog.java // 添加/编辑记录的对话框 │ └── ChartPanel.java // 统计图表面板 (可选) └── resources/ // 存放资源文件,如图标、数据库文件 └── finance.db // SQLite数据库文件 (首次运行后生成)

注意:com.yourname.finance应替换为你自己的包名。

3. 核心模块设计与实现

我们将采用经典的三层架构思想:模型(Model) - 数据访问层(DAO) - 视图(View)。

3.1 模型层 (Model):定义数据结构

首先创建Transaction类,它代表一条记账记录。这是一个简单的Java Bean,包含属性、构造方法、getter和setter。

// 文件路径:src/com/yourname/finance/model/Transaction.java package com.yourname.finance.model; import java.math.BigDecimal; import java.time.LocalDate; /** * 交易记录实体类 */ public class Transaction { private Integer id; // 主键,自增长 private LocalDate date; // 交易日期 private String type; // 类型:INCOME(收入) / EXPENSE(支出) private String category; // 类别:如“餐饮”、“交通”、“工资”等 private BigDecimal amount; // 金额,使用BigDecimal避免精度问题 private String description; // 备注 // 无参构造器 (通常用于反射或框架) public Transaction() { } // 全参构造器 (用于方便创建对象) public Transaction(Integer id, LocalDate date, String type, String category, BigDecimal amount, String description) { this.id = id; this.date = date; this.type = type; this.category = category; this.amount = amount; this.description = description; } // 省略所有属性的 getter 和 setter 方法... // 在IDE中可以使用快捷键(如Alt+Insert)自动生成。 public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public LocalDate getDate() { return date; } public void setDate(LocalDate date) { this.date = date; } public String getType() { return type; } public void setType(String type) { this.type = type; } public String getCategory() { return category; } public void setCategory(String category) { this.category = category; } public BigDecimal getAmount() { return amount; } public void setBigDecimal(BigDecimal amount) { this.amount = amount; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } @Override public String toString() { return "Transaction{" + "id=" + id + ", date=" + date + ", type='" + type + '\'' + ", category='" + category + '\'' + ", amount=" + amount + ", description='" + description + '\'' + '}'; } }

关键点:

  • 使用Integer而非int是为了能表示null(例如新记录尚未存入数据库时)。
  • 使用LocalDate处理日期,比旧的Date类更直观、安全。
  • 使用BigDecimal表示金额,这是金融计算的标准,能避免double或float带来的精度丢失问题。
  • 良好的toString()方法便于调试。

3.2 数据访问层 (DAO):封装数据库操作

这是项目的核心,负责与SQLite数据库交互。我们创建一个TransactionDAO类。

首先,我们需要一个数据库连接工具类DBUtil.java:

// 文件路径:src/com/yourname/finance/util/DBUtil.java package com.yourname.finance.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; /** * 数据库连接工具类 (单例模式) */ public class DBUtil { private static final String DB_URL = "jdbc:sqlite:finance.db"; // 数据库文件相对路径 private static Connection connection = null; // 私有构造器,防止外部实例化 private DBUtil() {} /** * 获取数据库连接 * @return Connection 对象,如果获取失败则返回 null */ public static Connection getConnection() { if (connection == null) { try { // 加载SQLite JDBC驱动 (新版驱动通常会自动注册) Class.forName("org.sqlite.JDBC"); // 建立连接。如果 finance.db 文件不存在,SQLite会自动创建它。 connection = DriverManager.getConnection(DB_URL); System.out.println("数据库连接成功!"); } catch (ClassNotFoundException | SQLException e) { System.err.println("连接数据库失败: " + e.getMessage()); e.printStackTrace(); return null; } } return connection; } /** * 关闭数据库连接 (本项目简单处理,实际可根据需要调整) */ public static void closeConnection() { if (connection != null) { try { connection.close(); connection = null; System.out.println("数据库连接已关闭。"); } catch (SQLException e) { System.err.println("关闭数据库连接时出错: " + e.getMessage()); } } } }

接下来是核心的TransactionDAO.java:

// 文件路径:src/com/yourname/finance/dao/TransactionDAO.java package com.yourname.finance.dao; import com.yourname.finance.model.Transaction; import com.yourname.finance.util.DBUtil; import java.math.BigDecimal; import java.sql.*; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; /** * 交易记录的数据访问对象 */ public class TransactionDAO { /** * 初始化数据库,创建表(如果不存在) */ public void initDatabase() { String sql = "CREATE TABLE IF NOT EXISTS transactions (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "date TEXT NOT NULL," + // SQLite没有专门的DATE类型,用TEXT存储ISO格式字符串 "type TEXT NOT NULL CHECK(type IN ('INCOME', 'EXPENSE'))," + "category TEXT NOT NULL," + "amount DECIMAL(10, 2) NOT NULL," + // 总共10位,小数2位 "description TEXT" + ");"; try (Connection conn = DBUtil.getConnection(); Statement stmt = conn.createStatement()) { stmt.execute(sql); System.out.println("表创建成功或已存在。"); } catch (SQLException e) { System.err.println("初始化数据库失败: " + e.getMessage()); e.printStackTrace(); } } /** * 添加一条交易记录 * @param transaction 要添加的交易对象 * @return 添加成功返回生成的ID,失败返回-1 */ public int addTransaction(Transaction transaction) { String sql = "INSERT INTO transactions (date, type, category, amount, description) VALUES (?, ?, ?, ?, ?)"; int generatedId = -1; try (Connection conn = DBUtil.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { pstmt.setString(1, transaction.getDate().toString()); // LocalDate转String pstmt.setString(2, transaction.getType()); pstmt.setString(3, transaction.getCategory()); pstmt.setBigDecimal(4, transaction.getAmount()); pstmt.setString(5, transaction.getDescription()); int affectedRows = pstmt.executeUpdate(); if (affectedRows > 0) { ResultSet rs = pstmt.getGeneratedKeys(); if (rs.next()) { generatedId = rs.getInt(1); } } } catch (SQLException e) { System.err.println("添加记录失败: " + e.getMessage()); e.printStackTrace(); } return generatedId; } /** * 查询所有交易记录 * @return 交易记录列表 */ public List<Transaction> getAllTransactions() { List<Transaction> list = new ArrayList<>(); String sql = "SELECT * FROM transactions ORDER BY date DESC, id DESC"; // 按日期倒序,最新的在前 try (Connection conn = DBUtil.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql)) { while (rs.next()) { Transaction t = new Transaction(); t.setId(rs.getInt("id")); t.setDate(LocalDate.parse(rs.getString("date"))); // String转LocalDate t.setType(rs.getString("type")); t.setCategory(rs.getString("category")); t.setBigDecimal(rs.getBigDecimal("amount")); t.setDescription(rs.getString("description")); list.add(t); } } catch (SQLException e) { System.err.println("查询所有记录失败: " + e.getMessage()); e.printStackTrace(); } return list; } /** * 根据ID更新一条交易记录 * @param transaction 包含更新信息的交易对象,其id必须有效 * @return 更新是否成功 */ public boolean updateTransaction(Transaction transaction) { String sql = "UPDATE transactions SET date=?, type=?, category=?, amount=?, description=? WHERE id=?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setString(1, transaction.getDate().toString()); pstmt.setString(2, transaction.getType()); pstmt.setString(3, transaction.getCategory()); pstmt.setBigDecimal(4, transaction.getAmount()); pstmt.setString(5, transaction.getDescription()); pstmt.setInt(6, transaction.getId()); int affectedRows = pstmt.executeUpdate(); return affectedRows > 0; } catch (SQLException e) { System.err.println("更新记录失败: " + e.getMessage()); e.printStackTrace(); return false; } } /** * 根据ID删除一条交易记录 * @param id 要删除的记录ID * @return 删除是否成功 */ public boolean deleteTransaction(int id) { String sql = "DELETE FROM transactions WHERE id=?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setInt(1, id); int affectedRows = pstmt.executeUpdate(); return affectedRows > 0; } catch (SQLException e) { System.err.println("删除记录失败: " + e.getMessage()); e.printStackTrace(); return false; } } /** * 根据条件查询交易记录(示例:按月份和类型筛选) * @param yearMonth 格式如 "2024-05" * @param type 类型,可为 null 表示查询所有类型 * @return 符合条件的交易记录列表 */ public List<Transaction> getTransactionsByCondition(String yearMonth, String type) { List<Transaction> list = new ArrayList<>(); StringBuilder sqlBuilder = new StringBuilder("SELECT * FROM transactions WHERE 1=1 "); List<Object> params = new ArrayList<>(); if (yearMonth != null && !yearMonth.trim().isEmpty()) { sqlBuilder.append("AND strftime('%Y-%m', date) = ? "); params.add(yearMonth); } if (type != null && !type.trim().isEmpty()) { sqlBuilder.append("AND type = ? "); params.add(type); } sqlBuilder.append("ORDER BY date DESC"); try (Connection conn = DBUtil.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sqlBuilder.toString())) { for (int i = 0; i < params.size(); i++) { pstmt.setObject(i + 1, params.get(i)); } ResultSet rs = pstmt.executeQuery(); while (rs.next()) { Transaction t = new Transaction(); t.setId(rs.getInt("id")); t.setDate(LocalDate.parse(rs.getString("date"))); t.setType(rs.getString("type")); t.setCategory(rs.getString("category")); t.setBigDecimal(rs.getBigDecimal("amount")); t.setDescription(rs.getString("description")); list.add(t); } } catch (SQLException e) { System.err.println("条件查询失败: " + e.getMessage()); e.printStackTrace(); } return list; } /** * 获取统计信息(总收入、总支出、结余) * @return 一个包含三个BigDecimal的数组:[总收入, 总支出, 结余] */ public BigDecimal[] getStatistics() { BigDecimal totalIncome = BigDecimal.ZERO; BigDecimal totalExpense = BigDecimal.ZERO; String sqlIncome = "SELECT SUM(amount) FROM transactions WHERE type='INCOME'"; String sqlExpense = "SELECT SUM(amount) FROM transactions WHERE type='EXPENSE'"; try (Connection conn = DBUtil.getConnection(); Statement stmt = conn.createStatement()) { ResultSet rsIncome = stmt.executeQuery(sqlIncome); if (rsIncome.next()) { totalIncome = rsIncome.getBigDecimal(1); if (totalIncome == null) totalIncome = BigDecimal.ZERO; } ResultSet rsExpense = stmt.executeQuery(sqlExpense); if (rsExpense.next()) { totalExpense = rsExpense.getBigDecimal(1); if (totalExpense == null) totalExpense = BigDecimal.ZERO; } } catch (SQLException e) { System.err.println("获取统计信息失败: " + e.getMessage()); e.printStackTrace(); } BigDecimal balance = totalIncome.subtract(totalExpense); return new BigDecimal[]{totalIncome, totalExpense, balance}; } }

关键点与最佳实践:

  1. 使用PreparedStatement: 所有带参数的SQL都使用PreparedStatement,可以有效防止SQL注入攻击,并且性能更好。
  2. 资源自动关闭: 使用try-with-resources语法确保Connection,Statement,ResultSet等资源在使用后被自动关闭,避免内存泄漏。
  3. 异常处理: 捕获SQLException并打印日志,在实际项目中应使用日志框架(如Log4j2, SLF4J)而非System.err。
  4. 条件查询构建:getTransactionsByCondition方法展示了如何动态构建SQL语句,这是一种灵活的处理方式。
  5. BigDecimal处理空值: 在统计查询中,如果SUM结果为NULL(表为空时),需要手动转换为BigDecimal.ZERO。

3.3 视图层 (View):构建图形用户界面

这是用户直接交互的部分。我们将创建主窗口MainFrame.java。

// 文件路径:src/com/yourname/finance/view/MainFrame.java package com.yourname.finance.view; import com.yourname.finance.dao.TransactionDAO; import com.yourname.finance.model.Transaction; import javax.swing.*; import javax.swing.table.DefaultTableModel; import java.awt.*; import java.awt.event.*; import java.math.BigDecimal; import java.util.List; /** * 记账本主界面 */ public class MainFrame extends JFrame { private TransactionDAO transactionDAO; private JTable table; private DefaultTableModel tableModel; private JLabel lblIncome, lblExpense, lblBalance; // 表格列名 private final String[] columnNames = {"ID", "日期", "类型", "类别", "金额", "备注"}; public MainFrame() { transactionDAO = new TransactionDAO(); initDatabase(); initUI(); loadTransactionData(); updateStatistics(); } private void initDatabase() { transactionDAO.initDatabase(); } private void initUI() { setTitle("个人记账本 - Java Swing + SQLite"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setSize(900, 600); setLocationRelativeTo(null); // 窗口居中 // 1. 顶部按钮面板 JPanel topPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); JButton btnAdd = new JButton("新增记录"); JButton btnEdit = new JButton("编辑记录"); JButton btnDelete = new JButton("删除记录"); JButton btnRefresh = new JButton("刷新"); JButton btnChart = new JButton("查看图表"); topPanel.add(btnAdd); topPanel.add(btnEdit); topPanel.add(btnDelete); topPanel.add(btnRefresh); topPanel.add(btnChart); // 2. 中间表格面板 tableModel = new DefaultTableModel(columnNames, 0) { @Override public boolean isCellEditable(int row, int column) { // 禁止直接编辑表格单元格,通过按钮操作 return false; } }; table = new JTable(tableModel); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // 单选 JScrollPane scrollPane = new JScrollPane(table); // 3. 底部统计信息面板 JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); lblIncome = new JLabel("总收入: 0.00"); lblExpense = new JLabel("总支出: 0.00"); lblBalance = new JLabel("结余: 0.00"); lblIncome.setForeground(Color.GREEN.darker()); lblExpense.setForeground(Color.RED); lblBalance.setForeground(Color.BLUE); bottomPanel.add(lblIncome); bottomPanel.add(Box.createHorizontalStrut(20)); // 添加间距 bottomPanel.add(lblExpense); bottomPanel.add(Box.createHorizontalStrut(20)); bottomPanel.add(lblBalance); // 4. 组装主界面 setLayout(new BorderLayout()); add(topPanel, BorderLayout.NORTH); add(scrollPane, BorderLayout.CENTER); add(bottomPanel, BorderLayout.SOUTH); // 5. 按钮事件监听 btnAdd.addActionListener(e -> addTransaction()); btnEdit.addActionListener(e -> editTransaction()); btnDelete.addActionListener(e -> deleteTransaction()); btnRefresh.addActionListener(e -> refreshData()); btnChart.addActionListener(e -> showChart()); } /** * 加载数据到表格 */ private void loadTransactionData() { // 清空现有数据 tableModel.setRowCount(0); List<Transaction> transactions = transactionDAO.getAllTransactions(); for (Transaction t : transactions) { Object[] rowData = { t.getId(), t.getDate().toString(), t.getType(), t.getCategory(), t.getAmount(), t.getDescription() }; tableModel.addRow(rowData); } } /** * 更新底部统计信息 */ private void updateStatistics() { BigDecimal[] stats = transactionDAO.getStatistics(); lblIncome.setText(String.format("总收入: %.2f", stats[0])); lblExpense.setText(String.format("总支出: %.2f", stats[1])); lblBalance.setText(String.format("结余: %.2f", stats[2])); } /** * 刷新数据(重新加载表格和统计) */ private void refreshData() { loadTransactionData(); updateStatistics(); JOptionPane.showMessageDialog(this, "数据已刷新!", "提示", JOptionPane.INFORMATION_MESSAGE); } /** * 新增记录 */ private void addTransaction() { // 这里可以弹出一个自定义的对话框(AddEditDialog) // 为了简化,我们先使用JOptionPane输入 // 实际项目中强烈建议创建一个独立的Dialog类 JTextField dateField = new JTextField(java.time.LocalDate.now().toString()); JComboBox<String> typeCombo = new JComboBox<>(new String[]{"INCOME", "EXPENSE"}); JTextField categoryField = new JTextField(); JTextField amountField = new JTextField(); JTextField descField = new JTextField(); Object[] message = { "日期 (YYYY-MM-DD):", dateField, "类型:", typeCombo, "类别:", categoryField, "金额:", amountField, "备注:", descField }; int option = JOptionPane.showConfirmDialog(this, message, "新增记录", JOptionPane.OK_CANCEL_OPTION); if (option == JOptionPane.OK_OPTION) { try { Transaction t = new Transaction(); t.setDate(java.time.LocalDate.parse(dateField.getText().trim())); t.setType((String) typeCombo.getSelectedItem()); t.setCategory(categoryField.getText().trim()); t.setBigDecimal(new BigDecimal(amountField.getText().trim())); t.setDescription(descField.getText().trim()); int newId = transactionDAO.addTransaction(t); if (newId > 0) { refreshData(); JOptionPane.showMessageDialog(this, "记录添加成功!ID: " + newId, "成功", JOptionPane.INFORMATION_MESSAGE); } else { JOptionPane.showMessageDialog(this, "记录添加失败!", "错误", JOptionPane.ERROR_MESSAGE); } } catch (Exception ex) { JOptionPane.showMessageDialog(this, "输入数据格式错误!\n" + ex.getMessage(), "输入错误", JOptionPane.ERROR_MESSAGE); } } } /** * 编辑选中记录 */ private void editTransaction() { int selectedRow = table.getSelectedRow(); if (selectedRow == -1) { JOptionPane.showMessageDialog(this, "请先选择要编辑的记录!", "提示", JOptionPane.WARNING_MESSAGE); return; } // 获取选中行的数据 int id = (int) tableModel.getValueAt(selectedRow, 0); String date = (String) tableModel.getValueAt(selectedRow, 1); String type = (String) tableModel.getValueAt(selectedRow, 2); String category = (String) tableModel.getValueAt(selectedRow, 3); BigDecimal amount = (BigDecimal) tableModel.getValueAt(selectedRow, 4); String desc = (String) tableModel.getValueAt(selectedRow, 5); // 弹出编辑对话框(这里同样简化,实际应使用独立Dialog) JTextField dateField = new JTextField(date); JComboBox<String> typeCombo = new JComboBox<>(new String[]{"INCOME", "EXPENSE"}); typeCombo.setSelectedItem(type); JTextField categoryField = new JTextField(category); JTextField amountField = new JTextField(amount.toString()); JTextField descField = new JTextField(desc); Object[] message = { "日期 (YYYY-MM-DD):", dateField, "类型:", typeCombo, "类别:", categoryField, "金额:", amountField, "备注:", descField }; int option = JOptionPane.showConfirmDialog(this, message, "编辑记录", JOptionPane.OK_CANCEL_OPTION); if (option == JOptionPane.OK_OPTION) { try { Transaction t = new Transaction(); t.setId(id); t.setDate(java.time.LocalDate.parse(dateField.getText().trim())); t.setType((String) typeCombo.getSelectedItem()); t.setCategory(categoryField.getText().trim()); t.setBigDecimal(new BigDecimal(amountField.getText().trim())); t.setDescription(descField.getText().trim()); boolean success = transactionDAO.updateTransaction(t); if (success) { refreshData(); JOptionPane.showMessageDialog(this, "记录更新成功!", "成功", JOptionPane.INFORMATION_MESSAGE); } else { JOptionPane.showMessageDialog(this, "记录更新失败!", "错误", JOptionPane.ERROR_MESSAGE); } } catch (Exception ex) { JOptionPane.showMessageDialog(this, "输入数据格式错误!\n" + ex.getMessage(), "输入错误", JOptionPane.ERROR_MESSAGE); } } } /** * 删除选中记录 */ private void deleteTransaction() { int selectedRow = table.getSelectedRow(); if (selectedRow == -1) { JOptionPane.showMessageDialog(this, "请先选择要删除的记录!", "提示", JOptionPane.WARNING_MESSAGE); return; } int id = (int) tableModel.getValueAt(selectedRow, 0); int confirm = JOptionPane.showConfirmDialog(this, "确定要删除ID为 " + id + " 的记录吗?", "确认删除", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); if (confirm == JOptionPane.YES_OPTION) { boolean success = transactionDAO.deleteTransaction(id); if (success) { refreshData(); JOptionPane.showMessageDialog(this, "记录删除成功!", "成功", JOptionPane.INFORMATION_MESSAGE); } else { JOptionPane.showMessageDialog(this, "记录删除失败!", "错误", JOptionPane.ERROR_MESSAGE); } } } /** * 显示统计图表(功能预留,可扩展) */ private void showChart() { JOptionPane.showMessageDialog(this, "图表功能待实现,可集成JFreeChart等库。", "功能预览", JOptionPane.INFORMATION_MESSAGE); // 未来可以在这里创建并显示一个ChartPanel } public static void main(String[] args) { // 使用SwingUtilities确保GUI创建在事件分发线程上 SwingUtilities.invokeLater(() -> { MainFrame frame = new MainFrame(); frame.setVisible(true); }); } }

3.4 运行与测试

  1. 确保所有代码已正确编写,项目结构完整。
  2. 在MainFrame类中右键,选择Run 'MainFrame.main()'。
  3. 程序启动后,会自动在项目根目录下创建finance.db数据库文件(如果不存在)。
  4. 点击“新增记录”按钮,输入测试数据。
  5. 观察表格是否成功显示数据,底部统计信息是否正确更新。
  6. 尝试编辑和删除功能。

至此,一个具备基本CRUD功能的可视化记账本已经完成!

4. 功能扩展与优化建议

基础版本已经可用,但一个健壮、好用的应用还需要更多打磨。以下是一些扩展方向:

4.1 改进用户界面 (UI/UX)

  • 使用更专业的对话框: 将JOptionPane输入替换为自定义的JDialog,例如AddEditDialog。可以设计更合理的布局,添加日期选择器(JDatePicker),下拉框预置常用类别等。
  • 表格美化: 设置表格行高、列宽,为“收入”和“支出”设置不同的颜色(如收入绿色、支出红色)。
  • 菜单栏: 添加JMenuBar,包含“文件”(退出、导出)、“编辑”(增删改)、“视图”(筛选)、“帮助”等菜单。
  • 筛选与搜索: 在界面顶部增加筛选组件,如按月份筛选的下拉框、按类别筛选的组合框、关键词搜索框等,并连接TransactionDAO中的条件查询方法。

4.2 增强数据层 (DAO)

  • 类别管理: 创建独立的Category表和CategoryDAO,实现类别的增删改查,并在记账时从数据库动态加载类别,而不是硬编码。
  • 分页查询: 当数据量很大时,实现分页功能,避免一次性加载所有数据。
  • 数据库连接池: 使用如 HikariCP 等连接池管理数据库连接,提升性能(对于SQLite桌面应用,此需求不强烈,但作为学习很好)。
  • 事务处理: 对于涉及多个步骤的操作(如批量删除),使用Connection的setAutoCommit(false)、commit()、rollback()来保证数据一致性。

4.3 添加数据可视化

  • 集成图表库: 使用JFreeChart这个强大的Java图表库来生成统计图表。
    1. 在项目中添加 JFreeChart 依赖(通过Maven或下载JAR)。
    2. 创建ChartPanel类,编写方法生成饼图(按类别支出比例)、柱状图(月度收支对比)等。
    3. 在主界面点击“查看图表”时,弹出一个新的JFrame或JDialog来展示ChartPanel。

4.4 数据导入导出

  • 导出为Excel/CSV: 使用Apache POI或OpenCSV库,将表格数据导出为.xlsx或.csv文件,方便用户用其他软件分析。
  • 数据备份与恢复: 提供功能将整个finance.db文件复制到指定位置进行备份,或从备份文件恢复。

4.5 异常处理与用户体验

  • 统一的错误提示: 使用更友好的对话框提示错误,而不是在控制台打印堆栈。
  • 输入验证: 在对话框提交前,验证日期格式、金额是否为数字、必填项是否为空等。
  • 日志记录: 引入SLF4J + Logback记录程序运行日志、用户操作和错误信息,便于排查问题。

5. 常见问题与排查思路

在开发过程中,你可能会遇到以下问题:

问题现象可能原因解决思路
程序启动时报ClassNotFoundException: org.sqlite.JDBCSQLite JDBC驱动JAR包未正确添加到项目依赖中。1. 检查Project Structure->Libraries中是否有sqlite-jdbc库。
2. 检查JAR文件路径是否正确,尝试删除后重新添加。
3. 如果使用Maven/Gradle,检查pom.xml或build.gradle依赖是否正确。
运行后找不到finance.db文件或数据未保存数据库文件路径问题。jdbc:sqlite:finance.db是相对路径,它会在程序启动的当前工作目录下创建文件。在IDE中运行,工作目录通常是项目根目录。1. 在项目根目录下寻找finance.db文件。
2. 使用绝对路径,如jdbc:sqlite:C:/Users/YourName/data/finance.db。
3. 在代码中打印new File(".").getAbsolutePath()查看当前工作目录。
表格中金额显示为科学计数法或格式不对BigDecimal在JTable中默认的渲染器可能不是我们想要的货币格式。1. 自定义表格的列渲染器。为金额列设置一个DefaultTableCellRenderer,重写其getTableCellRendererComponent方法,将数值格式化为两位小数。
2. 或者在向tableModel添加行数据时,将BigDecimal先格式化为String。
编辑/删除操作后,表格数据没刷新操作数据库成功后,没有刷新tableModel中的数据。确保在addTransaction,updateTransaction,deleteTransaction等方法执行成功后,调用loadTransactionData()或refreshData()重新从数据库加载数据并更新表格模型。
界面布局错乱或按钮无响应Swing组件创建和更新未在事件分发线程(EDT)上执行。确保所有对Swing组件的创建和修改都包装在SwingUtilities.invokeLater()中。主方法中的setVisible(true)已经这样做了。对于后台线程(如从数据库加载大量数据)更新UI时,也必须使用SwingUtilities.invokeLater()。
日期处理报错DateTimeParseException输入的日期字符串格式与LocalDate.parse()期望的ISO格式 (yyyy-MM-dd) 不符。1. 在前端使用日期选择器控件,避免手动输入。
2. 如果必须手动输入,在解析前进行格式校验,或使用DateTimeFormatter指定格式进行解析。

6. 项目总结与学习路线

通过这个“Java Swing + SQLite 记账本”项目,我们完成了一个小型桌面应用的完整开发流程。你不仅实践了Java核心语法,更重要的是掌握了如何将界面(Swing)、业务逻辑(Java Bean/Service)和数据持久化(JDBC + SQLite)三层清晰地组织在一起,这是任何规模应用开发的基础思想。

本文涉及的核心技能点回顾:

  • Java SE: 面向对象、集合框架、异常处理、时间日期API (LocalDate)、数值计算 (BigDecimal)。
  • Swing GUI: 窗口 (JFrame)、面板 (JPanel)、表格 (JTable)、按钮 (JButton)、标签 (JLabel)、对话框 (JOptionPane)、布局管理器 (BorderLayout,FlowLayout)、事件监听 (ActionListener)。
  • JDBC: 加载驱动、获取连接 (Connection)、执行语句 (Statement,PreparedStatement)、处理结果集 (ResultSet)、事务控制基础。
  • SQLite: 嵌入式数据库的使用,基本的SQL语法(CREATE TABLE,INSERT,SELECT,UPDATE,DELETE,SUM)。
  • 项目结构: 分层架构(Model, DAO, View),包的组织,工具类的编写。

下一步可以深入学习的方向:

  1. 升级技术栈: 尝试用JavaFX替代 Swing 构建更现代、更易维护的UI。学习使用Maven或Gradle管理项目依赖。
  2. 深化数据库: 学习更复杂的SQL查询(多表连接、子查询、索引优化)。尝试使用MyBatis或Hibernate这样的ORM框架来简化数据库操作。
  3. 架构演进: 将业务逻辑从DAO和View中抽离出来,形成独立的Service层,使结构更清晰。学习设计模式,如单例模式(我们的DBUtil)、观察者模式(Swing事件模型本身就是)等。
  4. 工程化实践: 为项目添加单元测试(JUnit),学习使用版本控制(Git),编写项目文档(README)。

这个项目是一个绝佳的起点,你可以基于它不断添加新功能,例如多用户登录、预算管理、周期性账单、数据报表等,在实践中持续提升你的Java全栈开发能力。动手将文中的代码敲一遍,并尝试实现一个扩展功能,是巩固学习成果的最佳方式。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询