Jonathan Miller

Software Dev.

Number Shaman.

Aftertouch

Multi-screen, threaded UI with PyQt5

Multiscreen, Multithread

Full project code available here

I have been working on a project recently that involves packaging utility python scripts I’ve written to do various things for use by nontechnical users. Therefore I wanted to build a GUI application that lets users drive the scripts. I opted to use the PyQt5 library to build this interface.

Two of the biggest challenges I came across while working on this project were creating a multiscreen application, similar to how a tabbed mobile app works, and running scripts asynchronously so they don’t lock up the main interface while running. Fortunately, both of these things have fairly elegant solutions in PyQt.

In this article, I will be sharing these solutions with you. We will be building a dummy app with PyQt5 GUI that contains a few things:

Each of these will have their own screen as well, and the user will be able to use a menu bar to switch between them. Let’s get started!

First, we will create the project structure, which will look as follows:

root/

src/

  longscript.py
  shortscript.py

interface.py

Next we will create a couple of simple scripts for our app to run:

shortscript.py

def main():
    print("This one is easy!")

longscript.py

import time

def main():
    print("This one might take a while. . .")
    time.sleep(10)
    print("Okay, it's done.")

Finally, we will create the barebones PyQt5 interface file for our project:

interface.py

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

import src.shortscript
import src.longscript

class window(QMainWindow):

    def __init__(self):
        super(window, self).__init__()
        self.setGeometry(50, 50, 500, 400)
        self.setFixedSize(500, 400)
        self.setWindowTitle('Multiscreen Threaded PyQt Interface')
        self.show()

def run():
    app = QApplication(sys.argv)
    Gui = window()
    sys.exit(app.exec_())

run()

Now, running interface.py will show us an PyQt GUI with a window title. We want our app to enable the user to run shortscript and longscript, both from their respective application screens, so this will be our next task.

The simplest way I’ve found to create and manage a multiscreen app is to create screens as their own class which extend QWidget, and then add a function to the main window class for each screen which can be connected to UI objects to load these screens. This will all leverage the setCentralWidget() method.

We will begin by adding the welcome screen to our simple app which users land on when they run it.

interface.py

#...

class window(QMainWindow):

    def __init__(self):
        super(window, self).__init__()
        self.setGeometry(50, 50, 500, 400)
        self.setFixedSize(500, 400)
        self.setWindowTitle('Multiscreen Threaded PyQt Interface')
        self.startUIWelcomeScreen()

    def startUIWelcomeScreen(self):
        self.WelcomeScreen = UIWelcomeScreen(self)
        self.setCentralWidget(self.WelcomeScreen)
        self.show()

class UIWelcomeScreen(QWidget):
    def __init__(self, parent=None):
        super(UIWelcomeScreen, self).__init__(parent)

        self.welcomelabel = QLabel('Welcome to the App!', self)
        self.welcomelabel.move(50, 100)

#...

Here we define a UIWelcomeScreen class, define a method to show the UIWelcomeScreen class, then change the self.show() call in our init method to call the Welcome screen’s custom show method.

Next, let’s define custom screens for our Shortscript and Longscript scripts, as well as a menu that will let our user navigate between these screens.

interface.py

#...

class window(QMainWindow):

    def __init__(self):
        #...

        self.statusBar()

        shortAction = QAction("&Short Script", self)
        shortAction.triggered.connect(self.startShortScreen)

        longAction = QAction("&Long Script", self)
        longAction.triggered.connect(self.startLongScreen)

        mainMenu = self.menuBar()
        tasksMenu = mainMenu.addMenu('&Tasks')
        tasksMenu.addAction(shortAction)
        tasksMenu.addAction(longAction)

        self.startUIWelcomeScreen()

    def startUIWelcomeScreen(self):
        self.WelcomeScreen = UIWelcomeScreen(self)
        self.setCentralWidget(self.WelcomeScreen)
        self.show()

    def startShortScreen(self):
        self.ShortScreen = UIShortScreen(self)
        self.setCentralWidget(self.ShortScreen)
        self.show()

    def startLongScreen(self):
        self.LongScreen = UILongScreen(self)
        self.setCentralWidget(self.LongScreen)
        self.show()

class UIShortScreen(QWidget):
    def __init__(self, parent=None):
        super(UIShortScreen, self).__init__(parent)

        self.shortButton = QPushButton('Short Script!', self)
        self.shortButton.clicked.connect(src.shortscript.main)
        self.shortButton.move(50, 100)

class UILongScreen(QWidget):
    def __init__(self, parent=None):
        super(UILongScreen, self).__init__(parent)

        self.longButton = QPushButton('Long Script!', self)
        self.longButton.clicked.connect(src.longscript.main)
        self.longButton.move(50, 100)

# ...

Here, we’ve added a menu bar with a tasks submenu that allows navigation to our new screens, and a new screen class and show method for each new screen that we want to create. Then we add a button for the user on each screen, connect them to our scripts, and voila. We now have a functional, multiscreen GUI for operating our user scripts.

(Worth noting that our show methods are definitely getting into DRY territory, and can probably be condensed into a single function taking an input parameter as our app grows more unwieldy.)

Our next task is to deal with threading the long script. If you load the application as is and attempt to run the long script (which takes 10 seconds to run), you will notice that the GUI is completely locked while the process is running. This can easily be tackled by utilizing QThread.

interface.py

#...

class UILongScreen(QWidget):
    def __init__(self, parent=None):
        super(UILongScreen, self).__init__(parent)

        self.longButton = QPushButton('Long Script!', self)
        self.longButton.clicked.connect(self.run_longscript)
        self.longButton.move(50, 100)

    def run_longscript(self):
        self.get_thread = LongThread()
        self.get_thread.start()
        
class LongThread(QThread):
    def __init__(self):
        QThread.__init__(self)

    def __del__(self):
        self.wait()

    def run(self):
        src.longscript.main()

#...

By simply defining a QThread for our action, then defining a method which creates an instance of our thread and linking that to our UI instead, we have now threaded our application. If you test it now, you can run the long script, then you are free to navigate the app and perform other tasks (even running the shortscript!)

Hopefully this is helpful to you in your own project. If you have any questions, feel free to email me!

The full project code is available here