You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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_())

关键修改说明

  1. 线程内路径管理:VideoThread用实例属性full_save_vpath代替全局变量,通过update_save_path信号接收主线程的路径更新,动态处理VideoWriter的创建和释放。
  2. 信号连接:MyWindow的send_save_path信号与线程的update_save_path信号连接,确保路径更新能安全跨线程传递。
  3. 路径更新时机:无论是启动前选择路径还是运行中修改路径,都能立即同步到线程,无需重启线程。
  4. 资源释放优化:线程停止时确保VideoWriter和摄像头资源都被正确释放,避免内存泄漏。

内容的提问来源于stack exchange,提问作者D121

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 07:39:56