20241110 《Python程序设计》 实验四报告
20241110 2024-2025-2 《Python程序设计》实验四报告
课程:《Python程序设计》
班级: 2411
姓名: 王方俊
学号:20241110
实验教师:王志强
实验日期:2025年5月14日
必修/选修: 公选课
一、实验目的
一开始我是打算做微信群发和点赞的,但后来我注意到微信平台加强了对使用第三方插件的打击动作,对外挂、模拟器等违规技术封禁。加上我觉得有些时候群发和点赞这样无脑批量完成未必对用户来说是件好事,甚至带来麻烦和尴尬。再三考虑后果断放弃这个想法。那应该做什么也是困扰了我很久,但好巧不巧,当我在宿舍里准备点外卖时意识到为什么不去食堂而是选择线上点的原因:虽然看似去食堂比拿外卖更方便,但是我不清楚今天有没有对胃的美食;我想吃的饭菜是否卖完;今天食堂是否还有座位等,这些不确定的因素导致我很多次放弃了去食堂“开盲盒”。
那有没有什么办法能够解决食堂的这类问题呢?我便联系到Python的结课作业,我是否可以通过编写一个点菜程序,类似点外卖那样在宿舍就可以点到我心仪的美食并预定好我的餐位?这样我不就是可以非常方便从容而且目的性极强的放心去食堂干饭了吗!于是我便开始了我长达13天的痛并快乐的实验。
二、实验内容
(一)环境搭建
SQLite3是Python内置数据库,用于存储菜品和订单数据
PyQt5是GUI库,用于创建图形界面(需要在终端输入pip install PyQt5 进行安装)如下图则为安装成功:

