Programming,  Python

[Python] PyQt – QThread (OTP Program)

GUI를 구현할 때 thread를 사용해 producer와 consumer를 각각 구현한다. PyQt에선 좀더 쉽게 사용할 수 있도록 QThread를 제공한다. 아래 프로그램은 하나의 thread가 tic을 튀기면서 signal을 또 다른 thread에게 전달해주어 또 다른 thread가 화면에 뿌리도록 한다.

import sys
import time

from PyQt5.QtWidgets import QWidget
from PyQt5.QtWidgets import QTextEdit
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QBoxLayout
from PyQt5.QtCore import Qt
from PyQt5.QtCore import QThread
from PyQt5.QtCore import pyqtSignal

class TicGenerator(QThread):
    tic = pyqtSignal(name="Tic")

    def __init__(self):
        QThread.__init__(self)

    def __del__(self):
        self.wait()

    def run(self):
        while True:
            t = int(time.time())
            if not t % 5 == 0:
                self.usleep(1)
                continue
            self.Tic.emit()
            self.msleep(1000)


class Form(QWidget):
    def __init__(self):
        QWidget.__init__(self, flags=Qt.Widget)
        self.te = QTextEdit()
        self.tic_gen = TicGenerator()
        self.init_widget()
        self.tic_gen.start()

    def init_widget(self):
        self.setWindowTitle("Custom Signal")
        form_lbx = QBoxLayout(QBoxLayout.TopToBottom, parent=self)
        self.setLayout(form_lbx)

        self.tic_gen.Tic.connect(
            lambda: self.te.insertPlainText(time.strftime("[%H:%M:%S] Tic!\n"))
        )

        form_lbx.addWidget(self.te)

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

Tic을 튀기는 thread의 동작은 TickGenerator(QThread)run() 함수에 기술된다. 내부 코드를 보면 현재 시간 초를 int로 받아와 5로 나눠질 때, 즉 5초마다 pyqtSignal().emit()을 수행시켜준다.

위 signal에 대한 연결은 실제 화면에 뿌려주는 main thread의 self.tic_gen.Tic.connect([함수 객체]) 내부에 선언한 함수의 동작을 수행한다. 실제 함수 객체 내부엔 현재 시간의 텍스트를 뿌려주도록 하고 있다.

Example

아래 코드는 금융에서 사용하는 OTP card의 예시다. 자세한 코드 리뷰는 코드의 주석으로 넣어서 설명한다.

import sys

from PyQt5.QtWidgets import QWidget
from PyQt5.QtWidgets import QLabel
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QBoxLayout
from PyQt5.QtCore import Qt
from PyQt5.QtCore import QThread
from PyQt5.QtCore import pyqtSignal
import string
import time
import random

class OtpTokenGenerator(QThread):
    value_changed = pyqtSignal(str, name="ValueChanged")
    expires_in = pyqtSignal(int, name="ExpiresIn")

    EXPIRE_TIME = 5

    def __init__(self):
        QThread.__init__(self)
        # Get uppercase
        self.characters = list(string.ascii_uppercase)
        self.token = self.generate()

    def __del__(self):
        self.wait()

    def generate(self):
        # Get random string
        random.shuffle(self.characters)
        # Parse 5 bytes only
        return ''.join(self.characters[0:5])

    def run(self):
        self.value_changed.emit(self.token)  
        while True:
            # 매 초마다 갱신, 5 - 4 - 3 - 2 - 1 순으로 감소됨
            t = int(time.time()) % self.EXPIRE_TIME
            self.expires_in.emit(self.EXPIRE_TIME - t)  
            if t != 0:
                self.usleep(1)
                continue
            # 5초에 한 번씩 수행
            self.token = self.generate()
            self.value_changed.emit(self.token)
            self.msleep(1000)


class Form(QWidget):
    def __init__(self):
        QWidget.__init__(self, flags=Qt.Widget)
        self.lb_token = QLabel()
        self.lb_expire_time = QLabel()
        self.otp_gen = OtpTokenGenerator()
        self.init_widget()
        # Thread gets started
        self.otp_gen.start()

    def init_widget(self):
        self.setWindowTitle("Custom Signal")
        form_lbx = QBoxLayout(QBoxLayout.TopToBottom, parent=self)
        self.setLayout(form_lbx)

        # token connection
        self.otp_gen.ValueChanged.connect(self.lb_token.setText)
        # count connection, the value should be changed to string type
        self.otp_gen.ExpiresIn.connect(lambda v: self.lb_expire_time.setText(str(v)))

        form_lbx.addWidget(self.lb_token)
        form_lbx.addWidget(self.lb_expire_time)

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

Leave a Reply

Your email address will not be published. Required fields are marked *