PyQt5+MySQL桌面日程管理系统:架构设计与实践
2026/9/1 22:09:45 网站建设 项目流程

简介:这是一套面向Python初学者与桌面应用开发者的个人日程管理实战项目,聚焦于GUI开发、数据库交互与多线程提醒等核心技能训练,适用于课程设计、毕业设计或小型工具开发参考。资源共48个文件,主体为40个Python模块(涵盖UI界面、业务逻辑、数据模型、工具函数等分层实现),辅以3个XML配置/界面描述文件、2个说明类TXT文档及少量开发环境配置文件,整体压缩包仅39KB,轻量易部署。已有52人学习下载,体现了其作为教学级项目的实用热度。读者可直接运行完整可交互的PyQt5图形界面,掌握MySQL连接封装、用户注册登录流程、日程CRUD操作、后台定时提醒线程实现,以及CSV格式数据导入导出功能;模块化目录结构清晰(含core、models、ui、utils等子包),便于理解分层架构设计思想与工程组织规范。 最近在整理手头代码,顺便把一个基于 Python 的桌面端个人日程管理系统完整地复现了一遍。这个项目技术栈很经典:PyQt5 做界面,MySQL 做数据存储,模块化设计,核心功能覆盖日程管理、提醒服务、用户认证。只要你的 Python 基础能写函数、懂一点类和对象,就能把这个系统吃透,而且整套代码可以直接拿来做课程设计、毕业设计,或者作为自己进入桌面应用开发的第一块跳板。

这不是什么高不可攀的架构,也没有炫技的成分,它更像一个“什么都有点、但都能讲清楚”的样本工程。界面用什么写、数据放哪里、提醒怎么触发、密码怎么存,每一块都有明确答案。我这次从头到尾把代码跑通、又把常见坑都踩了一遍,所以把这套系统的设计思路、实现细节、数据库表结构、提醒服务的两种触发方案,连同 PyQt5 和 MySQL 联调时容易翻车的点,一次性写清楚。

先说一下这稿文章适合谁看:正在学 Python 但只会写脚本、想试试图形界面开发的初级开发者;准备做课程设计或毕业设计、需要一套能演示能答辩的完整项目的在校学生;还有那些用 PyQt5 写过小工具、但一直没跟数据库连起来的人。文章不会特别长,但每段都尽量落在实处,看完你可以直接照着搭。

1. 为什么是“桌面端 + PyQt5 + MySQL”这套组合

1.1 桌面端日程管理,解决的是“工作台效率”问题

很多人第一反应是:日程管理谁还做桌面端?手机上随便一个日历 App 不香吗?这问题的答案取决于使用场景。手机端的日程管理强在“随时看”,弱在“集中处理”。你坐在电脑前写材料、跑数据的时候,切出去拿手机设置一个提醒,这个动作看起来只有几秒,但对注意力的打断是实打实的。桌面端工具的定位就是把日程这件事留在工作台上,跟编辑器、浏览器、聊天工具放在同一层,随时能看、随手能改,不用离开当前工作环境。

另外还有一个很现实的因素:个人软件的数据主权。用别人的日历服务,数据在别人的服务器上;自己做一个小而美的桌面工具,数据落在本地 MySQL 里,导出、备份、迁移都完全可控。这也是我这次选择本地 MySQL 而不是直接用云服务的一个原因——整个系统可以脱离外网独立运行,适合放在内网环境里当作个人效率工具来用。

1.2 PyQt5 不是唯一选择,但它是 Python 生态里最稳的那条路

Python 做桌面界面,常见选项无非几个:Tkinter、PyQt5/PySide、wxPython,甚至有人用 Electron 套壳。Tkinter 虽然内置、零安装成本,但控件风格还停留在上个时代,做一个简单表单还行,做带表格、日期选择、多窗口联动的日程管理,页面会显得很粗糙。Electron 的前端体验当然好,但打包体积动辄几百兆,而且整个项目会从 Python 技术栈变成 Node 技术栈,对一个想要“用 Python 跑通桌面端”的人来说,方向就偏了。

PyQt5 的优势在于:它是 Python 世界里对桌面应用支持最完整的框架,控件丰富、文档成熟、社区例子多。它基于 Qt 的事件循环机制,天然支持定时器(QTimer)、信号槽(Signal/Slot)、多线程(QThread),这套机制做提醒服务非常顺手。下面这段代码是典型的 PyQt5 窗口骨架,一个能运行的最小日程主窗口:

