15 Minute Apps
Common desktop apps in Python, using PyQt

A collection of 15 small — minute — desktop applications written in Python using the PyQt framework. These apps are intended as examples from which you can poke, hack and prod your way to writing your own tools.

The apps

The apps showcase various parts of the Qt framework, including advanced widgets, multimedia, graphics views and decorationless windows. However, the most generally interesting/feature complete applications are Minesweeper, Solitaire and Paint.

The source code for all apps is available in the Github repository. All code is MIT licensed and free to use to in your own projects.

Web Browser - "MooseAche"

Browser

An example web browser built with Python and Qt. Using the QtWebEngineWidgets system introduced in Qt5.6, this provides a single-window browsing experience with the usual controls, as well as saving and loading HTML.

Web Browser (tabbed) — "Mozzarella Ashbadger"

Browser tabbed

Mozarella Ashbadger is the latest revolution in web browsing! Go back and forward! Print! Save files! Get help! (you’ll need it). Any similarity to other browsers is entirely coincidental.

Minesweeper — "Moonsweeper"

Moonsweeper

Explore the mysterious moon of Q'tee without getting too close to the alien natives!

Moonsweeper is a single-player puzzle game base on the classic Minesweeper. The objective of the game is to explore the area around your landed space rocket, without coming too close to the deadly B'ug aliens. Your trusty tricounter will tell you the number of B'ugs in the vicinity.

Notepad — "No2Pads"

No2Pads

A very simple notepad clone using the QTextEdit widget to handle more or less everything. Supports file loading, saving and printing.

Calculator — "Calculon"

Calculon

A simple calculator application implemented in Python using PyQt. The UI was designed in Qt Designer and the calculator operations are implemented using simple stack-based logic.

Word Processor — "Megasolid Idiom"

Wordprocessor

The word processor for all your small, poorly formatted documents. An extension of the notepad, again using a QTextEdit but with rich text editing enabled.

Webcam/Snapshot — "NSAViewer"

Camera

With this webcam snapshot application you can take photos of what is currently being viewed by your webcam. Uses the Qt QtMultimedia framework for handling all the interaction with the camera, and supports multiple cameras if you have them.

Media Player — "Failamp"

Mediaplayer

Simple app to listen to and watch videos and audio files, with built in playlist. Uses QtMultimedia and QtMultimediaWidgets to handle playback and manage the playlist.

Post-it Notes - "Brown Note" (QtDesigner)

Brown note

Take temporary notes on your desktop, with this floating-note app. Notes are stored locally in a SQLite database.

Paint - "Piecasso" (QtDesigner)

Piecasso

Express yourself with PieCasso, the only painting app to feature ready made pictures of pie.

Piecasso is a clone of the Paint programme from Windows 95 (ish) with a few additions (and subtractions). The programme features standard tools including pen, brush, fill, spray can, eraser, text and a number of shapes.

Unzip - "7Pez" (QtDesigner)

Unzip

Unzip your files with a cat. Drag-drop your zip file onto the floaty cat, and fill up it's Pez-file repository. Press the head to release the Pez (your files) into the same folder.

Translator - "Translataarrr" (QtDesigner)

Translatarrrr

A translator from any language (supported by Google translate) to pirate. This uses a remote API from http://api.funtranslations.com for English to Pirate translation, and a unofficial Python wrapper around Google translate for other languages to English.

Weather - "Raindar" (QtDesigner)

Weather

Get your daily weather and 15hr forecast. Data is provided by the free weather and forecast API from https://openweathermap.org/

Currency converter - "Doughnut" (PyQtGraph)

Doughnut

This is a simple currency exchange rate tracker implemented in PyQt, using the fixer.io API for data. The default setup shows currency data for the preceding 180 days.

Solitaire - "Ronery" (QGraphicsScene)

Ronery

The classic card Solitaire (Klondike) game with a North Korea / Team America: World Police theme. The game is otherwise identical, offering the same options. Built with PyQt, it uses QGraphicsScene to handle the play area.

Getting started

To use each app you first need to install the requirements. In most cases the only requirements are PyQt5, and occasionally requests (for http requests). To install app-specific requirements change to the folder of the app and run:

python
pip3 install -r requirements.txt

Once the requirements are installed, you can run the app using Python 3.

python
python3 <filename>.py

The application window should appear.

Create GUI Applications with Python & Qt5
The easy way to create desktop applications

The complete guide to building GUI applications with PyQt5. From the basics of creating a desktop window to the key features you need to build real apps.

