自动驾驶


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.pylist_page.py
MySQL(pymysql) 用户数据存储(注册、登录、头像路径) DB_util.pyRes_Login_Widget.py
正则表达式(re) 文件名匹配(视频/照片按时间格式筛选) list_page.pybtn6_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) # 左右区域留1px分隔线

# 左侧按钮区(垂直布局,权重1)
button_layout = QVBoxLayout(self.button_container)
button_layout.setContentsMargins(15, 50, 15, 50) # 上下留50px边距
button_layout.setSpacing(30) # 按钮间距30px
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) # 权重1(占10%宽度)

# 右侧内容区(堆叠布局,权重9)
self.stacked_widget = QStackedWidget()
self.main_layout.addWidget(self.stacked_widget, 9) # 权重9(占90%宽度)

效果:左侧导航与右侧内容按 1:9 比例分配宽度,窗口缩放时比例不变,按钮始终垂直居中且不贴边。

(2)堆叠布局:实现页面切换(QStackedWidget)

核心作用:多个页面堆叠存储,同一时间仅显示一个页面,用于“列表页→详情页”“登录页→主页”等场景。
关键方法

  • 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()
# 添加两个页面(索引0:列表页,索引1:查看页)
self.photo_list_page = PhotoListPage(self)
self.photo_page = Photo(self)
self.stacked_widget.addWidget(self.photo_list_page) # 索引0
self.stacked_widget.addWidget(self.photo_page) # 索引1

# 绑定页面切换信号(列表页→查看页)
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) # 切换到索引1的查看页
self.photo_page.set_photo(photo_path) # 传递照片路径给查看页

# 返回列表页
def switch_to_list_page(self):
self.stacked_widget.setCurrentIndex(0) # 切换到索引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()
# 垂直渐变:从深灰黑(#16181E)到深灰蓝(#1E232D)
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; /* 8px圆角 */
padding: 8px 16px; /* 上下8px,左右16px内边距 */
font-size: 14px; /* 文字大小 */
border: 1px solid rgba(66, 99, 235, 0.5); /* 半透明边框 */
}
QPushButton:hover {
background-color: #3655d9; /* hover时加深颜色 */
box-shadow: 0 2px 8px rgba(66, 99, 235, 0.3); /* 添加阴影 */
}
QPushButton:pressed {
background-color: #2d4bc7; /* 点击时再加深 */
}

(3)列表与图标:QListWidget 图标模式(视频/照片列表)

核心需求:实现视频/照片的缩略图列表,需固定组件尺寸避免排列混乱。
项目实战示例(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)) # 缩略图尺寸(宽220,高140)
self.view.setResizeMode(QListWidget.Adjust) # 图标自适应宽度排列
self.view.setSpacing(20) # 图标之间间距20px
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
# 按钮点击触发 on_camera 函数
self.camera_btn = QPushButton("开启摄像头")
self.camera_btn.clicked.connect(self.on_camera)

# 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)自定义信号:跨页面传递自定义数据

当内置信号无法满足需求(如传递视频路径、照片路径)时,需自定义信号,步骤如下:

  1. 导入 pyqtSignalQObject
  2. 创建信号类(继承 QObject),定义信号(指定参数类型)
  3. 发送方在事件触发时发射信号(signal.emit(参数)
  4. 接收方绑定信号到槽函数,处理数据

项目实战示例(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
# 1. 导入必要模块
from PyQt5.QtCore import pyqtSignal, QObject

# 2. 定义全局信号管理器(用于跨页面通信)
class GlobalSignals(QObject):
video_path_signal = pyqtSignal(str) # 定义信号,参数为视频路径(字符串)

global_signals = GlobalSignals() # 创建信号实例

# 3. 发送方(list_page.py 视频列表项点击时发射信号)
class ListPage(QWidget):
def on_item_click(self, item):
video_path = item.data(Qt.UserRole) # 获取存储的视频路径
global_signals.video_path_signal.emit(video_path) # 发射信号

# 4. 接收方(player_page.py 绑定信号,接收路径并播放)
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)核心流程

  1. 打开摄像头:cv2.VideoCapture(0)(0 表示默认摄像头)
  2. 读取帧:ret, frame = cap.read()ret 为是否成功,frame 为帧数据)
  3. 帧格式转换:OpenCV 帧为 BGR 格式,PyQt 显示需 RGB 格式,需用 cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) 转换
  4. 帧显示:将转换后的帧转为 QImageQPixmap,设置到 QLabel
  5. 视频录制:cv2.VideoWriter 写入帧数据,设置编码格式、帧率、分辨率
  6. 资源释放:关闭摄像头(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):
# 1. 打开摄像头
self.video_capture = cv2.VideoCapture(0)
if not self.video_capture.isOpened():
self.video_widget.setText("无法打开摄像头")
return

# 2. 初始化视频录制器(按时间命名,避免重复)
current_datetime = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
video_path = f'video/{current_datetime}.mp4'
fourcc = cv2.VideoWriter_fourcc(*'mp4v') # MP4 编码
self.video_writer = cv2.VideoWriter(
video_path, # 输出路径
fourcc, # 编码格式
30.0, # 帧率(30帧/秒)
(800, 600) # 分辨率(需与显示帧尺寸一致)
)

