尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于Python+tkinter+MySQL的图书管理系统全流程实战解析

基于Python+tkinter+MySQL的图书管理系统全流程实战解析 简介面向计算机相关专业毕业设计、课程设计及大作业场景这是一套基于PythontkinterMySQL的图书管理系统完整项目。项目采用Python编写核心逻辑借助tkinter构建图形操作界面通过MySQL完成图书数据的持久化存储覆盖用户登录、图书添加、查询、删除、修改以及库存管理等典型功能模块。代码中包含完善的异常处理与错误提示数据库表结构设计也给出了合理方案适合希望提升GUI编程能力与数据库开发水平的学生借鉴。压缩包内共4个文件主要包含Python源码、PPT演示文稿、PDF项目说明文档和README说明文件整体大小约31.62MB。PPT可用于答辩展示源码可直接运行或在其基础上二次开发PDF文档有助于快速梳理系统架构与实现细节。目前已有272人学习下载能够为课程设计或毕业设计提供完整参考帮助读者理解桌面应用从界面到数据库的整套实现思路。1. 基于PythontkinterMySQL的图书管理系统要解决什么问题很多人觉得tkinter做GUI显得过时但放在毕业设计或课程设计里它反而是最稳妥的选择不需要额外安装PyQt这类第三方库python安装教程里的标准环境就自带tkinter用PyInstaller一打包就能跨机器运行底层接MySQL则能把数据管理从简单的文件读写提升到真正的数据库层面答辩时讲索引、事务、并发都有可聊的料。这个项目的本质是把图书的借、还、查、藏这些业务动作通过GUI事件驱动和SQL语句串联成一个完整闭环。开发时间往往不是花在界面拖拽上而是花在理解数据如何流转、异常如何回滚、多记录如何分页这些“看不见”的环节上。如果你正在做毕设或课设或者想补一个管理类项目的开发经验这篇文章从建库建表讲到界面交互再讲到打包发布把容易踩的坑和需要调的参数一次性写清楚。2. 从表结构到连接池图书管理系统的三层设计2.1 为什么是tkinter MySQL而不是PyQt SQLite在写任何代码之前先解决选型问题。我见过不少同学优先选PyQt理由是控件更现代还有Qt Designer能拖拽界面。但PyQt5的打包体积动辄上百MBGPL授权在答辩时也容易被老师追问。tkinter是Python标准库的一部分装好Python就有不需要额外pip install这对课设这种“机房环境随时可能被还原”的场景非常关键。数据库用MySQL而不是SQLite是因为毕设要求里往往写着“数据库系统”几个字。SQLite只是一个嵌入式文件没法讲事务日志、隔离级别而mysql安装配置教程几乎是后台开发者的必修课老师默认你会配。更重要的是MySQL支持多用户连接、行级锁和自定义索引后期做并发模拟时能拿出真实数据说话。组合学习成本打包体积答辩可讲内容tkinter MySQL低标准库25MB左右GUI事件驱动、SQL优化、事务、连接池PyQt SQLite中80MB信号槽、QSS、文件型数据库原理Web端 MySQL高依赖服务器HTTP协议、并发请求、前后端分离如果只做演示和答辩tkinter MySQL是投入产出比最高的组合。下面所有表结构和代码都基于这个组合进行。2.2 图书管理系统user、book、borrow三张核心表结构图书管理系统无论功能怎么扩展核心都围绕“谁能借、借了什么、什么时候还”来组织。我一般设计三张表用户表、图书表、借阅表。这里给出精简但可扩展的建表SQLCREATE DATABASE IF NOT EXISTS library_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE library_db; -- 用户表区分管理员和普通借阅者 CREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password CHAR(64) NOT NULL COMMENT SHA256哈希后的密码不存明文, real_name VARCHAR(50) DEFAULT NULL COMMENT 真实姓名, role ENUM(admin,reader) DEFAULT reader COMMENT 角色, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 图书表基本信息与库存状态 CREATE TABLE book ( id INT AUTO_INCREMENT PRIMARY KEY, isbn VARCHAR(20) UNIQUE COMMENT ISBN号, title VARCHAR(200) NOT NULL COMMENT 书名, author VARCHAR(100) DEFAULT NULL, publisher VARCHAR(100) DEFAULT NULL, category VARCHAR(50) DEFAULT NULL COMMENT 分类, total INT NOT NULL DEFAULT 1 COMMENT 总数量, available INT NOT NULL DEFAULT 1 COMMENT 可借数量, location VARCHAR(50) COMMENT 馆藏位置 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 借阅表一条借阅记录对应一本书的借出和归还 CREATE TABLE borrow ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, book_id INT NOT NULL, borrow_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, due_date DATETIME NOT NULL COMMENT 应还日期, return_date DATETIME DEFAULT NULL COMMENT 实际归还日期NULL表示未还, status ENUM(borrowed,returned,overdue) DEFAULT borrowed, FOREIGN KEY (user_id) REFERENCES user(id), FOREIGN KEY (book_id) REFERENCES book(id), INDEX idx_user_borrow (user_id, status), INDEX idx_book_available (available) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明available字段用来避免每次计算“总库存-借出数”但这也意味着更新时要保证一致性我会在借阅时用事务同时更新available和borrow。status可以用于后期统计逾期记录比如“有多少人借了书超过30天没还”。索引idx_book_available和idx_user_borrow对高频查询很有用第四章会用EXPLAIN验证它们是否生效。参数说明CHAR(64)是为了容纳SHA256的64位十六进制哈希值utf8mb4比utf8多支持生僻字和emojimysql安装配置教程里也建议默认使用。ENGINEInnoDB必须显式指定因为只有InnoDB支持事务和外键MyISAM在这两点上都不满足不适合图书管理这种对一致性有要求的场景。2.3 用PyMySQL建立连接与一个简单的连接池Python连接MySQL最常见的库是PyMySQL纯Python实现不用编译适合和tkinter一起打包。我的做法是单独建一个db.py模块管理连接不把数据库配置散落在各界面代码里。最基本的连接方式是这样import pymysql db_config { host: localhost, port: 3306, user: root, password: 123456, database: library_db, charset: utf8mb4, cursorclass: pymysql.cursors.DictCursor, autocommit: False, # 关闭自动提交事务由代码显式控制 } def get_connection(): return pymysql.connect(**db_config)注意autocommitFalse这是后面处理事务的关键。如果autocommitTrue那么conn.begin()不会生效每条SQL都会立刻提交数据一致性就无从谈起。连接池可以减少反复建立TCP连接的开销。tkinter是单击GUI频繁点击按钮时每次动态建立连接会明显卡顿。我会在db.py里用queue.Queue维护一个最小连接池import queue import pymysql class ConnectionPool: def __init__(self, max_conn5): self._pool queue.Queue(maxsizemax_conn) for _ in range(max_conn): self._pool.put(pymysql.connect(**db_config)) def acquire(self, timeout3): conn self._pool.get(timeouttimeout) try: # 连接可能被MySQL服务端超时断开ping失败则重建 conn.ping(reconnectTrue) except Exception: conn pymysql.connect(**db_config) return conn def release(self, conn): self._pool.put(conn)逻辑说明acquire从队列取一个现成的连接ping(reconnectTrue)会在连接失效时自动重连。release把用过的连接放回队列等待下一次复用。这里没有做线程锁因为tkinter的主要逻辑都在主线程跑同一时刻只有一个连接会被取出。如果你用后台线程做批量数据导入再考虑给池子加threading.Lock。参数说明max_conn5对单机单应用已经够用tkinter的事件循环决定了同一时间最多只有一个数据库操作在执行。当你的系统里同时有多个窗口查询时5个连接也能支撑。3. 用ttk.Treeview实现图书列表和增删改查3.1 登录窗口界面与数据库校验tkinter开发遵循“构建控件 定义事件函数 启动主循环”三步。这里使用tkinter.ttk子模块因为ttk的控件样式更接近现代系统你搜到的“tkinter ttk”就是指这一套组件。登录窗口的思路是用户输入用户名密码点击登录后台查询user表并校验SHA256哈希。import tkinter as tk from tkinter import ttk, messagebox import hashlib from db import get_connection class LoginWindow: def __init__(self): self.win tk.Tk() self.win.title(图书管理系统 - 登录) self.win.geometry(400x260) self.win.resizable(False, False) frame ttk.Frame(self.win, padding20) frame.pack(filltk.BOTH, expandTrue) ttk.Label(frame, text用户名).grid(row0, column0, pady5, stickytk.E) self.username_var tk.StringVar(valueadmin) ttk.Entry(frame, textvariableself.username_var, width25).grid(row0, column1, pady5) ttk.Label(frame, text密码).grid(row1, column0, pady5, stickytk.E) self.password_var tk.StringVar() ttk.Entry(frame, textvariableself.password_var, show*, width25).grid(row1, column1, pady5) ttk.Button(frame, text登录, commandself.check_login).grid(row2, column1, pady20, stickytk.EW) def check_login(self): username self.username_var.get().strip() password self.password_var.get() if not username or not password: messagebox.showwarning(提示, 用户名和密码不能为空) return sql SELECT id, role FROM user WHERE username%s AND password%s pwd_hash hashlib.sha256(password.encode()).hexdigest() try: conn get_connection() with conn.cursor() as cursor: cursor.execute(sql, (username, pwd_hash)) user cursor.fetchone() conn.close() except Exception as e: messagebox.showerror(数据库错误, str(e)) return if user: messagebox.showinfo(成功, 登录成功) self.win.destroy() from main_window import MainWindow MainWindow(user) else: messagebox.showerror(失败, 用户名或密码错误) def run(self): self.win.mainloop()逻辑说明密码在存储前经过SHA256哈希数据库里不会出现明文。SQL使用%s占位符即使密码中包含单引号等特殊字符也不会破坏SQL结构这是防止SQL注入最基本的手段。self.win.destroy()关闭登录窗口后创建主窗口实例这是tkinter中切换窗口的常用做法。参数说明show*让输入框以密文显示。stickytk.E让控件贴右对齐布局看起来更整齐。resizable(False, False)固定窗口大小避免登录界面被拉伸变形。这段代码放在login.py里直接运行就能看到窗口。3.2 主窗口布局tkinter左侧导航栏模板你搜索“tkinter左侧导航栏模板”得到的方案基本都用类似思路窗口左右分栏左边固定宽度放菜单按钮右边放内容区。我习惯用ttk.PanedWindow来做分栏方便拖动也更接近桌面应用的感觉。import tkinter as tk from tkinter import ttk class MainWindow: def __init__(self, user): self.user user self.win tk.Tk() self.win.title(图书管理系统) self.win.geometry(900x600) self.paned ttk.PanedWindow(self.win, orienttk.HORIZONTAL) self.paned.pack(filltk.BOTH, expandTrue) # 左侧导航栏 self.nav_frame ttk.Frame(self.paned, width160) self.nav_frame.pack_propagate(False) # 固定宽度防止子控件撑大 self.paned.add(self.nav_frame, weight0) # 右侧内容区 self.content_frame ttk.Frame(self.paned) self.paned.add(self.content_frame, weight1) buttons [ (图书管理, self.show_books), (借阅管理, self.show_borrows), (用户管理, self.show_users), ] for text, cmd in buttons: ttk.Button(self.nav_frame, texttext, commandcmd).pack( filltk.X, padx10, pady5) self.show_books() def switch_page(self, page_frame): for child in self.content_frame.winfo_children(): child.destroy() page_frame.pack(filltk.BOTH, expandTrue) def show_books(self): from pages.book_page import BookPage self.switch_page(BookPage(self.content_frame))逻辑说明pack_propagate(False)是让Frame的宽度不随子控件变化保证导航栏稳定在160像素。switch_page先清空右侧所有子控件再放入新页面这就是一个简单的单页应用效果。每个页面都是一个继承自ttk.Frame的类相互独立方便维护。参数说明weight0和weight1是PanedWindow为左右区域分配额外空间的比重。拖动窗口时右侧内容区会等比扩大左侧保持原始宽度。这个结构同样适合通讯录管理、商品管理等任何管理类项目替换按钮文字和页面类即可。3.3 Treeview表格展示与增删改查图书列表的核心控件是ttk.Treeview它支持多列展示、单选多选、绑定滚动条。下面的代码展示表格初始化和添加图书的完整路径。import tkinter as tk from tkinter import ttk, messagebox from db import get_connection class BookPage(ttk.Frame): def __init__(self, master): super().__init__(master) self.columns (id, isbn, title, author, publisher, category, available, total) self.tree ttk.Treeview(self, columnsself.columns, showheadings, height15) self.tree.pack(filltk.BOTH, expandTrue, padx10, pady10) headings {id: ID, isbn: ISBN, title: 书名, author: 作者, publisher: 出版社, category: 分类, available: 可借, total: 总量} widths {id: 40, isbn: 110, title: 180, author: 80, publisher: 100, category: 60, available: 60, total: 60} for col in self.columns: self.tree.heading(col, textheadings[col]) self.tree.column(col, widthwidths[col], anchortk.CENTER) self.refresh_books() def refresh_books(self): for item in self.tree.get_children(): self.tree.delete(item) sql (SELECT id, isbn, title, author, publisher, category, available, total FROM book ORDER BY id) conn get_connection() with conn.cursor() as cursor: cursor.execute(sql) rows cursor.fetchall() conn.close() for row in rows: self.tree.insert(, tk.END, valuestuple(row.values())) def add_book(self): title self.title_var.get().strip() if not title: messagebox.showwarning(提示, 书名不能为空) return total self.total_var.get().strip() if not total.isdigit() or int(total) 1: messagebox.showwarning(提示, 总量必须是大于0的整数) return sql (INSERT INTO book (isbn, title, author, publisher, category, total) VALUES (%s, %s, %s, %s, %s, %s)) try: conn get_connection() with conn.cursor() as cursor: cursor.execute(sql, (self.isbn_var.get().strip(), title, self.author_var.get().strip(), self.publisher_var.get().strip(), self.category_var.get().strip(), int(total))) conn.commit() conn.close() self.refresh_books() messagebox.showinfo(成功, 添加成功) except Exception as e: messagebox.showerror(错误, str(e))逻辑说明showheadings表示只显示列头不显示Treeview左侧的树形层级。refresh_books先清空现有行再全量查询数据量少时简单可靠。插入新书前做了两处校验书名非空、总量是正整数避免脏数据进库。integer和available的初始值默认等于total所以插入时只写了total。参数说明anchortk.CENTER让列内容居中。height15表示表格显示15行超出部分需要滚动条。你可以在refresh_books调用前创建垂直滚动条vsb ttk.Scrollbar(self, orienttk.VERTICAL, commandself.tree.yview)再设置self.tree.configure(yscrollcommandvsb.set)这样数据多时也能流畅查看。4. 让系统更稳的细节模糊查询、分页和事务处理4.1 模糊查询与参数化SQL防止SQL注入图书管理系统的查询框是必做模块最常见的是按书名或作者模糊查找。新手常犯的错误是直接拼接字符串# 错误示范绝对不能这么写 sql SELECT * FROM book WHERE title LIKE %{}%.format(keyword)keyword里如果包含单引号轻则查询报错重则被构造出 OR 11 --这样的注入攻击。正确写法是把通配符合并进参数值再通过占位符传参def search_books(self, keyword): sql (SELECT id, isbn, title, author, publisher, category, available, total FROM book WHERE title LIKE %s OR author LIKE %s) like_kw f%{keyword}% conn get_connection() with conn.cursor() as cursor: cursor.execute(sql, (like_kw, like_kw)) rows cursor.fetchall() conn.close() return rows逻辑说明%s是pymysql的占位符like_kw在传参前拼接通配符。这样既实现了LIKE模糊搜索又让pymysql对参数做安全转义恶意文本进入不了SQL执行体。如果用户输入了%或_它们会被当成MySQL通配符但不会破坏查询结构在图书管理这种低安全级别的场景下影响可以忽略。参数说明这里的LIKE %s默认为非二进制字符集的忽略大小写匹配。如果你在mysql创建索引时对title做了索引LIKE abc%才走索引而LIKE %abc%通常触发全表扫描这是MySQL的索引规则和数据量无关。4.2 分页查询LIMIT与COUNT的组合当图书数据超过一两百条一次性加载到Treeview会明显卡顿。分页需要两个动作查询当前页数据以及统计总数计算总页数。我习惯封装一个分页类class Pagination: def __init__(self, page_size20): self.page_size page_size self.current_page 1 self.total_pages 1 self.total_count 0 def load_data(self, keywordNone): base_sql FROM book WHERE 11 params [] if keyword: base_sql AND (title LIKE %s OR author LIKE %s) like f%{keyword}% params.extend([like, like]) conn get_connection() with conn.cursor() as cursor: # 先查总数 cursor.execute(fSELECT COUNT(*) AS cnt {base_sql}, params) self.total_count cursor.fetchone()[cnt] self.total_pages max(1, (self.total_count self.page_size - 1) // self.page_size) if self.current_page self.total_pages: self.current_page self.total_pages # 再查当前页 offset (self.current_page - 1) * self.page_size cursor.execute(fSELECT * {base_sql} ORDER BY id LIMIT %s, %s, params [offset, self.page_size]) rows cursor.fetchall() conn.close() return rows逻辑说明base_sql只写表名和WHERE条件这样COUNT(*)和SELECT *能复用同一套参数。总数算完后计算总页数如果当前页超出总页数就回退到最后一页。注意LIMIT的两个参数也通过占位符传入防止数字部分被注入。参数说明page_size20是经验值。Treeview在渲染50个以上item时滚动会有可见的掉帧。答辩演示时保持20比较稳妥。如果你的系统有上千册书还可以把page_size做成下拉框让老师自己选每页显示条数。4.3 借阅归还中的事务处理借书动作包含两个写操作向borrow表插入记录同时把book表的available减1。任何一个失败都会导致数据不一致所以必须放在事务里from datetime import datetime, timedelta def borrow_book(self, user_id, book_id): conn get_connection() try: conn.begin() with conn.cursor() as cursor: # 锁定该图书当前行防止并发超借 sql_check SELECT available FROM book WHERE id%s FOR UPDATE cursor.execute(sql_check, (book_id,)) row cursor.fetchone() if not row or row[available] 0: raise ValueError(没有可借的库存) due_date datetime.now() timedelta(days30) sql_insert (INSERT INTO borrow (user_id, book_id, due_date) VALUES (%s, %s, %s)) cursor.execute(sql_insert, (user_id, book_id, due_date)) sql_update UPDATE book SET available available - 1 WHERE id%s cursor.execute(sql_update, (book_id,)) conn.commit() return True except Exception: conn.rollback() raise finally: conn.close()逻辑说明SELECT ... FOR UPDATE对book这一行加排他锁直到事务提交或回滚。两个同学同时点借同一本书时第二个事务会等待第一个提交后拿到最新的available值从而判断出库存是否为0。这是最直观的并发控制方案答辩时能自然引出“锁”和“隔离级别”这两个概念。参数说明timedelta(days30)是默认借期可以从配置表读取。归还操作和借书对称把borrow表的return_date更新为当前时间status改为returned再把book表的available加1同样需要事务。这里再次强调db_config里的autocommit必须为False否则conn.begin()无效事务也就形同虚设。如果你想进一步简化调用可以把借阅逻辑写成MySQL存储过程通过cursor.callproc(borrow_book, (user_id, book_id))触发。这种做法把事务控制移动到数据库端但调试时需要在mysql命令行单独验证存储过程增加了一定的开发成本。我一般还是用Python层的事务可读性和维护性更好。4.4 MySQL连接参数与索引优化当借阅记录到达万级以后查询速度会明显下跌。不要盲目调服务器内存先用数据库命令定位瓶颈。常用的mysql数据库命令大全里EXPLAIN、SHOW INDEX、SHOW PROCESSLIST是排查的三板斧。下面是我的调优清单参数/操作推荐值/做法效果max_connections50-100防止连接池占满导致“Too many connections”wait_timeout配合连接池ping连接被服务端断开后自动重连innodb_buffer_pool_size物理内存的50%-70%InnoDB缓存表数据与索引降低磁盘IO复合索引(user_id, status)加速“某用户未还的书”查询EXPLAIN检查看type是否为ref或range判断索引是否生效用EXPLAIN验证索引是否生效EXPLAIN SELECT * FROM borrow WHERE user_id3 AND statusborrowed;如果结果里type为ALL说明全表扫描此时应在borrow表上执行ALTER TABLE borrow ADD INDEX idx_user_borrow (user_id, status)再跑一次EXPLAIN通常就会变成typeref。这组对照能写进课程设计的测试报告比单纯贴截图有说服力。5. 打包exe避开坑PyInstaller参数和配置文件外置代码写完后的临门一脚是打包成exe否则老师电脑上没有Python环境就没法演示。我常用的打包命令是pyinstaller -F -w --iconlibrary.ico -n 图书管理系统 main.py参数说明-F把所有依赖打进一个exe文件方便拷贝-w让程序运行时不弹出黑色控制台窗口--icon指定图标-n设置生成的程序名。打包完成后dist目录下会生成“图书管理系统.exe”直接双击运行。打包最常见的坑有两个。第一个是pymysql模块找不到解决办法是在main.py文件最顶部主动写import pymysql因为PyInstaller静态分析时无法识别某些动态导入的子模块。第二个是图标文件路径问题建议图标放在和main.py同级的目录打包时使用相对路径而不是绝对路径否则换一台机器图标就会消失。为了让exe能在不同MySQL环境上跑起来我建议把数据库连接配置外置到一个config.ini程序启动时动态读取避免每次改密码都重新打包import configparser import os def load_config(): conf configparser.ConfigParser() conf_file os.path.join(os.path.dirname(os.path.abspath(__file__)), config.ini) conf.read(conf_file, encodingutf-8) return { host: conf.get(mysql, host, fallbacklocalhost), user: conf.get(mysql, user, fallbackroot), password: conf.get(mysql, password, fallback), database: conf.get(mysql, database, fallbacklibrary_db), }配置文件内容[mysql] host localhost user root password 123456 database library_db集成到db.py时在构造db_config之前调用load_config()就能替换硬编码的字典。打包时把config.ini放在exe同目录老师换到自己的机器后只需要改用户名和密码不需要重新打包。答辩前多做一个加分小功能在系统里增加“数据统计”页面用聚合查询展示各个分类的图书数量和借出情况。例如SELECT category, COUNT(*) AS total, SUM(CASE WHEN available total THEN 1 ELSE 0 END) AS borrowed_count FROM book GROUP BY category;把这段SQL的结果渲染成表格或简单的柱状图老师会默认你掌握了聚合查询和子查询的综合运用。这样整个项目从界面到数据库再到打包发布每个环节都有可以展开讲的技术细节。本文还有配套的精品资源点击获取
返回列表