引言:为什么选择Tkinter开发个人记账本

Python Tkinter是Python标准库中内置的GUI开发工具,它不需要额外安装,非常适合初学者快速构建桌面应用程序。个人记账本是一个非常实用的项目,它涵盖了GUI编程的核心概念:界面布局、事件处理、数据存储、文件操作等。通过这个项目,你不仅能掌握Tkinter的基本用法,还能学习如何解决实际开发中常见的性能问题和数据安全问题。

在本教程中,我们将从零开始,逐步构建一个功能齐全的个人记账本应用。这个应用将具备以下功能:

  • 收入和支出记录的添加、删除和修改
  • 数据持久化存储(使用SQLite数据库)
  • 数据统计和可视化(图表展示)
  • 界面优化,避免卡顿
  • 数据备份和恢复机制,防止数据丢失

让我们开始吧!

第一部分:环境准备和项目结构设计

1.1 环境准备

确保你的电脑上安装了Python 3.x版本。Tkinter通常随Python一起安装,你可以通过以下命令检查是否已安装:

import tkinter
print(tkinter.TkVersion)

如果未安装,可以通过包管理器安装(例如在Ubuntu上:sudo apt-get install python3-tk)。

1.2 项目结构设计

为了保持代码的清晰和可维护性,我们将项目分为以下几个模块:

  • main.py:主程序入口,负责创建主窗口和启动应用
  • ui.py:包含所有GUI界面的代码,如主窗口、按钮、输入框等
  • database.py:负责数据库操作,包括数据的增删改查
  • utils.py:工具函数,如数据验证、备份恢复等
  • config.py:配置文件,定义常量如数据库路径、备份路径等

这种模块化设计有助于分离关注点,便于后期维护和扩展。

第二部分:构建基础GUI界面

2.1 创建主窗口

首先,我们创建一个主窗口,并设置基本的标题和大小。在main.py中编写以下代码:

import tkinter as tk
from ui import MainWindow

def main():
    root = tk.Tk()
    root.title("个人记账本")
    root.geometry("800x600")  # 设置窗口大小
    app = MainWindow(root)
    root.mainloop()

if __name__ == "__main__":
    main()

2.2 设计主界面布局

ui.py中,我们定义MainWindow类。主界面采用经典的布局:顶部是菜单栏,左侧是导航栏,右侧是主要内容区域(使用Frame切换)。我们使用grid布局管理器来精确控制组件位置。

import tkinter as tk
from tkinter import ttk, messagebox
import database as db
from utils import validate_amount, backup_data, restore_data

class MainWindow:
    def __init__(self, master):
        self.master = master
        self.create_menu()
        self.create_sidebar()
        self.create_main_area()
        self.current_frame = None
        self.show_home()  # 默认显示首页

    def create_menu(self):
        menu_bar = tk.Menu(self.master)
        # 文件菜单
        file_menu = tk.Menu(menu_bar, tearoff=0)
        file_menu.add_command(label="备份数据", command=self.backup_data)
        file_menu.add_command(label="恢复数据", command=self.restore_data)
        file_menu.add_separator()
        file_menu.add_command(label="退出", command=self.master.quit)
        menu_bar.add_cascade(label="文件", menu=file_menu)
        self.master.config(menu=menu_bar)

    def create_sidebar(self):
        sidebar = tk.Frame(self.master, width=150, bg="lightgray")
        sidebar.grid(row=1, column=0, sticky="nswe", padx=5, pady=5)
        
        btn_home = tk.Button(sidebar, text="首页", command=self.show_home, width=15)
        btn_home.grid(row=0, column=0, pady=5)
        
        btn_add = tk.Button(sidebar, text="添加记录", command=self.show_add, width=15)
        btn_add.grid(row=1, column=0, pady=5)
        
        btn_view = tk.Button(sidebar, text="查看记录", command=self.show_view, width=15)
        btn_view.grid(row=2, column=0, pady=5)
        
        btn_stats = tk.Button(sidebar, text="统计图表", command=self.show_stats, width=15)
        btn_stats.grid(row=3, column=0, pady=5)

    def create_main_area(self):
        self.main_area = tk.Frame(self.master)
        self.main_area.grid(row=1, column=1, sticky="nswe", padx=5, pady=5)
        # 配置列权重,使主区域可伸缩
        self.master.columnconfigure(1, weight=1)
        self.master.rowconfigure(1, weight=1)

    def show_home(self):
        self.clear_main_area()
        self.current_frame = HomeFrame(self.main_area)

    def show_add(self):
        self.clear_main_area()
        self.current_frame = AddRecordFrame(self.main_area)

    def show_view(self):
        self.clear_main_area()
        self.current_frame = ViewRecordsFrame(self.main_area)

    def show_stats(self):
        self.clear_main_area()
        self.current_frame = StatsFrame(self.main_area)

    def clear_main_area(self):
        for widget in self.main_area.winfo_children():
            widget.destroy()

    def backup_data(self):
        if backup_data():
            messagebox.showinfo("成功", "数据备份成功!")
        else:
            messagebox.showerror("错误", "数据备份失败!")

    def restore_data(self):
        if restore_data():
            messagebox.showinfo("成功", "数据恢复成功!")
            # 刷新当前视图
            if self.current_frame:
                self.current_frame.refresh()
        else:
            messagebox.showerror("错误", "数据恢复失败!")