Downloadable ebook (PDF, ePub) & Complete Source code

To support developers in [[ countryRegion ]] I give a [[ localizedDiscount[couponCode] ]]% discount with the code [[ couponCode ]] — Enjoy!

For [[ activeDiscount.description ]] I'm giving a [[ activeDiscount.discount ]]% discount with the code [[ couponCode ]] — Enjoy!

License

All code is licensed under an MIT license. This allows you to re-use the code freely, remixed in both commercial and non-commercial projects. The only requirement is to include the same license when distributing.

The intent here is to allow as many people as possible to make use of this code, so you if you have specific license requirements or questions, feel free to ask — the answer is probably "yes."

Icons used in the applications are by Yusuke Kamiyaman.

Continue reading

Simple threading in PyQt/PySide apps with .start() of QThreadPool  PySide

In PyQt version 5.15.0 and PySide 6.2.0, the .start() method of QThreadPool was extended to take a Python function, a Python method, or a PyQt/PySide slot, besides taking only a QRunnable object. This simplifies running Python code in the background, avoiding the hassle of creating a QRunnable object for each task. For more information about creating a QRunnable object for multithreading, see the multithreading tutorial. The .start() method schedules the execution of a function/method/slot on a separate thread using QThreadPool, so it avoids blocking the main GUI thread of your app. Therefore, if you have one or more long-running tasks that need to be completed or be running in the background, pass them to .start() and be done. We'll build a simple demo app that simulates a long-running task to show how .start() can move a user-defined Python function/method or a PyQt/PySide slot onto a separate thread. But first, let’s begin with a flawed approach. Blocking the GUI Our demo app is a sheep counter that counts upwards from 1. While this is happening, you can press a button to pick a sheep. And since picking a sheep is hard, it takes some time to complete. This is how our demo app looks like. Make sure you're using PyQt 5.15.0+ or PySide 6.2.0+; otherwise, the demo app won’t work for you. PySide6 PyQt6 PyQt5 python import time from PySide6.QtCore import Slot, QTimer from PySide6.QtWidgets import ( QLabel, QWidget, QMainWindow, QPushButton, QVBoxLayout, QApplication, ) class MainWindow(QMainWindow): def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) self.setFixedSize(250, 100) self.setWindowTitle("Sheep Picker") self.sheep_number = 1 self.timer = QTimer() self.picked_sheep_label = QLabel() self.counted_sheep_label = QLabel() self.layout = QVBoxLayout() self.main_widget = QWidget() self.pick_sheep_button = QPushButton("Pick a sheep!") self.layout.addWidget(self.counted_sheep_label) self.layout.addWidget(self.pick_sheep_button) self.layout.addWidget(self.picked_sheep_label) self.main_widget.setLayout(self.layout) self.setCentralWidget(self.main_widget) self.timer.timeout.connect(self.count_sheep) self.pick_sheep_button.pressed.connect(self.pick_sheep) self.timer.start() @Slot() def count_sheep(self): self.sheep_number += 1 self.counted_sheep_label.setText(f"Counted {self.sheep_number} sheep.") @Slot() def pick_sheep(self): self.picked_sheep_label.setText(f"Sheep {self.sheep_number} picked!") time.sleep(5) # This function represents a long-running task! if __name__ == "__main__": app = QApplication([]) main_window = MainWindow() main_window.show() app.exec() python import time from PyQt6.QtCore import pyqtSlot, QTimer from PyQt6.QtWidgets import ( QLabel, QWidget, QMainWindow, QPushButton, QVBoxLayout, QApplication, ) class MainWindow(QMainWindow): def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) self.setFixedSize(250, 100) self.setWindowTitle("Sheep Picker") self.sheep_number = 1 self.timer = QTimer() self.picked_sheep_label = QLabel() self.counted_sheep_label = QLabel() self.layout = QVBoxLayout() self.main_widget = QWidget() self.pick_sheep_button = QPushButton("Pick a sheep!") self.layout.addWidget(self.counted_sheep_label) self.layout.addWidget(self.pick_sheep_button) self.layout.addWidget(self.picked_sheep_label) self.main_widget.setLayout(self.layout) self.setCentralWidget(self.main_widget) self.timer.timeout.connect(self.count_sheep) self.pick_sheep_button.pressed.connect(self.pick_sheep) self.timer.start() @pyqtSlot() def count_sheep(self): self.sheep_number += 1 self.counted_sheep_label.setText(f"Counted {self.sheep_number} sheep.") @pyqtSlot() def pick_sheep(self): self.picked_sheep_label.setText(f"Sheep {self.sheep_number} picked!") time.sleep(5) # This function represents a long-running task! if __name__ == "__main__": app = QApplication([]) main_window = MainWindow() main_window.show() app.exec() python import time from PyQt5.QtCore import pyqtSlot, QTimer from PyQt5.QtWidgets import ( QLabel, QWidget, QMainWindow, QPushButton, QVBoxLayout, QApplication, ) class MainWindow(QMainWindow): def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) self.setFixedSize(250, 100) self.setWindowTitle("Sheep Picker") self.sheep_number = 1 self.timer = QTimer() self.picked_sheep_label = QLabel() self.counted_sheep_label = QLabel() self.layout = QVBoxLayout() self.main_widget = QWidget() self.pick_sheep_button = QPushButton("Pick a sheep!") self.layout.addWidget(self.counted_sheep_label) self.layout.addWidget(self.pick_sheep_button) self.layout.addWidget(self.picked_sheep_label) self.main_widget.setLayout(self.layout) self.setCentralWidget(self.main_widget) self.timer.timeout.connect(self.count_sheep) self.pick_sheep_button.pressed.connect(self.pick_sheep) self.timer.start() @pyqtSlot() def count_sheep(self): self.sheep_number += 1 self.counted_sheep_label.setText(f"Counted {self.sheep_number} sheep.") @pyqtSlot() def pick_sheep(self): self.picked_sheep_label.setText(f"Sheep {self.sheep_number} picked!") time.sleep(5) # This function represents a long-running task! if __name__ == "__main__": app = QApplication([]) main_window = MainWindow() main_window.show() app.exec() When you run the demo app and press the Pick a sheep! button, you’ll notice that for 5 seconds, the GUI is completely unresponsive. That's not good. The delay in GUI responsiveness comes from the line time.sleep(5) which pauses execution of Python code for 5 seconds. This was added to simulate a long-running task. We can, however, improve that by threading, as you’ll see later on. Feel free to experiment by increasing the length of the delay – pass a number greater than 5 to .sleep() – and you may notice that your operating system starts complaining about the demo app not responding. Run a task on a separate thread So, how can we improve the responsiveness of our demo app? This is where the extended .start() method of QThreadPool comes in! First, we need to import QThreadPool, so let’s do that. PySide6 PyQt6 PyQt5 python from PySide6.QtCore import QThreadPool python from PyQt6.QtCore import QThreadPool python from PyQt5.QtCore import QThreadPool Next, we need to create a QThreadPool instance. Let’s add python self.thread_manager = QThreadPool() to the __init__ block of the MainWindow class. Now, let’s create a pick_sheep_safely() slot. It will use the .start() method to call the long-running pick_sheep() slot and move it from the main GUI thread onto a separate thread. PySide PyQt python @Slot() def pick_sheep_safely(self): self.thread_manager.start(self.pick_sheep) # This is where the magic happens! python @pyqtSlot() def pick_sheep_safely(self): self.thread_manager.start(self.pick_sheep) # This is where the magic happens! Also, make sure that you connect the pick_sheep_safely() slot with the pressed signal of self.pick_sheep_button. So, in the __init__ block of the MainWindow class, you should have python self.pick_sheep_button.pressed.connect(self.pick_sheep_safely) And if you followed along, the code of our improved demo app should now be: PySide6 PyQt6 PyQt5 python import time from PySide6.QtCore import Slot, QThreadPool, QTimer from PySide6.QtWidgets import ( QLabel, QWidget, QMainWindow, QPushButton, QVBoxLayout, QApplication, ) class MainWindow(QMainWindow): def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) self.setFixedSize(250, 100) self.setWindowTitle("Sheep Picker") self.sheep_number = 1 self.timer = QTimer() self.picked_sheep_label = QLabel() self.counted_sheep_label = QLabel() self.layout = QVBoxLayout() self.main_widget = QWidget() self.thread_manager = QThreadPool() self.pick_sheep_button = QPushButton("Pick a sheep!") self.layout.addWidget(self.counted_sheep_label) self.layout.addWidget(self.pick_sheep_button) self.layout.addWidget(self.picked_sheep_label) self.main_widget.setLayout(self.layout) self.setCentralWidget(self.main_widget) self.timer.timeout.connect(self.count_sheep) self.pick_sheep_button.pressed.connect(self.pick_sheep_safely) self.timer.start() @Slot() def count_sheep(self): self.sheep_number += 1 self.counted_sheep_label.setText(f"Counted {self.sheep_number} sheep.") @Slot() def pick_sheep(self): self.picked_sheep_label.setText(f"Sheep {self.sheep_number} picked!") time.sleep(5) # This function doesn't affect GUI responsiveness anymore... @Slot() def pick_sheep_safely(self): self.thread_manager.start(self.pick_sheep) # ...since .start() is used! if __name__ == "__main__": app = QApplication([]) main_window = MainWindow() main_window.show() app.exec() python import time from PyQt6.QtCore import pyqtSlot, QThreadPool, QTimer from PyQt6.QtWidgets import ( QLabel, QWidget, QMainWindow, QPushButton, QVBoxLayout, QApplication, ) class MainWindow(QMainWindow): def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) self.setFixedSize(250, 100) self.setWindowTitle("Sheep Picker") self.sheep_number = 1 self.timer = QTimer() self.picked_sheep_label = QLabel() self.counted_sheep_label = QLabel() self.layout = QVBoxLayout() self.main_widget = QWidget() self.thread_manager = QThreadPool() self.pick_sheep_button = QPushButton("Pick a sheep!") self.layout.addWidget(self.counted_sheep_label) self.layout.addWidget(self.pick_sheep_button) self.layout.addWidget(self.picked_sheep_label) self.main_widget.setLayout(self.layout) self.setCentralWidget(self.main_widget) self.timer.timeout.connect(self.count_sheep) self.pick_sheep_button.pressed.connect(self.pick_sheep_safely) self.timer.start() @pyqtSlot() def count_sheep(self): self.sheep_number += 1 self.counted_sheep_label.setText(f"Counted {self.sheep_number} sheep.") @pyqtSlot() def pick_sheep(self): self.picked_sheep_label.setText(f"Sheep {self.sheep_number} picked!") time.sleep(5) # This function doesn't affect GUI responsiveness anymore... @pyqtSlot() def pick_sheep_safely(self): self.thread_manager.start(self.pick_sheep) # ...since .start() is used! if __name__ == "__main__": app = QApplication([]) main_window = MainWindow() main_window.show() app.exec() python import time from PyQt5.QtCore import pyqtSlot, QThreadPool, QTimer from PyQt5.QtWidgets import ( QLabel, QWidget, QMainWindow, QPushButton, QVBoxLayout, QApplication, ) class MainWindow(QMainWindow): def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) self.setFixedSize(250, 100) self.setWindowTitle("Sheep Picker") self.sheep_number = 1 self.timer = QTimer() self.picked_sheep_label = QLabel() self.counted_sheep_label = QLabel() self.layout = QVBoxLayout() self.main_widget = QWidget() self.thread_manager = QThreadPool() self.pick_sheep_button = QPushButton("Pick a sheep!") self.layout.addWidget(self.counted_sheep_label) self.layout.addWidget(self.pick_sheep_button) self.layout.addWidget(self.picked_sheep_label) self.main_widget.setLayout(self.layout) self.setCentralWidget(self.main_widget) self.timer.timeout.connect(self.count_sheep) self.pick_sheep_button.pressed.connect(self.pick_sheep_safely) self.timer.start() @pyqtSlot() def count_sheep(self): self.sheep_number += 1 self.counted_sheep_label.setText(f"Counted {self.sheep_number} sheep.") @pyqtSlot() def pick_sheep(self): self.picked_sheep_label.setText(f"Sheep {self.sheep_number} picked!") time.sleep(5) # This function doesn't affect GUI responsiveness anymore... @pyqtSlot() def pick_sheep_safely(self): self.thread_manager.start(self.pick_sheep) # ...since .start() is used! if __name__ == "__main__": app = QApplication([]) main_window = MainWindow() main_window.show() app.exec() When you press the Pick a sheep! button now, the pick_sheep() slot is executed on a separate thread and no longer blocks the main GUI thread. The sheep counting goes on, and the GUI remains responsive – even though our demo app still has to complete a long-running task in the background. Try increasing the length of the delay now – for example, time.sleep(10) – and notice that it doesn’t affect the GUI anymore. Conclusion And that’s it! I hope you’ll find the extended .start() method of QThreadPool helpful in any of your PyQt/PySide GUI apps that have long-running tasks to be executed in the background. More