
简介这是一份基于 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 status0 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 status1 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 status0 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, whenmidnight, backupCount30, encodingutf-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日志合并进主文件。这套系统我前后改了三版第一版界面和逻辑混在一起改一个费率要翻半天第二版拆了层但忘了加唯一索引测试时重复入场查了好久第三版才把日志和备份补上。如果你也在做类似的东西建议先把数据表和业务接口定清楚界面反而是最快能调好的部分。希望帮到你。本文还有配套的精品资源点击获取