# 3. 定时更新帧(30ms 一次,约 33 帧/秒,接近帧率)
self.timer = QTimer()
self.timer.timeout.connect(self.update_video)
self.timer.start(30)

def update_video(self):
# 4. 读取摄像头帧
ret, frame = self.video_capture.read()
if not ret:
self.video_widget.setText("无法获取视频帧")
return

# 5. 帧处理:调整尺寸+格式转换
frame_resized = cv2.resize(frame, (800, 600)) # 调整为显示尺寸
frame_rgb = cv2.cvtColor(frame_resized, cv2.COLOR_BGR2RGB) # BGR→RGB

# 6. 帧显示:QImage → QPixmap
h, w, ch = frame_rgb.shape # 高度、宽度、通道数
# QImage 参数:数据、宽度、高度、每行字节数(宽度×通道数)、格式
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) # 图片自适应标签尺寸

# 7. 录制视频:写入帧
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:
# 开启时记录开始时间并启动定时器(1秒检查一次)
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:
# 达到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:
# 1. 打开视频文件
cap = cv2.VideoCapture(video_path)
# 2. 读取第一帧(只读取一次,提升效率)
ret, frame = cap.read()
# 3. 释放资源(重要,避免视频文件被占用)
cap.release()

if ret:
# 4. 格式转换:BGR→RGB(适配PyQt显示)
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# 5. 转为QImage
h, w, c = frame_rgb.shape
qimg = QImage(frame_rgb.data, w, h, w * c, QImage.Format_RGB888)
# 6. 缩放为缩略图尺寸(保持比例,平滑缩放避免锯齿)
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 # 最大缩放3倍(避免过度模糊)
self.min_scale = 0.5 # 最小缩放0.5倍(避免过小)

def zoom_in(self):
# 放大:因子+0.1,不超过最大值
if self.scale_factor < self.max_scale:
self.scale_factor += 0.1
self.update_photo()

def zoom_out(self):
# 缩小:因子-0.1,不低于最小值
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 # 初始旋转角度(0°、90°、180°、270°循环)

def rotate_photo(self):
# 每次旋转90°,取模4确保循环
self.rotation = (self.rotation + 90) % 360
self.update_photo()

def update_photo(self):
# 1. 先旋转图片
transform = QTransform().rotate(self.rotation) # 创建旋转变换
rotated_pixmap = self.original_pixmap.transformed(transform, Qt.SmoothTransformation)
# 2. 再应用缩放
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)

场景:用户点击左侧“首页/音乐/视频”按钮,切换右侧内容页

完整交互流程:

  1. 初始化阶段

    • 主窗口加载时,初始化左侧按钮和右侧堆叠布局的页面
    • 建立“按钮→页面索引”的映射(如“首页”对应索引 0,“音乐”对应索引 1)
    • 默认选中“首页”按钮,右侧显示首页内容
  2. 用户操作阶段

    • 用户点击左侧按钮→触发 chang_page 函数,并传递目标页面索引
    • 切换堆叠布局的当前页面(setCurrentIndex(index)
    • 同步更新所有按钮的选中状态(如索引 1 对应“音乐”按钮,设置为 Checked=True
  3. 状态同步阶段

    • 确保“按钮选中状态”与“当前显示页面”一致,避免用户困惑(如页面在“音乐”,按钮却选中“视频”)

关键代码与逻辑解析:

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__()
# 1. 初始化左侧按钮
self.home_btn = QPushButton("首页")
self.music_btn = QPushButton("音乐")
self.video_btn = QPushButton("视频")
self.profile_btn = QPushButton("个人中心")

# 2. 初始化右侧堆叠布局(页面索引:0=首页,1=音乐,2=视频,3=个人中心)
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) # 0
self.stacked_widget.addWidget(self.music_page) # 1
self.stacked_widget.addWidget(self.video_page) # 2
self.stacked_widget.addWidget(self.profile_page) # 3

# 3. 按钮绑定点击事件(传递目标索引)
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))

# 4. 默认选中首页按钮
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 = valueself.cap.set(cv2.CAP_PROP_POS_FRAMES, self.current_frame)
4 选择倍速(如2x) 调整定时器间隔,实现倍速播放 self.speed = 2.0self.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)

场景:用户登录后进入个人中心,可上传头像、修改密码,系统同步更新数据库

