Pyqt5 多线程错误:QObject::connect: 无法对“QTextCursor”类型的参数进行排队

Posted

技术标签:

【中文标题】Pyqt5 多线程错误:QObject::connect: 无法对“QTextCursor”类型的参数进行排队【英文标题】:Pyqt5 Multi-threading Error: QObject::connect: Cannot queue arguments of type 'QTextCursor' 【发布时间】:2020-08-02 20:11:41 【问题描述】:

我正在开发一个类似于聊天应用程序的项目,每次我通过 pyqt5 的 gui 打开一个新线程时,都会出现错误说明:QObject::connect: Cannot queue arguments of type 'QTextCursor'。我真的不知道我做错了什么,非常感谢您的帮助。提前致谢。

这是我的代码:

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QInputDialog
from PyQt5.QtWidgets import QPlainTextEdit
from socket import socket, AF_INET6
from socket import SOCK_STREAM, SOCK_DGRAM
from socket import gethostbyname, gethostname 
from threading import Thread
import sys


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        self.MainWindow = MainWindow.setObjectName("MainWindow")
        MainWindow.resize(251, 335)

        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")

        self.BigBox = QtWidgets.QPlainTextEdit(self.centralwidget)
        self.BigBox.setGeometry(QtCore.QRect(10, 10, 231, 211))
        self.BigBox.setObjectName("BigBox")

        self.SmallBox = QtWidgets.QPlainTextEdit(self.centralwidget)
        self.SmallBox.setGeometry(QtCore.QRect(10, 230, 231, 31))
        self.SmallBox.setObjectName("SmallBox")

        self.hostButton = QtWidgets.QPushButton(self.centralwidget)
        self.hostButton.setGeometry(QtCore.QRect(90, 270, 75, 23))
        self.hostButton.setObjectName("hostButton")

        self.submitButton = QtWidgets.QPushButton(self.centralwidget)
        self.submitButton.setGeometry(QtCore.QRect(170, 270, 75, 23))
        self.submitButton.setObjectName("submitButton")

        self.connectButton = QtWidgets.QPushButton(self.centralwidget)
        self.connectButton.setGeometry(QtCore.QRect(10, 270, 75, 23))
        self.connectButton.setObjectName("connectButton")
        MainWindow.setCentralWidget(self.centralwidget)


        self.connectButton.clicked.connect(self.popForConnect)
        self.hostButton.clicked.connect(self.popForHost)
        self.submitButton.clicked.connect(self.takeValue)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "Chat Room"))
        self.hostButton.setText(_translate("MainWindow", "Host"))
        self.submitButton.setText(_translate("MainWindow", "Submit"))
        self.connectButton.setText(_translate("MainWindow", "Connect"))

    def popForConnect(self):
        self.ip, x = QInputDialog.getText(self.MainWindow, "Connection Options", "Enter The IP: ")
        self.port2, y = QInputDialog.getInt(self.MainWindow, "Connection Options", "Enter The Port: ")
        self.mainConnect()

    def mainConnect(self):
        self.hs = socket(AF_INET6, SOCK_STREAM)
        self.IPAddr = gethostbyname(gethostname())

        getMsg = Thread(target=self.getMessages)
        getMsg.start()

        try:
            self.hs.connect((self.ip, int(self.port2), 0, 0))
            self.hs.send(bytes("[+] Connection Established", "utf8"))
            self.sendMessages()
        except (ConnectionRefusedError, TimeoutError):
            self.BigBox.appendPlainText("[!] Server Is Currently Full")

    def getMessages(self):
        self.js = socket(AF_INET6, SOCK_DGRAM)
        self.js.bind(("", int(self.port2+2), 0, 0))
        while True:
            msg2 = self.js.recvfrom(1024)
            formatedMsg = msg2[0].decode("utf8")
            if formatedMsg == f"[self.IPAddr]: [!] User Disconnected" and formatedMsg[1:13] == self.IPAddr:
                self.BigBox.appendPlainText(formatedMsg)
                self.BigBox.repaint()
                self.js.close()
                sys.exit()
            self.BigBox.appendPlainText(formatedMsg)
            self.BigBox.repaint()

    def sendMessages(self):
        while True:
            msg3 = self.takeValue()
            if msg3 == "quit" or msg3 == "exit":
                self.hs.send(bytes("[!] User Disconnected", "utf8"))
                break
            self.hs.send(bytes(msg3, "utf8"))
        self.hs.close()

    def popForHost(self):
        TEMPVAR = 0
        N_CONN = 
        self.port, x = QInputDialog.getInt(self.MainWindow, "Host Options", "Enter The Port: ")
        self.mainHost(TEMPVAR, N_CONN)

    def mainHost(self, TEMPVAR, N_CONN):
        self.cs = socket(AF_INET6, SOCK_STREAM)

        self.vs = socket(AF_INET6, SOCK_DGRAM)
        self.vs.bind(("", int(self.port+1), 0, 0))

        self.cs.bind(("", int(self.port),0 ,0 ))
        self.BigBox.appendPlainText("[*] Listening on 0.0.0.0:"+str(self.port))
        self.BigBox.repaint()

        self.waitForConnections(TEMPVAR, N_CONN)

    def waitForConnections(self, TEMPVAR, N_CONN):
        for _ in range(2):
            self.cs.listen(1)
            self.conn, self.addr = self.cs.accept()

            self.BigBox.appendPlainText("[+] User Connected: "+str(self.addr[0])+ " Port: "+str(self.addr[1]))
            self.BigBox.repaint()
            N_CONN[self.addr[0]] = self.addr[1]

            prtMsg = Thread(target=self.printMessages, args=(TEMPVAR, N_CONN,))
            prtMsg.start()
            if TEMPVAR == 101:
                break
        TEMPVAR = 0

    def printMessages(self, TEMPVAR, N_CONN):
        while True:
            try:
                self.msg = self.conn.recv(1024).decode("utf8")
                if self.msg == "[!] User Disconnected":
                    self.BigBox.appendPlainText(self.formatedMsg(TEMPVAR, N_CONN))
                    self.BigBox.repaint()

                    N_CONN.pop(self.addr[0])
                    break
                self.BigBox.appendPlainText(self.formatedMsg(TEMPVAR, N_CONN))
                self.BigBox.repaint()
            except ConnectionResetError as e:
                self.BigBox.appendPlainText(f"[self.addr[0]]: [!] User Disconnected")
                self.BigBox.repaint()
                break
        self.conn.close()
        TEMPVAR = 101

    def formatedMsg(self, TEMPVAR, N_CONN):
        self.fmsg = f"[self.addr[0]]: "+self.msg

        for keys, values in N_CONN.items():
            self.vs.sendto(bytes(self.fmsg, "utf8"), (keys, int(self.port+2)))
        return self.fmsg


    def takeValue(self):
        return self.SmallBox.toPlainText()

