smart_driving 智能座舱系统学习笔记(完整版)
本笔记融合 技术栈解析、GUI开发、多媒体处理、逻辑交互、数据管理 五大维度,从“基础架构→核心技术→业务逻辑→优化扩展”层层递进,既覆盖代码实现细节,也拆解用户操作与系统响应的完整链路,适合项目复盘、技术沉淀与功能扩展参考。
一、项目整体架构与技术栈
1. 项目结构解析
采用 模块化分层设计,各文件/文件夹职责清晰,降低耦合度,便于维护和扩展,结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| smart_driving/ ├── main.py # 程序入口(欢迎页→登录页→主窗口跳转) ├── view/ # 核心界面模块(所有UI组件存放目录) │ ├── MainWin.py # 主窗口(左侧导航+右侧堆叠布局) │ ├── HomePage.py # 首页(集成各功能入口按钮) │ ├── btn1_HP.py # 摄像头/视频录制模块(20秒自动停止) │ ├── list_page.py # 视频列表(分页+日期筛选+缩略图提取) │ ├── player_page.py # 视频播放器(进度条+倍速+全屏) │ ├── btn6_HP.py # 照片管理(日历筛选+列表/查看页切换) │ ├── PhotoListPage.py # 照片列表页(复用视频列表逻辑) │ ├── photo.py # 照片查看页(缩放+旋转) │ ├── MusicPage.py # 音乐播放模块(播放+进度+音量控制) │ ├── Res_Login_Widget.py# 注册/登录界面(验证码+密码加密) │ ├── Code.py # 验证码组件(随机生成+点击刷新) │ └── btn7_HP.py # 个人中心(头像上传+密码修改) ├── smart_driving/ # 工具模块 │ └── DB_util.py # 数据库操作(用户注册/登录/信息修改) ├── Icon/ # 图标资源(按钮图标、标题图标) ├── image/ # 图片资源(背景图、默认头像) ├── video/ # 视频存储目录(录制的视频文件) ├── pic/ # 截图存储目录(摄像头截图) └── avatars/ # 用户头像存储目录
|
2. 核心技术栈
技术/框架 |
用途说明 |
关键文件/模块 |
PyQt5 |
GUI 开发(窗口、布局、组件、信号槽) |
所有 view/ 下的界面文件 |
OpenCV(cv2) |
视频/图像处理(摄像头采集、帧提取、存储) |
btn1_HP.py 、list_page.py |
MySQL(pymysql) |
用户数据存储(注册、登录、头像路径) |
DB_util.py 、Res_Login_Widget.py |
正则表达式(re) |
文件名匹配(视频/照片按时间格式筛选) |
list_page.py 、btn6_HP.py |
日期时间处理(datetime) |
文件名生成、日期筛选 |
所有涉及文件存储的模块 |
二、GUI 界面开发核心(PyQt5)
PyQt5 是界面开发的基石,核心在于 布局管理(实现自适应)和 组件样式(提升视觉体验),同时通过 信号槽 实现组件间通信。
1. 布局管理器:解决组件排列与页面切换
PyQt5 提供 4 种核心布局,项目中以 盒式布局 和 堆叠布局 为主,覆盖“组件排列”和“页面切换”两大需求。
(1)盒式布局:垂直/水平排列组件(QVBoxLayout/QHBoxLayout)
核心作用:自动分配组件尺寸,支持窗口缩放时自适应,避免组件重叠或错位。
关键方法:
addWidget(widget, stretch=0)
:添加组件,stretch
为拉伸权重(值越大占比越高)
addLayout(layout)
:嵌套子布局(如垂直布局中嵌套水平布局)
setContentsMargins(left, top, right, bottom)
:设置布局外边框距(避免组件贴边)
setSpacing(spacing)
:设置组件之间的间距(避免拥挤)
addStretch(stretch=1)
:添加弹性空间,推动组件到指定方向(如右侧留白、底部固定)
项目实战示例(MainWin.py
主窗口布局):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| self.main_layout = QHBoxLayout(central_widget) self.main_layout.setContentsMargins(0, 0, 0, 0) self.main_layout.setSpacing(1)
button_layout = QVBoxLayout(self.button_container) button_layout.setContentsMargins(15, 50, 15, 50) button_layout.setSpacing(30) button_layout.addWidget(self.home_btn) button_layout.addWidget(self.phone_btn) button_layout.addWidget(self.music_btn) button_layout.addStretch() self.main_layout.addWidget(self.button_container, 1)
self.stacked_widget = QStackedWidget() self.main_layout.addWidget(self.stacked_widget, 9)
|
效果:左侧导航与右侧内容按 1:9 比例分配宽度,窗口缩放时比例不变,按钮始终垂直居中且不贴边。
核心作用:多个页面堆叠存储,同一时间仅显示一个页面,用于“列表页→详情页”“登录页→主页”等场景。
关键方法:
addWidget(widget)
:添加页面(按顺序分配索引,从 0 开始)
setCurrentIndex(index)
:切换到指定索引的页面
currentIndex()
:获取当前显示页面的索引
widget(index)
:获取指定索引的页面组件(用于传递数据)
项目实战示例(btn6_HP.py
照片管理页面切换):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| self.stacked_widget = QStackedWidget()
self.photo_list_page = PhotoListPage(self) self.photo_page = Photo(self) self.stacked_widget.addWidget(self.photo_list_page) self.stacked_widget.addWidget(self.photo_page)
self.photo_list_page.photo_clicked.connect(self.switch_to_photo_page)
self.photo_page.back_signal.connect(self.switch_to_list_page)
def switch_to_photo_page(self, photo_path): self.stacked_widget.setCurrentIndex(1) self.photo_page.set_photo(photo_path)
def switch_to_list_page(self): self.stacked_widget.setCurrentIndex(0)
|
关键逻辑:通过 信号槽 传递页面切换指令和数据(如照片路径),实现页面间解耦,避免直接依赖。
2. 组件样式:用 QSS 实现现代 UI 风格
PyQt5 支持类似 CSS 的样式表(QSS),项目通过 QSS 实现 渐变背景、圆角组件、hover 动效,提升界面精致度。
(1)渐变背景:提升视觉层次感
两种实现方式:
- 方式 1:代码生成动态渐变(适配组件尺寸变化)
- 方式 2:QSS 静态定义渐变(适合固定区域)
代码生成渐变示例(MainWin.py
左侧导航):
1 2 3 4 5 6 7 8 9 10
| self.button_container = QWidget()
gradient = QLinearGradient(0, 0, 0, self.button_container.height()) gradient.setColorAt(0.0, QColor(22, 24, 30)) gradient.setColorAt(1.0, QColor(30, 35, 45))
palette = self.button_container.palette() palette.setBrush(QPalette.Window, QBrush(gradient)) self.button_container.setPalette(palette) self.button_container.setAutoFillBackground(True)
|
QSS 渐变示例(MusicPage.py
音乐播放区):
1 2 3 4 5
| QWidget#music_container { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #2d3241, stop:1 #232837); }
|
(2)组件美化:按钮、输入框、列表
核心样式属性:
background-color
:背景色(支持半透明 rgba
)
border-radius
:圆角(值越大越圆润,避免尖锐边角)
border
:边框(颜色、宽度、样式)
padding
:内边距(组件内部内容与边框的距离)
hover
/pressed
:组件交互状态的样式(增强反馈)
按钮样式示例(btn7_HP.py
个人中心按钮):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| QPushButton { background-color: #4263eb; color: white; border-radius: 8px; padding: 8px 16px; font-size: 14px; border: 1px solid rgba(66, 99, 235, 0.5); } QPushButton:hover { background-color: #3655d9; box-shadow: 0 2px 8px rgba(66, 99, 235, 0.3); } QPushButton:pressed { background-color: #2d4bc7; }
|
核心需求:实现视频/照片的缩略图列表,需固定组件尺寸避免排列混乱。
项目实战示例(list_page.py
视频列表):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| self.view = QListWidget() self.view.setViewMode(QListWidget.IconMode) self.view.setIconSize(QSize(220, 140)) self.view.setResizeMode(QListWidget.Adjust) self.view.setSpacing(20) self.view.setStyleSheet(''' QListWidget { background-color: rgba(40, 45, 60, 0.5); /* 半透明背景 */ border-radius: 10px; padding: 15px; } QListWidget::item { width: 220px; /* 固定每个项的宽度(与图标宽度一致) */ height: 180px; /* 固定高度(图标+文字) */ text-align: center; /* 文字居中 */ color: #e0e5ec; /* 文字浅灰色 */ } QListWidget::item:hover { background-color: rgba(255,255,255,0.05); /* 悬浮时高亮 */ border-radius: 8px; } ''')
|
关键细节:QListWidget::item
必须固定宽度,否则图标会随窗口拉伸变形;setIconSize
需与缩略图尺寸一致,确保显示清晰。
3. 信号与槽:组件间通信的核心机制
PyQt5 的 信号槽(Signal & Slot) 是实现“按钮点击→函数执行”“页面切换→数据传递”的核心,分为 内置信号 和 自定义信号。
(1)内置信号:组件自带的交互触发
常见内置信号:
QPushButton.clicked
:按钮点击时触发
QLineEdit.textChanged
:输入框文本变化时触发
QDateEdit.dateChanged
:日期选择器日期变化时触发
QListWidget.itemClicked
:列表项被点击时触发
项目实战示例(btn1_HP.py
摄像头开关):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| self.camera_btn = QPushButton("开启摄像头") self.camera_btn.clicked.connect(self.on_camera)
def on_camera(self): if self.homePage.is_camera_on: self.timer.stop() self.video_capture.release() self.homePage.is_camera_on = False else: self.homePage.is_camera_on = True self.start_camera()
|
(2)自定义信号:跨页面传递自定义数据
当内置信号无法满足需求(如传递视频路径、照片路径)时,需自定义信号,步骤如下:
- 导入
pyqtSignal
和 QObject
- 创建信号类(继承
QObject
),定义信号(指定参数类型)
- 发送方在事件触发时发射信号(
signal.emit(参数)
)
- 接收方绑定信号到槽函数,处理数据
项目实战示例(list_page.py
视频路径传递):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| from PyQt5.QtCore import pyqtSignal, QObject
class GlobalSignals(QObject): video_path_signal = pyqtSignal(str)
global_signals = GlobalSignals()
class ListPage(QWidget): def on_item_click(self, item): video_path = item.data(Qt.UserRole) global_signals.video_path_signal.emit(video_path)
class PlayerPage(QWidget): def __init__(self): super().__init__() global_signals.video_path_signal.connect(self.play_video) def play_video(self, video_path): self.video_path = video_path self.init_video()
|
优势:发送方与接收方无需直接引用,通过信号间接通信,降低模块耦合度,便于维护。
三、多媒体处理核心(OpenCV + PyQt5)
项目中多媒体处理涵盖 摄像头采集、视频录制、帧提取、照片编辑,核心依赖 OpenCV 处理图像/视频数据,再通过 PyQt5 显示到界面。
1. 摄像头采集与视频录制(btn1_HP.py)
(1)核心流程
- 打开摄像头:
cv2.VideoCapture(0)
(0 表示默认摄像头)
- 读取帧:
ret, frame = cap.read()
(ret
为是否成功,frame
为帧数据)
- 帧格式转换:OpenCV 帧为 BGR 格式,PyQt 显示需 RGB 格式,需用
cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
转换
- 帧显示:将转换后的帧转为
QImage
→ QPixmap
,设置到 QLabel
上
- 视频录制:
cv2.VideoWriter
写入帧数据,设置编码格式、帧率、分辨率
- 资源释放:关闭摄像头(
cap.release()
)和录制器(writer.release()
)
(2)关键代码解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| def start_camera(self): self.video_capture = cv2.VideoCapture(0) if not self.video_capture.isOpened(): self.video_widget.setText("无法打开摄像头") return current_datetime = datetime.now().strftime("%Y-%m-%d-%H-%M-%S") video_path = f'video/{current_datetime}.mp4' fourcc = cv2.VideoWriter_fourcc(*'mp4v') self.video_writer = cv2.VideoWriter( video_path, fourcc, 30.0, (800, 600) ) self.timer = QTimer() self.timer.timeout.connect(self.update_video) self.timer.start(30)
def update_video(self): ret, frame = self.video_capture.read() if not ret: self.video_widget.setText("无法获取视频帧") return frame_resized = cv2.resize(frame, (800, 600)) frame_rgb = cv2.cvtColor(frame_resized, cv2.COLOR_BGR2RGB) h, w, ch = frame_rgb.shape q_image = QImage(frame_rgb.data, w, h, w * ch, QImage.Format_RGB888) self.video_widget.setPixmap(QPixmap.fromImage(q_image)) self.video_widget.setScaledContents(True) self.video_writer.write(frame_resized)
|
(3)20 秒自动停止录制(核心扩展功能)
需求:无需手动操作,录制 20 秒后自动保存并退出。
实现思路:记录录制开始时间,定时检查已录制时长,达到 20 秒时触发关闭逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| def __init__(self): self.auto_stop_timer = QTimer() self.auto_stop_timer.timeout.connect(self.check_auto_stop) self.record_start_time = 0
def on_camera(self): if self.homePage.is_camera_on: self.auto_stop_timer.stop() else: self.record_start_time = time.time() self.auto_stop_timer.start(1000)
def check_auto_stop(self): elapsed_time = int(time.time() - self.record_start_time) if elapsed_time >= 20: QMessageBox.information(self, "自动停止", "已录制20秒,视频已保存到video目录") self.on_camera()
|
2. 视频缩略图提取(list_page.py)
核心需求
视频列表需显示每个视频的第一帧作为缩略图,核心是用 OpenCV 读取视频第一帧,再转为 PyQt 可显示的格式。
关键代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| def get_video_first_frame(self, video_path): try: cap = cv2.VideoCapture(video_path) ret, frame = cap.read() cap.release() if ret: frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, c = frame_rgb.shape qimg = QImage(frame_rgb.data, w, h, w * c, QImage.Format_RGB888) return QPixmap.fromImage(qimg).scaled( 220, 140, Qt.KeepAspectRatio, Qt.SmoothTransformation ) except Exception as e: print(f"提取视频帧失败:{e}") default_pixmap = QPixmap(220, 140) default_pixmap.fill(QColor(50, 55, 70)) return default_pixmap
|
关键细节:读取第一帧后必须调用 cap.release()
,否则视频文件会被占用,无法删除或修改。
3. 照片编辑(photo.py)
核心功能:缩放与旋转
通过 QPixmap
的变换方法实现照片的基础编辑,核心是维护 缩放因子 和 旋转角度 两个状态变量。
(1)缩放功能
思路:维护 scale_factor
变量(初始 1.0,代表原尺寸),每次点击“放大/缩小”按钮时调整因子,再重新绘制图片。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| def __init__(self): self.scale_factor = 1.0 self.max_scale = 3.0 self.min_scale = 0.5
def zoom_in(self): if self.scale_factor < self.max_scale: self.scale_factor += 0.1 self.update_photo()
def zoom_out(self): if self.scale_factor > self.min_scale: self.scale_factor -= 0.1 self.update_photo()
def update_photo(self): scaled_pixmap = self.original_pixmap.scaled( self.original_pixmap.width() * self.scale_factor, self.original_pixmap.height() * self.scale_factor, Qt.KeepAspectRatio, Qt.SmoothTransformation ) self.photo_label.setPixmap(scaled_pixmap)
|
(2)旋转功能
思路:维护 rotation
变量(初始 0°,每次旋转 90°),通过 QTransform.rotate()
实现旋转变换,再叠加缩放效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| def __init__(self): self.rotation = 0
def rotate_photo(self): self.rotation = (self.rotation + 90) % 360 self.update_photo()
def update_photo(self): transform = QTransform().rotate(self.rotation) rotated_pixmap = self.original_pixmap.transformed(transform, Qt.SmoothTransformation) scaled_pixmap = rotated_pixmap.scaled( rotated_pixmap.width() * self.scale_factor, rotated_pixmap.height() * self.scale_factor, Qt.KeepAspectRatio, Qt.SmoothTransformation ) self.photo_label.setPixmap(scaled_pixmap)
|
四、核心业务逻辑与交互流程
1. 主窗口导航与页面切换(MainWin.py)
场景:用户点击左侧“首页/音乐/视频”按钮,切换右侧内容页
完整交互流程:
初始化阶段:
- 主窗口加载时,初始化左侧按钮和右侧堆叠布局的页面
- 建立“按钮→页面索引”的映射(如“首页”对应索引 0,“音乐”对应索引 1)
- 默认选中“首页”按钮,右侧显示首页内容
用户操作阶段:
- 用户点击左侧按钮→触发
chang_page
函数,并传递目标页面索引
- 切换堆叠布局的当前页面(
setCurrentIndex(index)
)
- 同步更新所有按钮的选中状态(如索引 1 对应“音乐”按钮,设置为
Checked=True
)
状态同步阶段:
- 确保“按钮选中状态”与“当前显示页面”一致,避免用户困惑(如页面在“音乐”,按钮却选中“视频”)
关键代码与逻辑解析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| class MainWin(QMainWindow): def __init__(self): super().__init__() self.home_btn = QPushButton("首页") self.music_btn = QPushButton("音乐") self.video_btn = QPushButton("视频") self.profile_btn = QPushButton("个人中心") self.stacked_widget = QStackedWidget() self.home_page = HomePage() self.music_page = MusicPage() self.video_page = btn1_HP(self) self.profile_page = btn7_HP() self.stacked_widget.addWidget(self.home_page) self.stacked_widget.addWidget(self.music_page) self.stacked_widget.addWidget(self.video_page) self.stacked_widget.addWidget(self.profile_page) self.home_btn.clicked.connect(lambda: self.chang_page(0)) self.music_btn.clicked.connect(lambda: self.chang_page(1)) self.video_btn.clicked.connect(lambda: self.chang_page(2)) self.profile_btn.clicked.connect(lambda: self.chang_page(3)) self.home_btn.setChecked(True)
def chang_page(self, index): """核心切换逻辑:页面切换+按钮状态同步""" self.stacked_widget.setCurrentIndex(index) self.home_btn.setChecked(index == 0) self.music_btn.setChecked(index == 1) self.video_btn.setChecked(index == 2) self.profile_btn.setChecked(index == 3)
|
设计亮点:
- 用“数字索引”关联按钮与页面,逻辑清晰,新增页面只需增加索引和按钮,无需修改核心逻辑
- 通过
lambda
函数传递索引,避免为每个按钮写单独的槽函数,简化代码
- 强制同步按钮状态,确保界面交互一致性
2. 视频模块全流程逻辑(摄像头→录制→列表→播放)
视频模块是项目最复杂的功能,涉及 4个组件 的联动,完整链路覆盖“录制→存储→筛选→播放”,是理解“多组件协作”的核心案例。
2.1 摄像头录制逻辑(btn1_HP.py)
场景:用户点击“开启摄像头”按钮,系统启动摄像头并录制视频,20秒后自动保存退出
交互流程拆解:
步骤 |
用户操作/系统事件 |
系统响应 |
关键代码逻辑 |
1 |
点击“开启摄像头”按钮 |
检查当前状态(关闭则初始化摄像头) |
self.homePage.is_camera_on 标记为 True ,调用 start_camera() |
2 |
系统初始化摄像头 |
打开摄像头(cv2.VideoCapture(0) ),初始化录制器 |
生成按时间命名的视频路径,设置 cv2.VideoWriter 编码、帧率、分辨率 |
3 |
系统启动定时器 |
帧更新定时器(30ms/次)→ 读取并显示帧;自动停止定时器(1秒/次)→ 检查时长 |
self.timer.start(30) ,self.auto_stop_timer.start(1000) |
4 |
录制中(用户无操作) |
每秒检查已录制时长,达到20秒则触发关闭 |
elapsed_time = int(time.time() - self.record_start_time) ,若 ≥20 则调用 on_camera() |
5 |
自动停止 |
停止定时器,释放摄像头/录制器,提示用户 |
self.timer.stop() ,self.video_capture.release() ,QMessageBox 提示 |
2.2 视频列表与筛选逻辑(list_page.py)
场景:用户进入视频列表页,系统加载所有视频并按日期筛选,点击视频项跳转播放
交互流程拆解:
步骤 |
用户操作/系统事件 |
系统响应 |
关键代码逻辑 |
1 |
页面初始化 |
扫描 video/ 目录,筛选符合格式的视频 |
正则匹配 ^\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}\.mp4$ ,提取第一帧作为缩略图 |
2 |
用户选择日期(如2025-08-23) |
按日期筛选视频,重新加载列表 |
提取文件名前10位(filename[:10] ),匹配选择的日期,保留符合条件的视频 |
3 |
点击视频列表项 |
发射视频路径信号,通知主窗口切换页面 |
video_path = item.data(Qt.UserRole) ,global_signals.video_path_signal.emit(video_path) |
4 |
主窗口接收信号 |
切换到播放器页面,传递视频路径 |
主窗口监听信号,调用 self.stacked_widget.setCurrentIndex(播放器页面索引) |
2.3 视频播放逻辑(player_page.py)
场景:用户点击视频列表项,系统切换到播放器页面并自动播放视频,支持进度拖动、倍速播放
交互流程拆解:
步骤 |
用户操作/系统事件 |
系统响应 |
关键代码逻辑 |
1 |
接收视频路径信号 |
初始化视频捕获器,获取视频信息 |
self.cap = cv2.VideoCapture(video_path) ,获取总帧数(self.total_frames )和帧率(self.fps ) |
2 |
点击“播放”按钮 |
启动帧定时器,逐帧读取并显示 |
self.timer.start(self.timer_interval) (timer_interval = 1000 / self.fps / self.speed ) |
3 |
拖动进度条 |
跳转到指定帧,更新显示 |
self.current_frame = value ,self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.current_frame) |
4 |
选择倍速(如2x) |
调整定时器间隔,实现倍速播放 |
self.speed = 2.0 ,self.timer_interval = int(1000 / self.fps / self.speed) ,重启定时器 |
5 |
播放结束 |
自动暂停,重置进度条,提示用户 |
读取帧失败(ret=False )→ self.timer.stop() ,QMessageBox 提示“播放结束” |
视频模块逻辑亮点:
- 全链路解耦:通过自定义信号传递视频路径,列表页与播放器页无需直接引用,便于独立维护
- 状态统一管理:用
is_playing
(播放状态)、current_frame
(当前帧)等变量管理播放过程,避免逻辑混乱
- 代码复用:核心方法(如
update_frame
)被播放、进度拖动、倍速等操作调用,减少重复代码
- 异常兼容:处理“摄像头打开失败”“视频文件损坏”“帧率获取失败”等场景,避免程序崩溃
3. 个人中心用户信息管理(btn7_HP.py)
场景:用户登录后进入个人中心,可上传头像、修改密码,系统同步更新数据库
完整交互流程:
登录状态同步:
- 登录成功→全局信号(
username_signal
)发射用户名→个人中心接收并更新“当前用户”标签
- 调用
load_user_info()
从数据库查询用户头像路径→若存在则显示头像,否则显示默认边框
头像上传流程:
步骤 |
用户操作 |
系统响应 |
关键代码逻辑 |
1 |
点击“选择图片”按钮 |
打开文件对话框,限制图片格式 |
QFileDialog.getOpenFileName(..., "图片文件 (*.png *.jpg *.jpeg *.bmp)") |
2 |
选择图片文件 |
生成唯一文件名(用户名+原文件名+时间戳) |
save_filename = f"{self.current_username}_{name}_{timestamp}{ext}" |
3 |
系统保存图片 |
复制图片到 avatars/ 目录,更新数据库头像路径 |
shutil.copyfile(file_path, save_path) ,调用 self.db_util.db_update_avatar() |
4 |
更新成功/失败 |
刷新头像显示或提示错误 |
成功则 self.avatar_label.setPixmap() ,失败则删除本地文件并提示 |
密码修改流程:
步骤 |
用户操作 |
系统响应 |
关键代码逻辑 |
1 |
输入原密码、新密码、确认密码,点击“保存修改” |
前端校验(非空、一致、长度≥6位) |
校验 all([old_pwd, new_pwd, confirm_pwd]) 、new_pwd == confirm_pwd 、len(new_pwd) ≥6 |
2 |
前端校验通过 |
调用数据库方法,校验原密码并更新 |
调用 self.db_util.db_update_password() ,后端先校验原密码,正确则更新新密码(MD5加密) |
3 |
后端返回结果 |
提示成功/失败,清空输入 |
成功则 QMessageBox.information() 并 self.clear_inputs() ,失败则提示“原密码错误” |
个人中心逻辑亮点:
- 状态同步:通过全局信号实时同步登录状态,未登录时禁止操作,避免异常
- 数据一致性:头像上传时先保存本地文件再更新数据库,失败则删除本地文件(回滚),确保“本地文件”与“数据库记录”一致
- 分层校验:前端先做轻量级校验(非空、长度),后端再做核心校验(原密码正确性),减少无效数据库请求,提升用户体验
- 路径兼容:处理 Windows 路径分隔符(
\
转义为 \\
),避免数据库存储路径错误
五、数据管理与用户交互
1. 数据库操作(DB_util.py)
DB_util
类封装了所有数据库交互逻辑,核心功能包括 用户注册、登录、密码修改、头像更新,确保数据安全和操作统一。
(1)核心设计原则
- 统一连接管理:所有操作通过
pymysql.connect()
建立连接,参数集中在 __init__
中,便于修改
- 事务安全:增删改操作需
commit()
提交,失败时 rollback()
回滚,避免数据脏写
- 资源释放:通过
finally
块确保游标和连接关闭,避免内存泄漏和数据库连接占用
- 密码安全:所有密码操作使用 MySQL 内置
MD5()
加密,避免明文存储
(2)关键方法逻辑解析
① 用户注册(db_user_reg
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| def db_user_reg(self, username, password): conn = None cursor = None try: conn = pymysql.connect( host=self.host, port=self.port, user=self.user, password=self.password, database=self.database, charset='utf8' ) cursor = conn.cursor() sql = f""" INSERT INTO user (uname, upwd, createtime, imgpath, ustable) VALUES ('{username}', MD5('{password}'), NOW(), 'avatars/default.png', 1) """ cursor.execute(sql) conn.commit() return 1 except Exception as e: if conn: conn.rollback() print(f"注册失败:{e}") return 0 finally: if cursor: cursor.close() if conn: conn.close()
|
② 用户登录(db_user_login
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| def db_user_login(self, username, password): conn = None cursor = None try: conn = pymysql.connect( host=self.host, port=self.port, user=self.user, password=self.password, database=self.database, charset='utf8' ) cursor = conn.cursor() sql = f""" SELECT uname, imgpath FROM user WHERE uname = '{username}' AND upwd = MD5('{password}') """ cursor.execute(sql) result = cursor.fetchone() return result except Exception as e: print(f"登录失败:{e}") return None finally: if cursor: cursor.close() if conn: conn.close()
|
③ 密码修改(db_update_password
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| def db_update_password(self, username, old_pwd, new_pwd): conn = None cursor = None try: conn = pymysql.connect( host=self.host, port=self.port, user=self.user, password=self.password, database=self.database, charset='utf8' ) cursor = conn.cursor() check_sql = f""" SELECT 1 FROM user WHERE uname = '{username}' AND upwd = MD5('{old_pwd}') """ cursor.execute(check_sql) if not cursor.fetchone(): return 0 update_sql = f""" UPDATE user SET upwd = MD5('{new_pwd}') WHERE uname = '{username}' """ cursor.execute(update_sql) conn.commit() return 1 except Exception as e: if conn: conn.rollback() print(f"修改密码失败:{e}") return 0 finally: if cursor: cursor.close() if conn: conn.close()
|
数据库逻辑亮点:
- 事务完整性:注册、修改密码等写操作都有“提交/回滚”机制,确保数据库数据不脏
- 加密一致性:注册和登录都用
MD5()
加密密码,避免“注册加密、登录不加密”的逻辑错误
- 返回值统一:用 1/0/None 等明确的返回值,让调用方(如个人中心)能清晰判断操作结果,便于处理后续逻辑
2. 验证码组件(Code.py)
验证码用于防止恶意注册,项目中自定义 Code
类(继承 QLabel
),实现 随机字符生成、干扰线绘制、点击刷新 功能。
核心代码解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| from PyQt5.QtWidgets import QLabel from PyQt5.QtGui import QPainter, QColor, QFont from PyQt5.QtCore import Qt import random
class Code(QLabel): def __init__(self, parent=None): super().__init__(parent) self.setFixedSize(120, 40) self.__text = self.generate_code() self.setCursor(Qt.PointingHandCursor)
def generate_code(self): chars = 'abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ123456789' return ''.join([random.choice(chars) for _ in range(4)])
def paintEvent(self, event): super().paintEvent(event) painter = QPainter(self) painter.setPen(Qt.NoPen) bg_color = QColor(random.randint(240, 255), random.randint(240, 255), random.randint(240, 255)) painter.setBrush(bg_color) painter.drawRect(self.rect()) font = QFont('Arial', 16, QFont.Bold) painter.setFont(font) for i, char in enumerate(self.__text): char_color = QColor(random.randint(0, 120), random.randint(0, 120), random.randint(0, 120)) painter.setPen(char_color) painter.save() painter.translate(30 + i*20, 20) painter.rotate(random.randint(-30, 30)) painter.drawText(-10, 10, char) painter.restore() for _ in range(3): line_color = QColor(random.randint(0, 180), random.randint(0, 180), random.randint(0, 180)) painter.setPen(line_color) x1 = random.randint(0, self.width()) y1 = random.randint(0, self.height()) x2 = random.randint(0, self.width()) y2 = random.randint(0, self.height()) painter.drawLine(x1, y1, x2, y2)
def mousePressEvent(self, event): if event.button() == Qt.LeftButton: self.__text = self.generate_code() self.update()
def check_code(self, input_code): return self.__text.lower() == input_code.lower()
|
关键技巧:
paintEvent
:自定义绘制方法,负责背景、字符、干扰线的绘制,是验证码功能的核心
translate
+ rotate
:实现字符随机旋转,避免机器自动识别(如爬虫)
mousePressEvent
:监听鼠标点击事件,支持点击刷新,提升用户体验
check_code
:提供校验接口,对接注册逻辑,返回布尔值判断输入是否正确
六、通用交互设计模式与优化建议
1. 通用交互设计模式
项目中多个模块复用了相同的设计模式,掌握这些模式可快速理解和扩展功能:
(1)“状态标记+切换”模式
应用场景:摄像头开启/关闭、视频播放/暂停、密码显示/隐藏
核心逻辑:
- 定义布尔型状态标记(如
is_playing
、old_pwd_visible
)
- 切换时先取反状态标记,再根据新状态执行对应操作(如启动/停止定时器、切换输入框模式)
- 同步更新界面反馈(如按钮文字从“播放”改为“暂停”)
示例(密码显示/隐藏):
1 2 3 4 5 6 7 8 9
| def toggle_old_password(self): self.old_pwd_visible = not self.old_pwd_visible self.old_pwd_edit.setEchoMode( QLineEdit.Normal if self.old_pwd_visible else QLineEdit.Password ) self.update_password_icon(self.sender(), self.old_pwd_visible)
|
(2)“信号槽+数据传递”模式
应用场景:列表页→播放器页传递视频路径、登录成功→主窗口传递用户名
核心逻辑:
- 定义自定义信号(指定参数类型,如
pyqtSignal(str)
)
- 发送方在事件触发时发射信号,传递数据
- 接收方绑定信号到槽函数,处理数据
优势:解耦发送方和接收方,便于模块独立维护和扩展。
(3)“前端校验+后端校验”模式
应用场景:密码修改、用户注册
核心逻辑:
- 前端先做轻量级校验(非空、格式、长度),快速反馈用户,减少无效数据库请求
- 后端再做核心校验(如原密码正确性、用户名唯一性),确保数据安全
优势:平衡“用户体验”和“数据安全”,避免因前端校验缺失导致的频繁数据库交互,或因后端校验缺失导致的安全风险。
(4)“资源初始化→使用→释放”模式
应用场景:摄像头操作、数据库连接、视频文件读取
核心逻辑:
- 初始化阶段:创建资源实例,检查可用性(如
cap.isOpened()
)
- 使用阶段:调用资源方法执行操作(如读取帧、执行SQL)
- 释放阶段:通过
finally
块或关闭函数释放资源,避免泄漏
示例(数据库连接):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| def db_operation(self): conn = None cursor = None try: conn = pymysql.connect(...) cursor = conn.cursor() cursor.execute(sql) except Exception as e: finally: if cursor: cursor.close() if conn: conn.close()
|
2. 项目优化与扩展建议
(1)现有问题与优化方向
问题点 |
优化方案 |
优化价值 |
SQL 注入风险 |
用参数化查询替代字符串格式化(cursor.execute(sql, (param1, param2)) ) |
避免恶意SQL注入,提升数据安全 |
摄像头占用内存高 |
用多线程处理视频帧读取,避免阻塞 UI 线程 |
防止界面卡顿,提升用户体验 |
缩略图提取效率低 |
缓存已提取的缩略图(如用字典存储路径→缩略图映射) |
避免重复读取视频文件,提升列表加载速度 |
窗口缩放时组件变形 |
给关键组件设置 QSizePolicy (如 Expanding /Fixed ) |
确保界面在不同尺寸下显示正常,提升适配性 |
错误提示不友好 |
统一用 QMessageBox 提供详细错误信息,替换 print |
帮助用户快速定位问题,便于调试 |
(2)功能扩展建议
- 多摄像头支持:在
btn1_HP.py
中增加摄像头选择下拉框,通过 cv2.VideoCapture(index)
切换(0=默认,1=外接)
- 视频剪辑功能:集成
moviepy
库,在播放器页面增加“裁剪”按钮,支持选择起始/结束时间,生成新视频
- 音乐歌词显示:解析 LRC 歌词文件,同步歌词与音乐进度,显示到
MusicPage
界面
- 用户权限管理:在
user
表增加 role
字段(0=普通用户,1=管理员),管理员可查看所有用户的视频/照片
- 数据备份:增加“备份”按钮,定期将
video/
、avatars/
目录和数据库导出为压缩包,支持手动恢复
七、总结
本项目是 PyQt5 界面开发 与 OpenCV 多媒体处理 结合的典型案例,核心价值在于:
- 模块化设计:界面、逻辑、数据分层清晰,每个模块职责单一,便于维护和扩展(如视频列表逻辑可复用为照片列表)
- 交互体验优化:通过信号槽、状态同步、分层校验等设计,确保用户操作流畅,反馈及时
- 技术整合实用:涵盖 GUI 开发、多媒体处理、数据库操作、正则匹配等核心技能,贴近实际项目需求
通过学习本项目,不仅能掌握 PyQt5 和 OpenCV 的基础用法,更能理解“多组件协作”“跨页面通信”“数据一致性保障”等工程化思维,为后续开发更复杂的桌面应用(如视频编辑工具、监控系统)奠定基础。