(二)界面设计
由于食堂线上点菜系统必不可免的是人机交互,所以必须考虑界面架构规划。
1.整体布局
左侧为菜品展示区(可滚动,支持分类筛选和搜索),
右侧为购物车及订单提交区(显示选中菜品、总价、用户信息表单)。
2.技术点
布局管理器:
QVBoxLayout:垂直排列部件(如菜单标题、筛选栏)。
QHBoxLayout:水平排列部件(如价格标签、数量加减按钮)。
QGridLayout:网络布局(用于用户信息表单,如姓名、电话字段)。
滚动区域:QScrollArea用于显示大量菜品,避免界面溢出。
(三)界面搭建
1.导入必要库
import sys
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QScrollArea, QFrame, QMessageBox,
QSpinBox, QLineEdit, QComboBox, QDateEdit, QTimeEdit, QGridLayout) # 导入所有需要的UI组件
from PyQt5.QtCore import Qt, QDate, QTime
from PyQt5.QtGui import QFont # 用于设置字体
import sqlite3 # 数据库操作
PyQt5组件:用于创建窗口、按钮、输入框等界面元素。
sqlite3:操作 SQLite 数据库,实现数据存储。
2.主窗口类初始化
class CanteenOrderingSystem(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("食堂点菜系统") # 窗口标题
self.setGeometry(100, 100, 1000, 800) # 窗口位置和大小(x, y, width, height)
# 创建中心部件,用于容纳所有界面元素
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 主布局:左右分栏,左侧70%宽度,右侧30%宽度
main_layout = QHBoxLayout(central_widget)
self.create_menu_area(main_layout) # 创建左侧菜品区
self.create_order_area(main_layout) # 创建右侧订单区
QMainWindow:PyQt5 主窗口类,提供菜单栏、工具栏等结构。
QHBoxLayout:水平布局,将界面分为左右两部分。
3. 左侧菜品展示区
def create_menu_area(self, main_layout):
# 创建左侧框架
menu_frame = QFrame()
menu_frame.setFrameShape(QFrame.StyledPanel) # 添加边框样式
menu_layout = QVBoxLayout(menu_frame)
# 标题
title_label = QLabel("今日菜单")
title_label.setFont(QFont("SimHei", 20, QFont.Bold)) # 设置字体为黑体,大小20,加粗
title_label.setAlignment(Qt.AlignCenter) # 居中对齐
menu_layout.addWidget(title_label)
# 分类筛选栏
category_layout = QHBoxLayout()
category_label = QLabel("分类:")
self.category_combo = QComboBox()
self.category_combo.addItems(["全部", "川菜", "粤菜", "浙菜", "江浙小吃"]) # 添加分类选项
self.category_combo.currentTextChanged.connect(self.filter_menu_items) # 绑定分类筛选事件
category_layout.addWidget(category_label)
category_layout.addWidget(self.category_combo)
menu_layout.addLayout(category_layout)
# 搜索框
search_layout = QHBoxLayout()
search_label = QLabel("搜索:")
self.search_input = QLineEdit()
self.search_input.setPlaceholderText("输入菜品名称...")
self.search_input.textChanged.connect(self.filter_menu_items) # 绑定搜索事件
search_layout.addWidget(search_label)
search_layout.addWidget(self.search_input)
menu_layout.addLayout(search_layout)
# 滚动区域,用于显示菜品列表
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
content_widget = QWidget()
self.menu_content_layout = QVBoxLayout(content_widget) # 垂直布局存放菜品卡片
scroll_area.setWidget(content_widget)
menu_layout.addWidget(scroll_area)
main_layout.addWidget(menu_frame, 7) # 左侧占70%宽度
QScrollArea:包裹菜品列表,允许内容滚动。
QComboBox和QLineEdit:分别实现分类筛选和搜索功能,通过.connect()绑定事件处理函数filter_menu_items。
4.右侧订单区
def create_order_area(self, main_layout):
# 创建右侧框架
order_frame = QFrame()
order_layout = QVBoxLayout(order_frame)
# 购物车标题
title_label = QLabel("我的订单")
title_label.setFont(QFont("SimHei", 20, QFont.Bold))
title_label.setAlignment(Qt.AlignCenter)
order_layout.addWidget(title_label)
# 购物车内容区域(可滚动)
scroll_area = QScrollArea()
content_widget = QWidget()
self.order_items_layout = QVBoxLayout(content_widget) # 垂直布局存放购物车项
scroll_area.setWidget(content_widget)
order_layout.addWidget(scroll_area)
# 总价显示
total_layout = QHBoxLayout()
total_label = QLabel("总计:")
self.total_price_label = QLabel("¥0.00")
self.total_price_label.setStyleSheet("color: #e63946;") # 红色字体突出总价
total_layout.addWidget(total_label)
total_layout.addStretch() # 拉伸空白,使总价右对齐
total_layout.addWidget(self.total_price_label)
order_layout.addLayout(total_layout)
# 用户信息表单(姓名、电话、桌号等)
form_layout = QGridLayout()
form_layout.addWidget(QLabel("姓名:"), 0, 0)
self.name_input = QLineEdit()
form_layout.addWidget(self.name_input, 0, 1)
form_layout.addWidget(QLabel("电话:"), 1, 0)
self.phone_input = QLineEdit()
form_layout.addWidget(self.phone_input, 1, 1)
# 类似添加桌号、日期、时间字段...
order_layout.addLayout(form_layout)
# 提交订单按钮
submit_button = QPushButton("提交订单")
submit_button.clicked.connect(self.submit_order) # 绑定订单提交事件
order_layout.addWidget(submit_button)
main_layout.addWidget(order_frame, 3) # 右侧占30%宽度
QGridLayout:按行 / 列排列表单字段(如姓名在第 0 行第 0 列,输入框在第 0 行第 1 列)。
QPushButton:点击后触发submit_order函数,处理订单提交逻辑。
(四)数据库设计
由于菜品、吃客较多,文件显然不能满足数据存储查找,这是需要设计数据库。
1.数据库结构设计
表1:菜品表
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INTEGER | 主键(自动生成) |
| name | TEXT | 菜品名称 |
| category | TEXT | 分类(如川菜) |
| price | REAL | 价格 |
| description | TEXT | 配料描述 |
表 2:订单表(orders)
存储订单主信息,如用户姓名、总价、时间等。
表 3:订单详情表(order_details)
存储订单包含的具体菜品及数量,关联菜品表和订单表。
2.数据库初始化代码
def init_database(self):
# 连接数据库(文件名为canteen.db,自动创建)
self.conn = sqlite3.connect("canteen.db")
self.cursor = self.conn.cursor()
# 创建菜品表(无图片字段,简化版)
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS menu_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
category TEXT NOT NULL,
price REAL NOT NULL,
description TEXT
)
''')
# 添加示例数据(初始化时自动插入)
sample_items = [
("宫保鸡丁", "川菜", 32.0, "鸡肉、花生米、青椒"),
("鱼香肉丝", "川菜", 28.0, "猪肉丝、木耳、胡萝卜"),
# 更多示例数据...
]
self.cursor.executemany(
"INSERT INTO menu_items (name, category, price, description) VALUES (?, ?, ?, ?)",
sample_items
)
self.conn.commit() # 提交事务,保存数据
sqlite3.connect():连接数据库,若文件不存在则自动创建。
executemany():批量插入示例数据,避免重复编写插入语句。
3.数据查询与筛选
def load_menu_items(self):
"""从数据库加载所有菜品,显示到左侧列表"""
self.cursor.execute("SELECT id, name, category, price, description FROM menu_items")
items = self.cursor.fetchall() # 获取所有记录
for item in items:
item_id, name, category, price, description = item
self.add_menu_item_to_ui(item_id, name, category, price, description) # 调用函数创建菜品卡片
def filter_menu_items(self):
"""根据分类或搜索词筛选菜品"""
category = self.category_combo.currentText()
search_text = self.search_input.text().lower()
query = "SELECT id, name, category, price, description FROM menu_items WHERE 1=1"
params = []
if category != "全部":
query += " AND category = ?"
params.append(category)
if search_text:
query += " AND LOWER(name) LIKE ?"
params.append(f"%{search_text}%")
self.cursor.execute(query, params)
items = self.cursor.fetchall()
self.clear_menu_items() # 清空现有列表
for item in items:
self.add_menu_item_to_ui(*item) # 解包参数,创建筛选后的菜品卡片
CURD操作:SELECT语句用于查询数据,WHERE子句实现筛选逻辑。
模糊搜索:LIKE %search_text%匹配包含搜索词的菜品名称(不区分大小写)。
4.核心功能实现
这个程序我觉得最难为我,让我痛不欲生的就是这个内容,也就是我所谓的核心功能实现了。
既然是一个食堂线上点菜系统,那么核心就是加购和下单了。
而这又要分为两个部分解决:购物车的原理和订单的提交。
购物车逻辑:添加与管理
添加菜品到购物车
def add_menu_item_to_ui(self, item_id, name, category, price, description):
"""创建单个菜品卡片,添加到左侧列表"""
item_frame = QFrame()
item_frame.setStyleSheet("border: 1px solid #ddd; padding: 10px;")
layout = QVBoxLayout(item_frame)
layout.addWidget(QLabel(name, font=QFont("SimHei", 14, QFont.Bold)))
layout.addWidget(QLabel(f"分类:{category}", styleSheet="color: #666"))
layout.addWidget(QLabel(description, wordWrap=True)) # wordWrap=True自动换行
# 添加到购物车按钮
add_button = QPushButton("添加到购物车")
add_button.clicked.connect(lambda: self.add_to_cart(item_id, name, price))
layout.addWidget(add_button)
self.menu_content_layout.addWidget(item_frame) # 添加到垂直布局
hasattr(widget, 'item_id'):检查部件是否已绑定菜品 ID,避免重复添加。
QSpinBox:数值输入框,通过valueChanged信号实时触发总价更新。
购物车项操作
def add_to_cart(self, item_id, name, price):
"""处理添加到购物车逻辑"""
# 检查是否已存在该菜品
for i in range(self.order_items_layout.count()):
widget = self.order_items_layout.itemAt(i).widget()
if hasattr(widget, 'item_id') and widget.item_id == item_id:
# 存在则增加数量
spin_box = widget.findChild(QSpinBox)
spin_box.setValue(spin_box.value() + 1)
self.update_total_price() # 刷新总价
return
# 不存在则创建新购物车项
cart_item = QFrame()
cart_item.item_id = item_id # 保存菜品ID,用于后续关联
layout = QHBoxLayout(cart_item)
layout.addWidget(QLabel(name)) # 菜品名称
price_label = QLabel(f"¥{price:.2f}")
layout.addWidget(price_label)
# 数量调节组件(减号-输入框-加号)
spin_box = QSpinBox()
spin_box.setMinimum(1)
spin_box.setValue(1)
spin_box.valueChanged.connect(self.update_total_price) # 数量变化时刷新总价
minus_btn = QPushButton("-")
minus_btn.clicked.connect(lambda: self.change_quantity(spin_box, -1))
plus_btn = QPushButton("+")
plus_btn.clicked.connect(lambda: self.change_quantity(spin_box, +1))
layout.addWidget(minus_btn)
layout.addWidget(spin_box)
layout.addWidget(plus_btn)
self.order_items_layout.addWidget(cart_item)
self.update_total_price() # 首次添加时计算总价
hasattr(widget, 'item_id'):检查部件是否已绑定菜品 ID,避免重复添加。
QSpinBox:数值输入框,通过valueChanged信号实时触发总价更新。
订单提交:数据写入数据库
def submit_order(self):
"""处理订单提交逻辑"""
# 验证用户信息是否填写
if not self.name_input.text() or not self.phone_input.text():
QMessageBox.warning(self, "错误", "请填写姓名和电话!")
return
# 获取用户输入和购物车数据
customer_name = self.name_input.text()
phone = self.phone_input.text()
total_price = float(self.total_price_label.text()[1:]) # 提取总价(去掉¥符号)
order_date = QDate.currentDate().toString("yyyy-MM-dd")
order_time = QTime.currentTime().toString("HH:mm:ss")
# 开始数据库事务
try:
self.conn.execute("BEGIN")
# 插入订单主表
self.cursor.execute('''
INSERT INTO orders (customer_name, phone, total_price, order_date, order_time, status)
VALUES (?, ?, ?, ?, ?, "待处理")
''', (customer_name, phone, total_price, order_date, order_time))
order_id = self.cursor.lastrowid # 获取刚插入的订单ID
# 插入订单详情表(购物车中的所有菜品)
for i in range(self.order_items_layout.count()):
widget = self.order_items_layout.itemAt(i).widget()
item_id = widget.item_id
quantity = widget.findChild(QSpinBox).value()
price = float(widget.findChild(QLabel).text()[1:]) # 提取单价
self.cursor.execute('''
INSERT INTO order_details (order_id, item_id, quantity, price)
VALUES (?, ?, ?, ?)
''', (order_id, item_id, quantity, price))
self.conn.commit() # 提交事务
QMessageBox.information(self, "成功", f"订单提交成功!编号:{order_id}")
self.clear_cart() # 清空购物车
except Exception as e:
self.conn.rollback() # 失败则回滚
QMessageBox.critical(self, "错误", f"订单提交失败:{str(e)}")
事务处理:使用BEGIN和COMMIT确保订单主表和详情表数据一致,避免部分写入成功。
数据关联:通过order_id(订单表主键)和item_id(菜品表主键)关联订单与菜品。
源代码(600行完整版)
点击查看代码
import sys
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QScrollArea, QFrame, QMessageBox,
QSpinBox, QLineEdit, QComboBox, QDateEdit, QTimeEdit, QGridLayout)
from PyQt5.QtCore import Qt, QDate, QTime
from PyQt5.QtGui import QFont
import sqlite3
class CanteenOrderingSystem(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("食堂点菜系统")
self.setGeometry(100, 100, 1000, 800)
# 初始化数据库
self.init_database()
# 创建中心部件
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
# 创建主布局
self.main_layout = QHBoxLayout(self.central_widget)
# 创建左侧菜单区域
self.create_menu_area()
# 创建右侧订单区域
self.create_order_area()
# 加载菜品数据
self.load_menu_items()
def init_database(self):
"""初始化SQLite数据库"""
self.conn = sqlite3.connect("canteen.db")
self.cursor = self.conn.cursor()
# 创建菜品表(不含image_path字段)
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS menu_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
category TEXT NOT NULL,
price REAL NOT NULL,
description TEXT
)
''')
# 创建订单表
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
customer_name TEXT NOT NULL,
phone TEXT NOT NULL,
table_number TEXT NOT NULL,
order_date TEXT NOT NULL,
order_time TEXT NOT NULL,
total_price REAL NOT NULL,
status TEXT NOT NULL
)
''')
# 创建订单详情表
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS order_details (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER NOT NULL,
item_id INTEGER NOT NULL,
quantity INTEGER NOT NULL,
price REAL NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders (id),
FOREIGN KEY (item_id) REFERENCES menu_items (id)
)
''')
# 如果菜单为空,添加一些示例菜品
self.cursor.execute("SELECT COUNT(*) FROM menu_items")
count = self.cursor.fetchone()[0]
if count == 0:
sample_items = [
("宫保鸡丁", "川菜", 32.0, "鸡肉、花生米、青椒、红椒"),
("鱼香肉丝", "川菜", 28.0, "猪肉丝、木耳、胡萝卜、莴笋"),
("糖醋排骨", "浙菜", 42.0, "猪小排、番茄酱、白糖"),
("麻婆豆腐", "川菜", 18.0, "嫩豆腐、肉末、豆瓣酱"),
("东坡肉", "浙菜", 58.0, "五花肉、酱油、冰糖"),
("清蒸鲈鱼", "粤菜", 68.0, "新鲜鲈鱼、葱姜丝"),
("上汤娃娃菜", "粤菜", 26.0, "娃娃菜、皮蛋、虾仁"),
("白灼菜心", "粤菜", 22.0, "广东菜心、生抽"),
("扬州炒饭", "淮扬菜", 25.0, "米饭、火腿、鸡蛋、青豆"),
("小笼包", "江浙小吃", 18.0, "猪肉馅、薄面皮"),
("担担面", "川菜小吃", 16.0, "碱水面、肉末、红油"),
("阳春面", "江浙小吃", 12.0, "细面、葱油、清汤")
]
self.cursor.executemany(
"INSERT INTO menu_items (name, category, price, description) VALUES (?, ?, ?, ?)",
sample_items
)
self.conn.commit()
def create_menu_area(self):
"""创建左侧菜单区域"""
menu_frame = QFrame()
menu_frame.setFrameShape(QFrame.StyledPanel)
menu_layout = QVBoxLayout(menu_frame)
# 标题
title_label = QLabel("今日菜单")
title_label.setFont(QFont("SimHei", 20, QFont.Bold))
title_label.setAlignment(Qt.AlignCenter)
menu_layout.addWidget(title_label)
# 分类筛选
category_layout = QHBoxLayout()
category_label = QLabel("分类:")
category_label.setFont(QFont("SimHei", 12))
self.category_combo = QComboBox()
self.category_combo.addItem("全部")
self.category_combo.addItems(["川菜", "粤菜", "浙菜", "淮扬菜", "江浙小吃", "川菜小吃"])
self.category_combo.currentTextChanged.connect(self.filter_menu_items)
category_layout.addWidget(category_label)
category_layout.addWidget(self.category_combo)
category_layout.addStretch()
menu_layout.addLayout(category_layout)
# 搜索框
search_layout = QHBoxLayout()
search_label = QLabel("搜索:")
search_label.setFont(QFont("SimHei", 12))
self.search_input = QLineEdit()
self.search_input.setPlaceholderText("输入菜品名称...")
self.search_input.textChanged.connect(self.filter_menu_items)
search_layout.addWidget(search_label)
search_layout.addWidget(self.search_input)
menu_layout.addLayout(search_layout)
# 菜单滚动区域
self.menu_scroll_area = QScrollArea()
self.menu_scroll_area.setWidgetResizable(True)
self.menu_content_widget = QWidget()
self.menu_content_layout = QVBoxLayout(self.menu_content_widget)
self.menu_scroll_area.setWidget(self.menu_content_widget)
menu_layout.addWidget(self.menu_scroll_area)
self.main_layout.addWidget(menu_frame, 7) # 占70%宽度
def create_order_area(self):
"""创建右侧订单区域"""
order_frame = QFrame()
order_frame.setFrameShape(QFrame.StyledPanel)
order_layout = QVBoxLayout(order_frame)
# 标题
title_label = QLabel("我的订单")
title_label.setFont(QFont("SimHei", 20, QFont.Bold))
title_label.setAlignment(Qt.AlignCenter)
order_layout.addWidget(title_label)
# 订单详情区域
order_details_frame = QFrame()
order_details_frame.setFrameShape(QFrame.StyledPanel)
self.order_details_layout = QVBoxLayout(order_details_frame)
# 空订单提示
self.empty_order_label = QLabel("购物车为空")
self.empty_order_label.setFont(QFont("SimHei", 14))
self.empty_order_label.setAlignment(Qt.AlignCenter)
self.empty_order_label.setStyleSheet("color: #888;")
self.order_details_layout.addWidget(self.empty_order_label)
self.order_items_layout = QVBoxLayout()
self.order_details_layout.addLayout(self.order_items_layout)
order_scroll_area = QScrollArea()
order_scroll_area.setWidgetResizable(True)
order_scroll_area.setWidget(order_details_frame)
order_layout.addWidget(order_scroll_area)
# 总价
total_layout = QHBoxLayout()
total_label = QLabel("总计:")
total_label.setFont(QFont("SimHei", 16, QFont.Bold))
self.total_price_label = QLabel("¥0.00")
self.total_price_label.setFont(QFont("SimHei", 16, QFont.Bold))
self.total_price_label.setStyleSheet("color: #e63946;")
total_layout.addWidget(total_label)
total_layout.addStretch()
total_layout.addWidget(self.total_price_label)
order_layout.addLayout(total_layout)
# 客户信息
customer_info_frame = QFrame()
customer_info_frame.setFrameShape(QFrame.StyledPanel)
customer_info_layout = QVBoxLayout(customer_info_frame)
form_layout = QGridLayout()
# 姓名
name_label = QLabel("姓名:")
name_label.setFont(QFont("SimHei", 12))
self.name_input = QLineEdit()
self.name_input.setPlaceholderText("请输入您的姓名")
form_layout.addWidget(name_label, 0, 0)
form_layout.addWidget(self.name_input, 0, 1)
# 电话
phone_label = QLabel("电话:")
phone_label.setFont(QFont("SimHei", 12))
self.phone_input = QLineEdit()
self.phone_input.setPlaceholderText("请输入您的电话")
form_layout.addWidget(phone_label, 1, 0)
form_layout.addWidget(self.phone_input, 1, 1)
# 桌号
table_label = QLabel("桌号:")
table_label.setFont(QFont("SimHei", 12))
self.table_input = QLineEdit()
self.table_input.setPlaceholderText("请输入桌号")
form_layout.addWidget(table_label, 2, 0)
form_layout.addWidget(self.table_input, 2, 1)
# 日期
date_label = QLabel("日期:")
date_label.setFont(QFont("SimHei", 12))
self.date_input = QDateEdit()
self.date_input.setDate(QDate.currentDate())
self.date_input.setCalendarPopup(True)
form_layout.addWidget(date_label, 3, 0)
form_layout.addWidget(self.date_input, 3, 1)
# 时间
time_label = QLabel("时间:")
time_label.setFont(QFont("SimHei", 12))
self.time_input = QTimeEdit()
self.time_input.setTime(QTime.currentTime())
form_layout.addWidget(time_label, 4, 0)
form_layout.addWidget(self.time_input, 4, 1)
customer_info_layout.addLayout(form_layout)
order_layout.addWidget(customer_info_frame)
# 提交订单按钮
submit_button = QPushButton("提交订单")
submit_button.setFont(QFont("SimHei", 14, QFont.Bold))
submit_button.setStyleSheet("""
QPushButton {
background-color: #4CAF50;
color: white;
border-radius: 5px;
padding: 10px;
}
QPushButton:hover {
background-color: #45a049;
}
""")
submit_button.clicked.connect(self.submit_order)
order_layout.addWidget(submit_button)
self.main_layout.addWidget(order_frame, 3) # 占30%宽度
def load_menu_items(self):
"""从数据库加载菜品并显示"""
self.clear_menu_items()
# 明确指定需要的字段,避免解包错误
self.cursor.execute("SELECT id, name, category, price, description FROM menu_items")
items = self.cursor.fetchall()
for item in items:
item_id, name, category, price, description = item
self.add_menu_item(item_id, name, category, price, description)
def add_menu_item(self, item_id, name, category, price, description):
"""添加单个菜品到菜单区域"""
item_frame = QFrame()
item_frame.setFrameShape(QFrame.StyledPanel)
item_frame.setStyleSheet("""
QFrame {
border: 1px solid #ddd;
border-radius: 5px;
margin: 5px;
padding: 10px;
}
QFrame:hover {
border: 1px solid #4CAF50;
}
""")
item_layout = QVBoxLayout(item_frame)
# 菜品信息
name_layout = QHBoxLayout()
name_label = QLabel(name)
name_label.setFont(QFont("SimHei", 14, QFont.Bold))
price_label = QLabel(f"¥{price:.2f}")
price_label.setFont(QFont("SimHei", 14, QFont.Bold))
price_label.setStyleSheet("color: #e63946;")
name_layout.addWidget(name_label)
name_layout.addStretch()
name_layout.addWidget(price_label)
item_layout.addLayout(name_layout)
category_label = QLabel(f"分类: {category}")
category_label.setFont(QFont("SimHei", 10))
category_label.setStyleSheet("color: #888;")
item_layout.addWidget(category_label)
desc_label = QLabel(description)
desc_label.setFont(QFont("SimHei", 10))
desc_label.setWordWrap(True)
item_layout.addWidget(desc_label)
# 添加到购物车按钮
add_button = QPushButton("添加到购物车")
add_button.setFont(QFont("SimHei", 10))
add_button.setStyleSheet("""
QPushButton {
background-color: #4CAF50;
color: white;
border-radius: 3px;
padding: 5px;
}
QPushButton:hover {
background-color: #45a049;
}
""")
add_button.clicked.connect(lambda checked, id=item_id, n=name, p=price: self.add_to_cart(id, n, p))
item_layout.addWidget(add_button)
self.menu_content_layout.addWidget(item_frame)
def clear_menu_items(self):
"""清空菜单区域"""
while self.menu_content_layout.count():
item = self.menu_content_layout.takeAt(0)
widget = item.widget()
if widget:
widget.deleteLater()
def filter_menu_items(self):
"""根据分类和搜索词筛选菜品"""
category = self.category_combo.currentText()
search_text = self.search_input.text().lower()
self.clear_menu_items()
# 构建SQL查询
query = "SELECT id, name, category, price, description FROM menu_items WHERE 1=1"
params = []
if category != "全部":
query += " AND category = ?"
params.append(category)
if search_text:
query += " AND LOWER(name) LIKE ?"
params.append(f"%{search_text}%")
self.cursor.execute(query, params)
items = self.cursor.fetchall()
for item in items:
item_id, name, category, price, description = item
self.add_menu_item(item_id, name, category, price, description)
def add_to_cart(self, item_id, name, price):
"""添加菜品到购物车"""
# 隐藏空购物车提示
self.empty_order_label.hide()
# 检查是否已在购物车中
for i in range(self.order_items_layout.count()):
item_widget = self.order_items_layout.itemAt(i).widget()
if item_widget and hasattr(item_widget, 'item_id') and item_widget.item_id == item_id:
# 如果已存在,增加数量
spin_box = item_widget.findChild(QSpinBox)
spin_box.setValue(spin_box.value() + 1)
self.update_total_price()
return
# 创建新的购物车项
cart_item_frame = QFrame()
cart_item_frame.setFrameShape(QFrame.StyledPanel)
cart_item_frame.setStyleSheet("border: none; margin: 2px;")
cart_item_frame.item_id = item_id
cart_item_layout = QHBoxLayout(cart_item_frame)
# 菜品名称
name_label = QLabel(name)
name_label.setFont(QFont("SimHei", 12))
cart_item_layout.addWidget(name_label, 3)
# 单价
price_label = QLabel(f"¥{price:.2f}")
price_label.setFont(QFont("SimHei", 12))
cart_item_layout.addWidget(price_label, 1)
# 数量
quantity_layout = QHBoxLayout()
minus_button = QPushButton("-")
minus_button.setFixedSize(25, 25)
minus_button.setStyleSheet("""
QPushButton {
background-color: #f44336;
color: white;
border-radius: 3px;
}
QPushButton:hover {
background-color: #d32f2f;
}
""")
minus_button.clicked.connect(lambda checked, w=cart_item_frame: self.change_quantity(w, -1))
spin_box = QSpinBox()
spin_box.setMinimum(1)
spin_box.setValue(1)
spin_box.setFixedWidth(50)
spin_box.valueChanged.connect(self.update_total_price)
plus_button = QPushButton("+")
plus_button.setFixedSize(25, 25)
plus_button.setStyleSheet("""
QPushButton {
background-color: #4CAF50;
color: white;
border-radius: 3px;
}
QPushButton:hover {
background-color: #45a049;
}
""")
plus_button.clicked.connect(lambda checked, w=cart_item_frame: self.change_quantity(w, 1))
quantity_layout.addWidget(minus_button)
quantity_layout.addWidget(spin_box)
quantity_layout.addWidget(plus_button)
cart_item_layout.addLayout(quantity_layout, 2)
# 小计
subtotal_label = QLabel(f"¥{price:.2f}")
subtotal_label.setFont(QFont("SimHei", 12))
subtotal_label.setStyleSheet("color: #e63946;")
subtotal_label.setAlignment(Qt.AlignRight)
cart_item_layout.addWidget(subtotal_label, 2)
# 删除按钮
delete_button = QPushButton("删除")
delete_button.setStyleSheet("""
QPushButton {
color: #f44336;
border: 1px solid #f44336;
border-radius: 3px;
padding: 2px;
}
QPushButton:hover {
background-color: #ffebee;
}
""")
delete_button.clicked.connect(lambda checked, w=cart_item_frame: self.remove_from_cart(w))
cart_item_layout.addWidget(delete_button, 1)
# 保存引用以便更新小计
cart_item_frame.price_label = price_label
cart_item_frame.quantity_spinbox = spin_box
cart_item_frame.subtotal_label = subtotal_label
self.order_items_layout.addWidget(cart_item_frame)
self.update_total_price()
def change_quantity(self, cart_item_frame, delta):
"""更改购物车中菜品的数量"""
spin_box = cart_item_frame.quantity_spinbox
new_value = spin_box.value() + delta
if new_value >= 1:
spin_box.setValue(new_value)
def remove_from_cart(self, cart_item_frame):
"""从购物车中移除菜品"""
self.order_items_layout.removeWidget(cart_item_frame)
cart_item_frame.deleteLater()
self.update_total_price()
# 如果购物车为空,显示空购物车提示
if self.order_items_layout.count() == 0:
self.empty_order_label.show()
def update_total_price(self):
"""更新购物车总价"""
total_price = 0.0
for i in range(self.order_items_layout.count()):
item_widget = self.order_items_layout.itemAt(i).widget()
if item_widget:
price = float(item_widget.price_label.text().replace("¥", ""))
quantity = item_widget.quantity_spinbox.value()
subtotal = price * quantity
item_widget.subtotal_label.setText(f"¥{subtotal:.2f}")
total_price += subtotal
self.total_price_label.setText(f"¥{total_price:.2f}")
def submit_order(self):
"""提交订单"""
# 检查购物车是否为空
if self.order_items_layout.count() == 0:
QMessageBox.warning(self, "提交失败", "购物车为空,请先添加菜品!")
return
# 获取客户信息
name = self.name_input.text().strip()
phone = self.phone_input.text().strip()
table_number = self.table_input.text().strip()
order_date = self.date_input.date().toString("yyyy-MM-dd")
order_time = self.time_input.time().toString("HH:mm:ss")
# 验证客户信息
if not name:
QMessageBox.warning(self, "提交失败", "请输入您的姓名!")
return
if not phone:
QMessageBox.warning(self, "提交失败", "请输入您的电话!")
return
if not table_number:
QMessageBox.warning(self, "提交失败", "请输入桌号!")
return
# 获取总价
total_price = float(self.total_price_label.text().replace("¥", ""))
# 开始事务
self.conn.execute("BEGIN")
try:
# 插入订单主表
self.cursor.execute(
"""INSERT INTO orders
(customer_name, phone, table_number, order_date, order_time, total_price, status)
VALUES (?, ?, ?, ?, ?, ?, ?)""",
(name, phone, table_number, order_date, order_time, total_price, "待处理")
)
# 获取订单ID
order_id = self.cursor.lastrowid
# 插入订单详情
for i in range(self.order_items_layout.count()):
item_widget = self.order_items_layout.itemAt(i).widget()
if item_widget:
item_id = item_widget.item_id
quantity = item_widget.quantity_spinbox.value()
price = float(item_widget.price_label.text().replace("¥", ""))
self.cursor.execute(
"""INSERT INTO order_details
(order_id, item_id, quantity, price)
VALUES (?, ?, ?, ?)""",
(order_id, item_id, quantity, price)
)
# 提交事务
self.conn.commit()
# 显示成功消息
QMessageBox.information(self, "提交成功", f"订单提交成功!订单号:{order_id}")
# 清空购物车
while self.order_items_layout.count():
item = self.order_items_layout.takeAt(0)
widget = item.widget()
if widget:
widget.deleteLater()
self.empty_order_label.show()
self.total_price_label.setText("¥0.00")
# 清空客户信息
self.name_input.clear()
self.phone_input.clear()
self.table_input.clear()
except Exception as e:
# 回滚事务
self.conn.rollback()
QMessageBox.critical(self, "提交失败", f"订单提交失败:{str(e)}")
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setStyle("Fusion") # 使用Fusion风格,跨平台一致性更好
# 设置全局字体
font = QFont("SimHei")
app.setFont(font)
window = CanteenOrderingSystem()
window.show()
sys.exit(app.exec_())
三、实验结果
经过漫长而又艰辛的实验,报错终于在2025年5月31日晚上结束了罪恶的一生。
当我不知道第多少次敲完代码,点下运行时,奇迹终于发生了!

