简介:这是一份基于 Qt 开发的停车场管理系统源码,面向学习 C++ 与 Qt 图形界面编程的初学者及课程设计需求者,用于模拟商场地下停车场的入库、出库与查询流程。系统设定一个入口、一个出口,入口前设有便道,车位满时车辆在便道排队等候;停车场内部车位数量与便道等候数均可自定义,内部不要求先来先走,用数组存储即可,而便道采用队列结构保证先到车辆优先入场,完整覆盖入库、出库、查询等核心业务逻辑。资源包共 11 个文件,约 12KB,包含 3 个 cpp 源文件、2 个 h 头文件、2 个 ui 界面文件,以及 png 图片、qrc 资源、pro 工程与 user 配置等,结构紧凑、便于直接编译运行。目前已有 3963 人学习下载,读者可借此理解 Qt 信号槽、界面布局与队列数组的配合使用,掌握停车场调度模拟的完整实现思路,适合作为课程设计或 Qt 入门练手项目。
1. 从一块车牌到一张出场小票:QT停车场管理系统到底在管什么
很多做上位机的朋友第一次接到停车场项目时,脑子里蹦出来的画面是道闸起落、LED屏显示余额,觉得无非是串口发指令。真上手才发现,真正难缠的是「一辆车从进场到出场,中间状态怎么存、金额怎么算、界面怎么不卡」。QT停车场管理系统,有图形界面,这句话拆开看是三层:QT负责把操作员看到的界面做出来,停车场管理负责把车位、车辆、计费、记录这些业务串起来,图形界面则决定了收费员在高峰期能不能三秒完成一次放行。它适合两类人:一类是刚学完QT控件、想找个完整项目练手的开发者;另一类是小规模车场(几十到两三百个车位)的现场维护人员,需要一套能跑在工控机上、断网也能计费的本地系统。这篇文章不讲空泛的架构图,只讲我实际搭这套东西时,界面怎么分层、数据怎么落库、计费怎么不出错。
2. 界面与业务怎么分层:QT停车场管理系统的模块拆解
2.1 为什么把界面和计费逻辑拆成两层
新手最容易犯的错,是把计费公式直接写在按钮的槽函数里。点一次「出场结算」,槽函数里查数据库、算钱、弹窗、开闸全干完。单机测试没问题,一旦收费员手快连点两下,或者道闸反馈慢,界面就卡死,甚至重复扣费。我一般会把系统拆成三层:界面层(QT的窗口和控件)、业务层(车辆进出、计费、车位状态)、数据层(本地数据库)。界面层只负责收集输入和展示结果,业务层是纯逻辑,不依赖任何QT控件。这样做的直接好处是,计费逻辑可以单独写单元测试,不用启动整个图形界面。
拆层之后,模块边界也清楚了。常见的划分是:车辆入场模块、车辆出场模块、车位管理模块、计费规则模块、记录查询模块。入场模块负责识别车牌(或手动输入)、分配车位、写入场记录;出场模块负责计算停留时长、匹配费率、生成缴费金额、更新车位为空闲。车位管理模块维护一个车位状态表,图形界面上用不同颜色区分空闲和占用。记录查询模块给管理员按时间段翻流水。
2.2 用QT Designer搭主界面的最小步骤
图形界面不追求花哨,收费窗口的核心就三块:车牌输入框、当前车辆信息区、操作按钮区。我习惯用QT Designer先拖出布局,再在代码里绑定信号槽。下面是一个主窗口的最小骨架,用Python的PyQt5写,C++的QT写法结构一致,只是语法不同。
# main_window.py # 停车场主界面骨架,只保留核心控件和槽函数绑定 import sys from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QTextEdit) from PyQt5.QtCore import Qt class ParkingWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("停车场管理系统") self.resize(800, 500) central = QWidget() self.setCentralWidget(central) layout = QVBoxLayout(central) # 车牌输入行 input_row = QHBoxLayout() input_row.addWidget(QLabel("车牌号:")) self.plate_edit = QLineEdit() self.plate_edit.setPlaceholderText("例如 京A12345") input_row.addWidget(self.plate_edit) layout.addLayout(input_row) # 操作按钮行 btn_row = QHBoxLayout() self.btn_in = QPushButton("入场") self.btn_out = QPushButton("出场结算") btn_row.addWidget(self.btn_in) btn_row.addWidget(self.btn_out) layout.addLayout(btn_row) # 信息展示区 self.info_view = QTextEdit() self.info_view.setReadOnly(True) layout.addWidget(self.info_view) # 信号槽绑定,注意这里只调用业务层,不写计费逻辑 self.btn_in.clicked.connect(self.on_enter) self.btn_out.clicked.connect(self.on_exit) def on_enter(self): plate = self.plate_edit.text().strip() if not plate: self.info_view.append("请输入车牌号") return # 调用业务层入场接口,具体实现在后续章节 self.info_view.append(f"车辆 {plate} 请求入场") def on_exit(self): plate = self.plate_edit.text().strip() if not plate: self.info_view.append("请输入车牌号") return self.info_view.append(f"车辆 {plate} 请求出场") if __name__ == "__main__": app = QApplication(sys.argv) win = ParkingWindow() win.show() sys.exit(app.exec_())这段代码的逻辑说明:主窗口只做三件事——拿输入、绑按钮、显示文本。on_enter和on_exit里没有任何数据库操作和计费公式,它们只负责把车牌号取出来,然后调用业务层。参数上,setPlaceholderText给输入框一个提示,setReadOnly(True)防止操作员误改信息区。这样拆的好处是,后面业务层怎么改,界面代码几乎不用动。
2.3 业务层接口怎么定义才不返工
业务层对外只暴露几个方法:vehicle_enter(plate)、vehicle_exit(plate)、query_record(plate, start, end)。每个方法返回一个结构化的结果,比如入场返回车位号和入场时间,出场返回停留时长和应收金额。界面层拿到结果后只负责显示。我见过有人把业务层写成一个大类,所有方法都往里面塞,最后改一个费率要翻五百行。更稳的做法是按职责拆成EntryService、ExitService、BillingRule、ParkingLotRepo。ParkingLotRepo只管数据库读写,BillingRule只管算钱,ExitService把它们串起来。这样换数据库或者换费率规则时,改动范围是可控的。
3. 数据落库与计费:把「停多久、收多少钱」算准
3.1 本地数据库表结构怎么设计
停车场系统对数据的要求是「断电不丢、查询要快」。小规模车场用SQLite就够,单文件、免安装、QT里用QSqlDatabase直接连。核心表三张:parking_space(车位表)、vehicle_record(进出记录表)、billing_rule(费率表)。车位表存车位编号和状态,记录表存车牌、入场时间、出场时间、车位号、金额,费率表存不同时段的单价。
-- schema.sql -- 车位表:id为主键,status 0空闲 1占用 CREATE TABLE parking_space ( space_id INTEGER PRIMARY KEY, status INTEGER NOT NULL DEFAULT 0 ); -- 进出记录表:出场时间和金额允许为空,表示车辆还在场内 CREATE TABLE vehicle_record ( record_id INTEGER PRIMARY KEY AUTOINCREMENT, plate TEXT NOT NULL, space_id INTEGER, enter_time TEXT NOT NULL, exit_time TEXT, fee REAL, FOREIGN KEY (space_id) REFERENCES parking_space(space_id) ); -- 费率表:按小时计费,free_minutes为免费时长 CREATE TABLE billing_rule ( rule_id INTEGER PRIMARY KEY, price_per_hour REAL NOT NULL, free_minutes INTEGER NOT NULL DEFAULT 15 );逻辑说明:vehicle_record里exit_time和fee留空,是为了支持「车辆还在场内」这个状态。查询在场车辆时,只要exit_time IS NULL就是未出场。billing_rule单独建表而不是写死在代码里,是为了让管理员能在界面上改费率,不用重新编译。参数上,free_minutes默认15分钟,这是很多车场的常见做法,但具体值要按现场规定来。
3.2 入场和出场的完整流程与代码
入场流程:校验车牌格式、查是否已有未出场记录、找一个空闲车位、写入场记录、把车位状态改为占用。出场流程:查未出场记录、算出停留时长、按费率算钱、更新出场时间和金额、把车位状态改回空闲。下面用Python的sqlite3演示业务层核心逻辑。
# service.py # 业务层:入场、出场、计费,不依赖QT控件 import sqlite3 from datetime import datetime DB_PATH = "parking.db" def get_conn(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn def vehicle_enter(plate): conn = get_conn() cur = conn.cursor() # 检查是否已有未出场记录,防止重复入场 cur.execute("SELECT record_id FROM vehicle_record WHERE plate=? AND exit_time IS NULL", (plate,)) if cur.fetchone(): conn.close() return {"ok": False, "msg": "该车牌已在场内"} # 找一个空闲车位 cur.execute("SELECT space_id FROM parking_space WHERE status=0 LIMIT 1") row = cur.fetchone() if not row: conn.close() return {"ok": False, "msg": "车位已满"} space_id = row["space_id"] now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") cur.execute("INSERT INTO vehicle_record (plate, space_id, enter_time) VALUES (?,?,?)", (plate, space_id, now)) cur.execute("UPDATE parking_space SET status=1 WHERE space_id=?", (space_id,)) conn.commit() conn.close() return {"ok": True, "space_id": space_id, "enter_time": now} def vehicle_exit(plate): conn = get_conn() cur = conn.cursor() cur.execute("SELECT record_id, space_id, enter_time FROM vehicle_record WHERE plate=? AND exit_time IS NULL", (plate,)) row = cur.fetchone() if not row: conn.close() return {"ok": False, "msg": "未找到在场记录"} enter_time = datetime.strptime(row["enter_time"], "%Y-%m-%d %H:%M:%S") exit_time = datetime.now() minutes = int((exit_time - enter_time).total_seconds() // 60) # 读取费率 cur.execute("SELECT price_per_hour, free_minutes FROM billing_rule LIMIT 1") rule = cur.fetchone() fee = 0.0 if minutes > rule["free_minutes"]: billable = minutes - rule["free_minutes"] fee = round(billable / 60.0 * rule["price_per_hour"], 2) exit_str = exit_time.strftime("%Y-%m-%d %H:%M:%S") cur.execute("UPDATE vehicle_record SET exit_time=?, fee=? WHERE record_id=?", (exit_str, fee, row["record_id"])) cur.execute("UPDATE parking_space SET status=0 WHERE space_id=?", (row["space_id"],)) conn.commit() conn.close() return {"ok": True, "minutes": minutes, "fee": fee, "exit_time": exit_str}逻辑说明:vehicle_enter先查重再分配车位,两步都在同一个连接里,避免并发时两个请求拿到同一个车位。vehicle_exit用total_seconds() // 60取整分钟,不足一分钟按一分钟算,这是现场常见做法。计费时先减去免费时长,再按小时单价折算。参数上,price_per_hour和free_minutes从数据库读,改费率不用动代码。注意round(..., 2)保留两位小数,避免浮点误差导致金额出现0.30000000000000004这种值。
3.3 界面层怎么调用业务层并刷新显示
界面层拿到业务层返回的字典后,把结果拼成文本显示在信息区,同时可以刷新车位状态的颜色。下面是在主窗口里补上调用逻辑。
# 接 main_window.py,替换原来的 on_enter 和 on_exit from service import vehicle_enter, vehicle_exit def on_enter(self): plate = self.plate_edit.text().strip() if not plate: self.info_view.append("请输入车牌号") return result = vehicle_enter(plate) if result["ok"]: self.info_view.append(f"入场成功:{plate} 车位 {result['space_id']} 时间 {result['enter_time']}") else: self.info_view.append(f"入场失败:{result['msg']}") def on_exit(self): plate = self.plate_edit.text().strip() if not plate: self.info_view.append("请输入车牌号") return result = vehicle_exit(plate) if result["ok"]: self.info_view.append(f"出场成功:{plate} 停留 {result['minutes']} 分钟 应收 {result['fee']} 元") else: self.info_view.append(f"出场失败:{result['msg']}")逻辑说明:界面层只做输入校验和结果展示,所有业务判断都在service.py里。这样即使以后换成C++的QT,业务层逻辑也能平移。参数上,result["ok"]是统一的成功标志,界面根据它决定显示成功还是失败信息。
4. 避坑与排查:停车场系统最容易翻车的五个地方
4.1 车牌重复入场导致一车占两位
现象:同一辆车连续点两次「入场」,系统分配了两个车位,出场时只释放一个。原因:入场逻辑里先查重再分配,但两次点击间隔太短,第一次还没提交,第二次就查不到记录。解决:在数据库层给vehicle_record加一个部分唯一索引,只对exit_time IS NULL的记录生效,从根上防止重复。
CREATE UNIQUE INDEX idx_plate_active ON vehicle_record(plate) WHERE exit_time IS NULL;4.2 计费金额出现浮点误差
现象:应收3.3元,界面显示3.3000000000000003。原因:浮点数在二进制里无法精确表示0.1。解决:金额计算用decimal.Decimal,或者像上面代码那样在最后round到两位。更稳的做法是金额以「分」为单位存整数,显示时再除以100。
4.3 界面卡死,道闸没反应
现象:点「出场结算」后界面转圈,道闸迟迟不开。原因:串口通信或数据库查询写在主线程里,阻塞了QT事件循环。解决:把耗时操作放到QThread里,通过信号把结果传回主线程更新界面。数据库查询本身很快,但串口写指令如果设备没响应,默认超时可能好几秒。
4.4 断电后数据丢失或损坏
现象:工控机突然断电,重启后数据库打不开。原因:SQLite默认的日志模式在断电时可能留下未完成的事务。解决:开启WAL模式,并设置PRAGMA synchronous = NORMAL,在保证安全的前提下减少写入阻塞。
PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL;4.5 时间格式不统一导致算错停留时长
现象:入场时间存成「2025/01/01 08:00」,出场时用「2025-01-01 18:00」解析,结果报错或算出负数。原因:strptime的格式串和实际存储格式不匹配。解决:全系统统一用%Y-%m-%d %H:%M:%S,写入和读取都用同一个常量,别在代码里手写格式串。
5. 进阶技巧:让这套系统在现场更耐用的几个习惯
5.1 用配置文件管理费率和车位数量
费率写死在代码里,每次调价都要重新打包,现场维护很痛苦。我一般会在程序目录放一个config.ini,启动时读取费率和车位总数,再同步到数据库。这样管理员改配置文件重启即可,不用碰代码。
; config.ini [billing] price_per_hour = 5.0 free_minutes = 15 [lot] total_spaces = 120读取时用configparser,如果配置文件不存在就用默认值,避免程序起不来。
5.2 给关键操作加日志,方便事后对账
收费员说「我明明收了他20块」,系统显示10块,这种扯皮只能靠日志。在业务层的入场、出场、改费率三个动作里各写一条日志,记录时间、车牌、金额、操作员。日志按天切文件,保留30天。QT里可以用Python的logging模块,配置TimedRotatingFileHandler。
import logging from logging.handlers import TimedRotatingFileHandler handler = TimedRotatingFileHandler("parking.log", when="midnight", backupCount=30, encoding="utf-8") handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s")) logger = logging.getLogger("parking") logger.addHandler(handler) logger.setLevel(logging.INFO)5.3 界面刷新用信号槽,别用轮询
有人为了刷新车位状态,开一个QTimer每500毫秒查一次数据库。车少的时候没事,车多了数据库压力大,界面还会闪。更好的做法是业务层在车位状态变化时发一个信号,界面收到信号再刷新对应区域。QT的信号槽机制本来就是干这个的,用对了能省很多事。
5.4 出场时先算钱再开闸,顺序不能反
我踩过一次坑:出场逻辑里先给道闸发开闸指令,再算钱。结果有一次计费模块抛异常,车已经出去了,钱没收到。后来改成先算钱、写记录、提交事务,全部成功后再发开闸指令。如果开闸失败,记录里标记「待补开闸」,人工处理。这个顺序在高峰期尤其重要,宁可让车等两秒,也不能让钱漏掉。
5.5 定期备份数据库文件
SQLite就是一个文件,备份很简单,但很多人忘了做。我习惯在每天凌晨用QTimer触发一次备份,把parking.db复制到backup/目录,文件名带日期。保留最近7天。这样即使数据库损坏,最多丢一天数据。备份前先执行PRAGMA wal_checkpoint(TRUNCATE),确保WAL日志合并进主文件。
这套系统我前后改了三版,第一版界面和逻辑混在一起,改一个费率要翻半天;第二版拆了层,但忘了加唯一索引,测试时重复入场查了好久;第三版才把日志和备份补上。如果你也在做类似的东西,建议先把数据表和业务接口定清楚,界面反而是最快能调好的部分。希望帮到你。
本文还有配套的精品资源,点击获取