Search code examples
pythonmultithreadingsocketspyqt5python-multithreading

Pyqt5 Multi-threading Error: QObject::connect: Cannot queue arguments of type 'QTextCursor'


I am working on a project which acts like a chat application and everytime I open a new thread through the pyqt5's gui, there is an error stating: QObject::connect: Cannot queue arguments of type 'QTextCursor'. I don't really know what I am doing wrong and your help is greatly appreaciated. Thanks in advance.

Here is my code:

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

You can start the program by hosting it in a computer.[Press Host and enter the desired port]. Next, on the other computer you can select connect and type in the IP of the host machine and your chosen port.


Solution

  • The reason for that error is that you're trying to access GUI elements from another thread, which is something that must never be done.

    When you use appendPlainText, a lots happens "under the hood", including changes to the underlying QTextDocument of the text edit, which internally uses signal/slot connections to alter the document layout and notify the widget about it (this includes QTextCursor instances too). As explained before, Qt cannot do that from a separate thread.

    In order to correctly do all of that, you should not use a basic python Thread, but a QThread subclass instead, with custom signals that you can connect to to update the widgets.

    I cannot rewrite your example, as it's too extensive, but I can give you some advice about it.

    Create subclasses of QThread for the client and the host

    This is a very basic semi-pseudo-code of what could be implemented for the host class:

    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"))
    

    And then, in the main class, something like this:

    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)
    

    NEVER use blocking functions/statements in the main thread

    In your code you have waitForConnections, which is blocked until self.cs.accept() returns; this prevents the UI to correctly update (for example, while moving or resizing it) or receive any user interaction, including trying to close the window.

    Avoid repaint() unless really needed

    From the documentation:

    We suggest only using repaint() if you need an immediate repaint, for example during animation. In almost all circumstances update() is better, as it permits Qt to optimize for speed and minimize flicker.

    Generally speaking, you should only use repaint() only if you know what and why you're doing it, and doing it from another thread is not a good idea.

    Do not modify pyuic generated files

    Those files are intended to be used as they are, without any modification (read more about it to understand how to correctly use those files.
    Note that you should not even try to mimic their behavior. If you're completely building the UI from code, just subclass the QWidget you're using (in your case, QMainWindow).


    Other notes:

    • don't use string comparison to check connection/disconnection states; think about what would happen if I send a "[!] User Disconnected" message;
    • avoid unnecessary functions: for example, you have mainHost and waitForConnections that are actually executed one after the other; functions should be created for their reusability, if you only use them once, there's usually no real need for them;
    • avoid unnecessary instance attributes if you're not going to use them again (for instance, self.fmsg used in formatedMsg());
    • fixed geometries are rarely a good idea, always prefer using layout managers;
    • variables names (as with function names) should not be capitalized, nor uppercase (which is usually used for constants only), read more about these aspects in the Style Guide for Python Code;
    • you're setting TEMPVAR at the end of for and while cycles; since it's a local variable, doing that is meaningless;
    • the submitButton connection does nothing;

    Finally, instead of using python's socket, you can also use the Qt dedicated classes: QTcpSocket and QUdbSocket.