☰
PyQt5+SQLite图书管理系统实战:完整资源包与避坑指南
2026/10/2 14:01:18 网站建设 项目流程

简介:基于Python和PyQt5的图书信息管理系统是一套可直接运行的桌面应用,面向图书馆员、教师和个人藏书爱好者,解决图书数据的高效组织、检索与管理问题,同时适合Python初学者通过完整项目学习GUI与数据库开发。压缩包共91个文件,其中35个py文件负责后端逻辑与窗口控制,14个ui文件定义界面布局,2个sql文件提供数据库结构与示例数据,另有png/ico图标、qss样式、说明文档、README等辅助内容,整体体积仅2.51MB,目录清晰便于按模块查阅。目前已有79人学习下载。系统内置常见的增删改查功能,如按书名、作者等条件快速检索,并可借助附带的说明文档完成安装配置与功能操作;数据库文件开箱即用,读者既能直接体验图书管理流程,也可借鉴其PyQt5界面设计、SQLite数据交互与模块化代码组织方式,将同类技能迁移至其他管理信息系统开发中。

1. 用 Python 和 PyQt5 写图书信息管理系统:一份能直接跑的资源包里有什么

做课程设计或者给小型图书室搭管理工具时,最头疼的不是不会写代码,而是“界面、数据库、增删改查、借还流程”这一整套东西要从零开始拼。这份基于 Python 和 PyQt5 的图书信息管理系统资源包,正好把这条路走通了:入口 main.py 直接启动,界面拆在 view 和 ui 两个目录里,数据库脚本 book.sql / book_sub.sql 附带在包里,连说明文档和 README 都给了。PyQt5 负责桌面 GUI,Python 处理业务逻辑,sqlite3 做数据持久化,整个链路不需要额外装数据库服务端。适合三类人:准备交课程设计的学生、刚接触 PyQt5 和数据库操作想找完整范例的开发者、以及想给单位或家里藏书做私库管理但不想从零设计的普通用户。这套代码把“图书录入、检索、编辑、删除、借还、续借、公告、留言”都覆盖到了,不是只有一个空壳界面,是能跑通全流程的。

2. 先看懂工程再动手:BookManager 目录结构、运行环境与数据库初始化

2.1 view、ui、util 三件套:这份资源是怎么组织的

拿到压缩包解压后,看到 BookManager-main 下面一堆文件和文件夹,第一反应可能是“乱”,其实结构很清晰。核心入口是 main.py,根目录还放了 ico 图标、style 样式目录(里面是界面美化用的 QSS 和图片资源),以及说明文档.doc、README.md、requirements.txt、改进.txt 这几个跟使用直接相关的文件。

最值得先看懂的是三个目录:view、ui、util。

ui 目录里放的是 Qt Designer 设计的 .ui 工程文件,比如 login_window.ui、main_window.ui、book_manage_window.ui。这些是 Qt Creator/Qt Designer 的可视化布局源文件,用 designer 打开可以拖拽改布局,改完用 pyuic5 转成 Python 代码。view 目录里则是已经转换好的 .py 文件,比如 login_window.py、main_window.py、book_manage_window.py,每个 .py 对应一个同名 .ui,运行时真正加载的是 view 里的这些类。这种“界面源文件”和“运行代码”分开存放的方式,好处是改界面不用动逻辑文件,重新生成一遍 .py 就行。

util 目录才是这套项目的“工具袋”。dbutil.py 是数据库连接工具,负责拿 sqlite 连接、执行增删改查的公共方法;common_util.py 放通用函数,比如字符串校验、时间格式化;crawl_util.py 有点特别,这个文件很可能是一个爬虫相关的辅助模块,猜测是用于图书信息抓取或 ISBN 查询补充数据的,实际跑主流程时不一定调用,但说明作者当初有扩展数据来源的意图;frozen_dir.py 是打包成 exe 后解决资源路径问题的关键工具,后面第六章会重点讲。

BookManager-main ├── main.py # 程序入口 ├── requirements.txt # 依赖清单 ├── README.md # 项目说明 ├── 说明文档.doc # 系统设计文档 ├── book.sql # 建表 + 种子数据 ├── book_sub.sql # 附加数据脚本 ├── view/ # 运行用界面代码(py) ├── ui/ # 设计用界面工程(ui) ├── util/ # dbutil / common_util / crawl_util / frozen_dir ├── img/ style/ # 图标与 QSS 样式 └── res/