if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()
    sys.exit(app.exec_())

您可以通过将程序托管在计算机中来启动程序。[按主机并输入所需的端口]。接下来,在另一台计算机上,您可以选择连接并输入主机的 IP 和您选择的端口。

【问题讨论】:

【参考方案1】:

该错误的原因是您试图从另一个线程访问 GUI 元素,这是必须永远做的事情。

当您使用appendPlainText 时,“幕后”会发生很多事情,包括对文本编辑的底层QTextDocument 的更改,它在内部使用信号/插槽连接来更改文档布局并通知小部件(这也包括QTextCursor 实例)。如前所述,Qt 无法从单独的线程中执行此操作。

为了正确执行所有这些操作,您不应使用基本的 python Thread,而应使用 QThread 子类,并使用可以连接到更新小部件的自定义信号。

我不能重写你的例子,因为它太广泛了,但我可以给你一些建议。

为客户端和主机创建 QThread 的子类

这是可以为主机类实现的非常基本的半伪代码:

class Host(QtCore.QThread):
    newConnection = QtCore.pyqtSignal(object)
    messageReceived = QtCore.pyqtSignal(object)

    def __init__(self, port):
        super().__init__()
        self.port = port

    def run(self):
        while True:
            cs = socket(AF_INET6, SOCK_STREAM)
            cs.bind(("", int(self.port), 0, 0))
            conn, addr = self.cs.accept()
            self.newConnection.emit(addr)
            while True:
                self.messageReceived.emit(conn.recv(1024).decode("utf8"))

然后,在主类中,是这样的:

class MainWindow(QtWidgets.QMainWindow):
    def mainHost(self, port):
        self.socket = Host(port)
        self.socket.newConnection.connect(self.newConnection)
        self.socket.messageReceived.connect(self.BigBox.appendPlainText)
        self.socket.start()

    def newConnection(self, ip):
        self.BigBox.appendPlainText('New connection from '.format(ip)

切勿在主线程中使用阻塞函数/语句

在您的代码中,您有waitForConnections,它被阻止直到self.cs.accept() 返回;这会阻止 UI 正确更新(例如,在移动或调整其大小时)或接收任何用户交互,包括尝试关闭窗口。

除非真的需要,否则避免使用repaint()

来自documentation:

我们建议仅在需要立即重新绘制时才使用 repaint(),例如在动画期间。在几乎所有情况下 update() 都更好,因为它允许 Qt 优化速度并最大限度地减少闪烁。

一般来说,只有当您知道您在做什么以及为什么要这样做时,您才应该使用repaint(),并且从另一个线程执行它不是一个好主意。

不要修改pyuic生成的文件

这些文件旨在按原样使用,无需任何修改(阅读更多 about it 以了解如何正确使用这些文件。 请注意,您甚至不应该试图模仿他们的行为。如果您完全从代码构建 UI,只需子类化您正在使用的 QWidget(在您的情况下为 QMainWindow)。

其他说明:

不要使用字符串比较来检查连接/断开状态;想想如果我发送“[!] User Disconnected”消息会发生什么; 避免不必要的函数:例如,你有mainHostwaitForConnections,它们实际上是一个接一个地执行的;应该为它们的可重用性而创建函数,如果你只使用它们一次,通常不需要它们; 如果您不打算再次使用它们,请避免使用不必要的实例属性(例如,self.fmsgformatedMsg() 中使用); 固定的几何图形很少是个好主意,总是更喜欢使用布局管理器; 变量名称(与函数名称一样)不应大写,也不应大写(通常仅用于常量),请在Style Guide for Python Code 中阅读有关这些方面的更多信息; 您在 for 和 while 循环结束时设置了 TEMPVAR;因为它是一个本地变量,这样做是没有意义的; submitButton 连接什么都不做;

最后,除了使用python的socket,你还可以使用Qt的专用类:QTcpSocket和QUdbSocket。

【讨论】:

【参考方案2】:

使用 QThread 而不是线程。

此外,您可能希望使用 QTcpSocket 并对 QAbstractSocket 进行超级调用,而不是使用内置函数。

https://doc.qt.io/qt-5/qthread.html

https://doc.qt.io/qt-5/qtcpsocket.html

【讨论】:

以上是关于Pyqt5 多线程错误:QObject::connect: 无法对“QTextCursor”类型的参数进行排队的主要内容,如果未能解决你的问题,请参考以下文章

PyQt5自学记录——PyQt5多线程实现详解

Scrapy + pyqt5:信号仅在主线程错误中有效

PyQT5 多线程问题

PyQt5创建多线程

PyQt5使用多线程防止卡死

九PyQt5多线程编程