PyQt5中如何从QMainWindow向QThread发送信号传递数据?
解决QMainWindow向QThread传递保存路径的问题
核心问题分析
原代码的主要问题在于:
- 保存路径使用全局变量,主线程修改后无法同步到已启动的线程
VideoThread没有提供接收路径更新的接口,主线程无法主动传递新路径- 线程内的
VideoWriter初始化逻辑依赖全局变量,无法动态更新
解决方案步骤
1. 重构VideoThread类
- 将保存路径改为线程的实例属性,避免依赖全局变量
- 添加
update_save_path信号和对应的处理方法,支持主线程传递新路径 - 动态处理
VideoWriter的创建与释放,确保路径更新后立即生效
2. 修改MyWindow类
- 添加
send_save_path信号,用于向线程发送新路径 - 在
change_folder方法中获取用户选择的路径后,通过信号传递给线程 - 修复线程初始化逻辑,确保启动时能接收初始路径
修改后的完整代码
from PyQt5 import QtGui, QtWidgets, QtCore from PyQt5.QtWidgets import (QWidget, QApplication, QLabel, QGridLayout, QAction, QMainWindow, QStatusBar, QDesktopWidget, QFileDialog, QToolBar, QTextEdit) from PyQt5.QtGui import QPixmap, QIcon, QFont, QTextCursor from PyQt5.QtCore import pyqtSignal, Qt, QThread import sys import os import cv2 import time import numpy as np from datetime import datetime VERSION = "v1.00" IMG_SIZE = 640, 480 # 640,480 or 1280,720 or 1920,1080 CAP_API = cv2.CAP_DSHOW EXPOSURE = 0 TEXT_FONT = QFont("Courier", 10) camera_num = 1 CurrUser = os.environ.get('USERNAME') class VideoThread(QThread): change_pixmap_signal = pyqtSignal(np.ndarray) # 添加接收路径更新的信号 update_save_path = pyqtSignal(str) def __init__(self, *args, **kwargs): super().__init__() self._run_flag = True self.ThreadActive = False # 将保存路径设为实例属性 self.full_save_vpath = "" self.out = None # 连接信号到处理方法 self.update_save_path.connect(self._handle_save_path_update) def _handle_save_path_update(self, new_path): """处理保存路径更新,动态创建/释放VideoWriter""" # 如果之前有打开的VideoWriter,先释放 if self.out is not None: self.out.release() self.out = None # 更新路径 self.full_save_vpath = new_path # 如果线程正在运行且路径有效,创建新的VideoWriter if self.ThreadActive and self.full_save_vpath: width = IMG_SIZE[0] height = IMG_SIZE[1] fourcc = cv2.VideoWriter_fourcc(*'XVID') fps = 30 self.out = cv2.VideoWriter(self.full_save_vpath, fourcc, fps, (width, height)) def run(self): total_frames = 0 self.ThreadActive = True self.cap = cv2.VideoCapture(camera_num-1 + CAP_API) self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, IMG_SIZE[0]) self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, IMG_SIZE[1]) if EXPOSURE: self.cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0) self.cap.set(cv2.CAP_PROP_EXPOSURE, EXPOSURE) else: self.cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 1) prev_frame_time = 0 new_frame_time = 0 # 初始化VideoWriter(如果路径已设置) if self.full_save_vpath: width = IMG_SIZE[0] height = IMG_SIZE[1] fourcc = cv2.VideoWriter_fourcc(*'XVID') fps = 30 self.out = cv2.VideoWriter(self.full_save_vpath, fourcc, fps, (width, height)) while self.ThreadActive: ret, frame = self.cap.read() if not ret: break if ret: new_frame_time = time.time() fps_count = 1 / (new_frame_time - prev_frame_time) prev_frame_time = new_frame_time self.change_pixmap_signal.emit(frame) # 写入帧(如果VideoWriter已初始化) if self.out is not None: self.out.write(frame) total_frames += 1 # 释放资源 self.cap.release() if self.out is not None: self.out.release() def stop(self): self.ThreadActive = False self.wait() class MyWindow(QMainWindow): text_update = pyqtSignal(str) # 添加发送路径的信号 send_save_path = pyqtSignal(str) def __init__(self): super(MyWindow, self).__init__() self.thread = None self.save_seq = 0 self.timestr = time.strftime("%d-%b-%Y-%H_%M_%S") self.setWindowTitle(VERSION) self.setWindowIcon(QIcon('Icon/count.png')) cent = QDesktopWidget().availableGeometry().center() self.frameGeometry().moveCenter(cent) self.widget = QWidget() self.layout = QGridLayout(self.widget) self.display_width = 1280 self.display_height = 720 self.image_label = QLabel(self) self.image_label.resize(self.display_width, self.display_height) self.image_label.setAlignment(Qt.AlignCenter) self.layout.addWidget(self.image_label) self.setCentralWidget(self.widget) # 状态栏 self.status = QStatusBar() self.status.setStyleSheet("background : lightgray;") self.setStatusBar(self.status) # 菜单栏 menu_Bar = self.menuBar() file_menu = menu_Bar.addMenu('&File') play_menu = menu_Bar.addMenu('&Play') # 退出动作 exitAction = QAction(QIcon('Icon/exit.png'), '&Exit', self) exitAction.setShortcut('Ctrl+Q') exitAction.setStatusTip('Exit application') exitAction.triggered.connect(self.close) file_menu.addAction(exitAction) # 保存路径选择动作 saveAction = QAction('Save as', self) saveAction.setStatusTip("Change folder where video will be saved.") saveAction.triggered.connect(self.change_folder) file_menu.addAction(saveAction) # 启动动作 self.startAction = QAction(QIcon('Icon/rec_start.png'), 'Start Feed', self) self.startAction.setStatusTip("Start camera feed.") self.startAction.triggered.connect(self.start_feed) self.startAction.triggered.connect(self.start_inactive) self.startAction.setEnabled(True) play_menu.addAction(self.startAction) # 停止动作 self.stopAction = QAction(QIcon('Icon/rec_stop.png'), 'Stop Feed', self) self.stopAction.setStatusTip("Stop camera feed.") self.stopAction.triggered.connect(self.stop_feed) self.stopAction.triggered.connect(self.hide_frame) self.stopAction.triggered.connect(self.start_active) self.stopAction.setEnabled(False) play_menu.addAction(self.stopAction) # 工具栏 self.toolbar = QToolBar('Toolbar') self.addToolBar(self.toolbar) self.toolbar.setMovable(False) self.toolbar.addAction(exitAction) self.toolbar.addAction(self.startAction) self.toolbar.addAction(self.stopAction) def update_image(self, img_label): qt_img = self.convert_cv_qt(img_label) self.image_label.setPixmap(qt_img) def convert_cv_qt(self, frame): rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = rgb_image.shape bytes_per_line = ch * w convert_to_Qt_format = QtGui.QImage(rgb_image.data, w, h, bytes_per_line, QtGui.QImage.Format_RGB888) return QPixmap.fromImage(convert_to_Qt_format) def write(self, text): self.text_update.emit(str(text)) def flush(self): pass def append_text(self, text): cur = self.textbox.textCursor() cur.movePosition(QTextCursor.End) s = str(text) while s: head,sep,s = s.partition("\n") cur.insertText(head) if sep: cur.insertBlock() self.textbox.setTextCursor(cur) def start_feed(self): self.stopAction.setEnabled(True) # 创建线程实例 self.thread = VideoThread() # 连接主线程的路径信号到线程 self.send_save_path.connect(self.thread.update_save_path) self.thread.change_pixmap_signal.connect(self.update_image) self.thread.start() self.textbox = QTextEdit(self.widget) self.textbox.setFont(TEXT_FONT) self.textbox.setMaximumSize(640, 100) self.text_update.connect(self.append_text) sys.stdout = self print(datetime.now()) self.layout.addWidget(self.textbox, 1, 0) # 如果已经选择过路径,发送给线程 if hasattr(self, 'chosen_path'): self.send_save_path.emit(self.chosen_path) def start_inactive(self): self.startAction.setEnabled(False) def start_active(self): self.startAction.setEnabled(True) def stop_feed(self): if self.thread and self.thread.ThreadActive: self.thread.stop() self.stopAction.setEnabled(False) def hide_frame(self): self.widget = QWidget() self.layout = QGridLayout(self.widget) self.display_width = 1280 self.display_height = 720 self.image_label = QLabel(self) self.image_label.resize(self.display_width, self.display_height) self.image_label.move(0, 0) self.layout.addWidget(self.image_label) self.setCentralWidget(self.widget) def change_folder(self): path = QFileDialog.getExistingDirectory(self, "video location", "") if path: # 生成完整保存路径 self.chosen_path = os.path.join(path, "%04d-%s.mkv" % (self.save_seq, self.timestr)).replace(os.sep, '/') print(self.chosen_path) # 如果线程已启动,发送新路径 if self.thread and self.thread.isRunning(): self.send_save_path.emit(self.chosen_path) self.save_seq += 1 if __name__ == "__main__": app = QApplication(sys.argv) win = MyWindow() win.show() sys.exit(app.exec_())
关键修改说明
- 线程内路径管理:
VideoThread用实例属性full_save_vpath代替全局变量,通过update_save_path信号接收主线程的路径更新,动态处理VideoWriter的创建和释放。 - 信号连接:
MyWindow的send_save_path信号与线程的update_save_path信号连接,确保路径更新能安全跨线程传递。 - 路径更新时机:无论是启动前选择路径还是运行中修改路径,都能立即同步到线程,无需重启线程。
- 资源释放优化:线程停止时确保
VideoWriter和摄像头资源都被正确释放,避免内存泄漏。
内容的提问来源于stack exchange,提问作者D121
相关产品推荐
相关产品推荐

