如何修复自定义消息框以在父python pyqt5的新线程中工作

问题描述

我在pyqt5中设计了自己的消息框,效果很好。我删除了除基本部分以外的所有内容。这是我的代码

import sys
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import  QApplication,QWidget,QPushButton,QDialog,qframe,QLabel,QTextEdit
from threading import Thread
class messageBox():
    def __init__(self,parent):
        self.parent=parent
        self.parent_width=self.parent.geometry().width()
        self.parent_height=self.parent.geometry().height()
    #This method will flash the messageBox dialog when we click on parent    
    def flash(self,event):
                    QTimer.singleShot(50,lambda:self.toolbar.setStyleSheet("background-color: blue;\n"
        "border-top-left-radius: 20px;\n"
        "border-top-right-radius: 20px;\n"
        "") )
                    QTimer.singleShot(120,lambda:self.toolbar.setStyleSheet("background-color: red;\n"
        "border-top-left-radius: 20px;\n"
        "border-top-right-radius: 20px;\n"
        "") )           
       
    def showinfo(self,title="ShowInfo",msg="Hello,There!"):
        self.value=None
        self.pop = QDialog()
        self.pop.setGeometry(500,200,454,165)
        self.msg=msg
        self.pop.mousepressEvent=self.click
        self.pop.mouseMoveEvent=self.move
        self.frame = qframe(self.pop)
        self.frame.setStyleSheet("background-color: #1b1b1b;\n"
            "border-bottom-left-radius: 20px;\n"
            "border-bottom-right-radius: 20px;\n"
            "")
        self.frame.setGeometry(0,30,134)
        self.toolbar = qframe(self.pop)
        self.toolbar.setStyleSheet("background-color:red;\n"
            "border-top-left-radius: 20px;\n"
            "border-top-right-radius: 20px;\n"
            "")
        self.toolbar.setGeometry(0,30)
        #This makes the window to frameless
        self.pop.setwindowFlags(Qt.FramelessWindowHint|Qt.WindowStaysOnTopHint)                   
        self.pop.setAttribute(Qt.WA_TranslucentBackground,True)
        #self.cover will Cover a frame to its parent entirely
        self.cover=qframe(self.parent)   
        self.cover.resize(self.parent_width,self.parent_height)
        self.cover.setStyleSheet("background-color:transparent;")
        #you can see the frame by setting background to a color
        self.cover.show()
        self.cover.mousepressEvent=self.flash
        b1 = QPushButton("Ok",self.frame)
        b1.setStyleSheet('''QPushButton {background-color: red;
        border-style: outset;
        border-width: 2px;
        border-radius: 10px;
        border-color: beige;
        font: bold 14px;
        min-width: 60px;
        min-height: 25px;
        }''''''QPushButton:pressed{background-color: green;
        border-style: inset;}''')
        b1.move(350,100)
        b1.clicked.connect(self.on_close)
        self.pop.setwindowTitle("Dialog")
        self.pop.setwindowModality(Qt.WindowModal)
        self.pop.exec_()
        return self.value
    def on_close(self):
        self.value=True
        self.cover.deleteLater()
        self.pop.close()
    # move and click  methods are used to drag the dialog   
    def move(self,event):
            if event.buttons() ==Qt.LeftButton:
                    deltax = event.x()- self.xp
                    deltay = event.y() - self.yp
                    x = self.pop.x() + deltax
                    y = self.pop.y() + deltay
                    width,height=int(self.pop.geometry().width()),int(self.pop.geometry().height())
                    self.pop.setGeometry(int(x),int(y),width,height)
    def click(self,event):
           if event.buttons() == Qt.LeftButton:
               self.xp=event.x()
               self.yp=event.y()      
def window():
       app = QApplication(sys.argv)
       w = QWidget()
       w.setGeometry(100,100,400,400)
       b = QPushButton(w)
       b.setText("Hello World!")
       b.move(50,50)
       messBox=messageBox(w)
       def run(): 
               h=messBox.showinfo('hello','how r u?')
               print(h) 
       #Thread(target=run).start() 
       b.clicked.connect(run)
       w.setwindowTitle("PyQt Dialog demo")
       w.show()
       sys.exit(app.exec_())    