import sys from PyQt5.QtWidgets import QApplication, QMainWindow, QCalendarWidget, QListWidget class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("个人日程管理系统") self.resize(900, 600) # 左侧日历、右侧日程列表 self.calendar = QCalendarWidget(self) self.calendar.setGeometry(20, 20, 400, 300) self.listWidget = QListWidget(self) self.listWidget.setGeometry(440, 20, 420, 300) # 这里只展示控件布局逻辑,真正的数据联动见第 4 节 if __name__ == "__main__": app = QApplication(sys.argv) win = MainWindow() win.show() sys.exit(app.exec_())

app.exec_()就是整个系统的大循环,所有界面事件、定时器、键盘鼠标响应都从这里分发。新手学 PyQt5 的第一步不是背控件 API,而是先理解这个循环——后面所有跟“提醒”相关的逻辑,本质上都是在这个循环里挂一个定时任务。

1.3 MySQL 在这里的定位:本地数据仓库,而不是重负载服务

有人会问,一个个人日程管理,用 SQLite 文件不就行了吗?确实行,SQLite 零配置、单文件,适合轻量场景。但这个项目选择 MySQL 不是为了炫技,而是考虑三层因素:第一,当你以后想把系统改造成多人协作版(比如共享日程、团队日历),MySQL 的路是通的,SQLite 会受限;第二,MySQL 是面试和真实工作中最常遇到的数据库,把一个完整项目的数据层落在 MySQL 上,练习价值远高于 SQLite;第三,一旦数据量大到数十万条记录,MySQL 的索引优化、慢查询排查这些技能都能在这个系统里练到。

我们这里不搞复杂的分布式,不搞主从,就是一台本机 MySQL 服务,跑一个专用的schedule_db数据库。MySQL 的连接配置单独抽一个配置文件,避免把数据库账号密码硬编码在业务代码里。

# db_config.ini [mysql] host = 127.0.0.1 port = 3306 user = root password = your_password database = schedule_db charset = utf8mb4

2. 模块化设计落地:从目录结构到数据表

2.1 工程目录这样拆,后续加功能不头痛

“模块化”这个词经常被滥用,但在这个项目里它是实打实的设计原则。我推荐的目录结构是这样的:

schedule_system/ │ ├── main.py # 程序入口 ├── db_config.ini # 数据库配置 ├── requirements.txt # 依赖清单 │ ├── ui/ # 界面层 │ ├── __init__.py │ ├── login_window.py # 登录窗口 │ ├── main_window.py # 主窗口 │ └── schedule_dialog.py # 日程编辑对话框 │ ├── core/ # 业务逻辑层 │ ├── __init__.py │ ├── user_service.py # 用户认证相关 │ ├── schedule_service.py # 日程管理相关 │ └── reminder_service.py # 提醒服务相关 │ ├── db/ # 数据访问层 │ ├── __init__.py │ ├── db_helper.py # 数据库连接与通用操作 │ └── init_db.py # 建库建表脚本 │ └── utils/ # 工具类 ├── __init__.py └── encrypt.py # 密码哈希工具

为什么要这样拆?核心目的是让界面层、业务层、数据层互相解耦。你可以把数据层从 MySQL 换成 PostgreSQL,只动db/下面的代码,ui/core/基本不用改;你也可以把 PyQt5 界面换成命令行版本,业务逻辑依然能复用。很多课程设计项目最大的问题不是功能少,而是所有代码糊在几个文件里,最后想加一个“导出 Excel”的功能都无从下手。

2.2 数据库表设计:四张表搞定全部业务

这个项目需要两张核心表(用户表、日程表)加两张辅助表(提醒记录表、操作日志表),下面给出建表 SQL。

CREATE DATABASE IF NOT EXISTS schedule_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE schedule_db; -- 用户表 CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, salt VARCHAR(32) NOT NULL, email VARCHAR(100), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; -- 日程表 CREATE TABLE IF NOT EXISTS schedules ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, title VARCHAR(200) NOT NULL, content TEXT, schedule_date DATE NOT NULL, start_time TIME, end_time TIME, remind_before INT DEFAULT 10, -- 提前多少分钟提醒 is_finished TINYINT DEFAULT 0, -- 0未完成 1已完成 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_date (user_id, schedule_date), FOREIGN KEY (user_id) REFERENCES users(id) ) ENGINE=InnoDB;

