Qt 6.2.2 Released

We have released Qt 6.2.2 today. Along with close to 300 new bug fixes it brings security updates, an updated MinGW compiler and re-introduces two modules especially beneficial for automotive customers. 

Adobe XD Bridge TP  for Qt Design Studio released!

Adobe XD Bridge TP  for Qt Design Studio released!

Qt Design Studio is a UI design and development tool that enables designers and developers to rapidly prototype and develop complex UIs.

With Qt Design Studio 2.2.2 comes a technogly preview of the Qt Bridge for Adobe XD. If you want to try out the technolgy preview, then install the Qt Bridge for Adobe XD plugin to Adobe XD, double-click qtbridge.xdx in the xd_bridge folder in the installation directory of Qt Design Studio 2.2.2.
The latest documentation for the Qt Bridge for Adobe XD can be found here

Qt VS Tools for Visual Studio 2022

We are happy to announce the release of the Qt Visual Studio Tools (v2.8.0) extension for Visual Studio 2022. Installation packages are available for download at the VS Marketplace or directly in the VS 2022 IDE: select Extensions > Manage Extensions from the IDE menu, and then search for "qt".

API Stability is No Black Magic!

You might be surprised how much time R&D teams spend on fixing breaking APIs. Imagine you are the Head of Product, and your R&D team just finished a ground-breaking product after two years of software development. You are ramping up and fine-tuning the production with various global variants for six months to crank out that new product in volumes. Shipping large quantities of products, you get customer feedback that you haven’t received before, and you ask your R&D team to improve the software. Now the R&D team tells you that - instead of only fixing the customer issue - the team also needs to fix the APIs of the product because the latest release of the cross-platform UI toolkit broke several of the APIs you are using. Instead of spending a few days to the customer's issue, your team spends weeks updating everything, including API documentation and integration tests. Does this sound like an unrealistic scenario?

How to Integrate Qt WebAssembly (WASM) and the Browser using JavaScript

The introduction of WebAssembly on most major browsers around 2017 opened up a new world of possibilities. Apps and libraries written in languages other than JavaScript, like C++, Rust, C#, Go and Python, can now run embedded in your browser. This means that you can compile and publish native apps for the web, without in-the-middle stores, complex deployment steps and app reviews getting in the way of you and your users.

QML 3D: First impressions

If we think about it for a minute, it is impossible to imagine an app without a UI. We need buttons, checkboxes, dropdown menus and much more. Almost all of these controls are created using 2D elements. However, sometimes we need to put some depth into the UI.

During work on many different apps, I always „thought” only in 2 dimensions and it turned out to be kind of a trap. When you think about it there are plenty of cases when you need one extra dimension in your app. Let’s take the software for a 3D printer as an example. How can rendering a preview of a model to print be implemented only in two dimensions? This is why I took off in search of a solution that will allow me to create 3D elements in my applications using already known technologies such as QML. In ScytheStudio we can easily deepen our knowledge in the area that interests us, what is more, we gain not only time for development but also resources.

Fortunately, there is such thing as Qt Quick 3D, which provides us with an easy high-level API for creating elements in 3D using familiar QML language. It also covers areas like animations, cameras, lightning or materials. The goal of QtQuick 3D is to provide tools for programmers without in-depth knowledge of complex computer graphics.

A solution that doesn’t flood you with complicated mathematical formulas right at the start and fits my technology stack perfectly caught my attention right from the bat. My name is Vins and in this post, I will share, with you, my first impressions on exploring the topic of QML 3D.

Let’s start our journey!

I eagerly started by going through the documentation and watching video tutorials. After a short introduction to the world of Qt Quick 3D, I decided to start a simple project. The first thing I had to do was downloading the Qt3D module through the maintenance tool, as it is not included in the core module. After that, you just need to add an import statement in the qml file and voilà, you can start using Qt Quick 3D!

Below you can see my first program created using Qt Quick 3D. The lack of fluidity is due to the necessary compression of the GIF file.

import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15
import QtQuick3D 1.15
import QtQuick3D.Materials 1.15
import QtQuick3D.Helpers 1.15

 

Window {
  id: root

  width: 640
  height: 960
  visible: true
  title: qsTr("QML 3D")

  View3D {
    id: view
    anchors.fill: parent

    environment: SceneEnvironment {
      clearColor: "#112220"
      backgroundMode: SceneEnvironment.Color
    }

    Node {
      id: scene

      PerspectiveCamera {
        id: camera
        z: 300
      }

      DirectionalLight {
        z: 400
        brightness: 100
      }

      Model {
        source: "#Cube"

        materials: DefaultMaterial {
          diffuseColor: "green"
        }

        PropertyAnimation on eulerRotation.x {
          loops: Animation.Infinite
          duration: 5000
          to: 360
          from: 0
        }

        PropertyAnimation on eulerRotation.y {
          loops: Animation.Infinite
          duration: 5000
          to: 360
          from: 0
        }
      }
    }
  }
}

It may not look spectacular, but for 30 minutes of work, I’m proud of the results 😉

At the beginning of the project, you can see the initialization of a standard Window item. Next in the hierarchy is the View3D object. This is, so to speak, the root object for all 3D elements. I replaced the default environment attribute with a custom SceneEnviorment, with a custom background colour.

Next, you can see an object of the Node type, i.e. a class from which all Qt Quick 3D elements derive. In its body, I placed 3 elements: PerspectiveCamera, DirectionalLight and Model. Let’s analyse them one by one:

PerspectiveCamera provides us with a „view” of the entire scene. It is an eye through which we observe the scene.

DirectionalLight illuminates our whole scene. It is important not to forget about this element because without light most objects will be black (humans perceive colours because the light that bounces off materials is making us see colours).

Model is a physical object that we create. In this example, I used the built-in cube object and the default material which I changed to green. In order to make the cube move, I added two animations that change the rotation of the cube along the X and Y-axis.

Blender + Qt Quick 3D = Great Combo!

As we covered the basics, it’s time for something more difficult. Reading the documentation I found out that Qt Quick 3D supports using models created in applications like Blender, Maya or 3ds max. To import such a model, we need to convert them to QML code first. To do that we will use the built-in Balsam Asset Import Tool (It is located in the path: Qt/Tools/QtDesignStudio-XXX/qt5_design_studio_reduced_version where XXX is your actual QtDesignStudio version code). To demonstrate how this works, I created a simple „house” model in Blender.

Blender is a free solution with which anyone can become an architect

As we now have a model we can use the previously mentioned Balsam. After running it we get a .qml file that is able to be used in our program. A command for running it looks as follows:

balsam [options] source_file_name

 

Our „house” after conversion to QML

Time for a challenge!

I decided it was time to gather all the knowledge and create something more interesting.

The first thing was to add a sphere object to imitate the sun. To do this I used a Model object with source set to #Sphere and yellow colour. Inside it, I added the PointLight item which was responsible for the sun rays.

Next, I added a side menu allowing me to rotate the „house” on all axes, move the sun model, change the strength of its light, and two switches allowing me to turn off the scene light and the sun.

In order to add more interactivity to the application, I used the WasdController allowing me to move around the scene using the keys on my keyboard.


Home, sweet home

 

Summary and my thoughts

I must admit I was very positively surprised by Qt Quick 3D. As a person without any experience in 3D graphics, I was able to understand its mechanics and use it for my needs. The extensive documentation was helpful here, including examples and sample codes.

Moreover, the set-up itself was straightforward. All you had to do was download the appropriate library using the Qt maintenance tool and use it in your project. Everything you need in one place. There was no need to reconfigure the whole project or complicated attaching of files.

The thing that captivated me most was the simplicity. The high level of abstraction makes it easy to combine 2D interface elements with 3D objects without having to change my way of thinking.

Of course, the examples presented in this article might not be very ambitious, but they are simple examples that allow feeling satisfied. I also encourage you to try your hand with Qt Quick 3D package. You will be surprised how easy and fun it is.

If you would like to read a similar article – feel free to email us with your ideas. Who knows, maybe your idea will inspire us to create another article? Take care and stay tuned 😉

[updated for PyQt6] Actions — Toolbars & Menus — Defining toolbars, menus and keyboard shortcuts with QAction

Next we'll look at some of the common user interface elements, that you've probably seen in many other applications — toolbars and menus. We'll also explore the neat system Qt provides for minimising the duplication between different UI areas — QAction.

Toolbars

One of the most commonly seen user interface elements is the toolbar. Toolbars are bars of icons and/or text used to perform common tasks within an application, for which accessing via a menu would be cumbersome. They are one of the most common UI features seen in many applications. While some complex applications, particularly in the Microsoft Office suite, have migrated to contextual 'ribbon' interfaces, the standard toolbar is usually sufficient for the majority of applications you will create.

Standard GUI elements Standard GUI elements

We'll start with a simple skeleton application, which we can customize. Save the following code in a file named app.py -- this code all the imports you'll need for the later steps.

python
import sys
from PyQt5.QtWidgets import (
    QMainWindow, QApplication,
    QLabel, QToolBar, QAction, QStatusBar
)
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import Qt

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My Awesome App")


app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec()


python
import sys
from PyQt6.QtWidgets import (
    QMainWindow, QApplication,
    QLabel, QToolBar, QStatusBar
)
from PyQt6.QtGui import QAction, QIcon
from PyQt6.QtCore import Qt

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My Awesome App")


app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec()

If you're using PyQt6, notice that QAction is now available via the QtGui module.

Adding a toolbar

Let's start by adding a toolbar to our application.

In Qt toolbars are created from the QToolBar class. To start you create an instance of the class and then call .addToolbar on the QMainWindow. Passing a string in as the first parameter to QToolBar sets the toolbar's name, which will be used to identify the toolbar in the UI.

python
class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My Awesome App")

        label = QLabel("Hello!")
        label.setAlignment(Qt.AlignCenter)

        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        self.addToolBar(toolbar)


    def onMyToolBarButtonClick(self, s):
        print("click", s)

python
class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My Awesome App")

        label = QLabel("Hello!")
        label.setAlignment(Qt.AlignmentFlag.AlignCenter)

        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        self.addToolBar(toolbar)


    def onMyToolBarButtonClick(self, s):
        print("click", s)

Run it! You'll see a thin grey bar at the top of the window. This is your toolbar. Right click and click the name to toggle it off.

A window with a toolbar. A window with a toolbar.

How can I get my toolbar back!? Unfortunately once you remove a toolbar there is now no place to right click to re-add it. So as a general rule you want to either keep one toolbar un-removeable, or provide an alternative interface in the menus to turn toolbars on and off.

We should make the toolbar a bit more interesting. We could just add a QButton widget, but there is a better approach in Qt that gets you some additional features — and that is via QAction. QAction is a class that provides a way to describe abstract user interfaces. What this means in English, is that you can define multiple interface elements within a single object, unified by the effect that interacting with that element has. For example, it is common to have functions that are represented in the toolbar but also the menu — think of something like Edit->Cut which is present both in the Edit menu but also on the toolbar as a pair of scissors, and also through the keyboard shortcut Ctrl-X (Cmd-X on Mac).

Without QAction you would have to define this in multiple places. But with QAction you can define a single QAction, defining the triggered action, and then add this action to both the menu and the toolbar. Each QAction has names, status messages, icons and signals that you can connect to (and much more).

In the code below you can see this first QAction added.

