美文网首页
PyQt5 UI界面与业务逻辑分离

PyQt5 UI界面与业务逻辑分离

作者: 搬砖_工程师 | 来源:发表于2019-01-26 18:58 被阅读107次

    先说遇到的问题希望遇到和我一样问题的童鞋也可以成功解决。我在处理逻辑业务时候比较耗时经常造成界面未响应!!!。
    但是当使用python 的thread时候会造成主界面数据复原,暂时不知道什么原因。之后开始我的学习之路。
    因为qt界面的刷新相当于一直while,当有耗时多的任务时就会造成阻塞无法完成刷新,造成界面未响应。
    这时候就需要使用Qthread处理业务逻辑,主线程继续处理界面。分离ui界面与业务逻辑。

    要使用QThread开始一个线程,可以创建它的一个子类,然后覆盖其QThread.run()函数
    class Thread(QThread):
    
        def __init__(self):
            super().__init__()
    
        def run(self):
            # 线程相关代码
            pass
    
    # 创建一个新的线程
    thread = Thread()
    thread.start()
    

    在使用线程时可以直接得到Thread实例,调用其start()函数即可启动线程,线程启动后,会调用其实现的run方法,该方法就是线程的执行函数,当run()退出之后线程基本就结束了。

    QThread类中的常用方法:

    start() 启动线程
    wait() 阻止线程
    sleep(s) 强制当前线程睡眠s秒

    QThread类中的常用信号:

    started 在开始执行run()函数之前,从相关线程发射此信号
    finished 在程序完成业务逻辑时,从相关线程发射此信号

    当在窗口中显示的数据比较简单时,可以把读取数据的业务逻辑放在窗口的初始化代码中;但如果读取数据的时间比较长,比如网络请求数据的时间比较长,则可以把这部分逻辑放在QThread线程中,实现界面的数据显示和数据读取的分离.

    from PyQt5.QtCore import *
    from PyQt5.QtWidgets import *
    import sys
    
    class Worker(QThread):
        sinOut = pyqtSignal(str) # 自定义信号,执行run()函数时,从相关线程发射此信号
    
        def __init__(self, parent=None):
            super(Worker, self).__init__(parent)
            self.working = True
            self.num = 0
    
        def __del__(self):
            self.working = False
            self.wait()
    
        def run(self):
            while self.working == True:
                file_str = 'File index {0}'.format(self.num) # str.format()
                self.num += 1
    
                # 发出信号
                self.sinOut.emit(file_str)
    
                # 线程休眠2秒
                self.sleep(2)
    
    
    class MainWidget(QWidget):
        def __init__(self, parent=None):
            super(MainWidget, self).__init__(parent)
    
            self.setWindowTitle("QThread 例子")
    
            # 布局管理
            self.listFile = QListWidget()
            self.btnStart = QPushButton('开始')
            layout = QGridLayout(self)
            layout.addWidget(self.listFile, 0, 0, 1, 2)
            layout.addWidget(self.btnStart, 1, 1)
    
            # 连接开始按钮和槽函数
            self.btnStart.clicked.connect(self.slotStart)
    
            # 创建新线程,将自定义信号sinOut连接到slotAdd()槽函数
            self.thread = Worker()
            self.thread.sinOut.connect(self.slotAdd)
    
        # 开始按钮按下后使其不可用,启动线程
        def slotStart(self):
            self.btnStart.setEnabled(False)
            self.thread.start()
    
        # 在列表控件中动态添加字符串条目
        def slotAdd(self, file_inf):
            self.listFile.addItem(file_inf)
    
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        demo = MainWidget()
        demo.show()
        sys.exit(app.exec_())
    

    这个经典例子,虽然解决了界面的数据显示和数据读取的分离,但是如果数据的读取非常消耗时间,则会造成界面卡死,下面是一个需要耗费很长时间读取数据的例子。

    import sys from PyQt5.QtCore 
    import * from PyQt5.QtGui 
    import * from PyQt5.QtWidgets
    import * global sec sec = 0  
    def setTime():  
      global sec sec += 1 
       # LED显示数字+1 
      lcdNumber.display(sec)
    def work():  
        #每秒计数 
      timer.start(1000)
       # 开始一次非常耗时的计算 
       # 这里用一个2 000 000 000次的循环来模拟
       for i in range(200000000): 
          pass timer.stop() 
    if __name__ == "__main__": 
      app = QApplication(sys.argv) 
      top = QWidget() top.resize(300, 120) 
      # 垂直布局类
      QVBoxLayout layout = QVBoxLayout(top)
       # 添加控件
      lcdNumber = QLCDNumber() 
      layout.addWidget(lcdNumber) 
      button = QPushButton("测试") 
      layout.addWidget(button) 
      timer = QTimer() 
      # 每次计时结束,触发setTime 
      timer.timeout.connect(setTime) 
      # 连接测试按钮和槽函数
      work button.clicked.connect(work) 
      top.show() 
      sys.exit(app.exec_())
    

    程序的运行逻辑如下:

    这里写图片描述

    正常情况下,在点击按钮之后,LCD上的数字会随着时间发生变化,但是在实际运行过程中会发现点击按钮之后,程序界面直接停止响应,直到循环结束才开始重新更新,于是计时器始终显示为0。

    在上面这个程序中没有引入新的线程,PyQt中所有的窗口都在UI主线程中(就是执行了QApplication.exec()的线程),在这个线程中执行耗时的操作会阻塞UI线程,从而让窗口停止响应。

    为了避免出现上述问题,要使用QThread开启一个新的线程,在这个线程中完成耗时的操作:

    mport sys
    from PyQt5.QtCore import *
    from PyQt5.QtGui import *
    from PyQt5.QtWidgets import *
    
    global sec
    sec = 0
    
    # 增加了一个继承自QThread类的类,重新写了它的run()函数
    # run()函数即是新线程需要执行的:执行一个循环;发送计算完成的信号。
    class WorkThread(QThread):
        trigger = pyqtSignal()
    
        def __int__(self):
            super(WorkThread, self).__init__()
    
        def run(self):
            for i in range(2000000000):
                pass
    
            # 循环完毕后发出信号
            self.trigger.emit()
    
    
    def countTime():
        global sec
        sec += 1
        # LED显示数字+1
        lcdNumber.display(sec)
    
    
    def work():
        # 计时器每秒计数
        timer.start(1000)
        # 计时开始
        workThread.start()
        # 当获得循环完毕的信号时,停止计数
        workThread.trigger.connect(timeStop)
    
    
    def timeStop():
        timer.stop()
        print("运行结束用时", lcdNumber.value())
        global sec
        sec = 0
    
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        top = QWidget()
        top.resize(300, 120)
    
        # 垂直布局类QVBoxLayout
        layout = QVBoxLayout(top)
    
        # 加个显示屏
        lcdNumber = QLCDNumber()
        layout.addWidget(lcdNumber)
        button = QPushButton("测试")
        layout.addWidget(button)
    
        timer = QTimer()
        workThread = WorkThread()
    
        button.clicked.connect(work)
    
        # 每次计时结束,触发 countTime
        timer.timeout.connect(countTime)
    
        top.show()
        sys.exit(app.exec_())
    

    程序运行逻辑简单说明:

    按下按钮后,计时器开始计数,并启动一个新的线程,在这个线程里,执行一个循环并在循环结束时发送完成信号,在完成信号发出后,执行与之相关联的槽函数,关闭定时器。

    再次运行程序,界面有了响应。

    事件处理

    对于执行很耗时的程序来说,由于PyQt需要等待程序执行完毕才能进行下一步,这个过程表现在界面上就是卡顿;而如果在执行这个耗时程序时不断地运行QApplication.processEvents(),那么就可以实现一边执行耗时程序,一边刷新页面的功能,会给人一种相对更流畅的感觉,QApplication.processEvents()的使用方法是,在主函数执行耗时操作的地方,加入QApplication.processEvents(),processEvents()函数的使用方法简单来说就是刷新页面。(可以在table获取数据及时显示等操作使用)

    from PyQt5.QtWidgets import QWidget, QPushButton, QApplication, QListWidget, QGridLayout
    import sys
    import time
    
    
    class WinForm(QWidget):
    
        def __init__(self, parent=None):
            super(WinForm, self).__init__(parent)
            self.setWindowTitle("实时刷新界面例子")
            self.listFile = QListWidget()
            self.btnStart = QPushButton('开始')
    
            layout = QGridLayout(self)
            layout.addWidget(self.listFile, 0, 0, 1, 2)
            layout.addWidget(self.btnStart, 1, 1)
            self.setLayout(layout)
    
            self.btnStart.clicked.connect(self.slotAdd)
    
        def slotAdd(self):
            for n in range(10):
                str_n = 'File index {0}'.format(n)
                self.listFile.addItem(str_n)
                QApplication.processEvents()
                time.sleep(1)
    
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        form = WinForm()
        form.show()
        sys.exit(app.exec_())
    

    如果不添加QApplication.processEvents(),会在卡顿之后全部结果,添加之后,也不能保证每个都是逐行显示,只是比不加相对流畅一点,效果是不如多线程的。

    总结

    能用多线程尽量用多线程,不论数据处理还是界面流程性都优于QApplication.processEvents(),但是当数据量小的时候可以使用QApplication.processEvents(),代码比较简单。

    相关文章

      网友评论

          本文标题:PyQt5 UI界面与业务逻辑分离

          本文链接:https://www.haomeiwen.com/subject/fiaijqtx.html