PyQt

PyQt

2019-11-14T23:46:37.891Z

Setup

Installation

To install PyQt5 locally:

pipenv install pyqt5
pipenv install PyQt5==5.9.2 ## prevents win8 bugs

Design

Qt Designer

Installation

Qt Designer (40 MB) ships as a part of Qt Creator (IDE, 45 GB), or individually through FMan.

To install Qt Designer:

Templates

  • Dialog with buttons at bottom: Form with the OK and Cancel buttons in the bottom-right corner. Superclass is QDialog.
  • Dialog with buttons at right: Form with the OK and Cancel buttons in the top-right corner. Superclass is QDialog.
  • Dialog without buttons: Form whose superclass is QDialog.
  • Main window: Main application window with a menu bar and a toolbar that can be removed if not required. Superclass is QMainWindow.
  • Widget: Form whose superclass is QWidget.

Process

Select a template, drag and drop UI elements onto the canvas, edit their objectName properties and save the file with a .ui extension.

To use our .ui file with Python, you can load the .ui file directly:

import sys
from PyQt5 import QtWidgets, uic

app = QtWidgets.QApplication(sys.argv)

window = uic.loadUI("demo.ui")
window.show()
app.exec()

Or convert the .ui to Python and couple it.

pipenv run pyuic5 demo.ui -o demo.py ## inside project env

You can open the resulting Python file in an editor to take a look, although you should not edit this file. The power of using Qt Creator is being able to edit, tweak and update your application while you develop. Any changes made to this file will be lost when you update it. However, you can override and tweak anything you like when you import and use the file in your applications.

import sys
from PyQt5 import QtWidgets, uic

from MainWindow import Ui_MainWindow