if __name__ == '__main__':
   window()

但是唯一的问题是,当我从线程中调用诸如messBox.showinfo()之类的消息框方法时,它说由于子线程在新线程中而无法建立连接。我在该网站上浏览了有关工作线程的各种示例。但我无法正确理解工作线程。在这里,我注释掉了“线程部分”,然后单击按钮进行调用。有人能让我明白吗?谢谢!!!

解决方法

基本概念是只能从Qt主线程创建访问 UI元素。您不能创建新的窗口小部件,也不能直接从另一个线程访问(读取,但最重要的是写入)它们的属性。

为此,您必须与主线程进行通信,并且Qt信号和插槽可以完全用于此目的,因为Qt能够管理线程之间的通信而不会干扰其主循环:任何通信来自,外线程进入队列,然后在主线程允许后立即进行处理。请注意,使用信号和插槽时不应使用python线程,因为通常最好从QThread继承子类。为了与外部线程进行通讯,可以使用python Queues。

请注意,仅当某些处理需要一些时间才能完成(包括等待外部事件,例如来自网络请求的响应)时,才应使用外部线程。

在您的情况下,您不需要执行此操作,因此不需要其他线程(同样,您的代码执行应做的事情非常复杂,在以下示例中,我不会将其用作基础)

这是一个简单的示例,展示了如何实现QThread(“工作者”)。

from PyQt5 import QtCore,QtWidgets
from queue import Queue

class Worker(QtCore.QThread):
    testSignal = QtCore.pyqtSignal()
    def __init__(self):
        super().__init__()
        self.queue = Queue()

    def run(self):
        self.timer = QtCore.QElapsedTimer()
        timeout = -1
        while True:
            try:
                res = self.queue.get(timeout=.1)
            except:
                # if a timeout has been already set,check if it's expired
                if self.timer.hasExpired(timeout):
                    self.testSignal.emit()
                    self.timer.invalidate()
                    timeout = -1
                continue
            if res is None:
                self.timer.invalidate()
                timeout = -1
            else:
                timeout = res
                self.timer.start()

    def setTimeout(self,timeout):
        self.queue.put(timeout)

    def stopTimeout(self):
        self.queue.put(None)


class Window(QtWidgets.QDialog):
    def __init__(self):
        super().__init__()
        layout = QtWidgets.QVBoxLayout(self)
        self.spinBox = QtWidgets.QSpinBox(minimum=1000,maximum=5000,singleStep=500)
        layout.addWidget(self.spinBox)
        self.startButton = QtWidgets.QPushButton('Start')
        layout.addWidget(self.startButton)
        self.stopButton = QtWidgets.QPushButton('Stop')
        layout.addWidget(self.stopButton)
        self.stopButton.setEnabled(False)
        self.label = QtWidgets.QLabel()
        layout.addWidget(self.label)
        self.label.setAlignment(QtCore.Qt.AlignCenter)

        self.startButton.clicked.connect(self.startThreadTimeout)
        self.stopButton.clicked.connect(self.stopThreadTimeout)
        self.resetTimer = QtCore.QTimer(interval=1000,timeout=self.reset)

        self.worker = Worker()
        self.worker.start()
        self.worker.testSignal.connect(self.flash)

    def startThreadTimeout(self):
        self.reset()
        self.resetTimer.stop()
        self.worker.setTimeout(self.spinBox.value())
        self.startButton.setEnabled(False)
        self.stopButton.setEnabled(True)
        self.label.setText(
            'thread started,finishing in {}ms'.format(self.spinBox.value()))

    def stopThreadTimeout(self):
        # note that this does *not* stop the thread,but only its internal timeout check
        self.worker.stopTimeout()
        self.stopButton.setEnabled(False)
        self.startButton.setEnabled(True)
        self.reset()

    def flash(self):
        self.label.setStyleSheet('background: green')
        self.label.setText('finished')
        self.stopButton.setEnabled(False)
        self.startButton.setEnabled(True)
        self.resetTimer.start()

    def reset(self):
        self.label.setStyleSheet('')
        self.label.setText('')


if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec_())

请注意,上面的代码不是很有用,因为它只是出于学习目的:显然,您可以只在主线程中使用简单的普通QTimer.singleShot。