Building Desktop Apps with Python, Qt, and PyQt

Web apps are very popular, but there are still times when only a desktop app can deliver a great user experience. For instance, apps like Adobe After Effects or Word are still mainly used on the desktop so they can use dialog windows and keyboard shortcuts to improve the user's workflow. There are also times when an app has to be very memory efficient and will need to have low latency (graphic intensive apps, for example).

One popular option for developing cross-platform desktop apps is Electron and JavaScript. Another option is Python and PyQt5. PyQt5 is the interface that lets you use the C++-based Qt library from within Python. Qt is a graphical user interface toolkit for building desktop applications. It might be less popular, but I’m going to show you how effective PyQt5 can be for producing a desktop app. First, we’ll build a Hello World app before building a more useful desktop app for freelancer developers to calculate their taxes and hourly rates. You'll need a little bit of Python coding to follow this tutorial.

Python help Python best practice.png

Installing PyQt

You can install PyQt by installing Python and using pip with this command:

pip install PyQt5

You can reference the documentation for PyQt here and if some of the documentation is lacking, you can refer to the official Qt library documentation here.

The former documentation covers the major aspects of the PyQt interfact, while the latter is for the C++-based API of Qt.

Hello World in Python and Qt

Let’s start with a simple example that displays “Hello World”. You can view the full source code here.

We’re going to start by importing the PyQt5 widgets we'll need:

# hello.py
from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QGridLayout, QWidget

Widgets are the objects that are drawn on the screen as part of the graphical user interface in Qt. They are also known as components in other frameworks.

Setting up the application and window

Next, let's set up the application, the window, and layout for the window:

app = QApplication([])
window = QMainWindow()
widget = QWidget(window)
layout = QGridLayout()
widget.setLayout(layout)

To make a layout in Qt, you have to use a generic QWidget object because the window will display one widget at a time. There are other layout types that can be used, such as VBoxLayout to lay out widgets in a vertical direction, or you can use an HBoxLayout for a horizontal layout.

We’re using a grid layout where each widget added to the layout can be placed precisely into a particular row and column, and can span multiple rows or columns. If you were using Electron or making a web app, you would use something like Bootstrap CSS framework or CSS Flexbox to make a grid layout. With Qt, it’s built in and much easier to do.

Now, let’s create the “Hello world!” label:

label = QLabel()
label.setText('Hello world!')

Then we'll add it to the first row and column:

layout.addWidget(label, 0, 0)

Starting the Qt event loop

After that, we’re going to set the window widget to the layout widget, show the window, and start the Qt event loop which'll start the app:

window.setCentralWidget(widget)
window.show()
app.exec_()

Here’s how it'll look:
PyQt_Hello_World_DesktopApp.png

Building a Freelancing Utility App

Now that we've practiced with a simple Hello World app, let's build a desktop app for freelance developers. The window is going to show a table of weekly earnings and allow the freelancer to calculate which marginal tax bracket they fall under by using a dialog box.

You can view the full source code for this app here.

Let's start with some of the structure we used in the Hello World app:

# freelancer.py
from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QGridLayout, \
  QWidget, QPushButton, QTableWidget, QTableWidgetItem, QInputDialog

app = QApplication([])
window = QMainWindow()

screen = QWidget()
layout = QGridLayout()
screen.setLayout(layout)

We’re importing all the Qt widgets we need, and setting up the application and window. We'll also use the grid layout again. This is a good default, but for more complex desktop apps, you will want to evaluate the other layouts that are possible with Qt.

After all the widgets are created we will have this code at the very bottom of the file:

window.setCentralWidget(screen)
window.setWindowTitle('Freelancer Utility')
window.setMinimumSize(500, 200)
window.show()

app.exec_()

This will set the screen widget as the one to display, the title and size of the window, and show it. The app.exec_ kickstarts the Qt event loop.

Opening a Dialog Box For Input

We need something to display on the screen, so let’s start with something basic: two labels that show the freelancer’s yearly income and the top marginal tax rate they should be prepared for come tax season:

yearly_income = QLabel()
yearly_income.setText('Yearly Income: $0.00')
layout.addWidget(yearly_income, 0, 0)

