rsj0315 发表于 2021-2-22 13:02:39

pyqt5 多线程问题求助

两个py文件
一个ui的ui_Widget.py
# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'Widget.ui'
#
# Created by: PyQt5 UI code generator 5.15.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again.Do not edit this file unless you know what you are doing.


from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_Widget(object):
    def setupUi(self, Widget):
      Widget.setObjectName("Widget")
      Widget.resize(710, 593)
      sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
      sizePolicy.setHorizontalStretch(0)
      sizePolicy.setVerticalStretch(0)
      sizePolicy.setHeightForWidth(Widget.sizePolicy().hasHeightForWidth())
      Widget.setSizePolicy(sizePolicy)
      font = QtGui.QFont()
      font.setPointSize(16)
      Widget.setFont(font)
      self._2 = QtWidgets.QVBoxLayout(Widget)
      self._2.setContentsMargins(5, 5, 5, 5)
      self._2.setSpacing(6)
      self._2.setObjectName("_2")
      self.label = QtWidgets.QLabel(Widget)
      self.label.setEnabled(True)
      self.label.setText("")
      self.label.setPixmap(QtGui.QPixmap("22.jpg"))
      self.label.setObjectName("label")
      self._2.addWidget(self.label)
      self.groupBox_2 = QtWidgets.QGroupBox(Widget)
      self.groupBox_2.setObjectName("groupBox_2")
      self.verticalLayout = QtWidgets.QVBoxLayout(self.groupBox_2)
      self.verticalLayout.setContentsMargins(9, 9, 9, 9)
      self.verticalLayout.setSpacing(6)
      self.verticalLayout.setObjectName("verticalLayout")
      self.lineEdit = QtWidgets.QLineEdit(self.groupBox_2)
      self.lineEdit.setClearButtonEnabled(True)
      self.lineEdit.setObjectName("lineEdit")
      self.verticalLayout.addWidget(self.lineEdit)
      self._2.addWidget(self.groupBox_2)
      self.horizontalLayout = QtWidgets.QHBoxLayout()
      self.horizontalLayout.setSpacing(4)
      self.horizontalLayout.setObjectName("horizontalLayout")
      self.groupBox = QtWidgets.QGroupBox(Widget)
      self.groupBox.setObjectName("groupBox")
      self.gridLayout = QtWidgets.QGridLayout(self.groupBox)
      self.gridLayout.setContentsMargins(11, 11, 11, 11)
      self.gridLayout.setSpacing(6)
      self.gridLayout.setObjectName("gridLayout")
      self.comboBox = QtWidgets.QComboBox(self.groupBox)
      self.comboBox.setEditable(False)
      self.comboBox.setObjectName("comboBox")
      self.gridLayout.addWidget(self.comboBox, 1, 0, 1, 3)
      self.btnIniItems = QtWidgets.QPushButton(self.groupBox)
      self.btnIniItems.setObjectName("btnIniItems")
      self.gridLayout.addWidget(self.btnIniItems, 0, 0, 1, 1)
      self.btnClearItems = QtWidgets.QPushButton(self.groupBox)
      self.btnClearItems.setObjectName("btnClearItems")
      self.gridLayout.addWidget(self.btnClearItems, 0, 1, 1, 1)
      self.btnOkItems = QtWidgets.QPushButton(self.groupBox)
      self.btnOkItems.setStyleSheet("background-color: rgb(255, 0, 0);\n"
"font: 16pt \"MS Shell Dlg 2\";")
      self.btnOkItems.setObjectName("btnOkItems")
      self.gridLayout.addWidget(self.btnOkItems, 0, 2, 1, 1)
      self.horizontalLayout.addWidget(self.groupBox)
      self._2.addLayout(self.horizontalLayout)
      self.plainTextEdit = QtWidgets.QPlainTextEdit(Widget)
      self.plainTextEdit.setObjectName("plainTextEdit")
      self._2.addWidget(self.plainTextEdit)

      self.retranslateUi(Widget)
      self.comboBox.setCurrentIndex(-1)
      QtCore.QMetaObject.connectSlotsByName(Widget)

    def retranslateUi(self, Widget):
      _translate = QtCore.QCoreApplication.translate
      Widget.setWindowTitle(_translate("Widget", "Demo3_6 ComboBox"))
      self.groupBox_2.setTitle(_translate("Widget", "选择的工序如下"))
      self.groupBox.setTitle(_translate("Widget", "选择导出数据的工序"))
      self.btnIniItems.setText(_translate("Widget", "初始化列表"))
      self.btnClearItems.setText(_translate("Widget", "清除列表"))
      self.btnOkItems.setText(_translate("Widget", "确定"))