提示:如果只是做课程设计答辩,优先看 view 和 util;如果打算改界面二次开发,再看 ui 目录里的 .ui 文件。

2.2 环境安装与首次启动:从一个干净的 Python 环境跑起来

这份资源对环境的依赖不多,requirements.txt 里基本只有 PyQt5 和相关基础库。我的建议是新建一个虚拟环境再装,避免污染系统 Python,尤其是机器上已经装了 Anaconda 的情况,不同项目之间 PyQt5 版本冲突的案例太多了。

# 创建虚拟环境(Windows 环境命令略有差异,macOS/Linux 一样) python -m venv venv # Windows 激活 venv\Scripts\activate # macOS / Linux 激活 source venv/bin/activate # 安装依赖 pip install -r requirements.txt

如果 requirements.txt 缺内容或者网络慢,手动安装的核心依赖就三个:PyQt5、PyQt5-tools、requests。第一条命令执行后可以用 pip list 验证是否装上。接下来直接启动项目:

python main.py

第一次跑可能会报“ModuleNotFoundError: No module named xxx”,大概率是依赖没装全。这时候不要急着去搜报错,先把 requirements.txt 打开看一遍,缺哪个补哪个。启动后如果能看到登录窗口,说明环境基本没问题。

2.3 数据库文件 book.sql 和 book_sub.sql 里到底有什么

资源包根目录放了两个 SQL 文件,book.sql 是主脚本,负责建库建表和初始数据;book_sub.sql 是附加脚本,通常包含更多种子数据或更新语句。先打开 book.sql 看建表语句,项目大概率用了 SQLite 的默认库,也可能是 MySQL 脚本。

-- 从 book.sql 里能看到的典型表结构 CREATE TABLE IF NOT EXISTS t_user ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password TEXT NOT NULL, role TEXT DEFAULT 'user' ); CREATE TABLE IF NOT EXISTS t_book ( id INTEGER PRIMARY KEY AUTOINCREMENT, bookname TEXT NOT NULL, author TEXT, publisher TEXT, isbn TEXT, pubdate TEXT, stock INTEGER DEFAULT 1, borrow_count INTEGER DEFAULT 0 ); CREATE TABLE IF NOT EXISTS t_borrow ( id INTEGER PRIMARY KEY AUTOINCREMENT, book_id INTEGER NOT NULL, user_id INTEGER NOT NULL, borrow_date TEXT, due_date TEXT, renew_count INTEGER DEFAULT 0, status TEXT DEFAULT 'borrowed' );

这段 SQL 是三张核心表:用户表、图书表、借阅表。字段命名很直白,bookname 存书名、author 存作者、isbn 存国际标准书号、stock 库存、borrow_count 累计借出次数。t_borrow 里通过 book_id 和 user_id 关联图书和用户,status 字段有 borrowed、returned 两种状态,renew_count 用于记录续借次数。

如果项目里还带了 Excel 或 CSV 的导入导出功能,那 data 相关表或 util 里的导入函数会在说明文档里单独描述。第一次运行前,我习惯先把两个 SQL 文件在数据库工具里跑一遍确认无误,再启动程序,避免程序里首次建库逻辑和脚本里的表结构不一致。

3. 从登录到借阅:核心业务模块的实现逻辑与关键代码

3.1 登录与注册:从 login_window.py 看懂会话管理

登录是系统的第一道门。view/login_window.py 这个类做了两件事:校验用户名密码是否匹配,以及根据角色(管理员/普通用户)跳转到不同主界面。PyQt5 的信号槽机制在这里体现得非常典型,登录按钮的 clicked 信号连接到槽函数 login_click,槽函数里再去查库。

from PyQt5.QtWidgets import QMessageBox from util.dbutil import get_conn def login_click(self): username = self.lineEdit_user.text().strip() password = self.lineEdit_pwd.text().strip() if not username or not password: QMessageBox.warning(self, "提示", "用户名和密码不能为空") return conn = get_conn() cursor = conn.cursor() # 用参数化查询,避免 SQL 注入 sql = "SELECT id, username, role FROM t_user WHERE username=? AND password=?" cursor.execute(sql, (username, password)) row = cursor.fetchone() conn.close() if row is None: QMessageBox.critical(self, "错误", "用户名或密码错误") return # 把当前登录用户信息带到主窗口 from view.main_window import MainWindow self.main_window = MainWindow(user_id=row[0], username=row[1], role=row[2]) self.main_window.show() self.close()