2.3 首页界面

首页显示一些基本统计信息,如总收入、总支出和余额。在ui.py中添加HomeFrame类:

class HomeFrame(tk.Frame):
    def __init__(self, master):
        super().__init__(master)
        self.pack(fill="both", expand=True)
        self.create_widgets()

    def create_widgets(self):
        # 获取统计数据
        total_income = db.get_total("income")
        total_expense = db.get_total("expense")
        balance = total_income - total_expense

        # 显示标签
        tk.Label(self, text="欢迎使用个人记账本", font=("Arial", 16, "bold")).pack(pady=10)
        
        stats_frame = tk.Frame(self)
        stats_frame.pack(pady=20)
        
        tk.Label(stats_frame, text=f"总收入: {total_income:.2f}", font=("Arial", 12), fg="green").grid(row=0, column=0, padx=10, pady=5)
        tk.Label(stats_frame, text=f"总支出: {total_expense:.2f}", font=("Arial", 12), fg="red").grid(row=0, column=1, padx=10, pady=5)
        tk.Label(stats_frame, text=f"当前余额: {balance:.2f}", font=("Arial", 12), fg="blue").grid(row=0, column=2, padx=10, pady=5)

    def refresh(self):
        # 清空并重新创建组件以更新数据
        for widget in self.winfo_children():
            widget.destroy()
        self.create_widgets()

2.4 添加记录界面

添加记录界面包括输入框用于输入日期、金额、类型(收入/支出)和备注。我们添加验证确保金额为数字。在ui.py中添加AddRecordFrame类:

class AddRecordFrame(tk.Frame):
    def __init__(self, master):
        super().__init__(master)
        self.pack(fill="both", expand=True)
        self.create_widgets()

    def create_widgets(self):
        tk.Label(self, text="添加新记录", font=("Arial", 14, "bold")).pack(pady=10)

        form_frame = tk.Frame(self)
        form_frame.pack(pady=10)

        # 日期输入
        tk.Label(form_frame, text="日期 (YYYY-MM-DD):").grid(row=0, column=0, padx=5, pady=5, sticky="e")
        self.date_entry = tk.Entry(form_frame, width=20)
        self.date_entry.grid(row=0, column=1, padx=5, pady=5)
        # 默认当前日期
        from datetime import datetime
        self.date_entry.insert(0, datetime.now().strftime("%Y-%m-%d"))

        # 类型选择
        tk.Label(form_frame, text="类型:").grid(row=1, column=0, padx=5, pady=5, sticky="e")
        self.type_var = tk.StringVar(value="expense")
        tk.Radiobutton(form_frame, text="收入", variable=self.type_var, value="income").grid(row=1, column=1, padx=5, pady=5, sticky="w")
        tk.Radiobutton(form_frame, text="支出", variable=self.type_var, value="expense").grid(row=1, column=2, padx=5, pady=5, sticky="w")

        # 金额输入
        tk.Label(form_frame, text="金额:").grid(row=2, column=0, padx=5, pady=5, sticky="e")
        self.amount_entry = tk.Entry(form_frame, width=20)
        self.amount_entry.grid(row=2, column=1, padx=5, pady=5)

        # 备注输入
        tk.Label(form_frame, text="备注:").grid(row=3, column=0, padx=5, pady=5, sticky="e")
        self.note_entry = tk.Entry(form_frame, width=20)
        self.note_entry.grid(row=3, column=1, padx=5, pady=5)

        # 按钮
        btn_frame = tk.Frame(self)
        btn_frame.pack(pady=10)
        tk.Button(btn_frame, text="保存", command=self.save_record, width=10).pack(side="left", padx=5)
        tk.Button(btn_frame, text="清空", command=self.clear_fields, width=10).pack(side="left", padx=5)

    def save_record(self):
        date = self.date_entry.get()
        record_type = self.type_var.get()
        amount_str = self.amount_entry.get()
        note = self.note_entry.get()

        # 验证输入
        if not date or not amount_str:
            messagebox.showerror("错误", "日期和金额不能为空!")
            return
        if not validate_amount(amount_str):
            messagebox.showerror("错误", "金额必须为正数!")
            return
        amount = float(amount_str)

        # 保存到数据库
        try:
            db.add_record(date, record_type, amount, note)
            messagebox.showinfo("成功", "记录保存成功!")
            self.clear_fields()
        except Exception as e:
            messagebox.showerror("错误", f"保存失败: {e}")

    def clear_fields(self):
        self.date_entry.delete(0, tk.END)
        self.date_entry.insert(0, datetime.now().strftime("%Y-%m-%d"))
        self.amount_entry.delete(0, tk.END)
        self.note_entry.delete(0, tk.END)
        self.type_var.set("expense")

    def refresh(self):
        pass  # 此页面不需要刷新