第二个py,文件myWidget.py
import sys
from PyQt5.QtWidgets importQApplication, QWidget
from PyQt5.QtCore importpyqtSlot
from ui_Widget import Ui_Widget #ui界面
import itp_drawing as drawing #函数1
import itp_bunching as bunching #函数2
import itp_conductor as conductor #函数3
import itp_extruder as extruder #函数4
import itp_rubber as rubber #函数5
import itp_ccv as ccv #函数6

class QmyWidget(QWidget):
   def __init__(self, parent=None):
      super().__init__(parent)   #调用父类构造函数,创建窗体
      self.ui=Ui_Widget()      #创建UI对象
      self.ui.setupUi(self)      #构造UI界面

##==========由connectSlotsByName() 自动连接的槽函数====================      
   def on_btnIniItems_clicked(self):   ##“初始化列表”按钮
      self.ui.comboBox.clear()    #清除列表
      provinces=["拉丝","束丝","导体","挤出","橡胶","ccv"]    #列表数据
      for i in range(len(provinces)):
         self.ui.comboBox.addItem(provinces)

   def on_btnClearItems_clicked(self):    ##“清除列表”按钮
      self.ui.comboBox.clear()

   def on_btnOkItems_clicked(self):##“确定”按钮
      if self.ui.lineEdit.text().strip()=='拉丝':
         self.ui.plainTextEdit.clear()
         drawing.concat_file()
         self.ui.plainTextEdit.insertPlainText('拉丝数据导出完毕\n==========\n')
      elif self.ui.lineEdit.text().strip()=='束丝':
         self.ui.plainTextEdit.clear()
         bunching.read_bunching()
         self.ui.plainTextEdit.insertPlainText('束丝数据导出完毕\n==========\n')
      elif self.ui.lineEdit.text().strip() == '导体':
         self.ui.plainTextEdit.clear()
         conductor.concat_file()
         self.ui.plainTextEdit.insertPlainText('导体数据导出完毕\n==========\n')
      elif self.ui.lineEdit.text().strip()=='挤出':
         self.ui.plainTextEdit.clear()
         extruder.concat_file()
         self.ui.plainTextEdit.insertPlainText('挤出数据导出完毕\n==========\n')
      elif self.ui.lineEdit.text().strip()=='橡胶':
         self.ui.plainTextEdit.clear()
         rubber.concat_file()
         self.ui.plainTextEdit.insertPlainText('橡胶数据导出完毕\n==========\n')
      elif self.ui.lineEdit.text().strip()=='ccv':
         self.ui.plainTextEdit.clear()
         ccv.concat_file()
         self.ui.plainTextEdit.insertPlainText('CCV数据导出完毕\n==========\n')
      else:
         self.ui.plainTextEdit.clear()
         self.ui.plainTextEdit.insertPlainText('请选择正确的工序!!!\n==========\n')

   @pyqtSlot(str)    ##“简单的ComboBox”的当前项变化
   def on_comboBox_currentIndexChanged(self,curText):
      self.ui.lineEdit.setText(curText)

##===========窗体测试程序 ================================      
if__name__ == "__main__":         
   app = QApplication(sys.argv)   
   form=QmyWidget()      
   form.show()
   sys.exit(app.exec_())


现在的问题是,6个函数,选中某一个后,由于函数读取数据文本比较耗时,导致界面卡。
如何改成多线程模式?
比如把6个函数改成6个checkbutton,选中几个就新开几个线程,且主界面不死。

hrp 发表于 2021-2-22 14:55:36

用QThread,它的线程实例开始时发射started信号,线程结束后发射finished信号,可以用started和finished信号触发槽函数来更新界面。
为什么不用threading?因为子线程不能直接更新QT界面,只能用QT的信号机制来更新界面。
唯一要注意的只有QThread不能赋值给函数局部变量,应该赋值给实例属性或其他生命周期足够长的变量,否则函数一结束线程也将跟着被结束。

rsj0315 发表于 2021-2-23 09:16:03

hrp 发表于 2021-2-22 14:55
用QThread,它的线程实例开始时发射started信号,线程结束后发射finished信号,可以用started和finished信 ...

你的意思是在确认按钮这里,threading开6个线程,然后判断,选中了哪个?然后start选中的线程。是这样吗?

hrp 发表于 2021-2-23 10:23:43

rsj0315 发表于 2021-2-23 09:16
你的意思是在确认按钮这里,threading开6个线程,然后判断,选中了哪个?然后start选中的线程。是这样吗 ...