class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):  subclass from both
    def __init__(self, *args, obj=None, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        self.setupUi(self)  call this

app = QtWidgets.QApplication(sys.argv)

window = MainWindow()
window.show()
app.exec()

Another coupling example:

import sys
from PyQt5.QtWidgets import QDialog, QApplication
from demoLineEdit import *

class MyForm(QDialog):
    def __init__(self):
        super().__init__()
        self.ui = Ui_Dialog()
        self.ui.setupUi(self)
        self.ui.ButtonClickMe.clicked.connect(self.dispmessage)
        self.show()
    def dispmessage(self):
        self.ui.labelResponse.setText("Hello "
        self.ui.lineEditName.text())

if __name__== "__main__":
    app = QApplication(sys.argv)
    w = MyForm()
    w.show()
    sys.exit(app.exec_())

Free-form design

Create an app and a window. There must be only one app, but there may be many windows.

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

class MyMainWindow(QMainWindow): ## always needs a central widget
    ## subclassed QMainWindow because windows are hidden by default
    def __init__(self, *args, **kwargs):
        super(MyMainWindow, self).__init__(*args, **kwargs)

        self.setWindowTitle("My app")

        label = QLabel("This is a label")
        label.setAlignment(Qt.AlignCenter)

        self.setCentralWidget(label)
            Set the central widget of the Window. Widget will expand
            to take up all the space in the window by default.

app = QApplication(sys.argv)
window = MyMainWindow()
window.show()
app.exec_() ## enter the event loop

Running the basic application:

python myApp.py

sys.argv stores CLI arguments. If you know you will not use them:

app = QApplication([]) ## empty list instead of `sys.argv`

Any QWidget without a parent is its own window:

second_window = QWidget()

If you do not create a QMainWindow, any widget without a parent will be a window in its own right. Our custom PowerBar widget will appear as any normal window.

Event handling

A signal is an event, and a slot is a method that is executed on the occurrence of the event.

Signal example:

button.clicked.connect(on_button_clicked)

button.clicked is a signal, .connect() lets us couple it to slot, which is simply a function that us called whenever the signal is emitted. In Python, any function can be a slot.

Flow

  1. An application is constantly listening for events, i.e. interactions such as a mouse click, keyboard input, etc.

  2. Whenever a widget detects an event, it will emit a predefined signal for the event, passing along any related data as a default argument.

  3. This signal is connected, via .connect() on a class property inside the __init__ constructor, to a slot, which serves as an event handler class method.

Connection

Signal Slot
class property inside constructor class method
def __init__(self, *args, **kwargs):
    this.eventHappened.connect(eventHandler)

def eventHandler(self):
    print("event handler called!")

Examples

There are many ways to connect signals and slots:

self.eventHappened(self.eventHandler)

For example, QWidget and its child QMainWindow can emit the signal windowTitleChanged for any window title change event. If this event occurs, the widgets will emit the default signal windowTitleChanged along with the new title as an argument in a QString, to be handled in a method connected to the signal.

def __init__(self, *args, **kwargs):
    super(MainWindow, self).__init__(*args, **kwargs)

        signal connected to method on widget itself (class property)
    self.wTitleChanged.connect(self.handleWindowTitleChange)

## slot to handle event (class method)
def handleWindowTitleChange(self, new_title):
    print(f"New Window Title is: {new_title}")

Note: wTitleChanged should be read as windowTitleChanged. A bug in VS Code prevents correct indentation with the full name.

self.widget.eventHappened(self.eventHandler)

def __init__(self, *args, **kwargs):
    super(MainWindow, self).__init__(*args, **kwargs)

        signal on widget is widget connected to method on widget itself
    self.addButton.pressed.connect(self.add)

self.widget1.eventHappened(self.widget2.eventHandler)

A signal from one attached widget can also be connected to a method in a different attached widget. For example, the custom PowerBar widget below has a QDial whose signal is connected to the method trigger_refresh() belonging to another child Bar.

class PowerBar(QtWidgets.QWidget):

    def __init__(self, steps=5, *args, **kwargs):
        super(PowerBar, self).__init__(*args, **kwargs)

            PowerBar signal connected to child Bar method
        self.dial.valueChanged.connect(self.Bar.trigger_refresh)

Custom signals

class MyClass(QObject):
    signal = pyqtSignal(int)
    instance = MyClass()
    instance.signal.connect(lambda x: print(x * 2))
    instance.signal.emit(7)

Intercepting events

Whenever a widget detects an event, it emits the event-specific signal automatically, through a pre-coded method. Since the emission of signals is pre-coded in the framework, you usually simply specify which signals are connected to which event handlers.

However, you can also intercept events manually, i.e. you can override the method that emits the signal in a class, by creating a child that inherits from that class and overwriting the parent is method in the child class. By overwriting the method, you can (A) replace the signal emission with a different action, or (B) allow for signal emission and also take further action.

For example, the class QMainWindow has a method contextMenuEvent() that is emitted whenever a context menu (i.e., the right-click menu) is about to be shown. To intercept this event, you can override this method by subclassing QMainWindow and creating a method with the same name.

A. Replace signal emission with a different action:

class MyMainWindow(QMainWindow):

    ## ...

    def contextMenuEvent(self, event):
            new action, with no signal emitted
        print("context menu event occurred!")

B. Allow for signal emission and take further action:

class MyMainWindow(QMainWindow):

## ...

    def contextMenuEvent(self, event):
        print("Context menu event!")
        super(QMainWindow, self).contextMenuEvent(event)

Super() calls the parent is constructor and contextMenuEvent(event) calls the parent is method passing in the child is event.

Second explanation of intercepting events

Events in Qt are represented by instances of the QEvent class (or one of its subclasses). For example, QKeyEvent contains information about which key was pressed, and whether a modifier such as Ctrl was pressed along with it.

To process an event, Qt delivers it to one or more objects. For example: Suppose the user presses Esc while the dialog on page 38 is open. Qt first delivers the corresponding QKeyEvent to the Save button, because it currently has the keyboard focus. However, this button does not know how to handle Esc. So Qt next notices the button's parent, which in this case is the containing dialog window. This knows how to react to Esc and closes itself.

In addition to the very general event(...), which handles all events, there are also more specialised event handlers. A good example is keyPressEvent(e), which is only called for key presses. Its parameter e is always a QKeyEvent.

Example of intercepting a widget's default event:

class MainWindow(QMainWindow):

    def closeEvent(self, e):

        if not text.document().isModified():
            return

        answer = QMessageBox.question(
            window,
            None,
            "You have unsaved changes. Save before closing?",
            QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel
        )

        if answer & QMessageBox.Save:
            save()

        elif answer & QMessageBox.Cancel:
            e.ignore()

Logging

print("button pressed!", flush=True)

Traceback

def except_hook(cls, exception, traceback):
    sys.__excepthook__(cls, exception, traceback)
    sys.stderr.flush()

if __name__ == "__main__":
    sys.excepthook = except_hook

fbs

To install fbs locally:

pipenv install fbs ## at project root dir

To install PyInstaller (required by fbs) locally:

pipenv install PyInstaller ## at project root dir

To start a project:

pipenv run fbs startproject ## at project's root dir

The fbs startproject command generates an src dir.

To run a project:

pipenv run fbs run ## at project root dir

fbs will look for build/settings/base.json for entry point

The entry point is at: src/main/python/main.py

When building PyQt5 applications, there are typically a number of components or resources that are used throughout your app. These are commonly stored in the QMainWindow or as global vars which can get a bit messy as your application grows. The ApplicationContext provides a central location for initialising and storing these components, as well as providing access to some core fbs features. The ApplicationContext object also creates and holds a reference to a global QApplication object, available under ApplicationContext.app.

Creating executable and installer

(1) To create executable (i.e., to freeze):

pipenv run fbs freeze

(2) To create installer:

pipenv run fbs installer

If freezing fails, get pypiwin32:

pipenv install pypiwin32

Afterwards, executing the freeze might fail. If running target/admin_stock/admin_stock.exe causes a Module not found: distutils error, then find this file:

C:\Users\IvanOv\.virtualenvs\admin-stock-yFPjNmVr\Lib\site-packages\PyInstaller\hooks\pre_find_module_path\hook-distutils.py

and patch this method:

def pre_find_module_path(api):
    ## Absolute path of the system-wide "distutils" package
    ## when run from within a venv or None otherwise.
    distutils_dir = getattr(distutils, 'distutils_path', None)
    if distutils_dir is not None:

        ## PATCH, see https://github.com/pyinstaller/pyinstaller/issues/4064
        if distutils_dir.endswith('__init__.py'):
            distutils_dir = os.path.dirname(distutils_dir)

        ## Find this package in its parent directory.
        api.search_dirs = [os.path.dirname(distutils_dir)]

Misc

  • super(CustomDialog, self).__init__(*args, **kwargs)
  • event.accept() / event.ignore()
  • QtCore.pyqtSignal() / .emit()

In PyQt there is a difference between how a dialog is made: using exec_() means it is a modal dialog (won't allow you to touch the mainwindow until it is closed) and once closed, it is set for garbage collection.

So the layout does not become the parent of the widget. This makes sense, because widgets can only have other widgets as parents, and layouts are not widgets. All widgets added to a layout will eventually have their parents reset to the parent of the layout (whenever it gets one).

https://stackoverflow.com/questions/30241684/pyqt4-setparent-vs-deletelater

There are two types of dialogs:

  • Modal: It blocks the user from interacting with other parts of the application.
  • Modeless: It allows the user to interact with the rest of the application.

The class that is being inherited is called the super class or base class, and the inheriting class is called a derived class or subclass.

One mandatory first parameter is always defined in a method, and that first parameter is usually named self (though you can give any name to this parameter). The self parameter refers to the instance of the class that calls the method.

The following question arises: how does the layout know what the recommended size of the widget is? Basically, each widget has a property called sizeHint that contains the widget's recommended size. When the window is resized and the layout size also changes, it is through the sizeHint property of the widget that the layout managers know the size requirement of the widget.

In order to apply the size constraints on the widgets, you can make use of the following two properties:

  • minimumSize: If the window size is decreased, the widget will still not become smaller than the size specified in the minimumSize property.
  • maximumSize: Similarly, if the window is increased, the widget will not become larger than the size specified in the maximumSize property. When the preceding properties are set, the value specified in the sizeHint property will be overridden.

A dockable form. That is, you can dock this sign-in form to any of the four sides of the window—top, left, right, and bottom and can even use it as a floatable form

A thread is a small process created for executing a task independently.

Styling

Rich Text

def show_about_dialog():
    text = "<center>" \
    "<h1>Text Editor</h1>" \
    "&#8291;" \
    "<i mg src=icon.svg>" \
    "</center>" \
    "<p>Version 31.4.159.265358<br/>" \
    "Copyright &copy; Company Inc.</p>"
    QMessageBox.about(window, "About Text Editor", text)

This a Qt dialect called "rich text". It sometimes behaves differently from what you would expect in a browser. For example: Browsers display an <i mg> after a <h1> on a new line. Qt doesn't. That's why our code needs the special character #8291 ("invisible separator") to break the two apart. (We could have also used <br/> or <p>...</p> but they add too much space.)

Built-in styles

Built-in styles The coarsest way to change the appearance of your GUI is to set the global style.

app.setStyle("Fusion")

If you like a style but want to change its colors (eg. to a dark theme) then you can use QPalette and app.setPalette(...).

palette = QPalette()
palette.setColor(QPalette.ButtonText, Qt.red)
app.setPalette(palette)

QSS

In addition to the above, you can change the appearance of your application via Qt style sheets (QSS). This is Qt's analogue to CSS.

app.setStyleSheet("QPushButton { margin: 10ex; }")

Once your stylesheet reaches a certain size, it makes sense to put it into a separate file.

with open(appctxt.get_resource("styles.qss")) as f:
    appctxt.app.setStyleSheet(f.read())

In CSS, you can use #main-button to select an element by its ID. QSS has a similar feature: if you call .setObjectName("main-button") on a widget, then you can select it with #main-button. This is very useful for styling individual GUI elements.

Qt's support for High DPI Displays is not entirely seamless. In particular, margins and font sizes can vary wildly across displays. A way to alleviate this is to never use pixel measurements in stylesheets. Instead, use units that are independent of monitor resolution. These include pt, em and ex.

Custom rendering

The standard way to implement custom rendering in Qt is to override paintEvent() and use a QPainter to perform the actual drawing:

def mousePressEvent(self, e):
self._holes.append(e.pos())
super().mousePressEvent(e)

Miscellaneous

Qt's source code

As your usage of Qt becomes more advanced, you may encounter the need to read Qt's C++ source code. This too is nothing to be afraid of. Simply download the source archive called "single" from Qt's home page. Unpack and open the entire folder (!) with an editor that lets you navigate to files by name. (A great choice is Sublime Text with its shortcut Ctrl + P.) Then, you can find the implementation of any class such as QWidget by opening qwidget.cpp.

Design via QtQuick

A somewhat more recent technology is Qt Quick: It offers highly customizable graphics, fluid animations and is more targeted at mobile devices and touch displays. Qt Quick uses a markup language called QML for laying out UIs. It is functionally similar to HTML, CSS and JavaScript. In fact, it supports JavaScript as a scripting language. Its overall syntax is similar to JSON.

In general, you should use Widgets if you want a pretty standard desktop GUI. Otherwise, if you want highly customizable graphics and / or support for mobile devices and touch displays, use Qt Quick.

Ampersand

The ampersand in "&File" gives it the shortcut Alt + F on Windows and Linux. The character following the ampersand defines the shortcut.

Qt docs for C++

The equal sign = in Qt docs for C++.

QString QFileDialog::getOpenFileName(QWidget *parent = nullptr, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = nullptr, QFileDialog::Options options = Options())

Fortunately, all parameters are optional, as indicated by the = after each one.

The double colon :: in Qt docs for C++. You can generally replace these by single dots in Python. We did this when QFileDialog::getOpenFileName became QFileDialog.getOpenFileName.

Initializing a DB

Qt's Model/View classes, which we used above, separate the logic for loading data from the logic that displays it.x

from PyQt5.QtSql import *

app = QApplication([])

db = QSqlDatabase.addDatabase("QSQLITE")
db.setDatabaseName("projects.db")
db.open()

model = QSqlTableModel(None, db)
model.setTable("projects")
model.select()

view = QTableView()
view.setModel(model)
view.show()

app.exec_()

Profiling performance

As your application matures, it is not unlikely that you will run into performance issues at some point. Here is a simple, yet very effective way to debug them.

from my_implementation import run_app
import cProfile
cProfile.run("run_app()", sort="cumtime")

Once your app closes, this prints detailed statistics about how much time was spent in each function.

Errors in threads

Errors that occur in background threads are often not as visible as those in the main thread. If your app is not working as expected and you are using threads, maybe there are some errors you are not being shown.

LGPL

There are some obligations you do need to fulfill when using Qt under the LGPL.

You need to provide users with the license texts of the GPL and the LGPL, as well as the source code of Qt. Furthermore, you need to indicate that Qt is used in your application. And finally, you need to include Qt’s copyright notice.

See the last chapter in "Python and Qt: The Best Parts" by Michael Herrmann.