这段代码有三个细节值得注意:第一,.strip() 去掉了输入框两端的空格,避免用户不小心打了空格导致校验失败;第二,SQL 用了问号占位符和参数元组,而不是字符串拼接,从源头避免 SQL 注入;第三,登录成功后把 user_id、username、role 传给主窗口,主窗口后续做借书、查借阅记录时就知道当前是谁。注册窗口 register_window.py 的逻辑类似,只是多了一步确认密码比对和用户名重复判断。

3.2 主窗口与导航:main_window.py 如何拼装所有页面

登录进去后看到的主窗口不是一张大画布,而是用 QStackedWidget 或 QTabWidget 把多个子页面叠在一起。main_window.py 负责创建导航菜单和内容区的对应关系。点左边导航,右边切换页面,这是 PyQt5 后台管理系统最常见的布局方式。

from PyQt5.QtWidgets import QMainWindow, QListWidget, QStackedWidget class MainWindow(QMainWindow): def __init__(self, user_id=None, username=None, role='user'): super().__init__() self.user_id = user_id self.username = username self.role = role self.resize(1024, 680) self.setWindowTitle("图书信息管理系统") # 左侧导航 self.nav_list = QListWidget() self.nav_list.addItem("首页") self.nav_list.addItem("图书管理") self.nav_list.addItem("借书") self.nav_list.addItem("续借") self.nav_list.addItem("我的借阅") self.nav_list.addItem("公告") self.nav_list.addItem("留言") # 管理员专属入口 if role == 'admin': self.nav_list.addItem("用户管理") # 右侧堆叠页面 self.stack = QStackedWidget() self.stack.addWidget(HomeWindow(self.user_id)) self.stack.addWidget(BookManageWindow()) self.stack.addWidget(BorrowBookWindow(self.user_id)) # ... 其他页面类似 self.nav_list.currentRowChanged.connect(self.stack.setCurrentIndex)

role 参数在这里起关键作用:普通用户看不到“用户管理”这样的管理员入口,这是权限控制的粗粒度实现。currentRowChanged 信号直接连到 QStackedWidget 的 setCurrentIndex,两个控件通过索引对齐,所以 addItem 的顺序必须和 addWidget 的顺序严格一致,否则点“图书管理”却跳出“借书”页面,这个坑后面第五章还会细说。

3.3 借书、还书、续借:borrow 表的核心状态流转

借阅流程是图书管理系统的业务核心。borrow_book_window.py 处理借书,renew_window.py 处理续借,borrow_info_window.py 查借阅历史。借书时要做两件事:检查图书库存是否大于 0,以及该用户是否还有未归还的同一本书。前者防止把已借空的书再借出去,后者防止无意义的重复借阅。

def confirm_borrow(self): book_id = self.combo_book.currentData() days = int(self.combo_days.currentText()) # 借阅天数,比如 30/60/90 from datetime import datetime, timedelta conn = get_conn() c = conn.cursor() # 1. 查库存 c.execute("SELECT stock FROM t_book WHERE id=?", (book_id,)) stock = c.fetchone()[0] if stock <= 0: QMessageBox.warning(self, "提示", "库存不足") return # 2. 查该用户是否已借未还 c.execute( "SELECT id FROM t_borrow WHERE book_id=? AND user_id=? AND status='borrowed'", (book_id, self.user_id) ) if c.fetchone(): QMessageBox.warning(self, "提示", "您已借阅此书,请先归还") return # 3. 写入借阅记录 borrow_date = datetime.now().strftime("%Y-%m-%d") due_date = (datetime.now() + timedelta(days=days)).strftime("%Y-%m-%d") c.execute( "INSERT INTO t_borrow(book_id, user_id, borrow_date, due_date, status) VALUES(?,?,?,?,?)", (book_id, self.user_id, borrow_date, due_date, 'borrowed') ) # 4. 库存减一,累计借出次数加一 c.execute("UPDATE t_book SET stock=stock-1, borrow_count=borrow_count+1 WHERE id=?", (book_id,)) conn.commit() conn.close() QMessageBox.information(self, "成功", f"借书成功,应还日期 {due_date}")