完整交互流程:

  1. 登录状态同步

    • 登录成功→全局信号(username_signal)发射用户名→个人中心接收并更新“当前用户”标签
    • 调用 load_user_info() 从数据库查询用户头像路径→若存在则显示头像,否则显示默认边框
  2. 头像上传流程

    步骤 用户操作 系统响应 关键代码逻辑
    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(),失败则删除本地文件并提示
  3. 密码修改流程

    步骤 用户操作 系统响应 关键代码逻辑
    1 输入原密码、新密码、确认密码,点击“保存修改” 前端校验(非空、一致、长度≥6位) 校验 all([old_pwd, new_pwd, confirm_pwd])new_pwd == confirm_pwdlen(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:
# 1. 建立连接
conn = pymysql.connect(
host=self.host, port=self.port, user=self.user,
password=self.password, database=self.database, charset='utf8'
)
cursor = conn.cursor()

# 2. 执行SQL(密码用MD5加密)
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 # 成功:返回1
except Exception as e:
if conn:
conn.rollback() # 失败:回滚事务
print(f"注册失败:{e}")
return 0 # 失败:返回0
finally:
# 3. 释放资源(无论成功失败都执行)
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:密码加密后比对(与注册逻辑一致)
sql = f"""
SELECT uname, imgpath FROM user
WHERE uname = '{username}' AND upwd = MD5('{password}')
"""
cursor.execute(sql)
result = cursor.fetchone() # 获取一条结果(用户名唯一)
return result # 成功:返回(用户名, 头像路径);失败:返回None
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()

# 1. 先校验原密码是否正确
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 # 原密码错误:返回0

# 2. 原密码正确,更新新密码(MD5加密)
update_sql = f"""
UPDATE user
SET upwd = MD5('{new_pwd}')
WHERE uname = '{username}'
"""
cursor.execute(update_sql)
conn.commit()
return 1 # 成功:返回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):
# 1. 定义验证码字符集(排除易混淆字符:o、O、0、l、I)
chars = 'abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ123456789'
# 2. 生成4位随机字符
return ''.join([random.choice(chars) for _ in range(4)])

def paintEvent(self, event):
super().paintEvent(event)
painter = QPainter(self)
painter.setPen(Qt.NoPen) # 不画边框

# 1. 绘制背景(随机浅色调,避免单一)
bg_color = QColor(random.randint(240, 255), random.randint(240, 255), random.randint(240, 255))
painter.setBrush(bg_color)
painter.drawRect(self.rect())

# 2. 绘制验证码字符(随机颜色、旋转角度,增加识别难度)
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)
# 每个字符随机旋转(-30°~30°,避免整齐排列)
painter.save() # 保存当前绘制状态
painter.translate(30 + i*20, 20) # 字符位置(水平居中)
painter.rotate(random.randint(-30, 30)) # 随机旋转
painter.drawText(-10, 10, char) # 绘制字符
painter.restore() # 恢复绘制状态

# 3. 绘制干扰线(3条随机直线,干扰机器识别)
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() # 触发 paintEvent 重绘

def check_code(self, input_code):
# 验证码校验(不区分大小写,提升用户体验)
return self.__text.lower() == input_code.lower()

关键技巧

  • paintEvent:自定义绘制方法,负责背景、字符、干扰线的绘制,是验证码功能的核心
  • translate + rotate:实现字符随机旋转,避免机器自动识别(如爬虫)
  • mousePressEvent:监听鼠标点击事件,支持点击刷新,提升用户体验
  • check_code:提供校验接口,对接注册逻辑,返回布尔值判断输入是否正确

六、通用交互设计模式与优化建议

1. 通用交互设计模式

项目中多个模块复用了相同的设计模式,掌握这些模式可快速理解和扩展功能:

(1)“状态标记+切换”模式

应用场景:摄像头开启/关闭、视频播放/暂停、密码显示/隐藏
核心逻辑

  • 定义布尔型状态标记(如 is_playingold_pwd_visible
  • 切换时先取反状态标记,再根据新状态执行对应操作(如启动/停止定时器、切换输入框模式)
  • 同步更新界面反馈(如按钮文字从“播放”改为“暂停”)

示例(密码显示/隐藏)

1
2
3
4
5
6
7
8
9
def toggle_old_password(self):
# 1. 取反状态标记
self.old_pwd_visible = not self.old_pwd_visible
# 2. 根据新状态执行操作
self.old_pwd_edit.setEchoMode(
QLineEdit.Normal if self.old_pwd_visible else QLineEdit.Password
)
# 3. 同步界面反馈(更新图标)
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:
# 1. 初始化资源
conn = pymysql.connect(...)
cursor = conn.cursor()
# 2. 使用资源
cursor.execute(sql)
except Exception as e:
# 异常处理
finally:
# 3. 释放资源(无论成功失败)
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 多媒体处理 结合的典型案例,核心价值在于:

  1. 模块化设计:界面、逻辑、数据分层清晰,每个模块职责单一,便于维护和扩展(如视频列表逻辑可复用为照片列表)
  2. 交互体验优化:通过信号槽、状态同步、分层校验等设计,确保用户操作流畅,反馈及时
  3. 技术整合实用:涵盖 GUI 开发、多媒体处理、数据库操作、正则匹配等核心技能,贴近实际项目需求

通过学习本项目,不仅能掌握 PyQt5 和 OpenCV 的基础用法,更能理解“多组件协作”“跨页面通信”“数据一致性保障”等工程化思维,为后续开发更复杂的桌面应用(如视频编辑工具、监控系统)奠定基础。


文章作者: Xxx
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Xxx !
  目录