这里有两个设计细节值得注意。第一,user_id一定要建联合索引(user_id, schedule_date),因为日常查询几乎都是“查某一天某用户的日程”,这个索引能直接命中,避免全表扫描。第二,remind_before字段存的是“提前多少分钟”,而不是“具体提醒时间点”。这样设计的好处是:用户改日程时间时,提醒会自动跟着变,不用额外计算。这个理念叫“存储规则,不存储结果”,在很多业务系统里都适用。

2.3 连接层和业务层分离,避免 SQL 满天飞

数据访问层做了一个DBHelper类,封装连接和通用查询方法,业务层只负责组织参数和接收结果,不直接碰连接对象。

import pymysql from pymysql.cursors import DictCursor class DBHelper: def __init__(self, config): self.config = config def get_connection(self): return pymysql.connect( host=self.config['host'], port=int(self.config['port']), user=self.config['user'], password=self.config['password'], database=self.config['database'], charset=self.config.get('charset', 'utf8mb4'), cursorclass=DictCursor ) def execute(self, sql, args=None): conn = self.get_connection() try: with conn.cursor() as cursor: cursor.execute(sql, args) conn.commit() return cursor.lastrowid finally: conn.close() def query(self, sql, args=None): conn = self.get_connection() try: with conn.cursor() as cursor: cursor.execute(sql, args) return cursor.fetchall() finally: conn.close()

每次查询都新建连接用完即关,在个人桌面应用场景下完全够用,因为用户量就一个,不像 Web 后端有高并发压力。代码里用了DictCursor,查询结果直接是字典列表,在 PyQt5 界面里取值非常方便,例如row['title']而不是row[1]。这个细节很多人容易忽略,等你写了几百行界面代码之后就会发现,字典取值比元组下标舒服太多了。

3. 用户认证模块:登录功能远不止“存个密码”

3.1 密码存储用哈希加盐,别碰明文

个人系统的登录功能最容易被人轻视,但用户认证恰恰是系统安全的最低防线。同一套逻辑放到真正的业务系统里也是一样的:用户表里绝对不允许出现明文密码。我这里用hashlib做 SHA-256 加盐哈希,代码在utils/encrypt.py里:

import hashlib import os def generate_salt(length=16): return os.urandom(length).hex() def hash_password(password: str, salt: str) -> str: # 密码和盐拼接后,做 10000 次迭代哈希,增加暴力破解成本 digest = password + salt for _ in range(10000): digest = hashlib.sha256(digest.encode('utf-8')).hexdigest() return digest

加盐的作用是防止“彩虹表攻击”——如果只对密码做一次哈希,攻击者可以拿预计算好的哈希字典直接反向匹配;加了随机盐之后,每个用户的哈希值都不同,同样的密码在不同账号下存储结果完全不一样。你不需要一上来就引入 bcrypt 这类重量级库,用 hashlib 实现加盐哈希已经足够教会新手理解认证的核心逻辑,后面如果上线正式服务,再迁移到 bcrypt 即可。

注册的逻辑是:用户名重名检查 -> 生成盐 -> 计算哈希 -> 插入数据库。登录的逻辑是:查用户 -> 用库里存的盐重新计算哈希 -> 与库里存的哈希比对,一致则登录成功。

3.2 登录会话保持:全局记住“当前是谁”

登录成功后,系统得知道当前操作属于哪个用户。最简单直接的方式是在主窗口里保存一个current_user对象,但更好的做法是维护一个全局会话类。原因有两个:第一,提醒服务、修改密码、删除账号等功能散落在不同模块,每个地方都需要知道当前用户 ID,全局会话可以统一提供;第二,如果以后系统加了权限角色(普通用户/管理员),会话类里可以直接扩展。

# core/session.py class Session: _instance = None def __new__(cls, *args, **kwargs): if cls._instance is None: cls._instance = super().__new__(cls) cls._instance.user_id = None cls._instance.username = None return cls._instance def set_user(self, user_id: int, username: str): self.user_id = user_id self.username = username def clear(self): self.user_id = None self.username = None @property def is_logged_in(self) -> bool: return self.user_id is not None