tax_rate = QLabel()
tax_rate.setText('Highest Marginal Tax Rate: 0%')
layout.addWidget(tax_rate, 0, 1)

We’re using the QLabel widget for this and formatting the text in a particular way, before adding each widget to the grid layout. Both of the labels are in the same row, 0. The yearly income label is in the first column with the tax rate label is in the second column.

Creating a Button and Opening a QInputDialog

Let's add a “calculator” button:

button = QPushButton()
button.setText('Calculator')

We need to define the callback that will be called when this button is pressed:

def open_calculator():
    value, ok = QInputDialog.getDouble(
        window, # parent widget
        'Tax Calculator', # window title
        'Yearly Income:', # entry label
        min=0.0,
        max=1000000.0,
    )
    if not ok:
        return
    yearly_income.setText('Yearly Income: ${:,.2f}'.format(value))
    if value <= 9700:
        rate = 10.0
    elif value <= 39475:
        rate = 12.0
    elif value <= 84200:
        rate = 22.0
    elif value <= 160725:
        rate = 24.0
    elif value <= 204100:
        rate = 32.0
    tax_rate.setText('Highest Marginal Tax Rate: {}%'.format(rate))

The first thing to note is that we get two values which we'll have to unpack after calling QInputDialog.getDouble. The first value is the actual value entered by the user, the second value is whether or not the user pressed the “ok” button. We supplied a min and max value to constrain the value.

Notice that we are updating the two labels on the screen with updated values from the input dialog. QInputDialog has multiple static methods to ask for particular types of values from the user. We’re using getDouble here, but we could also use getInt to get an integer, or getText for a string.

Next, let's connect this callback to the button and position it in the layout:

button.clicked.connect(open_calculator)
layout.addWidget(button, 1, 0, 1, 2)

It’s going to be in the second row (remember the rows and columns are 0-based), and will span two columns.

Here’s how it'll look:
DesktopApp PyQt Qt Python.png

Displaying a Table of Data

Now we’ll display a table of the hours worked by the freelancing user and how much they've billed for that work.

We start by defining the table columns:

columns = ('Week', 'Hours Worked', 'Hourly Rate', 'Earned Income')

We’ll use placeholder data:

table_data = [
    [7, 40.0, 100.0],
    [8, 37.5, 85.0],
    [9, 65, 150.0],
]

Setting up the QTableWidget

We’re using the QTableWidget to create the table. Let's begin by setting the number of columns that will be displayed and their labels:

table = QTableWidget()
table.setColumnCount(len(columns))
table.setHorizontalHeaderLabels(columns)

Then we'll set the number of rows that will be displayed. This will match the number of rows we have in table_data:

table.setRowCount(len(table_data))

Using QTableWidgetItem to Display Data in the Table

Now, we'll set each row’s values from the table_data. We'll loop through each column’s values, create a QTableWidgetItem, and store in the exact table cell where it needs to be. We'll also calculate the sum and store it in the last column of the row:

for row_index, row in enumerate(table_data):
    # Set each column value in the table
    for column_index, value in enumerate(row):
        item = QTableWidgetItem(str(value))
        table.setItem(row_index, column_index, item)
    # Calculate the total and add it as another column
    table.setItem(row_index, 3, QTableWidgetItem(str(row[1] * row[2])))

Let's add the table to the layout in the third row, spanning two columns:

layout.addWidget(table, 2, 0, 1, 2)

Don’t forget we have app.exec_ and other code at the end of file to display the window.

Here's how it'll look:
PyQt Qt Python to build Desktop App.png

Conclusion

And that’s how you create a desktop app using Python and PyQt5, backed by the awesome Qt5 GUI framework. Instead of writing JavaScript for Electron or C++ with Qt, you can use Python and Qt to create your next desktop application for Windows, Mac OS X, or Linux. Leave a comment if you try it out, I'd love to know what you think.

Resources

  • Find the full source code of this tutorial here

  • Find the PyQt documentation here

  • Further setup and installation instructions for PyQt can be found here

Free Python Projects .png

Last updated on Jun 18, 2021