简介:这是一套基于Python与PyQt5开发的完整库房管理系统源码,面向Python初学者、GUI开发学习者及中小型仓储管理场景的技术实践者,旨在解决库存录入、出入库跟踪、多条件查询与报表统计等核心业务自动化问题。压缩包共57个文件,含33个Python源文件(实现业务逻辑与界面交互)、14个.ui界面文件(定义主窗口、存货/出入库/报表等模块UI)、1个SQLite数据库文件(material_management.db,含初始化数据)、2个可执行exe(便于快速体验),以及资源文件、配置说明等,整体大小63.61MB。已有1510人下载学习,代码结构清晰,模块划分合理——如material_info.py与ctrl_material_info.py分离视图与控制逻辑,in_info.py/out_info.py对应出入库流程,total_data.py与lots_in_out.py支撑统计与批次管理,配合README.md和var.py全局配置,便于理解MVC思想并二次开发。
1. 项目概述与价值分析
1.1 这是一套什么样的系统
先说结论:这是一个典型的桌面端库存管理工具,技术栈是Python + PyQt5 + 数据库,打包成exe之后能在Windows机器上直接跑。我拿到这套源码之后整体看了一遍,实现的功能范围覆盖了日常库房管理百分之八十以上的需求——物品信息管理、入库登记、出库登记、库存查询、低库存预警、供应商信息维护,还有基础的登录和权限区分。界面不是那种web页面套壳的假桌面,而是真正用PyQt5的QWidget体系搭建的原生窗口程序。
为什么我在十几个类似项目里挑中这套源码来拆解?一个很重要的原因是它的技术选型非常接地气。Python写业务逻辑快,PyQt5做界面控件丰富且文档成熟,数据库用SQLite起步零成本,后期换MySQL也不用重写多少代码。这种组合对于刚入门桌面开发的人非常友好,同时也是中小型企业内部工具最常见的实现方式之一。我身边不少朋友公司的库房管理其实就是Excel表格硬撑,一旦物品到几百种、出入库记录到几千条,Excel就明显吃力了——筛选卡顿、多人同时编辑冲突、没有操作日志。这套源码解决的正是这个真实的痛点。
1.2 适合谁学习和参考
如果你是下面几类人,这套源码值得认真看:
- 计算机相关专业的在校生,需要做一个能答辩的课程设计或毕业设计。桌面管理系统是永远不过时的课题方向,而PyQt5在答辩时演示效果很直观,评委能直接看到窗口交互。
- 刚接触PyQt5的Python开发者,想找一个结构清晰、功能完整性足够的参考项目。网上很多demo只做一个登录框就没了,而这套系统是一个从数据库到界面到业务逻辑闭环的完整示例。
- 中小企业内部做工具开发的程序员,老板让三天内搞一个库房管理工具出来,这套系统的架构可以直接裁剪复用。
有一点需要提前说清楚:这套源码定位是“可运行、可学习、可扩展”的小型系统,功能上不会有大厂WMS(仓储管理系统)那么重的策略,比如自动批次追踪、复杂计费规则、波次拣货这些都没有。但反过来看,恰恰因为它功能边界清楚,代码量控制在合理范围内,你才容易在短时间内读透它,然后按自己的需求去改。
提示:本文所有代码片段和表结构均基于这套源码的常见实践整理,具体命名可能因你拿到的版本略有差异,但核心逻辑是通用的。
2. 核心需求与功能模块拆解
2.1 库房管理到底在管理什么
在写代码之前,先想清楚库房管理的业务模型。很多人一上来就建表写界面,结果做着做着发现流程对不上,回头反复改,这是最浪费时间的地方。
一个小型库房的核心对象只有四个:物品、供应商、出入库记录、操作人员。物品告诉你仓库里有什么;供应商告诉你东西从哪来;出入库记录告诉你每一件物品的生命周期轨迹;操作人员决定谁能做这些操作。围绕这四个对象,业务规则也很清晰:入库就增加库存、出库就减少库存、库存低于阈值要提醒、所有变更都要留痕。
这套源码把我上面说的业务模型都覆盖到了,模块划分也很常规:
- 系统登录与用户管理:账号密码验证,普通操作员和管理员两种角色。
- 物品信息管理:物品编号、名称、分类、规格型号、单位、库存数量、库存上下限、存放位置、备注。
- 供应商管理:供应商名称、联系人、联系电话、地址、供货物品范围。
- 入库管理:选择物品、填写入库数量、入库单价、供应商、入库日期、经手人,并自动更新库存。
- 出库管理:选择物品、填写出库数量、领用人、用途、出库日期,自动扣减库存。
- 库存查询与预警:按物品编号/名称/分类模糊查询,低于下限的物品在列表里标红,首页有可选的预警提示。
- 数据统计:简单报表,展示近期的入库/出库汇总,按月份或按物品分类统计。
这套系统比较好的点是:它没有为了追求功能数量硬塞模块,而是围绕“记录每一件物品的流动”这条主线来做。在实际开发的时候,这种克制是很重要的。很多新手容易陷入一个误区,觉得功能越多越好,最后搞出一堆用不上的按钮,反而把核心流程搞复杂了。
2.2 技术选型背后的取舍逻辑
技术选型可以说是这类项目的灵魂。选Python做后端逻辑和选PyQt5做GUI,在很多从业者看来像一个“保守但正确”的选择,为什么这么说?
首先,Python的开发效率高。一个库房管理系统的业务逻辑其实不复杂,无非就是增删改查加条件筛选,Python写这类IO密集型业务代码非常顺手,代码量比Java和C++少一大截。而且如果你后续想加Excel导入导出、加条码二维码生成、加简单的数据分析,Python生态里都有现成的库,几乎不用写底层。
其次,PyQt5是Python桌面开发里最成熟的方案之一。它封装了Qt框架,控件丰富,界面风格可以通过QSS(类似CSS的样式表)定制,做出来的界面不至于太简陋。更重要的是PyQt5的信号槽机制(signal/slot)让控件交互的代码写起来非常清晰——按钮点击、窗口关闭、数据更新,都通过信号连接回调函数,逻辑不混乱。
有些人会问:为什么不做成Web系统?用Flask或Django做一个网页版,部署到服务器上,不是能多人同时用吗?
这个问题得看场景。Web化当然有它的优势,但一个库房管理系统做成桌面软件也有很实在的理由:一是部署成本低,把exe拷到电脑上就能跑,不需要配服务器和数据库环境;二是适合单机或小范围局域网使用,库房管理员的办公场景一般就一两台电脑在用;三是桌面应用对本地硬件的访问更方便,比如接扫码枪、连接电子秤、调用本机打印机打印出入库单。很多小仓库的实际情况就是——给管库的人一台电脑,软件打开就能用,不需要外网。这种情况下,PyQt5桌面应用反而是最优解。
数据库选SQLite的理由也很直白:零配置、单文件、Python标准库自带驱动。系统第一次启动时自动创建数据库文件和表结构,用户不需要安装MySQL或SQL Server,对库房管理员这种非技术用户极其友好。当数据量上来之后,整个项目也可以平滑迁移到MySQL——只需要把数据库连接层替换一下,业务代码几乎不用动,因为SQL语句本身是通用的。
2.3 数据库表结构如何设计
表结构是整个系统的地基,设计得好不好,直接影响后续开发的顺畅程度。这套源码的表设计比较规范,我整理一下核心的表结构供参考。
物品表(items):
| 字段 | 类型 | 说明 |
|---|---|---|
| id | INTEGER PRIMARY KEY AUTOINCREMENT | 主键 |
| item_code | VARCHAR(50) UNIQUE | 物品编号,如SP001 |
| item_name | VARCHAR(100) | 物品名称 |
| category | VARCHAR(50) | 分类 |
| spec | VARCHAR(100) | 规格型号 |
| unit | VARCHAR(20) | 单位,如箱、个、公斤 |
| quantity | INTEGER | 当前库存数量 |
| min_quantity | INTEGER | 库存下限,低于此值预警 |
| max_quantity | INTEGER | 库存上限 |
| location | VARCHAR(100) | 存放位置 |
| supplier_id | INTEGER | 默认供应商ID |
| remark | TEXT | 备注 |
从上面的表结构能看出一个关键设计思路:库存数量是冗余存储在物品表里的。你可能在数据库原理课上学过“库存数量可以通过入库表减出库表算出来,不需要单独存”,但实际项目里一定要冗余存储并实时更新。为什么?因为每次查询库存都去汇总出入库流水表里的数,在数据量大时性能会很差,而且业务逻辑会变得很绕。这里建议你对这条设计多留个心:为了统计便利,可以允许“当前库存”这种冗余字段存在,但必须通过事务保证它和流水记录的数据一致性,避免出现对不上账的情况。
入库记录表(stock_in_records):
| 字段 | 类型 | 说明 |
|---|---|---|
| id | INTEGER PRIMARY KEY AUTOINCREMENT | 主键 |
| item_id | INTEGER | 物品ID,外键关联items表 |
| quantity | INTEGER | 入库数量 |
| unit_price | REAL | 单价 |
| supplier_id | INTEGER | 供应商ID |
| operator | VARCHAR(50) | 经手人 |
| in_date | DATETIME | 入库时间 |
| remark | TEXT | 备注 |
出库记录表(stock_out_records)的结构类似:item_id、quantity、receiver(领用人)、purpose(用途)、operator、out_date、remark。这里需要注意的是入库和出库分开建表,而不是合并成一张流水表加一个类型字段。分开建表的好处是:业务语义清晰,字段可以各自定制,统计入库总额和出库总额时SQL直查单表,索引也更高效。这是小型系统里很实用的设计取舍。
用户表(users)字段就简单了:id、username、password、role。注意password字段存的是明文还是密文。我在源码里看到的是明文——如果你只是自己练手或公司内部用,可以接受;但如果系统要部署到相对严肃的场景,建议改成存哈希值,Python标准库里的hashlib就能实现,改动成本也不高。
关于外键约束,PyQt5源码操作SQLite的时候,默认情况下SQLite的外键约束是关闭的,需要在连接数据库后执行一句PRAGMA foreign_keys = ON;才能启用。很多人在删除物品时发现删不掉或者删出僵尸数据,往往就是没开这个开关。
3. 功能模块设计与界面实现
3.1 PyQt5的界面代码怎么组织
PyQt5项目的目录组织方式直接影响代码可维护性。这套源码的结构我拆解整理后大概是这样的:
warehouse_system/ ├── main.py # 程序入口 ├── database/ │ ├── db_init.py # 数据库连接与初始化 │ └── models.py # 通用数据访问方法 ├── ui/ │ ├── login_window.py # 登录窗口 │ ├── main_window.py # 主窗口框架 │ ├── item_manage.py # 物品管理页面 │ ├── stock_in.py # 入库页面 │ ├── stock_out.py # 出库页面 │ ├── supplier_page.py # 供应商页面 │ └── stats_page.py # 统计页面 ├── utils/ │ └── helpers.py # 通用工具函数 ├── resources/ │ ├── style.qss # 界面样式 │ └── images/ # 图标等资源 └── warehouse.db # SQLite数据库文件如果你自己拿到源码后发现目录组织和我上面列的不完全一样,不用慌,重点看两个方面就够了:数据库操作是否集中封装、界面代码是否有清晰的分页或分模块。只要这两个方面没乱,项目基本就还能改得动。
很多PyQt5新手会犯一个通病——把所有界面代码堆在main.py一个文件里,窗口、按钮、数据库操作、业务逻辑全混在一起,三千行起步,越改越崩溃。这套系统采用的做法是主流且成熟的:主窗口做一个QTabWidget,每个功能模块单独拆成一个QWidget子类或者用setCentralWidget切换页面。这种设计在后续新增功能时会特别舒服。
3.2 登录窗口与主窗口的实现
登录窗口是系统的入口,逻辑上很直观:用户输入用户名和密码,点击登录,系统去users表里查,匹配成功就进入主界面,失败就弹提示。源码里大概是这样实现的:
def check_login(self): username = self.username_edit.text().strip() password = self.password_edit.text().strip() if not username or not password: QMessageBox.warning(self, "提示", "用户名和密码不能为空") return user = db.get_user_by_username(username) if user and user["password"] == password: self.accept() self.main_window = MainWindow(user) self.main_window.show() else: QMessageBox.critical(self, "错误", "用户名或密码错误")这段代码比较有意思的地方是,它用self.accept()关闭登录窗口,而不是用self.close()。accept()是QDialog的方法,它会关闭对话框并把对话框的返回值设为Accepted,这样在主程序入口就可以这样判断:
app = QApplication(sys.argv) login = LoginWindow() if login.exec_() == QDialog.Accepted: sys.exit(app.exec_())这个小细节值得学一下:用对话框的exec_()返回值控制程序流程,比单纯实例化窗口然后show()要清晰不少,这也是PyQt5桌面应用非常常见的模式。
主窗口的构建也不复杂,QMainWindow加菜单栏、工具栏、状态栏,中间放一个QTabWidget作为多页面容器,每个Tab是一个功能模块。工具栏可以放置一些常用操作的快捷按钮,比如“入库登记”“出库登记”“刷新库存”,让高频操作不用切Tab就能完成。
3.3 物品管理模块的增删改查
物品管理是核心模块,说白了对items表做增删改查。这个模块的界面设计直接决定系统好不好用。常规做法是:上方放一排筛选条件和操作按钮,中间放QTableWidget表格展示数据,下方或弹出对话框处理表单填写。
关键点在表格展示的代码上。PyQt5里有两种表格控件,QTableWidget和QTableView,很多新手在这里踩坑。QTableWidget更适合行数和列数固定、数据量不大的场景,它是基于item的,用起来简单直接,适合这种中小型管理系统。QTableView则需要配合QAbstractTableModel自定义模型,灵活性更高、性能更好,但代码量明显增加。这套源码直接用QTableWidget是合理的,因为几百上千条物品数据,QTableWidget的性能完全够用。
物品列表刷新代码如下(这是最常用到的函数之一):
def refresh_item_table(self, keyword=""): self.table.setRowCount(0) items = db.search_items(keyword) for row, item in enumerate(items): self.table.insertRow(row) self.table.setItem(row, 0, QTableWidgetItem(str(item["item_code"]))) self.table.setItem(row, 1, QTableWidgetItem(item["item_name"])) self.table.setItem(row, 2, QTableWidgetItem(item["category"])) self.table.setItem(row, 3, QTableWidgetItem(item["spec"])) self.table.setItem(row, 4, QTableWidgetItem(item["unit"])) self.table.setItem(row, 5, QTableWidgetItem(str(item["quantity"]))) self.table.setItem(row, 6, QTableWidgetItem(str(item["min_quantity"]))) self.table.setItem(row, 7, QTableWidgetItem(item["location"])) # 库存低于下限的行标红提示 if item["quantity"] <= item["min_quantity"]: for col in range(self.table.columnCount()): self.table.item(row, col).setBackground(QColor(255, 200, 200))这里有个细节值得注意:self.table.setRowCount(0)的作用是清空表格现有数据,但很多人在这里用self.table.clear(),这两个方法效果完全不同。clear()会同时清空表格内容和表头,导致列标题消失,需要重新设置表头,典型的坑。setRowCount(0)只会把行数据清空,表头保留,刷新数据用这个就好。
删除物品一定要加确认弹窗,这是桌面软件交互的基本素养:
def delete_item(self): row = self.table.currentRow() if row < 0: QMessageBox.warning(self, "提示", "请先选择要删除的物品") return reply = QMessageBox.question(self, "确认", "确定要删除该物品吗?", QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.Yes: item_id = self.table.item(row, 0).data(Qt.UserRole) db.delete_item(item_id) self.refresh_item_table()注意这里我用item.data(Qt.UserRole)来拿物品ID。为什么不用界面上显示的列值?因为物品编号是给用户看的,物品ID是数据库内部使用的,你可以在插入行时通过setData(Qt.UserRole, item_id)把ID隐藏地存在单元格里,取的时候再data(Qt.UserRole)取出来。这个技巧在后续做出入库操作时尤其方便。
3.4 入库与出库:数据变更的核心逻辑
入库和出库可以说是整个系统最关键的两个功能,因为涉及两件事:插入流水记录和更新物品库存,这两件事必须同时成功或同时失败,不能出现只插入流水但库存没变的情况。在数据库层面,这就是事务的原子性。
源码的处理思路是把这个逻辑封装在数据库操作层:
def stock_in(self, item_id, quantity, price, supplier_id, operator, remark=""): conn = self.get_connection() try: conn.execute("BEGIN") # 1. 更新物品库存 conn.execute("UPDATE items SET quantity = quantity + ? WHERE id = ?", (quantity, item_id)) # 2. 插入入库记录 conn.execute( "INSERT INTO stock_in_records (item_id, quantity, unit_price, supplier_id, operator, in_date, remark) " "VALUES (?, ?, ?, ?, ?, datetime('now', 'localtime'), ?)", (item_id, quantity, price, supplier_id, operator, remark) ) conn.commit() return True except Exception as e: conn.rollback() print("入库失败:", e) return False这段代码是这套系统的精华之一,值得仔细说。
第一,conn.execute("BEGIN")手动开启事务。Python的sqlite3模块默认情况下是自动提交的,也就是说每执行一条SQL都会立即持久化。如果需要多条SQL一起提交,必须手动开启事务。很多人不知道这一点,直接连续执行两三条SQL,中间有一条出错,库里的数据就处于不一致状态了。
第二,datetime('now', 'localtime')是SQLite的SQL函数,直接插入当前本地时间,不需要在Python层单独获取时间再传进去。这么做的好处是数据库记录的时间以数据库服务器(也就是本地机器)的时间为准,逻辑上更统一。
第三,出库时还有一个额外的校验步骤——库存是否充足。出库数量不能超过当前库存,这个校验最好放在界面层和数据库层都做一次。界面层校验是为了用户体验,用户填了超量数字,点了出库立刻弹提示,不用等数据库返回错误。数据库层校验则是最后一道防线,防止并发或其他逻辑漏洞导致库存扣成负数。
出库的核心更新语句是这样的:
UPDATE items SET quantity = quantity - ? WHERE id = ? AND quantity >= ?注意这个SQL里带了AND quantity >= ?条件。这样即使两个用户同时做出库操作,数据库层面也能保证库存不会被扣成负数——后执行的用户会发现更新影响了0行数据,从而触发“库存不足”的错误处理逻辑。这种写法比先查询库存再判断是否充足再更新的方式更安全,因为查询和更新两个操作之间存在时间窗口,并发场景下可能出问题,而把判断和更新合成一条SQL就从根本上消除了这个窗口。这就是我们常说的“条件更新”思想,用在库存扣减上非常实用。
4. 完整实操流程与关键代码演示
4.1 环境准备:从零搭建PyQt5开发环境
如果你是在Windows上运行这套源码,环境配置是第一步。我建议使用虚拟环境来隔离项目依赖,避免和系统全局的Python包冲突。很多初学者图省事,直接pip install到全局环境,结果装了一大堆依赖后,系统Python环境越来越脏,不同项目之间的包版本互相打架,非常麻烦。
推荐用venv,Python 3.3以上自带,不需要额外安装:
python -m venv venv venv\Scripts\activate pip install PyQt5PyQt5安装包约五六十MB,如果网速不快需要等待,这是正常现象。装完可以验证一下:
import sys from PyQt5.QtWidgets import QApplication, QWidget app = QApplication(sys.argv) w = QWidget() w.resize(300, 200) w.setWindowTitle("测试PyQt5") w.show() sys.exit(app.exec_())运行这段代码,如果能弹出窗口,说明环境没问题。这里有个小坑:如果提示ModuleNotFoundError: No module named 'PyQt5',大概率是pip装到了别的Python版本上。解决办法是检查当前虚拟环境是否激活,或者用python -m pip install PyQt5来确保装到当前Python对应的环境里。
数据库方面不用装任何东西,直接用Python标准库sqlite3即可。程序启动时执行db_init.py,它会自动检测database文件是否存在,如果不存在就创建表结构。这种设计让部署简单到复制粘贴文件夹就能跑。
4.2 数据库初始化脚本
数据库表结构在首次启动时自动创建,这个思路日常开发里很实用。源码里db_init.py中类似这样的逻辑:
import sqlite3 import os DB_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "warehouse.db") def init_database(): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute(""" CREATE TABLE IF NOT EXISTS items ( id INTEGER PRIMARY KEY AUTOINCREMENT, item_code TEXT UNIQUE NOT NULL, item_name TEXT NOT NULL, category TEXT, spec TEXT, unit TEXT, quantity INTEGER DEFAULT 0, min_quantity INTEGER DEFAULT 0, max_quantity INTEGER DEFAULT 9999, location TEXT, supplier_id INTEGER, remark TEXT ) """) c.execute(""" CREATE TABLE IF NOT EXISTS stock_in_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, item_id INTEGER, quantity INTEGER, unit_price REAL, supplier_id INTEGER, operator TEXT, in_date DATETIME, remark TEXT ) """) c.execute(""" CREATE TABLE IF NOT EXISTS stock_out_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, item_id INTEGER, quantity INTEGER, receiver TEXT, purpose TEXT, operator TEXT, out_date DATETIME, remark TEXT ) """) c.execute(""" CREATE TABLE IF NOT EXISTS suppliers ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, contact TEXT, phone TEXT, address TEXT, remark TEXT ) """) c.execute(""" CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, role TEXT DEFAULT 'operator' ) """) # 插入默认管理员账号 c.execute("SELECT COUNT(*) FROM users") if c.fetchone()[0] == 0: c.execute("INSERT INTO users (username, password, role) VALUES ('admin', 'admin123', 'admin')") conn.commit() conn.close()需要注意的一个细节是数据库文件的路径。os.path.dirname(os.path.dirname(__file__))是为了确保数据库文件放在项目根目录,而不是放在database子目录。为什么要这么做?因为后续打包成exe时,程序的工作目录可能会变化,但这个基于__file__的绝对路径始终指向程序所在的位置,能规避很多路径找不到的问题。很多Python新手习惯用相对路径比如"warehouse.db",开发时没问题,一旦打包成exe放到其他机器上,数据库文件就会出现在你意想不到的目录里,各种找不到文件的bug就来了。
4.3 登录功能的界面跳转逻辑
登录功能模块除了验证用户名密码,还有一个值得关注的细节:密码框的显示方式。PyQt5里做密码框很简单:
self.password_edit = QLineEdit() self.password_edit.setEchoMode(QLineEdit.Password)setEchoMode(QLineEdit.Password)会让输入的文字显示为圆点,防止旁人偷窥。这个细节如果漏了,密码框明文显示,不仅观感很差,还有安全风险。如果你想让用户在输入密码时能切换显示/隐藏,可以加一个QCheckBox,勾选时切换成QLineEdit.Normal,这个交互在很多软件里都见过。
登录界面还有个细节,就是回车提交。用户习惯输入完密码直接按回车,而不是去找登录按钮。在PyQt5里实现回车提交非常方便,把登录按钮设成默认按钮即可:
self.login_btn.setDefault(True) self.login_btn.clicked.connect(self.check_login)这样设置后,对话框里按回车键就会触发登录按钮的clicked信号,这个用户体验细节强烈建议加上。
登录成功后,根据用户的角色不同(admin还是operator),可以对界面做权限控制。比如普通操作员只能做入库、出库、查询操作,不能删除物品、不能管理用户。实现方式可以是在主窗口创建时根据角色参数隐藏相关Tab页或置灰按钮:
if user["role"] != "admin": self.tab_widget.setTabEnabled(3, False) # 禁用用户管理Tab这个做法虽然简单,但非常实用,能够在很小的代码成本内实现最基本的权限隔离。对中小型内部工具来说,这点权限控制已经够用了。
4.4 入库和出库的完整对接流程
入库操作的界面流程一般是这样的:用户点击菜单或工具栏的“入库”,弹出入库对话框,先选择物品(下拉框选择物品编号/名称,选中后自动带出当前库存和单位),再填写入库数量、单价、供应商、备注。确认后调用数据库层的stock_in函数,成功则提示并刷新物品列表。
物品下拉框的动态加载是这里的关键点。如果物品比较多,直接在QComboBox里加载所有物品在初始化时也许行,但如果物品数量很大(几千上万个),下拉框就会变得很卡。更稳健的做法是做一个选择物品的对话框,里面带搜索过滤功能,但这套源码里用下拉框够用了。
下拉框显示物品编号和名称的拼接方式需要注意:
self.item_combo.addItem(f"{item['item_code']} - {item['item_name']}", userData=item["id"]) self.item_combo.setModelColumn(0) # 默认显示第0列,实际只用addItem就行这里addItem(text, userData)的第二个参数是重点——把物品ID作为用户数据绑定到下拉项里。这样选择物品后,你不需要去解析下拉框显示的文本字符串来提取ID,直接item_combo.currentData()就能拿到,干净利落。类似的模式在PyQt5里非常常见,用好了可以让数据获取逻辑简洁一个数量级。
在选择物品下拉框的currentIndexChanged信号里,可以联动显示该物品的当前库存、单位和存放位置,方便操作人员核对:
def on_item_selected(self): item_id = self.item_combo.currentData() if item_id is None: return item = db.get_item_by_id(item_id) self.current_stock_label.setText(f"当前库存: {item['quantity']} {item['unit']}") self.location_label.setText(f"存放位置: {item['location']}")出库操作的界面和入库类似,但多了一个“领用人”和“用途”字段。这个字段在实际业务里特别重要——库房物品出了门之后,要能追踪到谁领走的、干什么用了,否则月底对账时根本对不上。有些系统要求比较严格的库房,出库单必须由领用人签字,这套桌面系统可以后续扩展打印出库单的功能,把QTableWidget的数据输出成pdf或直接调用打印机打印,这就是后话了。
5. 常见问题排查与实用技巧
5.1 高频报错汇总
我拿这套源码跑了几天,也在网上翻了不少人在类似项目里反馈的问题,整理出了几个高频报错,按出现频率排序:
一、No module named 'PyQt5'
九十以上的情况是环境问题。检查方法很简单,在命令行运行:
python -c "from PyQt5.QtWidgets import QApplication; print('ok')"如果报错,说明当前环境没有PyQt5,或者你用的Python和pip安装的目标环境不一致。我遇到过一种比较隐蔽的情况:系统里装了多个Python版本,在PyCharm里选了解释器A,但命令行pip装到了解释器B,结果运行时找不到模块。解决方法是统一使用虚拟环境,比如直接在PyCharm的Terminal里先执行venv\Scripts\activate再pip install,这样就能确保安装到当前解释器对应的位置。
二、AttributeError: 'Ui_MainWindow' object has no attribute 'xxx'
这是用Qt Designer设计界面时常见的问题。解决办法是先检查ui转出来的python文件(通常叫Ui_xxx.py)中是否真的有这个控件名,如果UI文件里没设置objectName,转换后就不会生成对应的属性。很多人从网上找了别人的UI文件,改了界面后重新转py,但代码里还引用着旧控件名,就直接报这个错。
三、sqlite3.OperationalError: no such column: xxx
报这个错往往是数据库表结构和你代码里写的列名对不上。常见原因是:之前跑过旧版本的建表脚本,数据库文件已经存在了,后来改了表结构但数据库文件没同步。解决办法是删除warehouse.db文件重新生成,或者写一个数据库迁移脚本。建议不要只靠删库解决,正式项目里迁移脚本会越来越重要。
四、sqlite3.OperationalError: table items already exists
这个报错的原因和上面正好相反。代码里建表SQL用了CREATE TABLE而不是CREATE TABLE IF NOT EXISTS,当数据库文件已经存在表了,再次执行建表就会报错。解决方案是把建表语句全部改成CREATE TABLE IF NOT EXISTS,这是最稳妥的写法。
5.2 界面状态与视觉细节的坑
PyQt5程序在Windows上有两个非常常见的视觉问题。
第一,高分屏下字体发虚、界面模糊。很多人的电脑是2K或4K屏幕,Windows缩放设置是150%或200%,PyQt5程序如果没有设置高DPI适配,界面就会模糊得像隔了一层雾。解决方案是在入口文件最前面加上:
import ctypes try: ctypes.windll.shcore.SetProcessDpiAwareness(1) except Exception: pass或者用Qt自带的:
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)注意这两行代码必须在创建QApplication实例之前执行,否则无效。这个坑很多新手不知道,做完界面觉得“怎么这么糊”,还以为是系统问题。
第二,中文显示为乱码或方块。
在Python 3中字符串本身支持中文,PyQt5也支持,正常不会乱码。如果你用的是Windows且系统区域设置不是中文,建议在代码文件头部加:
# -*- coding: utf-8 -*-同时确保代码文件用UTF-8编码保存。还有一个容易忽略的点:如果数据库里的中文乱码了,很可能是SQLite连接时没有指定编码。SQLite默认以UTF-8存储,Python的sqlite3模块也以UTF-8处理,理论上不会乱码。如果真遇到乱码,检查是不是建表后插入数据时用了str()或repr()转了一次,比如repr('中文')会把字符串外的引号带进去。
5.3 打包exe时的关键配置
这套系统的最终交付形态,通常是打成一个exe文件,发给库房管理员双击运行。打包工具目前最常用的是PyInstaller,个人强烈建议用conda环境或干净的虚拟环境来打包,因为打进去的依赖越少,exe体积越小,越不容易出问题。
打包命令:
pip install pyinstaller pyinstaller -F -w main.py --name=库房管理系统 --hidden-import=PyQt5.sip参数含义:-F表示打包成单文件;-w表示不显示控制台窗口;--name指定输出文件名;--hidden-import=PyQt5.sip是解决PyQt5打包时经常遗漏sip模块的问题。
有些版本可能还需要加:
--hidden-import=sqlite3如果程序运行时报错“unable to load sqlite3”,就需要把这个也加进去。打包完成后的exe在dist目录下。注意,如果你的代码里引用了资源文件(图片、QSS样式等),需要把这些文件放到exe同目录下,或者用PyInstaller的--add-data参数把资源文件打包进去。用--add-data的话,程序里读取资源文件的路径也要做适配处理——因为打包后资源文件被解压到临时目录,__file__所在路径和运行时资源路径不一样。这个适配比较绕,最简单省事的方式是:直接把资源文件放到exe所在目录,代码里用绝对路径加sys.executable的目录来定位,这样逻辑最简单且有保底效果。
5.4 给这套系统的安全升级建议
如果你不满足于现有功能,想自己动手增强,我建议按优先级从高到低排列。第一优先是把密码明文存储改成哈希存储,使用hashlib做SHA-256加盐即可,虽然改动量小但对安全性的提升是质的飞跃。第二优先是给操作记录加审计日志,日志内容包括哪个用户、在什么时间、做了什么操作(新增/修改/删除),这种操作记录可以用一个独立的log表来存储。第三优先是给库存扣减逻辑加上并发保护。虽然SQLite本身是单写多读的,但两个线程同时更新同一行数据仍然可能出现覆盖问题,条件更新SQL加上事务配合基本可以规避。
此外,有条件的话,还可以把这个系统从SQLite平滑迁移到MySQL。迁移思路是把数据库操作层的方法从sqlite3语法改成PyMySQL语法,然后修改连接初始化函数。其他业务代码基本不用动,因为SQL语句在常用场景下差异不大,只有一些SQLite的特定函数需要替换,比如datetime('now', 'localtime')需要改成NOW()。
6. 项目后续扩展思路
6.1 从单机版到局域网共享版
库房管理系统最常见的一个进化路径,是从单机版升级到局域网共享版。比如整个仓库有3台电脑需要同时登录系统操作,SQLite就不能满足需求了,因为SQLite的并发写能力很弱。这时候建议迁移到MySQL或PostgreSQL。
实际操作上,你只需要改动数据库连接层。比如把get_connection函数从:
def get_connection(self): return sqlite3.connect(DB_PATH)改成:
def get_connection(self): import pymysql return pymysql.connect( host='192.168.1.100', user='warehouse_user', password='your_password', database='warehouse_db', charset='utf8mb4', cursorclass=pymysql.cursors.DictCursor )同时把所有的?占位符换成MySQL的%s。这两处改动做完,系统就能作为C/S架构在局域网内用了。这也印证了这套源码在架构上的优势——数据库操作集中封装,替换数据库驱动成本低。
6.2 给系统加条码扫码能力
仓库经常需要快速出入库,手动搜索物品再填数量效率太低。比较成熟的优化方案是接入条码扫描。USB扫码枪在Windows上默认模拟键盘输入,也就是说,焦点在输入框里时,扫码枪扫一下,物品编码就会像打字一样出现在输入框里并自动加上回车。
基于这个特性,改造起来非常轻量。你只需要在出入库对话框里加一个“扫码输入物品编号”的QLineEdit,设置returnPressed信号处理函数:
self.scan_edit.returnPressed.connect(self.handle_scan) def handle_scan(self): code = self.scan_edit.text().strip() item = db.get_item_by_code(code) if item: self.item_combo.setCurrentIndex_by_item_id(item["id"]) self.quantity_edit.setFocus() else: QMessageBox.warning(self, "提示", "未找到该物品") self.scan_edit.clear()扫码枪扫描后会自动按回车,触发returnPressed信号,程序拿到编码查库、定位物品,然后把光标切到数量输入框。整个操作流程可以做到一秒完成一件物品的录入,效率提升非常明显。至于二维码,可以用qrcode库把物品编号投影为二维码打印成标签贴到货架上,扫码枪直接对着标签扫就行。这些扩展都能基于现有功能快速加上,比重新开发效率高很多。
6.3 统计报表与图表可视化
纯表格的库存统计看久了容易犯困,如果有报表需求(例如月度出入库对比),建议用PyQt5的QtChart模块来画柱状图和折线图。QtChart是Qt官方图表库,PyQt5已经包含了基本模块,不需要额外安装。
逻辑也很简单:按月份聚合出入库数量,把结果渲染成柱状图。月份维度加两条系列——入库数量和出库数量,颜色区分开贴在QChartView控件里。这套源码里的统计数据可以这样取:
SELECT strftime('%Y-%m', in_date) AS month, SUM(quantity) AS total FROM stock_in_records GROUP BY month ORDER BY month DESC同理出库表也这样查。把两个结果集组合后填充到QBarSet里,然后addBarSet进QBarSeries,挂到QChart,最后setChart到QChartView,几十行代码就能出一个像样的统计图。
关于数据备份,SQLite本身只有一个warehouse.db文件,最简单可靠的备份方式就是在工具里加一个“备份数据库”按钮,把当前db文件复制成warehouse_20250115_bak.db。复制的时机建议放在每天首次启动时自动执行,Windows下可以用QTimer或者判断文件最后修改时间来触发。一周至少备份两次,这个习惯能帮你避免绝大部分数据丢失的悲剧。
结个尾。个人实际开发这类小系统的体会是:PyQt5桌面应用最适合的场景就是中小团队的内部工具,需求明确、用户量小、部署环境单一。它不需要高并发架构,不需要炫酷的前端框架,需要的是把数据管住、把流程理清、让人用着顺。这套库房管理系统源码恰恰把这几件事做对了。如果你正在找类似参考项目,建议不要只是跑起来看效果,而是顺着读一遍数据库表结构和几个核心业务逻辑函数——读出源码作者的建模思路和取舍方式,收获会比单纯“能运行”大得多。之后无论是改界面、加模块还是换数据库,你都不会觉得无从下手。
本文还有配套的精品资源,点击获取