不是,是定义一个方法,将6个按钮的点击信号都连接到这个方法(甚至你可以分别定义6个不同的方法,将6个按钮的点击信号分别连接到这6个方法)。
方法的内部,先判断是哪个按钮发射的点击信号(用sender获取信号的发射者),再根据不同按钮创建不同任务的线程,线程中处理数据,把结果赋值给一个实例属性a,线程的started和finished信号分别连接线程开始(如果有需要)和结束要执行的方法f,f方法根据a储存的结果更新界面(比如线程结束信号可以连接在界面显示表格的方法),这就实现了线程结束后自动更新界面。

Cool_Breeze 发表于 2021-2-23 10:36:45

hrp 发表于 2021-2-23 10:23
不是,是定义一个方法,将6个按钮的点击信号都连接到这个方法(甚至你可以分别定义6个不同的方法,将6个 ...

你的思路很清晰啊,很好!

rsj0315 发表于 2021-2-23 10:57:29

hrp 发表于 2021-2-23 10:23
不是,是定义一个方法,将6个按钮的点击信号都连接到这个方法(甚至你可以分别定义6个不同的方法,将6个 ...

感谢,我去研究下这个信号和槽的部分。有好的帖子或者demo欢迎分享

hrp 发表于 2021-2-23 11:48:17

本帖最后由 hrp 于 2021-2-23 11:55 编辑

rsj0315 发表于 2021-2-23 10:57
感谢,我去研究下这个信号和槽的部分。有好的帖子或者demo欢迎分享

我自己写的 AwesomePyKit 也用到多线程,可以看看,代码写的很烂还没时间重构。{:10_245:}
QThread的使用:在RunPyKit.py中搜索NewTask(继承自QThread)就可以了。
NewTask的定义:在library/libm.py里面。

hrp 发表于 2021-2-23 11:54:46

Cool_Breeze 发表于 2021-2-23 10:36
你的思路很清晰啊,很好!

之前写pykit的时候苦于在子线程中无法更新主界面,然后百度说需要通过信号更新主界面,最后摸索出来的办法{:10_250:}

rsj0315 发表于 2021-2-23 16:10:03

看到有人展示一个这样的,不传参数的
from PyQt5.Qt import (QApplication, QWidget, QPushButton,
                      QThread,QMutex,pyqtSignal)
import sys
import time

qmut_1 = QMutex() # 创建线程锁
qmut_2 = QMutex()
# 继承QThread
class Thread_1(QThread):# 线程1
    def __init__(self):
      super().__init__()

    def run(self):
      qmut_1.lock() # 加锁
      values =
      for i in values:
            print(i)
            time.sleep(0.5)# 休眠
      qmut_1.unlock() # 解锁


class Thread_2(QThread):# 线程2
    _signal =pyqtSignal()
    def __init__(self):
      super().__init__()

    def run(self):
      # qmut_2.lock()# 加锁
      values = ["a", "b", "c", "d", "e"]
      for i in values:
            print(i)
            time.sleep(0.5)
      # qmut_2.unlock()# 解锁
      self._signal.emit()


class MyWin(QWidget):
    def __init__(self):
      super().__init__()
      # 按钮初始化
      self.btn_1 = QPushButton('按钮1', self)
      self.btn_1.move(120, 80)
      self.btn_1.clicked.connect(self.click_1)# 绑定槽函数

      self.btn_2 = QPushButton('按钮2', self)
      self.btn_2.move(120, 120)
      self.btn_2.clicked.connect(self.click_2)# 绑定槽函数

    def click_1(self):
      self.thread_1 = Thread_1()# 创建线程
      self.thread_1.start()# 开始线程

    def click_2(self):
      self.btn_2.setEnabled(False)
      self.thread_2 = Thread_2()
      self.thread_2._signal.connect(self.set_btn)
      self.thread_2.start()

    def set_btn(self):
      self.btn_2.setEnabled(True)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    myshow = MyWin()
    myshow.show()
    sys.exit(app.exec_())

rsj0315 发表于 2021-2-23 16:12:16

还有一种这样的import sys
from PyQt5 import QtCore
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *

class Demo(QWidget):
    def __init__(self):
      super().__init__()
      self.setGeometry(100, 50, 500, 400)
      self.setWindowTitle('QThread')

      self.thread = Worker()
      self.list = QListWidget()
      self.btn = QPushButton('开始')
      layout = QGridLayout(self)
      layout.addWidget(self.list,0,0,1,2)
      layout.addWidget(self.btn,1,1)
      self.btn.clicked.connect(self.slotStart)
      self.thread.sinOut.connect(self.slotAdd)

    def slotAdd(self,inf):
      self.list.addItem(inf)
      #实时刷新页面
      QApplication.processEvents()

    def slotStart(self):
      self.btn.setEnabled(False)
      self.thread.start()

class Worker(QThread):
    #自定义信号
    sinOut = pyqtSignal(str)

    def __init__(self):
      super().__init__()
      self.working = True
      self.num = 0

    def __del__(self):
      self.working = False
      self.wait()

    def run(self):
      while self.working == True:
            file_str = 'File index{0}'.format(self.num)
            self.num += 1
            #发射信号
            self.sinOut.emit(file_str)
            #休眠2秒
            self.sleep(2)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    form = Demo()
    form.show()
    sys.exit(app.exec_())

hrp 发表于 2021-2-23 16:43:52

qthread自定义信号更方便

rsj0315 发表于 2021-2-23 17:21:48

hrp 发表于 2021-2-23 16:43
qthread自定义信号更方便

信号我像传递的是用pandas库清洗出来的dataframe,我看了emit的几种形式,好像传输不了这种

hrp 发表于 2021-2-23 17:41:43

rsj0315 发表于 2021-2-23 17:21
信号我像传递的是用pandas库清洗出来的dataframe,我看了emit的几种形式,好像传输不了这种

可以发射list等类型的啊,把数据装进列表再发射不就好了

rsj0315 发表于 2021-2-25 13:25:49

hrp 发表于 2021-2-23 17:41
可以发射list等类型的啊,把数据装进列表再发射不就好了

我先写了一个,是combox,选中后,点击按钮,然后开启线程,进行数据清洗。
你说的把dataframe发射出来,应该怎么写呢?list.append?
我没太懂。

目的:
我是想把这个dataframe用model/view模式在tableview上显示出来。
不知道是不是必须的用信号发射出来才能实现。
还是我直接在线程中就把数据显示出来。

你看看我写的,还没发射dataframe的
import sys
from PyQt5.QtWidgets importQApplication, QWidget
from PyQt5.QtCore importpyqtSlot, QThread,pyqtSignal
from ui_Widget import Ui_Widget
import itp_drawing as drawing

class Thread1(QThread):
   signal1 = pyqtSignal()
   def __init__(self):
      super(Thread1, self).__init__()

   def run(self):
      df = drawing.concat_file()



class QmyWidget(QWidget):
   def __init__(self, parent=None):
      super().__init__(parent)   #调用父类构造函数,创建窗体
      self.ui=Ui_Widget()      #创建UI对象
      self.ui.setupUi(self)      #构造UI界面


##==========由connectSlotsByName() 自动连接的槽函数====================      
   def on_btnIniItems_clicked(self):   ##“初始化列表”按钮
      self.ui.comboBox.clear()    #清除列表
      provinces=["拉丝","束丝","导体","挤出","橡胶","ccv"]    #列表数据
      for i in range(len(provinces)):
         self.ui.comboBox.addItem(provinces)

   def on_btnClearItems_clicked(self):    ##“清除列表”按钮
      self.ui.comboBox.clear()

   @pyqtSlot()
   def on_btnOkItems_clicked(self):##“确定”按钮
      self.thread1 = Thread1()
      self.thread1.start()
      
   @pyqtSlot(str)    ##“简单的ComboBox”的当前项变化
   def on_comboBox_currentIndexChanged(self,curText):
      self.ui.lineEdit.setText(curText)

##===========窗体测试程序 ================================      
if__name__ == "__main__":         
   app = QApplication(sys.argv)
   form=QmyWidget()
   form.show()
   sys.exit(app.exec_())


hrp 发表于 2021-2-25 18:14:57

rsj0315 发表于 2021-2-25 13:25
我先写了一个,是combox,选中后,点击按钮,然后开启线程,进行数据清洗。
你说的把dataframe发射出来 ...

不知道是不是必须的用信号发射出来才能实现。
还是我直接在线程中就把数据显示出来。
用信号发射出来不是必须的,但必须是在主线程中更新界面,也就是说你想在子线程中把数据显示出来是行不通的。
还是前面说过的两个方法:
1,类初始化时设置一个属性比如self.whatever = None。定义例如名为update_tableview的方法,方法内根据self.whatever的值设置tabwiew。定义一个清洗数据的方法getdata,方法内清洗数据并将最终结果赋值给self.whatever。实例化一个线程threadx(getdata),先将其finished信号连接至update_tableview,然后可以threadx.start(),当线程结束后,self.whatever的值已经变为清洗后的数据了,然后threadx的finished信号会触发update_tableview,update_tableview会根据self.whatever的值显示tableview。
2.在你的Thread1类中定义一个list类型的信号signal1=pyqtSignal(list),在处理完结果后,signal1.emit()将dataFrame包在列表内发射出去。在你的myWidget内定义一个接收一个参数a(即将被发射的)的显示tabwiew的方法update_tabview,update_tableview内根据a的值设置tableview。然后将Thread1的实例的signal1信号连接至update_tableview即可。

rsj0315 发表于 2021-2-26 09:18:04

hrp 发表于 2021-2-25 18:14
用信号发射出来不是必须的,但必须是在主线程中更新界面,也就是说你想在子线程中把数据显示出来是行不 ...

xm_zhu 发表于 2021-3-7 17:39:36

关注,还看不懂,等看懂了再来

rsj0315 发表于 2021-3-8 11:42:25

按照这个,子线程的额数据清洗后,存储在pkl,然后主线程显示表格的时候再读取pkl

591821661 发表于 2021-3-31 20:19:19

转一下大佬的代码,有个 @hrp
NewTask定义
from PyQt5.QtCore import QThread

class NewTask(QThread):
    def __init__(self, target, args=tuple()):
      super().__init__()
      self._args = args
      self._target = target

    def run(self):
      self._target(*self._args)

    def __repr__(self):
      return f"{self._target} with args:{self._args}"

    __str__ = __repr__

    def at_start(self, *callable_objs):
      for cab in callable_objs:
            if callable(cab):
                self.started.connect(cab)

    def at_finish(self, *callable_objs):
      for cab in callable_objs:
            if callable(cab):
                self.finished.connect(cab)
应用实例
      def do_install():
            for name, code in loop_install(
                cur_env,
                package_to_be_installed,
                pre=install_pre,
                user=user,
                index_url=index_url,
            ):
                item = self.cur_pkgs_info.setdefault(name, ["", "", ""])
                if not item:
                  item = "- N/A -"
                item = "安装成功" if code else "安装失败"

      thread_install_pkgs = NewTask(do_install)
      thread_install_pkgs.at_start(
            self.lock_widgets,
            lambda: self.show_loading("正在安装,请稍候..."),
      )
      thread_install_pkgs.at_finish(
            self.table_widget_pkgs_info_update,
            self.hide_loading,
            self.release_widgets,
      )
      thread_install_pkgs.start()
      self.thread_repo.put(thread_install_pkgs, 0)
进程池定义

from PyQt5.QtCore import QMutex
class ThreadRepo:
    def __init__(self, interval):
      """interval: 清理已结束线程的时间间隔,单位毫秒。"""
      self._thread_repo = []
      self._timer_clths = QTimer()
      self._mutex = QMutex()
      self._timer_clths.timeout.connect(self.clean)
      self._timer_clths.start(interval)
      self._flag_cleaning = False

    def put(self, threadhandle, level=0):
      """将(线程句柄、重要等级)元组加入线程仓库。"""
      self._mutex.lock()
      self._thread_repo.append((threadhandle, level))
      self._mutex.unlock()

    def clean(self):
      """清除已结束的线程。"""
      if self._flag_cleaning:
            return
      self._mutex.lock()
      index = 0
      self._flag_cleaning = True
      while index < len(self._thread_repo):
            if self._thread_repo.isRunning():
                index += 1
                continue
            del self._thread_repo
      self._flag_cleaning = False
      self._mutex.unlock()

    def stop_all(self):
      """
      按线程重要等级退出线程。
      0级:重要,安全退出;
      1级:不重要,立即退出;
      其他:未知等级,安全退出。
      """
      for thread, level in self._thread_repo:
            if level == 0:
                thread.quit()
            elif level == 1:
                thread.terminate()
            else:
                thread.quit()

    def kill_all(self):
      """立即终止所有线程。"""
      for thread, _ in self._thread_repo:
            thread.terminate()

    def is_empty(self):
      """返回线程仓库是否为空。"""
      return not self._thread_repo

均转自AwesomePyKit项目
开源精神!
Salute!

591821661 发表于 2021-3-31 22:00:59

我有一个大胆的想法 :之前的Threading线程结果放到queue里面 然后用Qthread 来读取这个queue里面的东西显示,也就是说 该 Qthread只用来做显示结果用 不参与数据处理过程 (我想到了Iphone系统 不论程序怎么卡 它系统都不会卡)主要是为了实现显示进程和算法进程的互不干扰@hrp 你觉得这个想法怎么样
页: [1] 2
查看完整版本: pyqt5 多线程问题求助