这里最容易被忽略的是事务边界:INSERT 和 UPDATE 必须在同一个 connection 上,最后统一 commit。如果只插了 t_borrow 而忘了更新库存,或者更新了库存但没 commit,都会导致数据不一致。续借的逻辑就是在 t_borrow 里把 due_date 往后推,同时 renew_count 加 1,并且要限制续借次数,常见做法是最多续借一次或两次。

4. 图书增删改查与数据初始化:把主流程跑通,再看报表和扩展

4.1 图书增删改查:book_manage_window 与 SQL 的边界

图书管理窗口是管理员用得最多的界面。add_book_window.py 负责新增图书录入,book_edit_window.py 负责修改,删除按钮往往在图书列表里直接触发。这套窗口的共性问题是:表单字段多,校验复杂,稍不注意就会插入一坨脏数据。bookname、author、publisher 这些字段用 QLineEdit 接收,isbn 需要额外校验长度和字符格式。

def add_book(self): bookname = self.edit_bookname.text().strip() author = self.edit_author.text().strip() publisher = self.edit_publisher.text().strip() isbn = self.edit_isbn.text().strip() pubdate = self.edit_pubdate.text().strip() stock = self.spin_stock.value() if not bookname: QMessageBox.warning(self, "提示", "书名不能为空") return conn = get_conn() c = conn.cursor() c.execute( "INSERT INTO t_book(bookname, author, publisher, isbn, pubdate, stock) VALUES(?,?,?,?,?,?)", (bookname, author, publisher, isbn, pubdate, stock) ) conn.commit() conn.close() self.load_data() # 刷新表格 QMessageBox.information(self, "成功", "图书添加成功")

添加成功后调用 load_data() 刷新表格,这是很多初学者容易漏掉的一步——数据库里已经有数据了,页面却不刷新,看起来像“没加进去”。删除和编辑的流程类似,删除前要弹确认框,防止误删;编辑的时候要把选中行的 id 记下来,否则 UPDATE 语句的 WHERE 条件找不到对象。

4.2 模糊检索与分页:让查询在数据量大时还能用

图书检索用的 SQL 是 LIKE 模糊匹配,常见写法是:

keyword = self.edit_keyword.text().strip() sql = "SELECT * FROM t_book WHERE bookname LIKE ? OR author LIKE ? OR publisher LIKE ?" c.execute(sql, (f"%{keyword}%", f"%{keyword}%", f"%{keyword}%"))

注意 f"%{keyword}%" 里的百分号是 SQL 通配符,表示任意字符。如果 keyword 为空,这条查询等价于“查全部”。数据量几十本的时候这么查没问题,到了几千本,LIKE 前置百分号会导致全表扫描,明显变慢。常见优化方案是把图书表按首字母或者分类字段建索引,或者用 limit 分页。

这套资源里如果图书表数据量不大,不分页也能接受;但如果你是拿它做课设答辩,最好在 book_manage_window 里加分页按钮:每次查询后面加 LIMIT 20 OFFSET 0,然后通过翻页按钮修改 offset。这种改动很小,但答辩时能加不少分,因为评委问“数据量大了怎么办”时你能当场给出方案。

4.3 generate_data.py:批量造数据与报表场景

包里还带了一个 generate_data.py,这个文件的作用是批量生成测试数据。连接数据库后循环插入随机书名、随机作者、随机出版社,一次性造几百条记录。

import random from util.dbutil import get_conn book_names = ["Python编程", "PyQt5实战", "数据库原理", "算法导论", "计算机网络"] authors = ["张三", "李四", "王五", "赵六"] publishers = ["清华大学出版社", "人民邮电出版社", "电子工业出版社"] conn = get_conn() c = conn.cursor() for i in range(200): bookname = random.choice(book_names) + str(random.randint(1, 100)) author = random.choice(authors) publisher = random.choice(publishers) isbn = f"978-{random.randint(10000000, 99999999)}" stock = random.randint(0, 10) c.execute( "INSERT INTO t_book(bookname, author, publisher, isbn, stock) VALUES(?,?,?,?,?)", (bookname, author, publisher, isbn, stock) ) conn.commit() conn.close()

