Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I keep GUI from freezing, while calling a function? (PyQT4, Python3)

Question Background:

I'm new to PyQT4. I am developing a program with it and I am web scraping to get data into my program. While the information is downloading my GUI locks up. I would like to call this function in a separate background thread, perhaps using QThread, but I am having a tough time wrapping my head around QThread, Qt in general, and the slot/signal way of communication.

I have read about making a generic worker thread that will call any function passed to it. I don't know how to implement it in my main file so that I can run my functions as a background process. If any example code could be shown, please explain each line in detail as I am not understanding the process.

Questions:

  1. How can I prevent my GUI from freezing while a function is running?
  2. How could I use a background thread to run functions from my class?

Code:

My ui is loaded in from an external file created by Qt 4 Designer.

Full files on Github

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName(_fromUtf8("MainWindow"))

main.py (Main file)

def connections():
    # If button is clicked, call summary(), which web scrapes 
    # for data. This could take 5-30 seconds, this freezes UI.
    ui.btnRefreshSummary.clicked.connect(lambda: summary())

# Refresh items in gui
def refresh_ui():
    if summary_data != []:
        ui.valWatching.setText(summary_data[0])
        ui.valBidding.setText(summary_data[1])
        ui.valWon.setText(summary_data[2])
        ui.valNotWon.setText(summary_data[3])
        ui.valPurchases.setText(summary_data[4])
        ui.valInvoices.setText(summary_data[5])

def login():
    # Scrape website and login while in background; 
    # This locks up GUI until it completes.
    # Pretend this sleep command is the time it takes to login
    time.sleep(5)  # <-This would lock it up for 5 seconds

if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    MainWindow = QtGui.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()
    connections()
    # Load credentials from file.
    with open('login.txt') as f:
        credentials = f.readline().strip().split(':')
    f.closed

    # Login, download summary, then refresh the UI.
    b = Biddergy()
    b.login(credentials[0],credentials[1])
    summary_data = b.summary()
    b.logout()

    refresh_ui()
    sys.exit(app.exec_())
like image 890
Matthew Kelley Avatar asked Mar 14 '23 07:03

Matthew Kelley


2 Answers

It's not clear from the example code why connections() would block (given the code it contains), or why login() shouldn't block (given that that is what login dialogs normally do). But anyway, the worker class in your example can be converted to a QThread like this:

class Worker(QThread):
    intReady = pyqtSignal(int)

    def run(self):
        for i in range(1, 10):
            time.sleep(1)
            self.intReady.emit(i)

and then it can be used like this:

    # connections()
    # login()
    def slot(arg='finished'): print(arg)
    thread = Worker()
    thread.intReady.connect(slot)
    thread.finished.connect(slot)
    thread.start()

There are many other ways of achieving the same thing - but which one is most appropriate, and how it would actually be implemented, depends on the details of how your application is intended to work.

like image 126
ekhumoro Avatar answered Mar 17 '23 19:03

ekhumoro


With this updated code, I am finally able to start my functions in the background. Now off to learning how to make my background thread communicate with the main UI thread. Much thanks to @ekhumoro for being extremely patient with me.

#!/usr/bin/env python3
from PySide import QtGui, QtCore
from PySide.QtCore import QThread, QObject, Signal, Slot
from main_gui import Ui_MainWindow  # my UI from Qt4 Designer(pyside-uic)
from Scrapers import Biddergy       # My own class
import sys
import queue

class BiddergyWrapper(QThread):
    def __init__(self, q, loop_time=1.0/60):
        self.q = q
        self.timeout = loop_time
        super(BiddergyWrapper, self).__init__()

    def onThread(self, function, *args, **kwargs):
        self.q.put((function, args, kwargs))

    def run(self):
        while True:
            try:
                function, args, kwargs = self.q.get(timeout=self.timeout)
                function(*args, **kwargs)
            except queue.Empty:
                self.idle()

    def idle(self):
        pass

    def _summary(self):
        b.summary()

    def summary(self):
        self.onThread(self._summary)

    def _login(self):
        b.login()

    def login(self):
        self.onThread(self._login())

if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    MainWindow = QtGui.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()
    ui.btnRefreshSummary.clicked.connect(lambda: bw.summary())

    # Load credentials from file.
    with open('login.txt') as f:
        credentials = f.readline().strip().split(':')

    # Login, download summary, then refresh the UI.
    b = Biddergy(credentials[0], credentials[1])
    request_queue = queue.Queue()
    bw = BiddergyWrapper(request_queue)
    bw.start()

    # Run QApplication
    app.exec_()
    # Begin "Graceful stop?"
    bw.quit()
    b.logout()
    sys.exit()
like image 27
Matthew Kelley Avatar answered Mar 17 '23 18:03

Matthew Kelley