用单例模式(Singleton)保证整个程序只有一个会话对象,登录窗口在认证成功之后调用Session().set_user(...),主窗口再通过Session().user_id拿到当前操作人。这种写法在 PyQt5 多窗口切换时特别省心——登录窗口切到主窗口,不需要把用户 ID 当参数一层层传递。

3.3 界面跳转与登录态的联动

登录成功关闭登录窗口、打开主窗口,这个流程在main.py入口控制。一个标准的做法是:主窗口作为QApplication的主事件循环,登录窗口作为前置弹窗。

# main.py 简化版 import sys from PyQt5.QtWidgets import QApplication, QMessageBox from ui.login_window import LoginWindow from ui.main_window import MainWindow def main(): app = QApplication(sys.argv) login_window = LoginWindow() if login_window.exec_() == LoginWindow.Accepted: main_window = MainWindow() main_window.show() sys.exit(app.exec_()) else: sys.exit(0) if __name__ == "__main__": main()

这里用login_window.exec_()启动一个模态对话框,只有当用户点“登录”且认证通过后,才进入真正的主窗口事件循环。如果用户直接点“取消”或关闭窗口,程序直接退出。注意登录触发不能放在__init__里,而是放在登录按钮的点击槽函数中,认证成功后调用self.accept()让对话框返回Accepted

4. 日程管理与提醒服务的核心实现

4.1 日程 CRUD:从表单校验到数据回填

日程管理的核心操作无非增删改查,但实现的时候有几个地方需要额外处理。新增和编辑共用一个ScheduleDialog,区别在于编辑时要先回填数据。这个对话框包含标题输入框、内容编辑框、日期选择器、起止时间控件、提前提醒分钟数下拉框。

保存按钮的槽函数是这样处理的:

def save_schedule(self): title = self.titleEdit.text().strip() schedule_date = self.dateEdit.date().toString("yyyy-MM-dd") start_time = self.startTimeEdit.time().toString("HH:mm") end_time = self.endTimeEdit.time().toString("HH:mm") remind_before = self.remindCombo.currentText() if not title: QMessageBox.warning(self, "提示", "日程标题不能为空") return # 校验结束时间晚于开始时间 if end_time <= start_time: QMessageBox.warning(self, "提示", "结束时间必须晚于开始时间") return # 组装数据,调用业务层保存 user_id = Session().user_id schedule_service.create_schedule( user_id, title, content, schedule_date, start_time, end_time, remind_before ) self.accept()

编辑逻辑是在对话框初始化时判断是否传入schedule_id,有则查询数据库并把结果填进各个控件,否则就是新增。这里有个经验之谈:时间控件的取值和赋值必须用统一的格式字符串,否则很容易出现“数据库存的是HH:mm,界面显示却带秒”的混乱。建议所有时间控件在读取和写入时都走同一个占位格式。

4.2 提醒服务的触发机制:轮询与定时器的取舍

提醒服务是整个系统最有技术含量、也最容易写崩的部分。常见的方案有两种:一种是主线程轮询数据库,另一种是独立线程轮询。需要先说清楚:在 PyQt5 里,更新界面控件的操作必须在主线程完成,任何子线程直接操作控件,轻则界面卡死,重则程序崩溃。所以我的做法是:主线程用一个QTimer每隔 30 秒查一次数据库,发现“到了提醒时间且还没提醒过”的日程,直接弹窗提示。

from PyQt5.QtCore import QTimer from datetime import datetime, timedelta from core.schedule_service import ScheduleService class ReminderManager: def __init__(self, main_window): self.main_window = main_window self.service = ScheduleService() self.timer = QTimer() self.timer.timeout.connect(self.check_reminders) self.timer.start(30000) # 每 30 秒检查一次 def check_reminders(self): user_id = Session().user_id if not user_id: return now = datetime.now() # 查询未来 1 分钟内需要提醒的日程 start = now end = now + timedelta(minutes=1) schedules = self.service.get_pending_reminders( user_id, start.strftime("%Y-%m-%d %H:%M:%S"), end.strftime("%Y-%m-%d %H:%M:%S") ) for s in schedules: self.show_reminder(s) self.service.mark_reminded(s['id'])