2.5 查看记录界面

查看记录界面使用Treeview显示所有记录,支持分页和搜索。在ui.py中添加ViewRecordsFrame类:

class ViewRecordsFrame(tk.Frame):
    def __init__(self, master):
        super().__init__(master)
        self.pack(fill="both", expand=True)
        self.page = 1
        self.page_size = 10
        self.search_query = ""
        self.create_widgets()

    def create_widgets(self):
        # 搜索框
        search_frame = tk.Frame(self)
        search_frame.pack(fill="x", pady=5)
        tk.Label(search_frame, text="搜索备注:").pack(side="left", padx=5)
        self.search_entry = tk.Entry(search_frame, width=20)
        self.search_entry.pack(side="left", padx=5)
        tk.Button(search_frame, text="搜索", command=self.search_records).pack(side="left", padx=5)
        tk.Button(search_frame, text="刷新", command=self.refresh).pack(side="left", padx=5)

        # Treeview显示记录
        columns = ("ID", "日期", "类型", "金额", "备注")
        self.tree = ttk.Treeview(self, columns=columns, show="headings")
        for col in columns:
            self.tree.heading(col, text=col)
            self.tree.column(col, width=100)
        self.tree.pack(fill="both", expand=True, pady=5)

        # 分页控件
        page_frame = tk.Frame(self)
        page_frame.pack(fill="x", pady=5)
        tk.Button(page_frame, text="上一页", command=self.prev_page).pack(side="left", padx=5)
        self.page_label = tk.Label(page_frame, text="第 1 页")
        self.page_label.pack(side="left", padx=5)
        tk.Button(page_frame, text="下一页", command=self.next_page).pack(side="left", padx=5)

        # 操作按钮
        btn_frame = tk.Frame(self)
        btn_frame.pack(pady=5)
        tk.Button(btn_frame, text="删除选中", command=self.delete_selected, fg="red").pack(side="left", padx=5)
        tk.Button(btn_frame, text="编辑选中", command=self.edit_selected).pack(side="left", padx=5)

        self.load_data()

    def load_data(self):
        # 清空Treeview
        for item in self.tree.get_children():
            self.tree.delete(item)
        
        # 从数据库获取数据,支持分页和搜索
        offset = (self.page - 1) * self.page_size
        records = db.get_records(limit=self.page_size, offset=offset, search=self.search_query)
        for record in records:
            self.tree.insert("", "end", values=(record[0], record[1], record[2], record[3], record[4]))

        # 更新页码标签
        total_records = db.get_total_records(self.search_query)
        total_pages = (total_records + self.page_size - 1) // self.page_size
        self.page_label.config(text=f"第 {self.page} / {total_pages} 页")

    def search_records(self):
        self.search_query = self.search_entry.get().strip()
        self.page = 1
        self.load_data()

    def prev_page(self):
        if self.page > 1:
            self.page -= 1
            self.load_data()

    def next_page(self):
        total_records = db.get_total_records(self.search_query)
        total_pages = (total_records + self.page_size - 1) // self.page_size
        if self.page < total_pages:
            self.page += 1
            self.load_data()

    def delete_selected(self):
        selected = self.tree.selection()
        if not selected:
            messagebox.showwarning("警告", "请先选择一条记录!")
            return
        if messagebox.askyesno("确认", "确定要删除选中的记录吗?"):
            record_id = self.tree.item(selected[0])['values'][0]
            db.delete_record(record_id)
            self.refresh()

    def edit_selected(self):
        selected = self.tree.selection()
        if not selected:
            messagebox.showwarning("警告", "请先选择一条记录!")
            return
        record_id = self.tree.item(selected[0])['values'][0]
        # 这里可以弹出一个编辑对话框,为简化,我们直接调用添加界面并预填充数据
        # 实际项目中应创建单独的编辑界面
        messagebox.showinfo("提示", "编辑功能可通过类似添加界面的对话框实现。")

    def refresh(self):
        self.load_data()