我继续手颤抖地在界面里输入我设计地选项,点击确定,屏住呼吸,缓缓睁开眼:
这一刻我做到了,一切都值得了!

实验运行视频
四、实验反思
这次的实验我报错无数失败无数,说实话我都不知到从何说起了,那我就每个部分挑几个代表性的说一说吧。
首先我要狠狠地批判一下AI大模型(因为中途受不了了我有点想直接生成但是直接被其劝退,报错64处让我断绝了直接生成的念头:

一些经典的错误及改进
1.界面问题

这张图放出来就知道我有多绝望了,好不容易躲过了报错,运行了这玩意555
后来找了好多资料,看了好多网上视频发现
这是新手常见的布局错误:
在create_menu_area方法中,直接将QVBoxLayout设置给了QScrollArea,而没有使用中间的 QWidget 作为容器。这会导致程序运行时左侧菜单区域无法正确显示菜品。
self.menu_content_layout = QVBoxLayout(self.menu_scroll_area) # ❌ 错误:直接设置布局到QScrollArea
正确为:
self.menu_content_widget = QWidget()
self.menu_content_layout = QVBoxLayout(self.menu_content_widget)
self.menu_scroll_area.setWidget(self.menu_content_widget)
2.数据库问题

像这种无法运行是我遇见的最多情况了。刚才说的那种错误虽然运行出来不合预期,但编译过了总是给我一线希望,而诸如这种报错我是感觉到两眼一黑的,但这又是家常便饭了。
这个报错没记错的话应该是一个星期没有解决(但是零碎时间做的实验)
为了舒缓压抑的心情我是将近一周没再管这个实验了,直到端午节我买足咖啡,血战一整天终于解决:
主键约束缺失
在menu_items表中,id字段没有定义为主键(缺少PRIMARY KEY约束)
这会导致无法保证 id 的唯一性,插入重复数据时不会报错
同时,由于没有自增属性,插入数据时需要手动提供 id 值
外键关联不完整
在order_details表中,外键关联没有设置ON DELETE CASCADE
当删除相关的订单或菜品时,订单详情记录不会自动删除,导致数据冗余
可能会造成 "孤儿记录",即引用了不存在的订单或菜品
SQL 注入风险:
在submit_order方法中,直接将用户输入的数据拼接到 SQL 语句中
用户可以通过输入特殊字符(如单引号)来篡改 SQL 语句
例如,在 "姓名" 字段输入:' OR '1'='1 会导致 SQL 条件永远为真
修复方法
为menu_items表的id字段添加PRIMARY KEY AUTOINCREMENT约束
在外键关联中添加ON DELETE CASCADE选项
始终使用参数化查询(如?占位符)来防止 SQL 注入
数据库初始化(修复版)
def init_database(self):
self.conn = sqlite3.connect("canteen.db")
self.cursor = self.conn.cursor()
# 修复1:菜品表添加主键
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS menu_items (
id INTEGER PRIMARY KEY AUTOINCREMENT, # ✅ 主键自增
name TEXT NOT NULL,
category TEXT NOT NULL,
price REAL NOT NULL,
description TEXT
)
''')
# 修复2:订单详情表添加级联删除
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS order_details (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER NOT NULL,
item_id INTEGER NOT NULL,
quantity INTEGER NOT NULL,
price REAL NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders (id) ON DELETE CASCADE, # ✅ 级联删除
FOREIGN KEY (item_id) REFERENCES menu_items (id) ON DELETE CASCADE # ✅ 级联删除
)
''')
# 修复3:使用参数化插入数据
sample_items = [("宫保鸡丁", "川菜", 32.0, "..."), ...]
self.cursor.executemany(
"INSERT INTO menu_items (name, category, price, description) VALUES (?, ?, ?, ?)",
sample_items
)
self.conn.commit()
左侧菜单区域布局(修复版)
def create_menu_area(self):
# ... 其他代码 ...
# 修复:正确嵌套QScrollArea和布局
self.menu_scroll_area = QScrollArea()
self.menu_scroll_area.setWidgetResizable(True)
# 添加中间容器Widget
self.menu_content_widget = QWidget()
self.menu_content_layout = QVBoxLayout(self.menu_content_widget)
self.menu_scroll_area.setWidget(self.menu_content_widget) # 关键修复步骤
menu_layout.addWidget(self.menu_scroll_area)
订单提交(修复 SQL 注入)
def submit_order(self):
# ... 获取用户输入 ...
try:
# 修复:使用参数化查询防止SQL注入
self.cursor.execute(
"""INSERT INTO orders (customer_name, phone, table_number, order_date, order_time, total_price, status)
VALUES (?, ?, ?, ?, ?, ?, ?)""",
(name, phone, table_number, order_date, order_time, total_price, "待处理")
)
# 订单详情同样使用参数化查询
self.cursor.execute(
"""INSERT INTO order_details (order_id, item_id, quantity, price)
VALUES (?, ?, ?, ?)""",
(order_id, item_id, quantity, price)
)
self.conn.commit()
except Exception as e:
self.conn.rollback()
QMessageBox.critical(self, "错误", f"订单提交失败:{str(e)}")
这应该是我遇到最大的问题了,还有其他小问题当然不胜其数(大部分是我理解不到位)
我觉得没有必要一一列举了,过程很艰辛,结果很甜蜜!
五、实验总结
这个食堂线上点菜系统实验我应该是做了13天左右(中途休息了一周实在干不动了),可以说是耗尽了我的精力。
一开始被PyQt5的布局逻辑绕得晕头转向,明明照抄教程代码,左侧菜品区却始终空白,后来才发现是QScrollArea没加中间容器Widget,就像搭积木少了关键一块,整个结构都立不起来。数据库部分更是“重灾区”,主键没设导致数据混乱,写订单时直接拼接用户输入,差点踩进SQL注入的坑,每次调试都像拆弹一样小心翼翼。
端午节一整天我就像着了迷的疯子,从早八在图书馆做实验到晚上十点半闭馆被赶走,到教室里继续做到十一点半宵禁回宿舍继续感到凌晨两点,终于完成了我的实验及报告。看到去年一个学姐14小时做完实验我真的羡慕敬佩,我整整做了13天,600行代码,无数次报错让我痛不欲生,但当界面上菜品能随滚动条滑动,购物车数量变更能实时计算总价,提交订单后数据乖乖躺在数据库表中时,成就感直接拉满!这13天里,从对着报错代码抓耳挠腮,到能独立分析问题、查博客改逻辑,甚至给数据库加级联删除、用参数化查询防注入,每一步都走得磕磕绊绊却无比扎实。现在看着自己“养大”的系统,终于懂了编程不是灵光乍现,而是把每个细节磨到发亮的坚持,那些掉过的坑,最终都成了往上爬的台阶。
六、参考资料
1.PyQt5 笔记(01):嵌套布局
2.Hello PyQt5(五)PyQt5布局管理
3.PyQt5 快速开发实战
4.如何修复不工作的Tkinter嵌套框架;它不允许小部件
5.PyQt5的相对布局管理的实现
6.Python 小白从零开始 PyQt5 项目实战(5)布局管理
7.Python 经典面试题汇总之数据库篇
8.常见的数据库错误的解决方法!必看!(含安装插件常见错误)
9.数据库常见告警、报错与解决方法记录
10.MySQL 数据库“十宗罪”(十大经典错误案例)
七、全课总结
时间过的好快,一学期Python课就这样结束了。
我不禁回忆起我与Python课及王老师的点点滴滴。
思绪回到大一上学期,那时候我刚到电科院,加入了七号创新社,群主(指导老师)是王志强老师,之后群里说正好有个缺人的红星杯大学生创新项目,也是王老师指导,虽然我懵懵懂懂,但我抱着尝试的态度加入跟着学长学姐试着了解大创的流程,虽然最后遗憾未能获奖,但是学到的思维和计划书写法等是使我收益无穷,终于在本学期的青创北京挑战杯一雪前耻成功获奖。
但这只是我对老师的单方面认识,说到与老师最开始交流还是在小红书。由于一次偶然的机会,在小红书上刷到一个电科院的账号,我立即点开翻主页,发现一大堆惊艳的美照,竟然把电科院看似普通无常的地方拍的这么美。细细察看文案,推断博主应该是老师(后来学姐告诉我这是王志强老师),有些文案是在凌晨清晨或是深夜工作,但流露出的不是抱怨而是对生活的期待,我真的很震撼,反思自己为什么总是在困难中内耗?生活中不缺少美,而是缺乏像老师这样发现美的眼睛。
时间来到选课的时候,看着各种选项我说实话感到迷茫。但看到王志强老师的课我没有任何犹豫就选择了Python,尽管我基础不好,但我坚信仅仅在我碎片里了解的王老师值得我认真学习学习。寒假的时候我趁着空闲时间提前学了一下Python基础,并且向老师申请当课代表,热爱Python,愿意分担工作。
终于到了第一次Python课了,也是我第一次真正看到王老师!虽然我Python基础较弱,但老师深入浅出的讲解确实让我收获颇丰。我的代码写的不熟,但老师逐行解释逐行敲代码的教学方式让我轻松跟上老师的节奏。从打印hello python到编写比较数字大小,再到socket通讯,网络爬虫,每一步都留下了我们的踏实的脚印,每一步都有我的顿悟和欢喜。王老师讲解Python时常常从我们熟悉的c语言引入,介绍相似点与不同点,使我们自然过渡,易于接受。课前提问环节督促同学们及时复习,(但如果课后能留一点知识问答类的作业就更好了,可以辅助同学们梳理课程内容)课中谈论大家其乐融融,群里总是欢声笑语,学习氛围浓厚。
天下没有不散的宴席,Python课在大家依依不舍中画上了圆满的句号。王老师在最后送给我们的教导我记忆犹新:不依赖ai;大学更应该奋斗;脚踏实地时也要抬头看路。我不禁结合到我自己这一年的经历,虽然学生工作的业绩,比赛成绩正在我的拼搏中取得一些成效,但我的部分核心实力的低谷导致的瓶颈却让我现在自责内耗,一度怀疑自己想就此沉沦。王老师不仅在教导我们Python更是在为我们人生指路,既然生活本身就是亦苦亦甜的,那我为什么不积极地去活着呢。
最后我也想用老师用的这句话总结本文“那些看似波澜不惊的日复一日,终将在某一天,让我们看到坚持的意义”。功不唐捐,玉汝于成。未来的日子里我会带着这份信念继续奋斗,默默成长,向着远方出发!

浙公网安备 33010602011771号