这里的关键 SQL 是查询满足两个条件的日程:提醒时间落在当前时间之后remind_before分钟内,且is_reminded标记为 0。这个“分钟窗口”设计很巧妙——QTimer每 30 秒触发一次,每次查未来 1 分钟的日程,即使某次检查因为界面阻塞延迟了几秒,也不会漏掉提醒。

需要注意mark_reminded这个动作。如果不做提醒去重,同一个日程会被弹窗轰炸好几遍;如果做了去重,又必须保证弹窗确实展示给用户了。所以我的方案是:先弹窗,弹窗属于阻塞操作,用户点掉之后才执行mark_reminded。如果你的弹窗是非阻塞的,那应该在弹窗创建后立即标记,避免多个弹窗同时出现。

4.3 过期日程的自动处理与状态流转

日程有一个is_finished字段,主界面用复选框展示。打开主窗口时,默认只显示“今天及以后”的日程,同时给一个筛选项可以查看“已完成/未完成/全部”。过期但未完成的日程不会消失,而是会以灰字显示在列表底部,方便用户回顾和补办。

这一块的 SQL 查询条件会根据筛选状态动态拼接,我给你一个比较通用的写法:

def get_schedules(self, user_id, date_str, status): sql = "SELECT * FROM schedules WHERE user_id = %s AND schedule_date >= %s" params = [user_id, date_str] if status == "unfinished": sql += " AND is_finished = 0" elif status == "finished": sql += " AND is_finished = 1" sql += " ORDER BY schedule_date, start_time" return self.db.query(sql, params)

状态列在 PyQt5 里做成QTableWidget的复选框列,点击之后更新is_finished字段。这个联动逻辑很简单,但很考验对“模型/视图”分离的理解:界面上的点击事件只是触发了一个更新数据库的操作,真正的数据源始终是数据库,界面只是数据的一个投影。

5. PyQt5 + MySQL 联调中的五个高频坑

5.1 界面卡死的根源:别把耗时操作放在主线程

新手最容易踩的坑是:登录按钮点击后直接执行 SQL 查询,如果数据库响应慢或者连接超时,整个窗口会卡住不动。原因是exec_()事件循环被阻塞了,界面无法重绘,用户交互全部失效。

解决思路有两个方向。简单场景下,把耗时操作放到QThread子线程中执行,执行完通过信号把结果传回主线程更新界面。下面是一个线程化查询的通用模板:

from PyQt5.QtCore import QThread, pyqtSignal class QueryThread(QThread): result_ready = pyqtSignal(list) def __init__(self, query_func, *args): super().__init__() self.query_func = query_func self.args = args def run(self): result = self.query_func(*self.args) self.result_ready.emit(result)

使用时只需创建一个线程对象,连接result_ready信号到界面的刷新函数,然后thread.start()。这里强调一点:子线程里绝对不能直接调用QLabel.setText()这类 UI 操作,只能通过信号把数据传回主线程。这是 PyQt5 的铁律,违反一次就崩一次。

5.2 中文乱码的根源:连接参数、建库字符集、表字段三处要统一

MySQL 中文乱码是重灾区,而且麻烦在于它可能来自三个不同的环节。连接串里写没写charset='utf8mb4'、数据库有没有指定utf8mb4字符集、表中字段是不是utf8mb4,任何一环不一致,都会或早或晚地暴露乱码问题。

一个很隐蔽的场景是:数据库建库时用了utf8,但连接参数用utf8mb4,或者反过来,某些特殊字符(比如 emoji)会直接变成问号。我的建议是“三统一”:建库语句用DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci、连接参数固定charset='utf8mb4'、表字段不单独指定字符集则默认跟随库。按照这个原则配置,至少中文和 emoji 不会出乱码。如果改完还乱,检查一下 Windows 终端编码,那是另一个维度的问题。

5.3 驱动“找不到”怎么办:PyMySQL 与 mysql-connector 的选择

Python 连 MySQL 的驱动一般用 PyMySQL,因为它是纯 Python 实现的,安装最省事。pip install pymysql即可。但如果你的代码里写的是import MySQLdb,这是另一套依赖,PyMySQL 默认不会安装。

这里有个兼容技巧:在项目入口或db_helper.py顶部加一行:

try: import pymysql pymysql.install_as_MySQLdb() except ImportError: pass