2.6 统计图表界面

统计界面使用matplotlib绘制简单的柱状图。首先,确保安装matplotlib:pip install matplotlib。在ui.py中添加StatsFrame类:

import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

class StatsFrame(tk.Frame):
    def __init__(self, master):
        super().__init__(master)
        self.pack(fill="both", expand=True)
        self.create_widgets()

    def create_widgets(self):
        tk.Label(self, text="收支统计图", font=("Arial", 14, "bold")).pack(pady=10)

        # 获取数据
        data = db.get_stats_data()  # 返回按日期分组的收入和支出
        if not data:
            tk.Label(self, text="暂无数据").pack(pady=20)
            return

        dates = [item[0] for item in data]
        incomes = [item[1] for item in data]
        expenses = [item[2] for item in data]

        # 创建matplotlib图形
        fig, ax = plt.subplots(figsize=(8, 4))
        x = range(len(dates))
        width = 0.35
        ax.bar([i - width/2 for i in x], incomes, width, label='收入', color='green')
        ax.bar([i + width/2 for i in x], expenses, width, label='支出', color='red')
        ax.set_xlabel('日期')
        ax.set_ylabel('金额')
        ax.set_title('每日收支统计')
        ax.set_xticks(x)
        ax.set_xticklabels(dates, rotation=45)
        ax.legend()

        # 嵌入到Tkinter
        canvas = FigureCanvasTkAgg(fig, master=self)
        canvas.draw()
        canvas.get_tk_widget().pack(fill="both", expand=True, pady=10)

    def refresh(self):
        for widget in self.winfo_children():
            widget.destroy()
        self.create_widgets()

第三部分:数据库操作(数据持久化)

3.1 数据库设计

我们使用SQLite数据库来存储记录。数据库表结构如下:

  • 表名:records
  • 字段:id (INTEGER PRIMARY KEY), date (TEXT), type (TEXT), amount (REAL), note (TEXT)

database.py中编写代码:

import sqlite3
import os
from config import DB_PATH

