카테고리 없음

[Python] 스레드로 Qt5 UI 갱신 처리하기

채윤아빠 2023. 3. 29. 15:10
728x90
반응형

개요

PyQt5를 잘 모르는 상태에서 기존에 해왔던 Thread 클래스를 이용하여 작업 스레드를 별도로 작성하여 UI를 직접 갱신하려 하였더니 다음과 같은 오류가 발생하면서, 프로그램이 죽어버리는 문제가 있었습니다.

QObject::setParent: Cannot set parent, new parent is in a different thread
QObject::setParent: Cannot set parent, new parent is in a different thread
QObject::setParent: Cannot set parent, new parent is in a different thread
QBackingStore::endPaint() called with active painter; did you forget to destroy it or call QPainter::end() on it?
QBackingStore::endPaint() called with active painter; did you forget to destroy it or call QPainter::end() on it?

이 문제를 해결하기 위해서는 기존 Thread 클래스를 이용하는 것이 아닌, QThread 및 pyqtSignal 신호를 이용해야 합니다.

이번 글에서는 QThread 및 pyqtSignal를 이용하여 UI를 갱신할 수 있는 스레드를 작성하는 방법에 대하여 알아보겠습니다.


QThread, pyqtSignal, pyqtSlot

Qt5 UI를 스레드로 UI를 갱신하기 위해서는 QThread, pyqtSignal, pyqtSlot 세 가지 클래스를 활용해야 합니다.

Thread 대신에 QThread를 상속받아 UI와 관계없이 백그라운드로 작업할 내용을 구현하고, 스레드 내부에 pyqtSignal을 생성하고 UI를 갱신할 필요가 있을 때마다 pyqtSignal를 emit() 합니다.
emit() 된 pyqtSignal은 pyqtSlot을 호출하여 UI를 갱신하도록 합니다.


아래 예제는 일반적인 로그인 창과는 다르게 timer를 주고, 지정된 시간이 지나면 로그인 창이 자동으로 닫히도록 QThread 등을 이용하여 구현한 예제입니다.
아래의 예제를 바탕으로 QThread, pyqtSignal, pyqtSlot 세 가지 클래스의 활용법을 고민해 보시기 바랍니다.

from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

import sys


class LoginDialog(QDialog):

    def __init__(self):
        super(LoginDialog, self).__init__()
        self.setupUi()


    def setupUi(self):
        self.setWindowTitle("Login Dialog")
        self.resize(254, 113)
        self.verticalLayout = QVBoxLayout(self)
        self.gridLayout = QGridLayout()

        self.label = QLabel(self)
        self.label.setText("ID :")
        self.gridLayout.addWidget(self.label, 0, 0, 1, 1)

        self.lineEdit = QLineEdit(self)
        self.gridLayout.addWidget(self.lineEdit, 0, 1, 1, 1)

        self.label_2 = QLabel(self)
        self.label_2.setText("Password :")
        self.gridLayout.addWidget(self.label_2, 2, 0, 1, 1)

        self.lineEdit_2 = QLineEdit(self)
        self.gridLayout.addWidget(self.lineEdit_2, 2, 1, 1, 1)

        self.lbTimer = QLabel(self)
        self.lbTimer.setText("10 seconds remain...")
        self.gridLayout.addWidget(self.lbTimer, 3, 1, 1, 1)

        self.verticalLayout.addLayout(self.gridLayout)

        self.buttonBox = QDialogButtonBox(self)
        self.buttonBox.setObjectName(u"buttonBox")
        self.buttonBox.setOrientation(Qt.Horizontal)
        self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok)

        self.verticalLayout.addWidget(self.buttonBox)


    def closeDialog(self):
        self.done(0)


    @pyqtSlot(int)
    def handleTimer(self, sec):
        self.lbTimer.setText(f"{sec} seconds remain...")
        if (sec == 0):
            self.done(0)


class LoginTimer(QThread):
    timer_signal = pyqtSignal(int)

    def __init__(self, parent: QDialog = None, sec: int = 10) -> None:
        super().__init__()
        self._parent = parent
        self._sec = sec
        self._is_stopped = False
        self.timer_signal.connect(parent.handleTimer)


    def run(self) -> None:
        while (not self._is_stopped):
            self.timer_signal.emit(self._sec)
            self.sleep(1)

            self._sec -= 1
            if (self._sec == 0):
                break

        self.timer_signal.emit(self._sec)
        print("thread end")


if __name__ == '__main__':
    from os import environ
    from PyQt5 import QtWidgets, QtCore, QtGui #pyqt stuff

    def openLoginDialog():
        loginDialog = LoginDialog()
        thread = LoginTimer(loginDialog, 15)
        thread.start()
        loginDialog.exec()

    environ['QT_AUTO_SCREEN_SCALE_FACTOR'] = '1'  # 모니터 해상도에 따른 폰트 및 컨트롤 크기 자동 조정

    app = QApplication(sys.argv)
    button = QPushButton("Timer Login Demo")
    button.clicked.connect(openLoginDialog)
    button.show()
    app.exec_()

위와 같이 로그인 대화상자가 열리면 타이머가 동작하고 0초가 되면 자동으로 대화상자가 닫히게 됩니다.


참고자료