这行代码的效果是让 PyMySQL 伪装成 MySQLdb,这样即使老代码里写的是from MySQLdb import connect,也能正常工作。如果你的环境是 Python 3.10 以上,建议直接使用 PyMySQL 的 API,不要套兼容层,减少不确定性。

另外一个高频报错是ModuleNotFoundError: No module named 'pymysql',这个大概率是环境问题:你pip install到了全局环境,但 IDE 或打包解释器用的是虚拟环境。解决方式是在项目根目录建虚拟环境,用pip install -r requirements.txt安装,同时确认运行解释器指向虚拟环境。

5.4 表结构变更导致查询报错的排查思路

开发过程中难免要改表结构,比如给 schedules 表新增一个category字段。如果只改数据库不改代码,或者只改代码不改数据库,都会出现字段对不上的报错。排查思路要形成条件反射:先看报错信息里有没有“Unknown column”这个关键词,有就是 SQL 里的字段在表中不存在;再看查询结果返回的字典里有没有KeyError,有就是代码里取了一个查询结果中不存在的键。

遇到这类问题,我通常先在 MySQL 客户端里执行一遍同样的 SQL,确认数据库层的真实结构,再回头检查代码。DESC schedules;命令能查看表结构,这个动作虽然简单,但能省下大量跟报错死磕的时间。

5.5 PyInstaller 打包后的资源路径问题

桌面应用最终要打包成 exe 才方便分发,但 PyInstaller 打包后,代码里写的db_config.ini相对路径经常会失效。因为打包后的程序运行时,当前工作目录可能不是 exe 所在目录,而是启动它的地方。正确的做法是用sys._MEIPASS处理资源路径:

import os import sys def resource_path(relative_path): try: base_path = sys._MEIPASS except AttributeError: base_path = os.path.abspath(".") return os.path.join(base_path, relative_path)

在打包配置里把db_config.ini加进资源文件,代码中获取配置文件时统一用resource_path("db_config.ini")。这样无论是源码运行还是打包后运行,路径都能正确解析。这里的sys._MEIPASS是 PyInstaller 在临时目录解压资源时设置的属性,源码运行时不存在,所以用 try-except 兜底。

6. 打包发布与后续扩展建议

PyInstaller 打包 PyQt5 项目的推荐命令:

pip install pyinstaller pyinstaller -w -F --name schedule_system --icon=app.ico main.py

-w表示不显示控制台窗口,-F表示打包成单文件,--name指定输出文件名。如果程序里用了图片、配置文件等资源,需要加--add-data "db_config.ini;."(Windows 下路径分隔符是分号,Linux 或 macOS 是冒号)。单文件打包方便分发,但启动时会有解压到临时目录的开销,如果你更看重启动速度,可以去掉-F,用文件夹模式。

打包后还有一件要交代的事:MySQL 服务不能跟着 exe 走。你分发的是程序本体,但用户机器上得有 MySQL 服务并手动建立数据库。最省事的做法是让程序首次启动时执行init_db.py自动建库建表,再把数据库连接配置做成首次启动引导界面。这个体验就跟商业软件差不多了,但已经超出基础版本的范围,作为进阶目标更合适。

整个系统后续可以扩展的方向很多,我自己比较推荐按以下顺序迭代:第一,给日程表加上“分类/标签”字段,界面增加分类筛选;第二,把提醒方式从弹窗扩展到系统通知,甚至接入邮件提醒;第三,做一个数据导入导出模块,支持 iCalendar 格式,这样能和手机日历互通;第四,引入 SQLAlchemy 作为 ORM 层,让数据操作更接近业务语言而不是裸 SQL。每一条都不难,但都会让系统的完成度上一个台阶。

我在实际开发中还有一个体会:这个项目最值得反复琢磨的不是某个控件的用法,而是“一个桌面应用从事件循环到数据库落盘,整个链路是怎么串起来的”。把这条链路理清了,你对 PyQt5 事件循环、MySQL 参数化查询、分层架构这些知识的理解,会比看十篇教程都深。如果你在复现过程中卡在某个报错上,先别急着搜答案,试着从报错信息反推是哪一层出了问题——这个排查习惯,才是做这个项目最大的收获。

本文还有配套的精品资源,点击获取

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

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

立即咨询