为什么要造数据?第一,测试模糊检索时,数据太少看不出效果,搜一个关键词可能只返回一两条;第二,测试报表功能时,比如按出版社统计图书数量、按借阅次数排序,必须要有足够多的样本才能看出趋势;第三,答辩演示时不至于只对着三本书演示界面。random.randint(0, 10) 设置随机库存,造出来的数据有零库存的书,可以顺便验证“库存不足时借书被拦截”的逻辑。

4.4 数据导出与可视化:报表能力怎么补

标题里的热搜词有“python数据分析与可视化”,这套资源原始版本不一定内置了完整的报表模块,但数据结构完全支持你补。比如想统计哪个作者被借阅次数最多,一条 SQL 就解决:

SELECT author, SUM(borrow_count) AS total_borrow FROM t_book GROUP BY author ORDER BY total_borrow DESC;

查出来的结果可以接到 matplotlib 或 pyecharts 画柱状图、饼图。PyQt5 窗口里嵌 matplotlib 图表的方式是 FigureCanvasQTAgg,把 FigureCanvas 实例 addWidget 到布局里,数据一刷新图表也跟着刷新。从“管理数据”升级到“用数据辅助决策”,整个项目的成品感会立刻上一个台阶。

5. PyQt5 与 SQLite 实战避坑:五个高频翻车现场

5.1 信号槽连不上:按钮点击没反应的排查顺序

现象:代码明明写了 button.clicked.connect(self.func),但运行后点击按钮毫无反应,或只报错“AttributeError: 'xx' object has no attribute 'func'”。

原因:最常见的是槽函数名拼写不一致,或者类里没定义这个函数;另一种是窗口对象没有被引用,局部变量在函数结束时被 Python 垃圾回收,信号自然连不上。

解决:先检查函数名是否拼写一致,注意 PyQt5 的信号连接是在 QObject 初始化时完成的,如果对象被 GC,连接也会断。用局部变量创建窗口时记得保存 self.window = MainWindow(),避免窗口一闪而过。排查时在槽函数第一行加 print("clicked"),能最快确认信号到底有没有进入槽函数。

5.2 PyQt5 与 SQLite 的类型错位:QVariant 和 Python 原生类型的拉扯

现象:从表格控件里取出来的数据明明看起来是数字,做四则运算时却报 “unsupported operand type(s) for -: 'str' and 'int'”。

原因:QTableWidget 的 item.text() 拿回的永远是字符串,就算单元格里显示的是“5”,它也是字符串“5”。而 SQLite 的 row_factory 用 sqlite3.Row 时返回的可变参数在 PyQt5 某些版本里会被包装成 QVariant,直接传给 int() 偶尔会出怪事。

解决:从界面控件取值后养成先转型再运算的习惯。int(item.text()),float(item.text()),用到时间就 from datetime import datetime 后显式 strptime。SQLite 查询结果取出来后,也先做一次类型转换,不要拿原始数据直接拼界面。

5.3 数据库写入报 database is locked:并发问题的真实来源

现象:两个窗口同时操作数据库,比如借书窗口正在 INSERT,后台报表线程在 SELECT,程序抛 “sqlite3.OperationalError: database is locked”。

原因:SQLite 是文件级锁,同一时刻只允许一个写事务。PyQt5 的 UI 是单线程的,正常不会撞锁,但一旦你用了 QThread 做后台任务,或者某个长事务没 commit 就挂在那,写锁会被一直持有。

解决:第一,所有数据库操作放到主线程做,或者统一放到一个数据库工具类里串行执行;第二,事务写完立刻 conn.commit(),不要等到函数末尾;第三,如果非要多线程访问,给 sqlite3.connect 加 timeout 参数,比如 sqlite3.connect('book.db', timeout=10)。最稳妥的做法是常驻一个连接对象,业务层共用它,而不是每次操作都建立新连接。

5.4 中文乱码:从 .ui 文件到 SQL 的编码链路

现象:界面标题正常,但数据库里查出来的作者名在表格里显示成“浣犲ソ”,或者 QMessageBox 里弹出一串问号。