python
class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My Awesome App")

        label = QLabel("Hello!")
        label.setAlignment(Qt.AlignCenter)

        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        self.addToolBar(toolbar)

        button_action = QAction("Your button", self)
        button_action.setStatusTip("This is your button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        toolbar.addAction(button_action)

    def onMyToolBarButtonClick(self, s):
        print("click", s)

python
class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My Awesome App")

        label = QLabel("Hello!")
        label.setAlignment(Qt.AlignmentFlag.AlignCenter)

        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        self.addToolBar(toolbar)

        button_action = QAction("Your button", self)
        button_action.setStatusTip("This is your button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        toolbar.addAction(button_action)

    def onMyToolBarButtonClick(self, s):
        print("click", s)


To start with we create the function that will accept the signal from the QAction so we can see if it is working. Next we define the QAction itself. When creating the instance we can pass a label for the action and/or an icon. You must also pass in any QObject to act as the parent for the action — here we're passing self as a reference to our main window. Strangely for QAction the parent element is passed in as the final parameter.

Next, we can opt to set a status tip — this text will be displayed on the status bar once we have one. Finally we connect the .triggered signal to the custom function. This signal will fire whenever the QAction is triggered (or activated).

Run it! You should see your button with the label that you have defined. Click on it and the our custom function will emit "click" and the status of the button.

Toolbar showing our QAction button. Toolbar showing our QAction button.

Why is the signal always false? The signal passed indicates whether the button is checked, and since our button is not checkable — just clickable — it is always false. We'll show how to make it checkable shortly.

Next we can add a status bar.

We create a status bar object by calling QStatusBar to get a new status bar object and then passing this into .setStatusBar. Since we don't need to change the statusBar settings we can also just pass it in as we create it, in a single line:

python
class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My Awesome App")

        label = QLabel("Hello!")
        label.setAlignment(Qt.AlignCenter)

        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        self.addToolBar(toolbar)

        button_action = QAction("Your button", self)
        button_action.setStatusTip("This is your button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        toolbar.addAction(button_action)

        self.setStatusBar(QStatusBar(self))


    def onMyToolBarButtonClick(self, s):
        print("click", s)

python
class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My Awesome App")

        label = QLabel("Hello!")
        label.setAlignment(Qt.AlignmentFlag.AlignCenter)

        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        self.addToolBar(toolbar)

        button_action = QAction("Your button", self)
        button_action.setStatusTip("This is your button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        toolbar.addAction(button_action)

        self.setStatusBar(QStatusBar(self))

    def onMyToolBarButtonClick(self, s):
        print("click", s)

Run it! Hover your mouse over the toolbar button and you will see the status text in the status bar.

Status bar text is updated as we hover our actions. Status bar text is updated as we hover our actions.

Next we're going to turn our QAction toggleable — so clicking will turn it on, clicking again will turn it off. To do this, we simple call setCheckable(True) on the QAction object.

python
class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My Awesome App")

        label = QLabel("Hello!")
        label.setAlignment(Qt.AlignCenter)

        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        self.addToolBar(toolbar)

        button_action = QAction("Your button", self)
        button_action.setStatusTip("This is your button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        button_action.setCheckable(True)
        toolbar.addAction(button_action)

        self.setStatusBar(QStatusBar(self))

    def onMyToolBarButtonClick(self, s):
        print("click", s)

python
class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My Awesome App")

        label = QLabel("Hello!")
        label.setAlignment(Qt.AlignmentFlag.AlignCenter)

        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        self.addToolBar(toolbar)

        button_action = QAction("Your button", self)
        button_action.setStatusTip("This is your button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        button_action.setCheckable(True)
        toolbar.addAction(button_action)

        self.setStatusBar(QStatusBar(self))

    def onMyToolBarButtonClick(self, s):
        print("click", s)


Run it! Click on the button to see it toggle from checked to unchecked state. Note that custom slot function we create now alternates outputting True and False.

The toolbar button toggled on. The toolbar button toggled on.

There is also a .toggled signal, which only emits a signal when the button is toggled. But the effect is identical so it is mostly pointless.

Things look pretty shabby right now — so let's add an icon to our button. For this I recommend you download the fugue icon set by designer Yusuke Kamiyamane. It's a great set of beautiful 16x16 icons that can give your apps a nice professional look. It is freely available with only attribution required when you distribute your application — although I am sure the designer would appreciate some cash too if you have some spare.

Fugue Icon Set — Yusuke Kamiyamane Fugue Icon Set — Yusuke Kamiyamane

Select an image from the set (in the examples here I've selected the file bug.png) and copy it into the same folder as your source code. We can create a QIcon object by passing the name of the file to the class, e.g. QIcon('bug.png') -- if you place the file in another folder you will need a full relative or absolute path to it. Finally, to add the icon to the QAction (and therefore the button) we simply pass it in as the first parameter when creating the QAction.

You also need to let the toolbar know how large your icons are, otherwise your icon will be surrounded by a lot of padding. You can do this by calling .setIconSize() with a QSize object.

python
class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My Awesome App")

        label = QLabel("Hello!")
        label.setAlignment(Qt.AlignCenter)

        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        toolbar.setIconSize(QSize(16,16))
        self.addToolBar(toolbar)

        button_action = QAction(QIcon("bug.png"), "Your button", self)
        button_action.setStatusTip("This is your button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        button_action.setCheckable(True)
        toolbar.addAction(button_action)

        self.setStatusBar(QStatusBar(self))


    def onMyToolBarButtonClick(self, s):
        print("click", s)

python
class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My Awesome App")

        label = QLabel("Hello!")
        label.setAlignment(Qt.AlignmentFlag.AlignCenter)

        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        toolbar.setIconSize(QSize(16,16))
        self.addToolBar(toolbar)

        button_action = QAction(QIcon("bug.png"), "Your button", self)
        button_action.setStatusTip("This is your button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        button_action.setCheckable(True)
        toolbar.addAction(button_action)

        self.setStatusBar(QStatusBar(self))


    def onMyToolBarButtonClick(self, s):
        print("click", s)

Run it! The QAction is now represented by an icon. Everything should function exactly as it did before.

Our action button with an icon. Our action button with an icon.

Note that Qt uses your operating system default settings to determine whether to show an icon, text or an icon and text in the toolbar. But you can override this by using .setToolButtonStyle. This slot accepts any of the following flags from the Qt. namespace:

PyQt5 flag / PyQt6 flag (long name) Behavior
Qt.ToolButtonIconOnly / Qt.ToolButtonIconOnly Icon only, no text
Qt.ToolButtonTextOnly / Qt.ToolButtonTextOnly Text only, no icon
Qt.ToolButtonTextBesideIcon / Qt.ToolButtonTextBesideIcon Icon and text, with text beside the icon
Qt.ToolButtonTextUnderIcon / Qt.ToolButtonTextUnderIcon Icon and text, with text under the icon
Qt.ToolButtonFollowStyle / Qt.ToolButtonFollowStyle Follow the host desktop style

The default value is Qt.ToolButtonFollowStyle, meaning that your application will default to following the standard/global setting for the desktop on which the application runs. This is generally recommended to make your application feel as native as possible.

Finally, we can add a few more bits and bobs to the toolbar. We'll add a second button and a checkbox widget. As mentioned you can literally put any widget in here, so feel free to go crazy.

python
import sys

from PyQt5.QtCore import QSize, Qt
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import (
    QAction,
    QApplication,
    QCheckBox,
    QLabel,
    QMainWindow,
    QStatusBar,
    QToolBar,
)

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        label = QLabel("Hello!")
        label.setAlignment(Qt.AlignCenter)

        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        toolbar.setIconSize(QSize(16, 16))
        self.addToolBar(toolbar)

        button_action = QAction(QIcon("bug.png"), "&Your button", self)
        button_action.setStatusTip("This is your button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        button_action.setCheckable(True)
        toolbar.addAction(button_action)

        toolbar.addSeparator()

        button_action2 = QAction(QIcon("bug.png"), "Your &button2", self)
        button_action2.setStatusTip("This is your button2")
        button_action2.triggered.connect(self.onMyToolBarButtonClick)
        button_action2.setCheckable(True)
        toolbar.addAction(button_action2)

        toolbar.addWidget(QLabel("Hello"))
        toolbar.addWidget(QCheckBox())

        self.setStatusBar(QStatusBar(self))

    def onMyToolBarButtonClick(self, s):
        print("click", s)


app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()


python
import sys

from PyQt6.QtCore import QSize, Qt
from PyQt6.QtGui import QAction, QIcon
from PyQt6.QtWidgets import (
    QApplication,
    QCheckBox,
    QLabel,
    QMainWindow,
    QStatusBar,
    QToolBar,
)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        label = QLabel("Hello!")
        label.setAlignment(Qt.AlignmentFlag.AlignCenter)

        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        toolbar.setIconSize(QSize(16, 16))
        self.addToolBar(toolbar)

        button_action = QAction(QIcon("bug.png"), "&Your button", self)
        button_action.setStatusTip("This is your button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        button_action.setCheckable(True)
        toolbar.addAction(button_action)

        toolbar.addSeparator()

        button_action2 = QAction(QIcon("bug.png"), "Your &button2", self)
        button_action2.setStatusTip("This is your button2")
        button_action2.triggered.connect(self.onMyToolBarButtonClick)
        button_action2.setCheckable(True)
        toolbar.addAction(button_action2)

        toolbar.addWidget(QLabel("Hello"))
        toolbar.addWidget(QCheckBox())

        self.setStatusBar(QStatusBar(self))

    def onMyToolBarButtonClick(self, s):
        print("click", s)


app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()


Run it! Now you see multiple buttons and a checkbox.

Toolbar with an action and two widgets. Toolbar with an action and two widgets.

Menus are another standard component of UIs. Typically they are on the top of the window, or the top of a screen on macOS. They allow access to all standard application functions. A few standard menus exist — for example File, Edit, Help. Menus can be nested to create hierarchical trees of functions and they often support and display keyboard shortcuts for fast access to their functions.

Standard GUI elements - Menus Standard GUI elements - Menus

To create a menu, we create a menubar we call .menuBar() on the QMainWindow. We add a menu on our menu bar by calling .addMenu(), passing in the name of the menu. I've called it '&File'. The ampersand defines a quick key to jump to this menu when pressing Alt.

This won't be visible on macOS. Note that this is different to a keyboard shortcut — we'll cover that shortly.

This is where the power of actions comes in to play. We can reuse the already existing QAction to add the same function to the menu. To add an action you call .addAction passing in one of our defined actions.

python
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        label = QLabel("Hello!")
        label.setAlignment(Qt.AlignCenter)

        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        toolbar.setIconSize(QSize(16, 16))
        self.addToolBar(toolbar)

        button_action = QAction(QIcon("bug.png"), "&Your button", self)
        button_action.setStatusTip("This is your button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        button_action.setCheckable(True)
        toolbar.addAction(button_action)

        toolbar.addSeparator()

        button_action2 = QAction(QIcon("bug.png"), "Your &button2", self)
        button_action2.setStatusTip("This is your button2")
        button_action2.triggered.connect(self.onMyToolBarButtonClick)
        button_action2.setCheckable(True)
        toolbar.addAction(button_action2)

        toolbar.addWidget(QLabel("Hello"))
        toolbar.addWidget(QCheckBox())

        self.setStatusBar(QStatusBar(self))

        menu = self.menuBar()

        file_menu = menu.addMenu("&File")
        file_menu.addAction(button_action)

    def onMyToolBarButtonClick(self, s):
        print("click", s)


python

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        label = QLabel("Hello!")
        label.setAlignment(Qt.AlignmentFlag.AlignCenter)

        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        toolbar.setIconSize(QSize(16, 16))
        self.addToolBar(toolbar)

        button_action = QAction(QIcon("bug.png"), "&Your button", self)
        button_action.setStatusTip("This is your button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        button_action.setCheckable(True)
        toolbar.addAction(button_action)

        toolbar.addSeparator()

        button_action2 = QAction(QIcon("bug.png"), "Your &button2", self)
        button_action2.setStatusTip("This is your button2")
        button_action2.triggered.connect(self.onMyToolBarButtonClick)
        button_action2.setCheckable(True)
        toolbar.addAction(button_action2)

        toolbar.addWidget(QLabel("Hello"))
        toolbar.addWidget(QCheckBox())

        self.setStatusBar(QStatusBar(self))

        menu = self.menuBar()

        file_menu = menu.addMenu("&File")
        file_menu.addAction(button_action)

    def onMyToolBarButtonClick(self, s):
        print("click", s)

Click the item in the menu and you will notice that it is toggleable — it inherits the features of the QAction.

Menu shown on the window -- on macOS this will be at the top of the screen. Menu shown on the window -- on macOS this will be at the top of the screen.

Let's add some more things to the menu. Here we'll add a separator to the menu, which will appear as a horizontal line in the menu, and then add the second QAction we created.

python
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        label = QLabel("Hello!")
        label.setAlignment(Qt.AlignCenter)

        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        toolbar.setIconSize(QSize(16, 16))
        self.addToolBar(toolbar)

        button_action = QAction(QIcon("bug.png"), "&Your button", self)
        button_action.setStatusTip("This is your button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        button_action.setCheckable(True)
        toolbar.addAction(button_action)

        toolbar.addSeparator()

        button_action2 = QAction(QIcon("bug.png"), "Your &button2", self)
        button_action2.setStatusTip("This is your button2")
        button_action2.triggered.connect(self.onMyToolBarButtonClick)
        button_action2.setCheckable(True)
        toolbar.addAction(button_action2)

        toolbar.addWidget(QLabel("Hello"))
        toolbar.addWidget(QCheckBox())

        self.setStatusBar(QStatusBar(self))

        menu = self.menuBar()

        file_menu = menu.addMenu("&File")
        file_menu.addAction(button_action)
        file_menu.addSeparator()
        file_menu.addAction(button_action2)

    def onMyToolBarButtonClick(self, s):
        print("click", s)


python
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        label = QLabel("Hello!")
        label.setAlignment(Qt.AlignmentFlag.AlignCenter)

        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        toolbar.setIconSize(QSize(16, 16))
        self.addToolBar(toolbar)

        button_action = QAction(QIcon("bug.png"), "&Your button", self)
        button_action.setStatusTip("This is your button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        button_action.setCheckable(True)
        toolbar.addAction(button_action)

        toolbar.addSeparator()

        button_action2 = QAction(QIcon("bug.png"), "Your &button2", self)
        button_action2.setStatusTip("This is your button2")
        button_action2.triggered.connect(self.onMyToolBarButtonClick)
        button_action2.setCheckable(True)
        toolbar.addAction(button_action2)

        toolbar.addWidget(QLabel("Hello"))
        toolbar.addWidget(QCheckBox())

        self.setStatusBar(QStatusBar(self))

        menu = self.menuBar()

        file_menu = menu.addMenu("&File")
        file_menu.addAction(button_action)
        file_menu.addSeparator()
        file_menu.addAction(button_action2)

    def onMyToolBarButtonClick(self, s):
        print("click", s)

Run it! You should see two menu items with a line between them.

Our actions showing in the menu. Our actions showing in the menu.

You can also use ampersand to add accelerator keys to the menu to allow a single key to be used to jump to a menu item when it is open. Again this doesn't work on macOS.

To add a submenu, you simply create a new menu by calling addMenu() on the parent menu. You can then add actions to it as normal. For example:

python
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        label = QLabel("Hello!")
        label.setAlignment(Qt.AlignCenter)

        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        toolbar.setIconSize(QSize(16, 16))
        self.addToolBar(toolbar)

        button_action = QAction(QIcon("bug.png"), "&Your button", self)
        button_action.setStatusTip("This is your button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        button_action.setCheckable(True)
        toolbar.addAction(button_action)

        toolbar.addSeparator()

        button_action2 = QAction(QIcon("bug.png"), "Your &button2", self)
        button_action2.setStatusTip("This is your button2")
        button_action2.triggered.connect(self.onMyToolBarButtonClick)
        button_action2.setCheckable(True)
        toolbar.addAction(button_action2)

        toolbar.addWidget(QLabel("Hello"))
        toolbar.addWidget(QCheckBox())

        self.setStatusBar(QStatusBar(self))

        menu = self.menuBar()

        file_menu = menu.addMenu("&File")
        file_menu.addAction(button_action)
        file_menu.addSeparator()

        file_submenu = file_menu.addMenu("Submenu")
        file_submenu.addAction(button_action2)

    def onMyToolBarButtonClick(self, s):
        print("click", s)

python
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        label = QLabel("Hello!")
        label.setAlignment(Qt.AlignmentFlag.AlignCenter)

        self.setCentralWidget(label)

        toolbar = QToolBar("My main toolbar")
        toolbar.setIconSize(QSize(16, 16))
        self.addToolBar(toolbar)

        button_action = QAction(QIcon("bug.png"), "&Your button", self)
        button_action.setStatusTip("This is your button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        button_action.setCheckable(True)
        toolbar.addAction(button_action)

        toolbar.addSeparator()

        button_action2 = QAction(QIcon("bug.png"), "Your &button2", self)
        button_action2.setStatusTip("This is your button2")
        button_action2.triggered.connect(self.onMyToolBarButtonClick)
        button_action2.setCheckable(True)
        toolbar.addAction(button_action2)

        toolbar.addWidget(QLabel("Hello"))
        toolbar.addWidget(QCheckBox())

        self.setStatusBar(QStatusBar(self))

        menu = self.menuBar()

        file_menu = menu.addMenu("&File")
        file_menu.addAction(button_action)
        file_menu.addSeparator()

        file_submenu = file_menu.addMenu("Submenu")
        file_submenu.addAction(button_action2)

    def onMyToolBarButtonClick(self, s):
        print("click", s)

Submenu nested in the File menu. Submenu nested in the File menu.

Finally we'll add a keyboard shortcut to the QAction. You define a keyboard shortcut by passing setKeySequence() and passing in the key sequence. Any defined key sequences will appear in the menu.

Note that the keyboard shortcut is associated with the QAction and will still work whether or not the QAction is added to a menu or a toolbar.

Key sequences can be defined in multiple ways - either by passing as text, using key names from the Qt namespace, or using the defined key sequences from the Qt namespace. Use the latter wherever you can to ensure compliance with the operating system standards.

The completed code, showing the toolbar buttons and menus is shown below.

```python:PyQt5

class MainWindow(QMainWindow): def init(self): super().init()

python
    self.setWindowTitle("My App")

    label = QLabel("Hello!")

    # The `Qt` namespace has a lot of attributes to customize
    # widgets. See: http://doc.qt.io/qt-5/qt.html
    label.setAlignment(Qt.AlignCenter)

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

    toolbar = QToolBar("My main toolbar")
    toolbar.setIconSize(QSize(16, 16))
    self.addToolBar(toolbar)

    button_action = QAction(QIcon("bug.png"), "&Your button", self)
    button_action.setStatusTip("This is your button")
    button_action.triggered.connect(self.onMyToolBarButtonClick)
    button_action.setCheckable(True)
    # You can enter keyboard shortcuts using key names (e.g. Ctrl+p)
    # Qt.namespace identifiers (e.g. Qt.CTRL + Qt.Key_P)
    # or system agnostic identifiers (e.g. QKeySequence.Print)
    button_action.setShortcut(QKeySequence("Ctrl+p"))
    toolbar.addAction(button_action)

    toolbar.addSeparator()

    button_action2 = QAction(QIcon("bug.png"), "Your &button2", self)
    button_action2.setStatusTip("This is your button2")
    button_action2.triggered.connect(self.onMyToolBarButtonClick)
    button_action2.setCheckable(True)
    toolbar.addAction(button_action)

    toolbar.addWidget(QLabel("Hello"))
    toolbar.addWidget(QCheckBox())

    self.setStatusBar(QStatusBar(self))

    menu = self.menuBar()

    file_menu = menu.addMenu("&File")
    file_menu.addAction(button_action)

    file_menu.addSeparator()

    file_submenu = file_menu.addMenu("Submenu")

    file_submenu.addAction(button_action2)

def onMyToolBarButtonClick(self, s):
    print("click", s)

python
```python:PyQt6

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        label = QLabel("Hello!")

        # The `Qt` namespace has a lot of attributes to customize
        # widgets. See: http://doc.qt.io/qt-5/qt.html
        label.setAlignment(Qt.AlignmentFlag.AlignCenter)

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

        toolbar = QToolBar("My main toolbar")
        toolbar.setIconSize(QSize(16, 16))
        self.addToolBar(toolbar)

        button_action = QAction(QIcon("bug.png"), "&Your button", self)
        button_action.setStatusTip("This is your button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        button_action.setCheckable(True)
        # You can enter keyboard shortcuts using key names (e.g. Ctrl+p)
        # Qt.namespace identifiers (e.g. Qt.CTRL + Qt.Key_P)
        # or system agnostic identifiers (e.g. QKeySequence.Print)
        button_action.setShortcut(QKeySequence("Ctrl+p"))
        toolbar.addAction(button_action)

        toolbar.addSeparator()

        button_action2 = QAction(QIcon("bug.png"), "Your &button2", self)
        button_action2.setStatusTip("This is your button2")
        button_action2.triggered.connect(self.onMyToolBarButtonClick)
        button_action2.setCheckable(True)
        toolbar.addAction(button_action)

        toolbar.addWidget(QLabel("Hello"))
        toolbar.addWidget(QCheckBox())

        self.setStatusBar(QStatusBar(self))

        menu = self.menuBar()

        file_menu = menu.addMenu("&File")
        file_menu.addAction(button_action)

        file_menu.addSeparator()

        file_submenu = file_menu.addMenu("Submenu")

        file_submenu.addAction(button_action2)

    def onMyToolBarButtonClick(self, s):
        print("click", s)


Experiment with building your own menus using QAction and QMenu.

For more, see the complete PyQt5 tutorial.

[updated for PySide6] Creating your first app with PySide — A simple Hello World! application with Python and Qt

PySide, also known as Qt for Python, is a Python library for creating GUI applications using the Qt toolkit. PySide is the official binding for Qt on Python and is now developed by The Qt Company itself.

There are two major versions available: PySide2 based on Qt5 and PySide6 based on Qt6. Both versions are almost completely compatible aside from imports, and lack of support for some advanced modules in Qt6.

In this tutorial we'll learn how to use PySide to create desktop applications with Python.

First we'll create a series of simple windows on your desktop to ensure that PySide is working and introduce some of the basic concepts. Then we'll take a brief look at the event loop and how it relates to GUI programming in Python. Finally we'll look at Qt's QMainWindow which offers some useful common interface elements such as toolbars and menus. These will be explored in more detail in the subsequent tutorials.

The entire PySide tutorial features both PySide2 and PySide6 code examples. You can use whichever you prefer.

Creating an application

Let's create our first application! To start create a new Python file — you can call it whatever you like (e.g. app.py) and save it somewhere accessible. We'll write our simple app in this file.

We'll be editing within this file as we go along, and you may want to come back to earlier versions of your code, so remember to keep regular backups.

The source code for the application is shown below. Type it in verbatim, and be careful not to make mistakes. If you do mess up, Python will let you know what's wrong.

python
from PySide2.QtWidgets import QApplication, QWidget

# Only needed for access to command line arguments
import sys

# You need one (and only one) QApplication instance per application.
# Pass in sys.argv to allow command line arguments for your app.
# If you know you won't use command line arguments QApplication([]) works too.
app = QApplication(sys.argv)

# Create a Qt widget, which will be our window.
window = QWidget()
window.show()  # IMPORTANT!!!!! Windows are hidden by default.

# Start the event loop.
app.exec_()


# Your application won't reach here until you exit and the event
# loop has stopped.

python
from PySide6.QtWidgets import QApplication, QWidget

# Only needed for access to command line arguments
import sys

# You need one (and only one) QApplication instance per application.
# Pass in sys.argv to allow command line arguments for your app.
# If you know you won't use command line arguments QApplication([]) works too.
app = QApplication(sys.argv)

# Create a Qt widget, which will be our window.
window = QWidget()
window.show()  # IMPORTANT!!!!! Windows are hidden by default.

# Start the event loop.
app.exec_()

# Your application won't reach here until you exit and the event
# loop has stopped.

First, launch your application. You can run it from the command line like any other Python script, for example --

bash
python3 app.py

Run it! You will now see your window. Qt automatically creates a window with the normal window decorations and you can drag it around and resize it like any window.

What you'll see will depend on what platform you're running this example on. The image below shows the window as displayed on Windows, macOS and Linux (Ubuntu).

Our window, as seen on Windows, macOS and Linux. Our window, as seen on Windows, macOS and Linux.

Stepping through the code

Let's step through the code line by line, so we understand exactly what is happening.

First, we import the PySide classes that we need for the application. Here we're importing QApplication, the application handler and QWidget, a basic empty GUI widget, both from the QtWidgets module.

python
from PySide2.QtWidgets import QApplication, QWidget

python
from PySide6.QtWidgets import QApplication, QWidget

The main modules for Qt are QtWidgets, QtGui and QtCore.

You could do from <module> import * but this kind of global import is generally frowned upon in Python, so we'll avoid it here.

Next we create an instance of QApplication, passing in sys.arg, which is Python list containing the command line arguments passed to the application.

python
app = QApplication(sys.argv)

If you know you won't be using command line arguments to control Qt you can pass in an empty list instead, e.g.

python
app = QApplication([])

Next we create an instance of a QWidget using the variable name window.

python
window = QWidget()
window.show()

In Qt all top level widgets are windows -- that is, they don't have a parent and are not nested within another widget or layout. This means you can technically create a window using any widget you like.

Widgets without a parent are invisible by default. So, after creating the window object, we must always call .show() to make it visible. You can remove the .show() and run the app, but you'll have no way to quit it!

What is a window? - Holds the user-interface of your application - Every application needs at least one (...but can have more) - Application will (by default) exit when last window is closed

Finally, we call app.exec_() to start up the event loop.

What's the event loop?

Before getting the window on the screen, there are a few key concepts to introduce about how applications are organised in the Qt world. If you're already familiar with event loops you can safely skip to the next section.

The core of every Qt Applications is the QApplication class. Every application needs one — and only one — QApplication object to function. This object holds the event loop of your application — the core loop which governs all user interaction with the GUI.

The event loop in Qt.

Each interaction with your application — whether a press of a key, click of a mouse, or mouse movement — generates an event which is placed on the event queue. In the event loop, the queue is checked on each iteration and if a waiting event is found, the event and control is passed to the specific event handler for the event. The event handler deals with the event, then passes control back to the event loop to wait for more events. There is only one running event loop per application.

The QApplication class - QApplication holds the Qt event loop - One QApplication instance required - You application sits waiting in the event loop until an action is taken - There is only one event loop at any time

The underscore is there because exec was a reserved word in Python 2.7. PySide handles this by appending an underscore to the name used in the Qt library. You'll also see .print_() methods on widgets for example.

QMainWindow

As we discovered in the last part, in Qt any widgets can be windows. For example, if you replace QtWidget with QPushButton. In the example below, you would get a window with a single push-able button in it.

python
import sys
from PySide2.QtWidgets import QApplication, QPushButton

app = QApplication(sys.argv)

window = QPushButton("Push Me")
window.show()

app.exec_()

python
import sys
from PySide6.QtWidgets import QApplication, QPushButton

app = QApplication(sys.argv)

window = QPushButton("Push Me")
window.show()

app.exec_()


This is neat, but not really very useful -- it's rare that you need a UI that consists of only a single control! But, as we'll discover later, the ability to nest widgets within other widgets using layouts means you can construct complex UIs inside an empty QWidget.

But, Qt already has a solution for you -- the QMainWindow. This is a pre-made widget which provides a lot of standard window features you'll make use of in your apps, including toolbars, menus, a statusbar, dockable widgets and more. We'll look at these advanced features later, but for now, we'll add a simple empty QMainWindow to our application.

python
import sys
from PySide2.QtWidgets import QApplication, QMainWindow

app = QApplication(sys.argv)

window = QMainWindow()
window.show()

# Start the event loop.
app.exec_()


python
import sys
from PySide6.QtWidgets import QApplication, QMainWindow

app = QApplication(sys.argv)

window = QMainWindow()
window.show()

# Start the event loop.
app.exec_()


Run it! You will now see your main window. It looks exactly the same as before!

So our QMainWindow isn't very interesting at the moment. We can fix that by adding some content. If you want to create a custom window, the best approach is to subclass QMainWindow and then include the setup for the window in the __init__ block. This allows the window behavior to be self contained. We can add our own subclass of QMainWindow — call it MainWindow to keep things simple.

python
import sys

from PySide2.QtCore import QSize, Qt
from PySide2.QtWidgets import QApplication, QMainWindow, QPushButton


# Subclass QMainWindow to customize your application's main window
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        button = QPushButton("Press Me!")

        # Set the central widget of the Window.
        self.setCentralWidget(button)


app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec_()

python
import sys

from PySide2.QtCore import QSize, Qt
from PySide2.QtWidgets import QApplication, QMainWindow, QPushButton


# Subclass QMainWindow to customize your application's main window
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        button = QPushButton("Press Me!")

        # Set the central widget of the Window.
        self.setCentralWidget(button)


app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec_()


For this demo we're using a QPushButton. The core Qt widgets are always imported from the QtWidgets namespace, as are the QMainWindow and QApplication classes. When using QMainWindow we use .setCentralWidget to place a widget (here a QPushButton) in the QMainWindow -- by default it takes the whole of the window. We'll look at how to add multiple widgets to windows in the layouts tutorial.

When you subclass a Qt class you must always call the super __init__ function to allow Qt to set up the object.

In our __init__ block we first use .setWindowTitle() to change the title of our main window. Then we add our first widget — a QPushButton — to the middle of the window. This is one of the basic widgets available in Qt. When creating the button you can pass in the text that you want the button to display.

Finally, we call .setCentralWidget() on the window. This is a QMainWindow specific function that allows you to set the widget that goes in the middle of the window.

Run it! You will now see your window again, but this time with the QPushButton widget in the middle. Pressing the button will do nothing, we'll sort that next.

Our QMainWindow with a single QPushButton on Windows, macOS and Linux. Our QMainWindow with a single QPushButton on Windows, macOS and Linux.

We'll cover more widgets in detail shortly but if you're impatient and would like to jump ahead you can take a look at the http://doc.qt.io/qt-5/widget-classes.html#basic-widget-classes[QWidget documentation]. Try adding the different widgets to your window!

Sizing windows and widgets

The window is currently freely resizable -- if you grab any corner with your mouse you can drag and resize it to any size you want. While it's good to let your users resize your applications, sometimes you may want to place restrictions on minimum or maximum sizes, or lock a window to a fixed size.

In Qt sizes are defined using a QSize object. This accepts width and height parameters in that order. For example, the following will create a fixed size window of 400x300 pixels.

python
import sys

from PySide2.QtCore import QSize, Qt
from PySide2.QtWidgets import QApplication, QMainWindow, QPushButton


# Subclass QMainWindow to customize your application's main window
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        button = QPushButton("Press Me!")

        self.setFixedSize(QSize(400, 300))

        # Set the central widget of the Window.
        self.setCentralWidget(button)


app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec_()


python
import sys

from PySide6.QtCore import QSize, Qt
from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton


# Subclass QMainWindow to customize your application's main window
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        button = QPushButton("Press Me!")

        self.setFixedSize(QSize(400, 300))

        # Set the central widget of the Window.
        self.setCentralWidget(button)


app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec_()


Run it! You will see a fixed size window -- try and resize it, it won't work.

Our fixed-size window, notice that the _maximize_ control is disabled on Windows & Linux. On macOS you _can_ maximize the app to fill the screen, but the central widget will not resize. Our fixed-size window, notice that the _maximize control is disabled on Windows & Linux. On macOS you can maximize the app to fill the screen, but the central widget will not resize._

As well as .setFixedSize() you can also call .setMinimumSize() and .setMaximumSize() to set the minimum and maximum sizes respectively. Experiment with this yourself!

You can use these size methods on any widget.

In this section we've covered the QApplication class, the QMainWindow class, the event loop and experimented with adding a simple widget to a window. In the next section we'll take a look at the mechanisms Qt provides for widgets and windows to communicate with one another and your own code.

For an in-depth guide to building Python GUIs with PySide2 see my book, Create GUI Applications with Python & Qt5.

[updated for PyQt6] Widgets — Using Qt5's library of built-in widgets to build your applications

In Qt (and most User Interfaces) ‘widget’ is the name given to a component of the UI that the user can interact with. User interfaces are made up of multiple widgets, arranged within the window.

Qt comes with a large selection of widgets available, and even allows you to create your own custom and customized widgets.

A quick demo

First let's have a look at some of the most common PyQt widgets. The following code creates a range of PyQt widgets and adds them to a window layout so you can see them together.

We'll cover how layouts work in Qt in the next tutorial.

python
import sys

from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
    QApplication,
    QCheckBox,
    QComboBox,
    QDateEdit,
    QDateTimeEdit,
    QDial,
    QDoubleSpinBox,
    QFontComboBox,
    QLabel,
    QLCDNumber,
    QLineEdit,
    QMainWindow,
    QProgressBar,
    QPushButton,
    QRadioButton,
    QSlider,
    QSpinBox,
    QTimeEdit,
    QVBoxLayout,
    QWidget,
)


# Subclass QMainWindow to customize your application's main window
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("Widgets App")

        layout = QVBoxLayout()
        widgets = [
            QCheckBox,
            QComboBox,
            QDateEdit,
            QDateTimeEdit,
            QDial,
            QDoubleSpinBox,
            QFontComboBox,
            QLCDNumber,
            QLabel,
            QLineEdit,
            QProgressBar,
            QPushButton,
            QRadioButton,
            QSlider,
            QSpinBox,
            QTimeEdit,
        ]

        for w in widgets:
            layout.addWidget(w())

        widget = QWidget()
        widget.setLayout(layout)

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


app = QApplication(sys.argv)
window = MainWindow()
window.show()

app.exec()


python
import sys

from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
    QApplication,
    QCheckBox,
    QComboBox,
    QDateEdit,
    QDateTimeEdit,
    QDial,
    QDoubleSpinBox,
    QFontComboBox,
    QLabel,
    QLCDNumber,
    QLineEdit,
    QMainWindow,
    QProgressBar,
    QPushButton,
    QRadioButton,
    QSlider,
    QSpinBox,
    QTimeEdit,
    QVBoxLayout,
    QWidget,
)


# Subclass QMainWindow to customize your application's main window
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("Widgets App")

        layout = QVBoxLayout()
        widgets = [
            QCheckBox,
            QComboBox,
            QDateEdit,
            QDateTimeEdit,
            QDial,
            QDoubleSpinBox,
            QFontComboBox,
            QLCDNumber,
            QLabel,
            QLineEdit,
            QProgressBar,
            QPushButton,
            QRadioButton,
            QSlider,
            QSpinBox,
            QTimeEdit,
        ]

        for w in widgets:
            layout.addWidget(w())

        widget = QWidget()
        widget.setLayout(layout)

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


app = QApplication(sys.argv)
window = MainWindow()
window.show()

app.exec()


Run it! You'll see a window appear containing all the widgets we've created.

Big ol' list of widgets on Windows, Mac & Ubuntu Linux. Big ol' list of widgets on Windows, Mac & Ubuntu Linux.

Lets have a look at all the example widgets, from top to bottom:

Widget What it does
QCheckbox A checkbox
QComboBox A dropdown list box
QDateEdit For editing dates and datetimes
QDateTimeEdit For editing dates and datetimes
QDial Rotatable dial
QDoubleSpinbox A number spinner for floats
QFontComboBox A list of fonts
QLCDNumber A quite ugly LCD display
QLabel Just a label, not interactive
QLineEdit Enter a line of text
QProgressBar A progress bar
QPushButton A button
QRadioButton A toggle set, with only one active item
QSlider A slider
QSpinBox An integer spinner
QTimeEdit For editing times

There are far more widgets than this, but they don’t fit so well! You can see them all by checking the Qt documentation.

Next, we'll step through some of the most commonly used widgets and look at them in more detail. To experiment with the widgets we'll need a simple application to put them in. Save the following code to a file named app.py and run it to make sure it's working.

python
import sys
from PyQt5.QtWidgets import (
    QMainWindow, QApplication,
    QLabel, QCheckBox, QComboBox, QListBox, QLineEdit,
    QLineEdit, QSpinBox, QDoubleSpinBox, QSlider
)
from PyQt5.QtCore import Qt

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My App")


app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec()


python
import sys
from PyQt6.QtWidgets import (
    QMainWindow, QApplication,
    QLabel, QCheckBox, QComboBox, QListBox, QLineEdit,
    QLineEdit, QSpinBox, QDoubleSpinBox, QSlider
)
from PyQt6.QtCore import Qt

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My App")


app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec()

In the code above we've imported a number of Qt widgets. Now we'll step through each of those widgets in turn, adding them to our application and seeing how they behave.

QLabel

We'll start the tour with QLabel, arguably one of the simplest widgets available in the Qt toolbox. This is a simple one-line piece of text that you can position in your application. You can set the text by passing in a str as you create it:

python
widget = QLabel("Hello")

Or, by using the .setText() method:

python
widget = QLabel("1")  # The label is created with the text 1.
widget.setText("2")   # The label now shows 2.

You can also adjust font parameters, such as the size of the font or the alignment of text in the widget.

python
class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My App")

        widget = QLabel("Hello")
        font = widget.font()
        font.setPointSize(30)
        widget.setFont(font)
        widget.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)

        self.setCentralWidget(widget)

python
class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My App")

        widget = QLabel("Hello")
        font = widget.font()
        font.setPointSize(30)
        widget.setFont(font)
        widget.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter)

        self.setCentralWidget(widget)

QLabel on Windows, Mac & Ubuntu Linux. QLabel on Windows, Mac & Ubuntu Linux.

Font tip Note that if you want to change the properties of a widget font it is usually better to get the current font, update it and then apply it back. This ensures the font face remains in keeping with the desktop conventions.

The alignment is specified by using a flag from the Qt. namespace. The flags available for horizontal alignment are:

PyQt5 flag / PyQt6 flag (long name) Behavior
Qt.AlignLeft / Qt.AlignmentFlag.AlignLeft Aligns with the left edge.
Qt.AlignRight / Qt.AlignmentFlag.AlignRight Aligns with the right edge.
Qt.AlignHCenter / Qt.AlignmentFlag.AlignHCenter Centers horizontally in the available space.
Qt.AlignJustify / Qt.AlignmentFlag.AlignJustify Justifies the text in the available space.

The flags available for vertical alignment are:

PyQt5 flag / PyQt6 flag (long name) Behavior
Qt.AlignTop / Qt.AlignmentFlag.AlignTop Aligns with the top.
Qt.AlignBottom / Qt.AlignmentFlag.AlignBottom Aligns with the bottom.
Qt.AlignVCenter / Qt.AlignmentFlag.AlignVCenter Centers vertically in the available space.

You can combine flags together using pipes (|), however note that you can only use vertical or horizontal alignment flag at a time.

python
align_top_left = Qt.AlignLeft | Qt.AlignTop

Note that you use an OR pipe (`|`) to combine the two flags (not A &amp; B). This is because the flags are non-overlapping bitmasks. e.g. Qt.AlignmentFlag.AlignLeft has the hexadecimal value 0x0001, while Qt.AlignmentFlag.AlignBottom is 0x0040. By ORing together we get the value 0x0041 representing 'bottom left'. This principle applies to all other combinatorial Qt flags. If this is gibberish to you, feel free to ignore and move on. Just remember to use |

Finally, there is also a shorthand flag that centers in both directions simultaneously:

PyQt5 Flag PyQt5 Flag Behavior
Qt.AlignCenter Qt.AlignmentFlag.AlignCenter Centers horizontally and vertically

Weirdly, you can also use QLabel to display an image using .setPixmap(). This accepts an pixmap, which you can create by passing an image filename to QPixmap. In the example files provided with this book you can find a file otje.jpg which you can display in your window as follows:

python
widget.setPixmap(QPixmap('otje.jpg'))

"Otje" the cat. "Otje" the cat.

What a lovely face. By default the image scales while maintaining its aspect ratio. If you want it to stretch and scale to fit the window completely you can set .setScaledContents(True) on the QLabel.

python
widget.setScaledContents(True)

QCheckBox

The next widget to look at is QCheckBox() which, as the name suggests, presents a checkable box to the user. However, as with all Qt widgets there are number of configurable options to change the widget behaviors.

python
class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My App")

        widget = QCheckBox()
        widget.setCheckState(Qt.Checked)

        # For tristate: widget.setCheckState(Qt.PartiallyChecked)
        # Or: widget.setTriState(True)
        widget.stateChanged.connect(self.show_state)

        self.setCentralWidget(widget)


    def show_state(self, s):
        print(s == Qt.Checked)
        print(s)

python
class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My App")

        widget = QCheckBox()
        widget.setCheckState(Qt.CheckState.Checked)

        # For tristate: widget.setCheckState(Qt.PartiallyChecked)
        # Or: widget.setTriState(True)
        widget.stateChanged.connect(self.show_state)

        self.setCentralWidget(widget)


    def show_state(self, s):
        print(s == Qt.CheckState.Checked)
        print(s)

QCheckBox on Windows, Mac & Ubuntu Linux. QCheckBox on Windows, Mac & Ubuntu Linux.

You can set a checkbox state programmatically using .setChecked or .setCheckState. The former accepts either True or False representing checked or unchecked respectively. However, with .setCheckState you also specify a particular checked state using a Qt. namespace flag:

PyQt5 flag / PyQt6 flag (long name) Behavior
Qt.Unchecked / Qt.CheckState.Unchecked Item is unchecked
Qt.PartiallyChecked / Qt.CheckState.PartiallyChecked Item is partially checked
Qt.Checked / Qt.CheckState.Checked Item is unchecked

A checkbox that supports a partially-checked (Qt.CheckState.PartiallyChecked) state is commonly referred to as 'tri-state', that is being neither on nor off. A checkbox in this state is commonly shown as a greyed out checkbox, and is commonly used in hierarchical checkbox arrangements where sub-items are linked to parent checkboxes.

If you set the value to Qt.CheckState.PartiallyChecked the checkbox will become tristate. You can also .setTriState(True) to set tristate support on a You can also set a checkbox to be tri-state without setting the current state to partially checked by using .setTriState(True)

You may notice that when the script is running the current state number is displayed as an int with checked = 2, unchecked = 0, and partially checked = 1. You don’t need to remember these values, the Qt.Checked namespace variable == 2 for example. This is the value of these state's respective flags. This means you can test state using state == Qt.Checked.

QComboBox

The QComboBox is a drop down list, closed by default with an arrow to open it. You can select a single item from the list, with the currently selected item being shown as a label on the widget. The combo box is suited to selection of a choice from a long list of options.

You have probably seen the combo box used for selection of font faces, or size, in word processing applications. Although Qt actually provides a specific font-selection combo box as QFontComboBox.

You can add items to a QComboBox by passing a list of strings to .addItems(). Items will be added in the order they are provided.

python
class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My App")

        widget = QComboBox()
        widget.addItems(["One", "Two", "Three"])

        # Sends the current index (position) of the selected item.
        widget.currentIndexChanged.connect( self.index_changed )

        # There is an alternate signal to send the text.
        widget.textChanged.connect( self.text_changed )

        self.setCentralWidget(widget)


    def index_changed(self, i): # i is an int
        print(i)

    def text_changed(self, s): # s is a str
        print(s)

QComboBox on Windows, Mac & Ubuntu Linux. QComboBox on Windows, Mac & Ubuntu Linux.

The .currentIndexChanged signal is triggered when the currently selected item is updated, by default passing the index of the selected item in the list. There is also a .currentTextChanged signal which instead provides the label of the currently selected item, which is often more useful.

QComboBox can also be editable, allowing users to enter values not currently in the list and either have them inserted, or simply used as a value. To make the box editable:

python
widget.setEditable(True)

You can also set a flag to determine how the insert is handled. These flags are stored on the QComboBox class itself and are listed below:

PyQt5 flag / PyQt6 flag (long name) Behavior
QComboBox.NoInsert / QComboBox.InsertPolicy.NoInsert No insert
QComboBox.InsertAtTop / QComboBox.InsertPolicy.InsertAtTop Insert as first item
QComboBox.InsertAtCurrent / QComboBox.InsertPolicy.InsertAtCurrent Replace currently selected item
QComboBox.InsertAtBottom / QComboBox.InsertPolicy.InsertAtBottom Insert after last item
QComboBox.InsertAfterCurrent / QComboBox.InsertPolicy.InsertAfterCurrent Insert after current item
QComboBox.InsertBeforeCurrent / QComboBox.InsertPolicy.InsertBeforeCurrent Insert before current item
QComboBox.InsertAlphabetically / QComboBox.InsertPolicy.InsertAlphabetically Insert in alphabetical order

To use these, apply the flag as follows:

python
widget.setInsertPolicy(QComboBox.InsertPolicy.InsertAlphabetically)

You can also limit the number of items allowed in the box by using .setMaxCount, e.g.

python
widget.setMaxCount(10)

For a more in-depth look at the QComboBox take a look at my QComboBox documentation.

QListWidget

This widget is similar to QComboBox, except options are presented as a scrollable list of items. It also supports selection of multiple items at once. A QListWidget offers an currentItemChanged signal which sends the QListItem (the element of the list widget), and a currentTextChanged signal which sends the text of the current item.

python
class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My App")

        widget = QListWidget()
        widget.addItems(["One", "Two", "Three"])

        widget.currentItemChanged.connect(self.index_changed)
        widget.currentTextChanged.connect(self.text_changed)

        self.setCentralWidget(widget)


    def index_changed(self, i): # Not an index, i is a QListItem
        print(i.text())

    def text_changed(self, s): # s is a str
        print(s)

QListWidget on Windows, Mac & Ubuntu Linux. QListWidget on Windows, Mac & Ubuntu Linux.

QLineEdit

The QLineEdit widget is a simple single-line text editing box, into which users can type input. These are used for form fields, or settings where there is no restricted list of valid inputs. For example, when entering an email address, or computer name.

python
class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle("My App")

        widget = QLineEdit()
        widget.setMaxLength(10)
        widget.setPlaceholderText("Enter your text")

        #widget.setReadOnly(True) # uncomment this to make readonly

        widget.returnPressed.connect(self.return_pressed)
        widget.selectionChanged.connect(self.selection_changed)
        widget.textChanged.connect(self.text_changed)
        widget.textEdited.connect(self.text_edited)

        self.setCentralWidget(widget)


    def return_pressed(self):
        print("Return pressed!")
        self.centralWidget().setText("BOOM!")

    def selection_changed(self):
        print("Selection changed")
        print(self.centralWidget().selectedText())

    def text_changed(self, s):
        print("Text changed...")
        print(s)

    def text_edited(self, s):
        print("Text edited...")
        print(s)

QLineEdit on Windows, Mac & Ubuntu Linux. QLineEdit on Windows, Mac & Ubuntu Linux.

As demonstrated in the above code, you can set a maximum length for the text in a line edit.

The QLineEdit has a number of signals available for different editing events including when return is pressed (by the user), when the user selection is changed. There are also two edit signals, one for when the text in the box has been edited and one for when it has been changed. The distinction here is between user edits and programmatic changes. The textEdited signal is only sent when the user edits text.

Additionally, it is possible to perform input validation using an input mask to define which characters are supported and where. This can be applied to the field as follows:

python
widget.setInputMask('000.000.000.000;_')

The above would allow a series of 3-digit numbers separated with periods, and could therefore be used to validate IPv4 addresses.

QSpinBox and QDoubleSpinBox

QSpinBox provides a small numerical input box with arrows to increase and decrease the value. QSpinBox supports integers while the related widget QDoubleSpinBox supports floats.

python
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        widget = QSpinBox()
        # Or: widget = QDoubleSpinBox()

        widget.setMinimum(-10)
        widget.setMaximum(3)
        # Or: widget.setRange(-10,3)

        widget.setPrefix("$")
        widget.setSuffix("c")
        widget.setSingleStep(3)  # Or e.g. 0.5 for QDoubleSpinBox
        widget.valueChanged.connect(self.value_changed)
        widget.textChanged.connect(self.value_changed_str)

        self.setCentralWidget(widget)

    def value_changed(self, i):
        print(i)

    def value_changed_str(self, s):
        print(s)

Run it and you'll see a numeric entry box. The value shows pre and post fix units, and is limited to the range +3 to -10.

QSpinBox on Windows, Mac & Ubuntu Linux. QSpinBox on Windows, Mac & Ubuntu Linux.

The demonstration code above shows the various features that are available for the widget.

To set the range of acceptable values you can use setMinimum and setMaximum, or alternatively use setRange to set both simultaneously. Annotation of value types is supported with both prefixes and suffixes that can be added to the number, e.g. for currency markers or units using .setPrefix and .setSuffix respectively.

Clicking on the up and down arrows on the widget will increase or decrease the value in the widget by an amount, which can be set using .setSingleStep. Note that this has no effect on the values that are acceptable to the widget.

Both QSpinBox and QDoubleSpinBox have a .valueChanged signal which fires whenever their value is altered. The raw .valueChanged signal sends the numeric value (either an int or a float) while .textChanged sends the value as a string, including both the prefix and suffix characters.

QSlider

QSlider provides a slide-bar widget, which functions internally much like a QDoubleSpinBox. Rather than display the current value numerically, it is represented by the position of the slider handle along the length of the widget. This is often useful when providing adjustment between two extremes, but where absolute accuracy is not required. The most common use of this type of widget is for volume controls.

There is an additional .sliderMoved signal that is triggered whenever the slider moves position and a .sliderPressed signal that emits whenever the slider is clicked.

python
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        widget = QSlider()

        widget.setMinimum(-10)
        widget.setMaximum(3)
        # Or: widget.setRange(-10,3)

        widget.setSingleStep(3)

        widget.valueChanged.connect(self.value_changed)
        widget.sliderMoved.connect(self.slider_position)
        widget.sliderPressed.connect(self.slider_pressed)
        widget.sliderReleased.connect(self.slider_released)

        self.setCentralWidget(widget)

    def value_changed(self, i):
        print(i)

    def slider_position(self, p):
        print("position", p)

    def slider_pressed(self):
        print("Pressed!")

    def slider_released(self):
        print("Released")

Run this and you'll see a slider widget. Drag the slider to change the value.

QSlider on Windows, Mac & Ubuntu Linux. QSlider on Windows, Mac & Ubuntu Linux.

You can also construct a slider with a vertical or horizontal orientation by passing the orientation in as you create it. The orientation flags are defined in the Qt. namespace. For example --

python
widget.QSlider(Qt.Vertical)

python
widget.QSlider(Qt.Orientiation.Vertical)

Or --

python
widget.QSlider(Qt.Horizontal)

python
widget.QSlider(Qt.Orientiation.Horizontal)

QDial

Finally, the QDial is a rotatable widget that functions just like the slider, but appears as an analogue dial. This looks nice, but from a UI perspective is not particularly user friendly. However, they are often used in audio applications as representation of real-world analogue dials.

python
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")

        widget = QDial()
        widget.setRange(-10, 100)
        widget.setSingleStep(0.5)

        widget.valueChanged.connect(self.value_changed)
        widget.sliderMoved.connect(self.slider_position)
        widget.sliderPressed.connect(self.slider_pressed)
        widget.sliderReleased.connect(self.slider_released)

        self.setCentralWidget(widget)

    def value_changed(self, i):
        print(i)

    def slider_position(self, p):
        print("position", p)

    def slider_pressed(self):
        print("Pressed!")

    def slider_released(self):
        print("Released")

Run this and you'll see a circular dial, rotate it to select a number from the range.

QDial on Windows, Mac & Ubuntu Linux. QDial on Windows, Mac & Ubuntu Linux.

The signals are the same as for QSlider and retain the same names (e.g. .sliderMoved).

Conclusion

This concludes our brief tour of the common widgets used in PyQt applications. To see the full list of available widgets, including all their signals and attributes, take a look at the Qt documentation.

For an in-depth guide to building Python GUIs with PyQt5 see my book, Create GUI Applications with Python & Qt5.

Fast Duplicate Tracking

In 2019, I optimized QStringList::removeDuplicates() by using std::pmp::unordered_set with a std::pmr::monotonic_buffer_resource, when available. The class that I wrote to encapsulate this optimization has since been re-implemented three times. The latest iteration has recently landed in KDToolBox.

If you have code that looks a bit like this:

SomeLocalContainer seen;
~~~~
    if (seen.contains(x))
        continue;
    seen.insert(x);

then you should read on.

Before my change, the QStringList::removeDuplicates() function used a QSet to keep track of “seen” strings, so it could remove already-seen strings from the list the second time they are encountered, thereby removing all duplicate strings from the list.

There are quite a few problems with this approach.

Double Lookup

The contains() + insert() anti-pattern performs two look-ups for the same key, because the position that contains() internally has calculated is lost by the time contains() returns and thus has to be re-calculated in insert() again. We know that the position will be the same, but the compiler doesn’t. The STL associative containers return a pair (boooooo!) of iterator and bool, with the latter indicating whether the item was inserted (true) or was found to pre-exist (false). Since we want to insert anyway, we can just attempt the insert() and check whether it changed something:

std::unordered_set<~~~> seen;
~~~~
    if (auto [it, inserted] = seen.insert(x); !inserted)
        continue; // was already present
    // no need for seen.insert(x): already done

While this is still quite a mouthful, we’ve managed to reduce the look-ups by half.

The Qt containers lack this convenient API, of course, instead forcing the user to check whether the size of the container changed, like this:

QSet<~~~~> seen;
~~~~
    const auto oldSize = seen.size();
    seen.insert(x);
    if (seen.size() == oldSize)
        continue; // was already present
    // no need for seen.insert(x): already inserted

It’s odd, but works, too.

Allocations, Allocations

While QSet or std::unordered_set are fast once constructed, they both allocate a new node per element inserted. So in the usual case where you don’t have (many) duplicates, you’re performing O(N) memory allocations, N being the number of candidates to check.

How nice it would be if we could combine the speed of a hash table with the cache-friendliness of, say, a QVarLengthArray. Oh, wait. We can!

In C++17, we finally got some of the Bloomberg Allocators merged and, while quite a few implementations are still behind implementing them, that shouldn’t deter us, thanks to feature test macros.

Taking a look at the std::pmr namespace, we find a few template aliases that appear to implant a non-standard allocator, std::pmr::polymorphic_allocator, into the usual range of STL containers, including vector and unordered_set.

If you want to know all the gory details of that class, book one of KDAB’s Modern C++ trainings. Suffice to say that std::pmr::polymorphic_allocator implements an allocator that gets memory from a std::pmr::memory_resource, which is an interface you can inherit to implement your own custom allocation strategies.

Enter monotonic_buffer_resource

But we don’t even need to do that. We just need to plug one of the existing memory_resources, std::pmr::monotonic_buffer_resource, into std::pmr::unordered_set.

The monotonic_buffer_resource class implements a grow-only memory pool over a static char buffer where deallocation is a no-op and allocation just ups a pointer. In case the initial buffer is exhausted, monotonic_buffer_resource allocates a new block and continues to allocate from there. If that’s exhausted too, it allocates an even larger block, etc.

This allocator is as fast as it gets, but it isn’t thread-safe and it never frees memory (until destroyed itself). But that’s perfect for building temporary containers whose lifetimes are bound by a function.

The setup is a bit verbose, due to the flexibility of the std::pmr framework. But it’s always the same:

char buffer[1024]; // or whatever size fits
std::pmr::monotonic_buffer_resource res{buffer, sizeof buffer};
std::unordered_set<~~~> seen(N, &res); // N: expected number of elements
~~~~
    if (auto [it, ins] = seen.insert(x); !ins)
        continue;
    // already inserted

Now the first 1 KiB of memory is allocated on the stack, and only then do we go to the heap. Passing the expected number of elements to the unordered_set constructor (that’s like a reserve(), not a resize()!) is very important here because, while a re-hash wouldn’t take the nodes out of buffer, it would waste memory for the new bucket list because monotonic_buffer_resource doesn’t re-use freed-up memory again, thus creating gaps in our precious, buffer.

Tying It All Together

So, we have three things that we need to keep in mind:

  1. Avoid double lookups,
  2. Use std::pmr::monotonic_buffer_resource to avoid memory allocations,
  3. …but fall back to QSet or std::unordered_set when std::pmr isn’t available.

Then, it’s just a question of writing a good hash function (which has recently become much simpler).

QDuplicateTracker (private API) bound these together for the first time, with a much nicer API:

QDuplicateTracker<~~~> seen;
seen.reserve(N);
~~~~
    if (seen.hasSeen(x))
        continue;
    // already inserted

Once you know which patterns to look for, the use-cases seem endless: Qt changes in topic “qduplicatetracker”.

I don’t recommend using QDuplicateTracker from Qt 5 anymore though, since, as I mentioned, I have taken this class through another two iterations in customer projects, finally culminating in the addition to KDToolBox. That’s the version you should be using.

Alternatively, it should be possible, even in Qt projects, to use the Qt 6 version, which I have recently improved for the Qt 6.3 release next year, simply by copying its header file into your project and replacing the QHasher internal struct (which depends on implementation details of Qt 6’s QHash container) with a template argument.

I invite you to study the source-code and post any questions that you may have in the comments below.

Happy duplicate tracking!

About KDAB

If you like this article and want to read similar material, consider subscribing via our RSS feed.

Subscribe to KDAB TV for similar informative short video content.

KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.

The post Fast Duplicate Tracking appeared first on KDAB.

QComboBox — Drop-down selection widget

The QComboBox is a simple widget for presenting a list of options to your users in PyQt, taking up the minimum amount of screen space. The widget can have a single selected item, which is displayed in the widget area. When selected a QComboBox pops out a list of possible values from which you can select. A QComboBox can also - optionally - be made editable, so your users can enter their own values onto the list of possible values.

QComboBox QComboBox in its closed and open state

Populating a QComboBox

Items can be added or inserted to a QComboBox, where adding appends the item to the end of the current list, while insert inserts them in a specific index in the existing list. There are convenience methods for adding multiple items to a combobox and also for adding icons to the list. The following example will demonstrate each of these using a series of comboboxes.

python
from PyQt5.QtWidgets import QComboBox, QMainWindow, QApplication, QWidget, QVBoxLayout
from PyQt5.QtGui import QIcon
import sys


class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()

        combobox1 = QComboBox()
        combobox1.addItem('One')
        combobox1.addItem('Two')
        combobox1.addItem('Three')
        combobox1.addItem('Four')

        combobox2 = QComboBox()
        combobox2.addItems(['One', 'Two', 'Three', 'Four'])

        combobox3 = QComboBox()
        combobox3.addItems(['One', 'Two', 'Three', 'Four'])
        combobox3.insertItem(2, 'Hello!')

        combobox4 = QComboBox()
        combobox4.addItems(['One', 'Two', 'Three', 'Four'])
        combobox4.insertItems(2, ['Hello!', 'again'])

        combobox5 = QComboBox()
        icon_penguin = QIcon('animal-penguin.png')
        icon_monkey = QIcon('animal-monkey.png')
        icon_bauble = QIcon('bauble.png')
        combobox5.addItem(icon_penguin, 'Linux')
        combobox5.addItem(icon_monkey, 'Monkeyix')
        combobox5.insertItem(1, icon_bauble, 'Baublix')

        layout = QVBoxLayout()
        layout.addWidget(combobox1)
        layout.addWidget(combobox2)
        layout.addWidget(combobox3)
        layout.addWidget(combobox4)
        layout.addWidget(combobox5)

        container = QWidget()
        container.setLayout(layout)

        self.setCentralWidget(container)

    def current_text_changed(self, s):
        print("Current text: ", s)


app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec_()

Run the example and compare the result with each series of add and insert steps.

Adding items to a QComboBox Adding items to a QComboBox

You can replace the text at a specific index in the list by calling .setItemText() for example.

python
widget.setItemText(3, 'new text')

Finally, you can clear a QComboBox -- removing all items in it -- by calling .clear().

python
widget.clear()

QComboBox signals

The QComboBox emits a number of signals on state changes. When the currently selected item changes, the widget emits .currentIndexChanged() and .currentTextChanged() signals. The first receives the index of the selected entry while the second receives the text of that item. There is a further signal .activated() which is emitted for user-selections whether the index is changed or not: selecting the same entry again will still emit the signal. Highlighting an entry in the QComboBox popup list will emit the .highlighted() signal.

The following example demonstrates these signals in action.

python
from PyQt5.QtWidgets import QComboBox, QMainWindow, QApplication
import sys


class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()

        combobox = QComboBox()
        combobox.addItems(['One', 'Two', 'Three', 'Four'])

        # Connect signals to the methods.
        combobox.activated.connect(self.activated)
        combobox.currentTextChanged.connect(self.text_changed)
        combobox.currentIndexChanged.connect(self.index_changed)

        self.setCentralWidget(combobox)

    def activated(Self, index):
        print("Activated index:", index)

    def text_changed(self, s):
        print("Text changed:", s)

    def index_changed(self, index):
        print("Index changed", index)

app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec_()

If you run the example the signals emitted as you interact with the QComboBox will be shown in the console, giving output like that shown below. Note that when re-selecting the same entry, only the .activated() signal is emitted.

python
Index changed 1
Text changed: Two
Activated index: 1
Index changed 2
Text changed: Three
Activated index: 2
Index changed 3
Text changed: Four
Activated index: 3
Activated index: 3
Activated index: 3

Getting the current state

In addition to the signals, QComboBox has a few methods for getting the current state at any time. For example, you can use .currentIndex() to get the index of the currently selected item in the combobox, or use .currentText() to get the text. With the index you can also look up the text of a specific item using .itemText(). Finally, you can use .count() to get the total number of items in the combobox list.

In the example below, we use the activated signal we've already seen to hook up a number of methods which get information about the combobox and display this in the console. As you select any item in the combobox every method will run printing a message to the console.

python
from PyQt5.QtWidgets import QComboBox, QMainWindow, QApplication
import sys


class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()

        # Keep a reference to combobox on self, so we can access it in our methods.
        self.combobox = QComboBox()
        self.combobox.addItems(['One', 'Two', 'Three', 'Four'])

        # Connect signal to our methods.
        self.combobox.activated.connect(self.check_index)
        self.combobox.activated.connect(self.current_text)
        self.combobox.activated.connect(self.current_text_via_index)
        self.combobox.activated.connect(self.current_count)

        self.setCentralWidget(self.combobox)

    def check_index(self, index):
        cindex = self.combobox.currentIndex()
        print(f"Index signal: {index}, currentIndex {cindex}")

    def current_text(self, _): # We receive the index, but don't use it.
        ctext = self.combobox.currentText()
        print("Current text", ctext)

    def current_text_via_index(self, index):
        ctext = self.combobox.itemText(index)  # Get the text at index.
        print("Current itemText", ctext)

    def current_count(self, index):
        count = self.combobox.count()
        print(f"Index {index+1}/{count}")


app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec_()

Running this and selecting different items in the combobox will show the outputs.

python
Current text Two
Current itemText Two
Index 1/4
Index signal: 2, currentIndex 2
Current text Three
Current itemText Three
Index 2/4
Index signal: 3, currentIndex 3
Current text Four
Current itemText Four
Index 3/4

Editable comboboxes

You can set a QComboBox to be editable allowing the user to type enter the values not currently in the list. Entered values can be added to the list, or just used as a value which is not inserted. To enable editing you can call .setEditable(True) on the widget, while control of how inserts operate is handled through the insert policy. This is set by calling .setInsertPolicy() passing one of the following flags:

Flag Value Behavior
QComboBox.NoInsert 0 No insert
QComboBox.InsertAtTop 1 Insert as first item
QComboBox.InsertAtCurrent 2 Replace currently selected item
QComboBox.InsertAtBottom 3 Insert after last item
QComboBox.InsertAfterCurrent 4 Insert after current item
QComboBox.InsertBeforeCurrent 5 Insert before current item
QComboBox.InsertAlphabetically 6 Insert in alphabetical order

For PyQt6 use the fully-qualified flag name, i.e. QComboBox.InsertPolicy.NoInsert

For example, to insert new values alphabetically, you would use.

python
widget.setInsertPolicy(QComboBox.InsertAlphabetically)

If the combobox is set to be editable, but QComboBox.NoInsert is set as the insert policy, users will be able to enter their own custom values, but they will not be added to the list. In the following example we have two QComboBox widgets: one which is our editable box and the second which controls the insert policy of the first. Note that since the values of the flags (see above) are just numbers from 0 to 6, we can use the index to determine the flag.

python
from PyQt5.QtWidgets import QComboBox, QMainWindow, QApplication, QWidget, QVBoxLayout
import sys


class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()

        # Keep a reference to combobox on self, so we can access it in our methods.
        self.combobox = QComboBox()
        self.combobox.addItems(['One', 'Two', 'Three', 'Four'])
        self.combobox.setEditable(True)

        self.combobox.currentTextChanged.connect(self.current_text_changed)

        # Insert policies
        self.insertpolicy = QComboBox()
        self.insertpolicy.addItems([
            'NoInsert',
            'InsertAtTop',
            'InsertAtCurrent',
            'InsertAtBottom',
            'InsertAfterCurrent',
            'InsertBeforeCUrrent',
            'InsertAlphabetically'
        ])
        # The index in the insertpolicy combobox (0-6) is the correct flag value to set
        # to enable that insert policy.
        self.insertpolicy.currentIndexChanged.connect(self.combobox.setInsertPolicy)

        layout = QVBoxLayout()
        layout.addWidget(self.combobox)
        layout.addWidget(self.insertpolicy)

        container = QWidget()
        container.setLayout(layout)

        self.setCentralWidget(container)

    def current_text_changed(self, s):
        print("Current text: ", s)


app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec_()

If you run this example you'll notice you can now type into the (top) editable combobox and submit them by pressing Enter. The .currentTextChanged() signal will be emitted as you type into the field.

python
Current text:  w
Current text:  wq
Current text:  wqe
Current text:  wqew
Current text:  wqeww
Current text:  wqewwq
Current text:  wqewwqe

By default the insert policy is set to QComboBox.NoInsert so entered values will not be added, just set as the value of the combobox. By selecting the insert policy in the second combobox you can change this behavior, having new entries added to the list.

Editing a QComboBox with insert policy Editing a QComboBox with insert policy

When you allow users to add values to the list, you may wish to constrain the maximum number of entries. This can be done using .setMaxCount, e.g.

python
widget.setMaxCount(10)

For more, see the complete PyQt6 tutorial.

Q&A: How to show a custom cursor on a PyQtGraph plot? — Changing the OS cursor and implementing a custom crosshair

When working with plots changing the cursor can help with pointing accuracy. For example, if you need to isolate particular points in a scatter plot a cross-hair will give you a more accurate view of where you are pointing than the standard arrow cursor.

Changing the mouse cursor

Changing the mouse cursor in a PyQtGraph is fairly simple and uses Qt's built-in support for custom cursors over a widget. Since the PyQtGraph PlotWidget is a subclass of QGraphicsWidget (and therefore QWidget) you can use the standard methods.

python
    cursor = Qt.CrossCursor
    self.graphWidget.setCursor(cursor)

For the complete list of cursor types, see the Qt documentation. The example below shows this in action on a simple PyQtGraph plot.

python
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMainWindow, QApplication
from pyqtgraph import PlotWidget, plot
import pyqtgraph as pg
import sys  # We need sys so that we can pass argv to QApplication
import os

class MainWindow(QMainWindow):

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

        self.graphWidget = pg.PlotWidget()
        self.setCentralWidget(self.graphWidget)

        hour = [1,2,3,4,5,6,7,8,9,10]
        temperature = [30,32,34,32,33,31,29,32,35,45]

        #Add Background colour to white
        self.graphWidget.setBackground('w')
        # Add Title
        self.graphWidget.setTitle("Your Title Here", color="b", size="30pt")
        # Add Axis Labels
        styles = {"color": "#f00", "font-size": "20px"}
        self.graphWidget.setLabel("left", "Temperature (°C)", **styles)
        self.graphWidget.setLabel("bottom", "Hour (H)", **styles)
        #Add legend
        self.graphWidget.addLegend()
        #Add grid
        self.graphWidget.showGrid(x=True, y=True)
        #Set Range
        self.graphWidget.setXRange(0, 10, padding=0)
        self.graphWidget.setYRange(20, 55, padding=0)

        pen = pg.mkPen(color=(255, 0, 0))

        self.graphWidget.plot(hour, temperature, name="Sensor 1",  pen=pen, symbol='+', symbolSize=30, symbolBrush=('b'))

        # Set the cursor for the plotwidget. The mouse cursor will change when over the plot.
        cursor = Qt.CrossCursor
        self.graphWidget.setCursor(cursor)


app = QApplication(sys.argv)
main = MainWindow()
main.show()
app.exec_()

If you run this you'll see a cross-hair cursor appear when you hover your mouse over the plot.

Mouse cursor as crosshair Crosshair cursor on plot

Drawing a custom cursor

If you want to show a completely custom cursor, for example an full-size crosshair over the plot, things are a little trickier. In this case you need to draw the cursor yourself by adding elements to the plot and then listening for mouse events and updating their position.

The code below is an updated version of the plot above, with two crosshair lines added using PyQtGraphs InfiniteLine objects. The lines are drawn in the __init__ method, and then updated in the mouseMoved method.

You could also add any other QGraphicsScene item here instead.

The important part is the SignalProxy which forwards mouse movements to our custom method update_crosshair. The rateLimit parameter sets the maximum number of signals per second which will be sent, to avoid overloading our handler/app with excessive events.

python
from PyQt5.QtWidgets import QMainWindow, QApplication
from PyQt5.QtCore import Qt
from pyqtgraph import PlotWidget, plot
import pyqtgraph as pg
import sys  # We need sys so that we can pass argv to QApplication
import os


class MainWindow(QMainWindow):

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

        self.graphWidget = pg.PlotWidget()
        self.setCentralWidget(self.graphWidget)

        hour = [1,2,3,4,5,6,7,8,9,10]
        temperature = [30,32,34,32,33,31,29,32,35,45]

        #Add Background colour to white
        self.graphWidget.setBackground('w')
        # Add Title
        self.graphWidget.setTitle("Your Title Here", color="b", size="30pt")
        # Add Axis Labels
        styles = {"color": "#f00", "font-size": "20px"}
        self.graphWidget.setLabel("left", "Temperature (°C)", **styles)
        self.graphWidget.setLabel("bottom", "Hour (H)", **styles)
        #Add legend
        self.graphWidget.addLegend()
        #Add grid
        self.graphWidget.showGrid(x=True, y=True)
        #Set Range
        self.graphWidget.setXRange(0, 10, padding=0)
        self.graphWidget.setYRange(20, 55, padding=0)

        pen = pg.mkPen(color=(255, 0, 0))
        self.graphWidget.plot(hour, temperature, name="Sensor 1",  pen=pen, symbol='+', symbolSize=30, symbolBrush=('b'))

        # Add crosshair lines.
        self.crosshair_v = pg.InfiniteLine(angle=90, movable=False)
        self.crosshair_h = pg.InfiniteLine(angle=0, movable=False)
        self.graphWidget.addItem(self.crosshair_v, ignoreBounds=True)
        self.graphWidget.addItem(self.crosshair_h, ignoreBounds=True)

        self.proxy = pg.SignalProxy(self.graphWidget.scene().sigMouseMoved, rateLimit=60, slot=self.update_crosshair)

    def update_crosshair(self, e):
        pos = e[0]
        if self.graphWidget.sceneBoundingRect().contains(pos):
            mousePoint = self.graphWidget.getPlotItem().vb.mapSceneToView(pos)
            self.crosshair_v.setPos(mousePoint.x())
            self.crosshair_h.setPos(mousePoint.y())


app = QApplication(sys.argv)
main = MainWindow()
main.show()
app.exec_()



If you run this example, in addition to the mouse cursor you'll also see a faint yellow line overlaying the plot.

Drawing the custom cursor lines The custom cursor with default mouse cursor hidden

To hide the default mouse cursor over the plot set the curser type to Qt.BlankCursor. For example.

python
class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        # ... rest of __init__ as before

        # Hide the cursor over the plot.
        cursor = Qt.BlankCursor
        self.graphWidget.setCursor(cursor)


This will give the following result when hovering over the plot.

Drawing the custom cursor lines The custom cursor with default mouse cursor hidden

For more, see the complete PyQt5 tutorial.

Q&A: How to fix widgets appearing as separate windows? — Understanding Qt parents and layouts and the effect on widget position

One very common issue when you start creating GUI applications with Python & Qt is having your widgets either disappear or start popping out of your interface as independent windows. This is pretty confusing if you don't understand why it happens, but once you do it's usually very easy to fix.

The key points --

  1. Any widget without a parent is a window. If you create a widget but don't set a parent it will become a separate window.
  2. Widgets without parents (i.e. any windows) are hidden by default and remain hidden until they are shown either by you, or automatically by Qt (for example when selecting tabs in a QTabWidget).
  3. Adding widgets to layouts sets their parent. Widgets not in layouts must have parents set explicitly.
  4. Removing a widget from a layout doesn't remove its parent.
  5. You can remove the parent from a widget by setting the parent to None. This will turn it into a window!

Below is a small example to demonstrate these effects. We create a window, add a layout and a widget.

python
import sys
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QLabel, QApplication

class Window(QWidget):

    def __init__(self):
        super().__init__()

        # This is the widget that we will experiment with: note that we don't
        # set a parenet, and don't add it to the layout.
        self.mywidget = QLabel("Hello")
        self.mywidget.show()  # We need to show it (or focus it) to make it visible.

        add_parent = QPushButton("Add parent")
        add_parent.clicked.connect(self.add_parent)

        remove_parent = QPushButton("Remove parent")
        remove_parent.clicked.connect(self.remove_parent)

        add_layout = QPushButton("Add layout")
        add_layout.clicked.connect(self.add_layout)

        remove_layout = QPushButton("Remove layout")
        remove_layout.clicked.connect(self.remove_layout)

        self.vlayout = QVBoxLayout()

        self.vlayout.addWidget(add_parent)
        self.vlayout.addWidget(remove_parent)
        self.vlayout.addWidget(add_layout)
        self.vlayout.addWidget(remove_layout)

        self.setLayout(self.vlayout)

    def add_parent(self):
        self.mywidget.setParent(self)
        self.mywidget.move(0,0)
        self.mywidget.show()
        print("Added parent, parent is:", self.mywidget.parent())

    def remove_parent(self):
        self.mywidget.setParent(None)
        self.mywidget.show()
        print("Removed parent, parent is:", self.mywidget.parent())

    def add_layout(self):
        self.vlayout.addWidget(self.mywidget)
        print("Added layout, parent is:", self.mywidget.parent())

    def remove_layout(self):
        self.vlayout.removeWidget(self.mywidget)
        print("Removed layout, parent is:", self.mywidget.parent())

app = QApplication(sys.argv)

w = Window()
w.show()

app.exec()

python
import sys
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QPushButton, QLabel, QApplication


class Window(QWidget):

    def __init__(self):
        super().__init__()

        # This is the widget that we will experiment with: note that we don't
        # set a parenet, and don't add it to the layout.
        self.mywidget = QLabel("Hello")
        self.mywidget.show()  # We need to show it (or focus it) to make it visible.

        add_parent = QPushButton("Add parent")
        add_parent.clicked.connect(self.add_parent)

        remove_parent = QPushButton("Remove parent")
        remove_parent.clicked.connect(self.remove_parent)

        add_layout = QPushButton("Add layout")
        add_layout.clicked.connect(self.add_layout)

        remove_layout = QPushButton("Remove layout")
        remove_layout.clicked.connect(self.remove_layout)

        self.vlayout = QVBoxLayout()

        self.vlayout.addWidget(add_parent)
        self.vlayout.addWidget(remove_parent)
        self.vlayout.addWidget(add_layout)
        self.vlayout.addWidget(remove_layout)

        self.setLayout(self.vlayout)

    def add_parent(self):
        self.mywidget.setParent(self)
        self.mywidget.move(0,0)
        self.mywidget.show()
        print("Added parent, parent is:", self.mywidget.parent())

    def remove_parent(self):
        self.mywidget.setParent(None)
        self.mywidget.show()
        print("Removed parent, parent is:", self.mywidget.parent())

    def add_layout(self):
        self.vlayout.addWidget(self.mywidget)
        print("Added layout, parent is:", self.mywidget.parent())

    def remove_layout(self):
        self.vlayout.removeWidget(self.mywidget)
        print("Removed layout, parent is:", self.mywidget.parent())


app = QApplication(sys.argv)

w = Window()
w.show()

app.exec()

python
import sys
from PySide2.QtWidgets import QWidget, QVBoxLayout, QPushButton, QLabel, QApplication


class Window(QWidget):

    def __init__(self):
        super().__init__()

        # This is the widget that we will experiment with: note that we don't
        # set a parenet, and don't add it to the layout.
        self.mywidget = QLabel("Hello")
        self.mywidget.show()  # We need to show it (or focus it) to make it visible.

        add_parent = QPushButton("Add parent")
        add_parent.clicked.connect(self.add_parent)

        remove_parent = QPushButton("Remove parent")
        remove_parent.clicked.connect(self.remove_parent)

        add_layout = QPushButton("Add layout")
        add_layout.clicked.connect(self.add_layout)

        remove_layout = QPushButton("Remove layout")
        remove_layout.clicked.connect(self.remove_layout)

        self.vlayout = QVBoxLayout()

        self.vlayout.addWidget(add_parent)
        self.vlayout.addWidget(remove_parent)
        self.vlayout.addWidget(add_layout)
        self.vlayout.addWidget(remove_layout)

        self.setLayout(self.vlayout)

    def add_parent(self):
        self.mywidget.setParent(self)
        self.mywidget.move(0,0)
        self.mywidget.show()
        print("Added parent, parent is:", self.mywidget.parent())

    def remove_parent(self):
        self.mywidget.setParent(None)
        self.mywidget.show()
        print("Removed parent, parent is:", self.mywidget.parent())

    def add_layout(self):
        self.vlayout.addWidget(self.mywidget)
        print("Added layout, parent is:", self.mywidget.parent())

    def remove_layout(self):
        self.vlayout.removeWidget(self.mywidget)
        print("Removed layout, parent is:", self.mywidget.parent())

app = QApplication(sys.argv)

w = Window()
w.show()

app.exec_()

python
import sys
from PySide6.QtWidgets import QWidget, QVBoxLayout, QPushButton, QLabel, QApplication


class Window(QWidget):

    def __init__(self):
        super().__init__()

        # This is the widget that we will experiment with: note that we don't
        # set a parenet, and don't add it to the layout.
        self.mywidget = QLabel("Hello")
        self.mywidget.show()  # We need to show it (or focus it) to make it visible.

        add_parent = QPushButton("Add parent")
        add_parent.clicked.connect(self.add_parent)

        remove_parent = QPushButton("Remove parent")
        remove_parent.clicked.connect(self.remove_parent)

        add_layout = QPushButton("Add layout")
        add_layout.clicked.connect(self.add_layout)

        remove_layout = QPushButton("Remove layout")
        remove_layout.clicked.connect(self.remove_layout)

        self.vlayout = QVBoxLayout()

        self.vlayout.addWidget(add_parent)
        self.vlayout.addWidget(remove_parent)
        self.vlayout.addWidget(add_layout)
        self.vlayout.addWidget(remove_layout)

        self.setLayout(self.vlayout)

    def add_parent(self):
        self.mywidget.setParent(self)
        self.mywidget.move(0,0)
        self.mywidget.show()
        print("Added parent, parent is:", self.mywidget.parent())

    def remove_parent(self):
        self.mywidget.setParent(None)
        self.mywidget.show()
        print("Removed parent, parent is:", self.mywidget.parent())

    def add_layout(self):
        self.vlayout.addWidget(self.mywidget)
        print("Added layout, parent is:", self.mywidget.parent())

    def remove_layout(self):
        self.vlayout.removeWidget(self.mywidget)
        print("Removed layout, parent is:", self.mywidget.parent())

app = QApplication(sys.argv)

w = Window()
w.show()

app.exec_()

In the initial state the widget isn't added to any layout and has no parent, so when the application is run you'll actually see two floating windows. Push the control buttons to experiment with adding the widget to the layout, setting a parent, clearing the parent and removing it from the layout.

The starting view

Each time the state changes the current parent will be printed to the console. Notice that when you add the widget to the layout, the widget receives the Window as it's parent. This is the normal behavior in Qt: adding a widget to a layout will set it's parent to the widget on which the layout is applied (the container widget). In this case this is the Window itself.

python
Added layout, parent is: <__main__.Window(0x15413656090) at 0x000001542CB98688>

If you remove the widget from the layout, it's parent is not automatically cleared. The widget will remain inside the window, near it's original position. But the layout will re-lay the other items over it.

The widget is removed from the layout, but remains in place

If you remove the parent the widget will return to it's floating state.

Note that when setting the parent on the window we also call .move(0, 0) to move it to a specific point relative to the parent window. This is necessary to ensure we can see the widget: if you remove this, setting the parent will just make the widget disappear. If you have widgets disappearing in your apps, this is a good that they have a parent set but are not properly positioned or added to a layout. As a general rule, use layouts to avoid hitting these kinds of issues!

For an in-depth guide to building Python GUIs with PySide2 see my book, Create GUI Applications with Python & Qt5.

KDE Frameworks – Part 1: KConfig

The KDE Community has been developing a variety of Free Software products using Qt for 25 years now. Among them, the Plasma Desktop Environment, creativity tools like Krita and Kdenlive, educational applications like GCompris, groupware suites like Kontact and countless other applications, utilities, and widgets.

Qt is famous for its rich set of high-quality, cross-platform APIs. However, it does not cover every single use case. Indeed, that would be impossible. So, to fill in the gaps, over time, KDE has created code that has been incorporated into many KDE projects. To foster reusing these battle-tested solutions outside KDE projects, we share this code in the form of modular libraries.

We call these libraries KDE Frameworks.

Currently, there are 83 KDE Frameworks available that offer a wide range of features. For example, KNotifications allows you to create popup notifications on Windows, macOS, Linux, and Android without having to write platform-specific code. Other Frameworks provide wrappers for specialized libraries or interfaces, making them easier to use by Qt programmers. The bluez-qt framework, for example, provides a Qt-style interface to the bluez D-Bus API. Some Frameworks are a collection of useful classes like KWidgetsAddons, which contains a number of useful widgets that are not part of QtWidgets.

As a Qt developer, you likely have used software built using KDE Frameworks without even knowing it. The syntax-highlighting framework that powers KDE applications like Kate and KDevelop is also used in Qt Creator.

A text editor showing source code with syntax highlighting

Syntax highlighting in Kate is powered by the Syntax Highlighting Framework

There are many advantages to leveraging KDE Frameworks. In this series, we will examine some of them, providing practical and real-world examples that will help you learn how to incorporate KDE Frameworks into your own products.

In this first blog in the series, I’d like to introduce you to KConfig.

KConfig is one of the most used frameworks. It allows developers to store and fetch configuration data in the filesystem. Its basic functionality is similar to Qt’s own QSettings, however it provides several additional features.

Before we can use KConfig in our application, we need to add it to our build system. For CMake, this is done as follows:

find_package(KF5Config)

...

target_link_libraries(myapp PRIVATE KF5::ConfigCore)

If your application uses QMake, all you need is:

QT += KConfigCore

The following code shows the basic usage of KConfig:

#include <KConfig>
#include <KConfigGroup>
#include <QDebug>

int main() {

    KConfig config("myappsettings");

    KConfigGroup general = config.group("General");

    qDebug() << general.readEntry("someSetting", "A default value");

    general.writeEntry("someSetting", "A new value");

    qDebug() << general.readEntry("someSetting", "A default value");

}

First, a KConfig object is created. By default, the config is saved to a file with the specified name in QStandardPaths::GenericConfigLocation, however the exact location can be tweaked.

The config entries are organized in groups. Each KConfig object can contain a number of groups and each group holds a number of key-value pairs containing the config data.

To read a config entry, first create a KConfigGroup from the KConfig object and then use readEntry to query a specific key. readEntry takes an optional default value that is used when no data for that key is stored.

To write a setting, writeEntry is used. The data is not immediatley written to the disk. When the KConfigGroup object is destructed, all pending write operations are executed. It is possible to force writing to the disk by using the sync() method.

So far, all of this is possible to do with QSettings as well. So, what’s the benefit of using KConfig?

Both QSettings and KConfig allow for config cascading. Here, config values are read from two locations: a system-wide one and a per-user one. This allows the defining of system-wide defaults and gives users the ability to override the value for them. However, in an enterprise setup, this may be undesirable. KConfig allows system administrators to mark settings as immutable to prevent users from overriding the provided default. This does not require any code changes in the application. The application can query whether a certain key is marked as immutable to disable the relevant UI pieces.

Sometimes two processes access the same config file. Here, it’s important that a process is notified when the other process changes the config so it can react accordingly. KConfigWatcher allows notifying another process about a config change. It does this via D-Bus. So, it works only on systems where D-Bus is available (i.e. Linux).

This plain usage of KConfig (and QSettings) has a number of drawbacks. The library/compiler has no information about the structure of the configuration data. Most of the access is done using string identifiers. These are prone to typing errors and the compiler cannot verify those at build time. There is also no information about the data type of a configuration entry, e.g., whether an entry is a single string, a list of strings, or an integer. Another problem is that KConfig cannot directly be used in a QML context.

KConfig offers the KConfigXT mechanism that solves both of these problems. It is based on a XML description of the configuration data structure. At compile time this information is used to generate a C++ class that is used to access the config. The class can also have the entries exposed as properties so it can be consumed by QML directly.

The above example expressed as XML description looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
      http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
  <kcfgfile name="myappsettings" />
  <group name="General">
    <entry name="someSetting" type="String">
      <label>A setting</label>
      <default>A default value</default>
    </entry>
  </group>
</kcfg>

This is stored in the myappsettings.kcfg file.

The behavior of KConfigXT is controlled by a separate config file, myappsettings.kcfgc:

File=myappsettings.kcfg
ClassName=MyAppSettings
Mutators=true
DefaultValueGetters=true
GenerateProperties=true

Then the above code example becomes:

#include "myappsettings.h"
#include <QDebug>

int main() {

    MyAppSettings settings;

    qDebug() << settings.someSetting();

    settings.setSomeSetting("A new value");

    qDebug() << settings.someSetting();
}

You can also expose the settings to QML by doing:

qmlRegisterSingletonInstance<MyAppSettings>("myapp", 1, 0, "Settings", &settings);

and use them via properties:

import myapp 1.0

console.log(Settings.someSetting)

The settings code is generated by the kconfig_compiler_kf5 executable. As a developer you usually don’t interact with it directly though. Instead there is a CMake macro that takes care of the details.

kconfig_add_kcfg_files(myapp myappsettings.kcfgc)

For a full introduction to KConfigXT, please consult its documentation.

Stay tuned for more posts introducing interesting KDE Frameworks!

About KDAB

If you like this article and want to read similar material, consider subscribing via our RSS feed.

Subscribe to KDAB TV for similar informative short video content.

KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.

The post KDE Frameworks – Part 1: KConfig appeared first on KDAB.

New Release: KD Reports 2.0.0

KD ReportsVersion 2.0.0 of KD Reports has just been released!

KD Reports creates all kinds of reports from within Qt applications. These reports are printable and exportable from code and XML descriptions. KD Reports is a developer tool used in source code, but it allows the use of templates that are created by design staff. Reports may contain text paragraphs, tables, headlines, charts, headers and footers and more. Read more about KD Reports here.

KD Reports 2.0.0 is the first version that fully supports Qt 6 as well as Qt 5. Both the Qt 6 version and Qt 5 version can be installed at the same time. Additionally, the QMake buildsystem has been removed in this version of KD Reports in favour of CMake. Last but not least, Python bindings for KD Reports have been added.

The release highlights can be found at: https://github.com/KDAB/KDReports/releases/tag/kdreports-2.0.0

About KDAB

If you like this article and want to read similar material, consider subscribing via our RSS feed.

Subscribe to KDAB TV for similar informative short video content.

KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.

The post New Release: KD Reports 2.0.0 appeared first on KDAB.