def init_db():
    """初始化数据库,如果不存在则创建"""
    if not os.path.exists(os.path.dirname(DB_PATH)):
        os.makedirs(os.path.dirname(DB_PATH))
    conn = sqlite3.connect(DB_PATH)
    cursor = conn.cursor()
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS records (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            date TEXT NOT NULL,
            type TEXT NOT NULL,
            amount REAL NOT NULL,
            note TEXT
        )
    ''')
    conn.commit()
    conn.close()

def add_record(date, record_type, amount, note):
    """添加记录"""
    conn = sqlite3.connect(DB_PATH)
    cursor = conn.cursor()
    cursor.execute('''
        INSERT INTO records (date, type, amount, note) VALUES (?, ?, ?, ?)
    ''', (date, record_type, amount, note))
    conn.commit()
    conn.close()

def get_records(limit=10, offset=0, search=""):
    """获取记录列表,支持分页和搜索"""
    conn = sqlite3.connect(DB_PATH)
    cursor = conn.cursor()
    query = "SELECT * FROM records"
    params = []
    if search:
        query += " WHERE note LIKE ?"
        params.append(f"%{search}%")
    query += " ORDER BY date DESC LIMIT ? OFFSET ?"
    params.extend([limit, offset])
    cursor.execute(query, params)
    records = cursor.fetchall()
    conn.close()
    return records

def get_total(record_type=None):
    """获取总收入或总支出"""
    conn = sqlite3.connect(DB_PATH)
    cursor = conn.cursor()
    if record_type:
        cursor.execute("SELECT SUM(amount) FROM records WHERE type = ?", (record_type,))
    else:
        cursor.execute("SELECT SUM(amount) FROM records")
    result = cursor.fetchone()[0] or 0.0
    conn.close()
    return result

def delete_record(record_id):
    """删除记录"""
    conn = sqlite3.connect(DB_PATH)
    cursor = conn.cursor()
    cursor.execute("DELETE FROM records WHERE id = ?", (record_id,))
    conn.commit()
    conn.close()

def get_total_records(search=""):
    """获取总记录数"""
    conn = sqlite3.connect(DB_PATH)
    cursor = conn.cursor()
    if search:
        cursor.execute("SELECT COUNT(*) FROM records WHERE note LIKE ?", (f"%{search}%",))
    else:
        cursor.execute("SELECT COUNT(*) FROM records")
    result = cursor.fetchone()[0]
    conn.close()
    return result

def get_stats_data():
    """获取统计数据,按日期分组"""
    conn = sqlite3.connect(DB_PATH)
    cursor = conn.cursor()
    cursor.execute('''
        SELECT date,
               SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END) as income,
               SUM(CASE WHEN type = 'expense' THEN amount ELSE 0 END) as expense
        FROM records
        GROUP BY date
        ORDER BY date
        LIMIT 30  -- 限制显示最近30天的数据
    ''')
    data = cursor.fetchall()
    conn.close()
    return data

# 初始化数据库
init_db()

config.py中定义数据库路径:

import os
DB_PATH = os.path.join(os.path.expanduser("~"), "accounting_book", "records.db")
BACKUP_PATH = os.path.join(os.path.expanduser("~"), "accounting_book", "backup")

第四部分:解决常见问题

4.1 解决界面卡顿问题

在GUI应用中,如果执行耗时操作(如大量数据查询或复杂计算),会导致界面卡顿。Tkinter是单线程的,主线程负责界面更新。解决方法是使用多线程或异步处理。

4.1.1 使用多线程优化数据加载

修改ViewRecordsFrameload_data方法,使用线程在后台加载数据,完成后更新界面。引入threading模块。

ui.py中导入threadingqueue

import threading
import queue

修改ViewRecordsFrame

class ViewRecordsFrame(tk.Frame):
    def __init__(self, master):
        super().__init__(master)
        self.pack(fill="both", expand=True)
        self.page = 1
        self.page_size = 10
        self.search_query = ""
        self.data_queue = queue.Queue()  # 用于线程间通信
        self.create_widgets()
        self.check_queue()  # 启动队列检查

    def create_widgets(self):
        # ...(同上,省略重复代码)...
        # 添加加载指示器
        self.loading_label = tk.Label(self, text="", fg="blue")
        self.loading_label.pack(pady=2)

    def load_data(self):
        # 显示加载提示
        self.loading_label.config(text="正在加载数据...")
        # 启动线程加载数据
        thread = threading.Thread(target=self._load_data_thread, daemon=True)
        thread.start()

    def _load_data_thread(self):
        try:
            offset = (self.page - 1) * self.page_size
            records = db.get_records(limit=self.page_size, offset=offset, search=self.search_query)
            total_records = db.get_total_records(self.search_query)
            self.data_queue.put((records, total_records))
        except Exception as e:
            self.data_queue.put(e)

    def check_queue(self):
        try:
            data = self.data_queue.get_nowait()
            if isinstance(data, Exception):
                messagebox.showerror("错误", f"加载数据失败: {data}")
            else:
                records, total_records = data
                # 更新Treeview
                for item in self.tree.get_children():
                    self.tree.delete(item)
                for record in records:
                    self.tree.insert("", "end", values=(record[0], record[1], record[2], record[3], record[4]))
                # 更新页码
                total_pages = (total_records + self.page_size - 1) // self.page_size
                self.page_label.config(text=f"第 {self.page} / {total_pages} 页")
                self.loading_label.config(text="")
        except queue.Empty:
            pass
        finally:
            self.master.after(100, self.check_queue)  # 每100ms检查一次队列

    # 其他方法保持不变...

4.1.2 使用after方法避免阻塞

对于简单的操作,可以使用root.after()来分批处理。例如,在统计图表生成时,如果数据量大,可以分批计算。但在我们的例子中,数据量不大,多线程已足够。

4.2 解决数据丢失问题

数据丢失可能由于程序崩溃、误操作或硬件故障导致。我们通过以下方式解决:

  • 自动备份:定期或在关键操作后备份数据库。
  • 恢复机制:提供手动恢复备份的功能。
  • 事务处理:确保数据库操作的原子性。

4.2.1 数据备份和恢复

utils.py中实现备份和恢复功能:

import shutil
import os
from config import DB_PATH, BACKUP_PATH

def backup_data():
    """备份数据库到指定目录"""
    try:
        if not os.path.exists(BACKUP_PATH):
            os.makedirs(BACKUP_PATH)
        backup_file = os.path.join(BACKUP_PATH, f"backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.db")
        shutil.copy2(DB_PATH, backup_file)
        # 保留最近5个备份
        backups = sorted([f for f in os.listdir(BACKUP_PATH) if f.startswith("backup_")])
        for old_backup in backups[:-5]:
            os.remove(os.path.join(BACKUP_PATH, old_backup))
        return True
    except Exception as e:
        print(f"备份失败: {e}")
        return False

def restore_data():
    """从最新备份恢复数据"""
    try:
        if not os.path.exists(BACKUP_PATH):
            return False
        backups = [f for f in os.listdir(BACKUP_PATH) if f.startswith("backup_")]
        if not backups:
            return False
        latest_backup = os.path.join(BACKUP_PATH, sorted(backups)[-1])
        shutil.copy2(latest_backup, DB_PATH)
        return True
    except Exception as e:
        print(f"恢复失败: {e}")
        return False

MainWindow的菜单中,我们已经添加了备份和恢复命令。此外,可以在添加记录后自动备份:

# 在AddRecordFrame的save_record方法中,保存成功后调用backup_data()
# 但为避免频繁备份,可以设置条件,如每10条记录备份一次,或使用定时器。

4.2.2 事务处理

在数据库操作中,使用事务确保一致性。例如,在批量删除时:

def delete_multiple_records(ids):
    conn = sqlite3.connect(DB_PATH)
    cursor = conn.cursor()
    try:
        cursor.execute("BEGIN TRANSACTION")
        for record_id in ids:
            cursor.execute("DELETE FROM records WHERE id = ?", (record_id,))
        conn.commit()
    except:
        conn.rollback()
        raise
    finally:
        conn.close()

在我们的应用中,单条删除已足够,但扩展时可以考虑。

第五部分:完整代码整合和运行

5.1 完整代码结构

现在,我们将所有部分整合。确保你的项目目录如下:

accounting_book/
├── main.py
├── ui.py
├── database.py
├── utils.py
└── config.py

5.2 运行应用

在终端运行:

python main.py

应用将启动,你可以测试添加记录、查看、统计、备份和恢复功能。

5.3 测试和调试

  • 测试添加记录:输入各种数据,检查验证是否生效。
  • 测试界面卡顿:添加大量数据(例如1000条),然后快速切换页面,观察是否卡顿。如果卡顿,调整线程优先级或使用concurrent.futures
  • 测试数据丢失:模拟崩溃(强制关闭程序),然后重启应用,检查数据是否还在(因为SQLite是事务性的)。然后测试备份恢复。

第六部分:扩展和优化建议

6.1 功能扩展

  • 用户认证:添加登录界面,使用tkinterToplevel窗口。
  • 数据导出:将记录导出为CSV或Excel文件,使用pandas库。
  • 提醒功能:使用schedule库实现定期提醒记账。

6.2 性能优化

  • 数据库索引:在datetype字段添加索引,提高查询速度:
    
    cursor.execute("CREATE INDEX IF NOT EXISTS idx_date ON records(date)")
    
  • 界面优化:使用ttk主题使界面更美观,避免使用过多的嵌套Frame。
  • 内存管理:在统计图表界面,关闭matplotlib图形以释放内存:plt.close(fig)

6.3 常见问题总结

  • 界面卡顿:始终将耗时操作放入线程,使用队列通信。
  • 数据丢失:定期备份,使用数据库事务,避免直接操作文件。
  • 跨平台兼容:Tkinter在Windows、macOS和Linux上表现良好,但路径处理使用os.path确保兼容。

通过本教程,你应该能独立完成一个功能齐全的个人记账本。如果遇到问题,可以参考Python官方文档或Tkinter教程。继续实践,你会更熟练地掌握GUI编程!