原因:SQLite 存储的文本本身是 UTF-8 的,SQL 脚本导入时如果终端或 restore 工具的编码不对,中文字符就会被转乱。另外 PyQt5 在 Python 3 下对 str 的处理是原生的,但如果 .ui 文件里带的是 GBK 注释或默认字体不兼容,加载也会出问题。

解决:SQL 文件统一存成 UTF-8(不带 BOM),导入时指定编码 sqlite3 db < book.sql 前先确认终端编码;Python 代码第一行或文件头不用写 # -- coding: utf-8 --(Python 3 默认就是 UTF-8),但所有源文件用同一编码保存;连接 SQLite 时可以 PRAGMA encoding = "UTF-8" 兜底。遇到已经乱掉的库,最好的后悔药是删掉重建,用正确编码重新导入。

5.5 打包后找不到模板和资源:frozen_dir.py 为什么是必需品

现象:源码运行一切正常,PyInstaller 打包成 exe 后,双击启动报找不到 ui 文件、ico 图标,或者提示 “ModuleNotFoundError: No module named view.main_window”。

原因:PyInstaller 打包时把代码冻结在一个临时解压目录里,运行时 sys.path[0] 指向的是临时目录,而资源文件却没被打进包内,或相对路径全部失效。

解决:包里自带的 frozen_dir.py 就是干这个的。它的核心逻辑是:打包后用 sys.executable 的目录作为资源根目录,源码运行时用file的目录作为根目录。以下是一个典型实现:

import sys import os def app_path(relative_path=""): """获取程序运行时的真实路径,兼容源码运行与 PyInstaller 打包""" if getattr(sys, 'frozen', False): base = os.path.dirname(sys.executable) else: base = os.path.dirname(__file__) return os.path.join(base, relative_path)

所有加载图标、加载 .ui 文件、拼接数据库路径的地方,都改用 app_path() 返回的绝对路径,比如 self.setWindowIcon(QIcon(app_path("ico/logo.png")))。数据库文件也一样,写路径时不要写 book.db,而是写在可写目录下,避免程序装到 Program Files 后权限不足。

6. 进阶:用 QSS 改观感、用 PyInstaller 打包,以及一个关于“先跑通再美化”的教训

6.1 QSS 让界面摆脱“课程设计脸”

资源里 style 和 res 两个目录就是做这件事的。QSS 长得很像 CSS,PyQt5 所有继承自 QWidget 的控件都可以设 setStyleSheet。比如把登录按钮改成圆角渐变:

self.btn_login.setStyleSheet(""" QPushButton { background-color: #2d8cf0; color: white; border-radius: 6px; padding: 8px 16px; } QPushButton:hover { background-color: #57a3f3; } QPushButton:pressed { background-color: #1a7ad4; } """)

把整份 QSS 汇总到 style.qss 文件里,主窗口启动时读取一遍再 setStyleSheet(“”) 设置到顶层窗口,全部子控件都会继承这份皮肤。注意 QSS 里不支持所有 CSS 选择器,伪类只有 hover、pressed、focus 等少数几种,不要拿它当 CSS 写,否则效果不会生效。

6.2 PyInstaller 打包参数与入口选择

打包命令我习惯写成一行脚本,把窗口模式和图标路径都带上:

pyinstaller -w -F --icon=ico/app.ico --hidden-import view.main_window main.py

-w 表示无控制台窗口,-F 打包成单文件,--icon 指定 exe 图标,--hidden-import 把动态导入的 view 子模块强制打包进去。打包完先在 dist 目录跑一遍,如果报找不到数据库或资源,就按上一章 frozen_dir.py 的路径方法改代码再重新打包。

6.3 一个关于“先跑通再美化”的教训

我最早拿到这套资源时,第一反应是先去看 ui 目录和样式图片,花了两小时想怎么把界面做得更好看,结果连登录都登不进去——因为没先建库。后来每次接手这类项目,都强行规定自己按固定顺序走一遍:先看 README 和说明文档,再跑 book.sql 建库,然后 python main.py 跑通默认账号登录,最后才是研究代码结构和美化界面。先跑通再拆,比先拆再跑省事得多。希望这份包里的结构和踩坑经验,能让你少走这段弯路,也希望帮到你。

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

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

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

立即咨询