RESTful Client Applications in Qt 6.7 and Forward

Qt 6.7 introduces convenience improvements for implementing typical RESTful/HTTP client applications. The goal was/is to reduce the repeating networking boilerplate code by up to 40% by addressing the small but systematically repeating needs in a more convenient way. 

These include a new QHttpHeaders class for representing HTTP headers, QNetworkRequestFactory for creating API-specific requests,  QRestAccessManager class for addressing small but often-repeating pieces of code, and QRestReply class for extracting the data from replies and checking for errors. QNetworkRequestFactory, QRestAccessManager, and QRestReply are released as Technical Previews in Qt 6.7.

Qt for MCUs 2.7 released

A new version of Qt for MCUs is available, bringing new features to the Qt Quick Ultralite engine, additional MCU target platforms, and various improvements to our GUI framework for resource-constrained embedded systems.

Reducing Visual Studio Installations with Toolchains

If you work on C++ projects on Windows that need to be built with multiple Visual Studio C++ compiler versions, you need some way to manage the installations of all these build environments. Either you have multiple IDEs installed, or you know about build tools (https://aka.ms/vs/17/release/vs_BuildTools.exe) and maybe keep only the latest full VS IDE plus older Build Tools.

However, it turns out that you can have just the latest IDE but with multiple toolchains installed for older compiler targets. You won’t even need the Build Tools.

To use these toolchains you need to install them in your chosen VS installation and then call vcvarsall.bat with an appropriate parameter.

You can even have no IDE installed if you don’t need it but only the Build Tools with the required toolchains. That’s useful when you use a different IDE like JetBrains Clion or Visual Studio Code. Note, however, that to be license- compliant, you still need a valid Visual Studio subscription.

Installing the toolchain

  1. Go to the Visual Studio Installer and click “Modify” on your main VS version (2022 in my case).
  2. Go to “Individual components” and search for the appropriate toolchain. For example, to get the latest VS2019 C++ compiler in VS 2022 installer, you need to look at this:Screenshot of Visual Studio 2022 installer illustrating how to locate Visual Studio 2019 toolchainHow do you know that 14.29 corresponds to VS 2019? Well, you have to consult this table: https://en.wikipedia.org/wiki/Microsoft_Visual_C%2B%2B#Internal_version_numbering – look at the “runtime library version” column as that’s the C++ compiler version really.
  3. Finish the installation of the desired components.

Setting up the environment from cmd.exe

The only thing you need to do to start with a different toolchain is to pass an option to your vcvarsall.bat invocation:

C:\Users\mikom>"c:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Auxiliary\Build\vcvarsall.bat" -vcvars_ver=14.29

With such a call, I get a shell where cl.exe indeed uses the VS2019 compiler variant:

Screenshot of windows command prompt showing the results of using the discussed technique. It shows that visual studio 2019 compiler, version 19.29 is set up from Visual Studio 2022 installation folder

As you can see, I called vcvarsall.bat from VS2022 yet I got VS2019 variant of the compiler For more info about vcvarsall.bat see: https://learn.microsoft.com/en-us/cpp/build/building-on-the-command-line.

Setting up the environment for PowerShell

If you do your compilation in Powershell, vcvarsall.bat is not very helpful. It will spawn an underlying cmd.exe, set the necessary env vars inside it, and close it without altering your PowerShell environment.

(You may try to do some hacks of printing the environment in the child cmd.exe and adopting it to your Pwsh shell, but that’s a hack).

For setting up a development environment from PowerShell, Microsoft introduced a PowerShell module that does just that.

To get it, you first have to load the module:

Import-Module "C:\Program Files\Microsoft Visual Studio\2022\Professional\Common7\Tools\Microsoft.VisualStudio.DevShell.dll"

And then call the Enter-VsDevShell cmdlet with appropriate parameters:

Enter-VsDevShell -VsInstallPath 'C:\Program Files\Microsoft Visual Studio\2022\Professional\' -DevCmdArguments "-vcvars_ver=14.29" -Arch amd64 -SkipAutomaticLocation

This cmdlet internally passes arguments to vcvarsall.bat so you specify the toolchain version as above.

With this invocation, you get your desired cl.exe compiler:

Screenshot of windows powershell console showing the results of using the discussed technique. It shows that visual studio 2019 compiler, version 19.29 is set up from Visual Studio 2022 installation folder

For more info about Powershell DevShell module see: https://learn.microsoft.com/en-us/visualstudio/ide/reference/command-prompt-powershell

Setting up Qt Creator Kit with a toolchain

Unfortunately, Qt Creator doesn’t detect the toolchains in a single Visual Studio installation as multiple kits. You have to configure the compiler and the kit yourself:

Screenshot showing the Qt Creator kit setup illustrating how to configure custom Visual Studio compiler version through a toolchain

I am a script, how do I know where Visual Studio is installed?

If you want to query the system for VS installation path programatically (to find either vcvarsall.bat or the DevShell PowerShell module) you can use the vswhere tool (https://github.com/microsoft/vswhere).

It’s a small-ish (0.5MB) self-contained .exe so you can just drop it in your repository and don’t care if it’s in the system. You can also install it with winget:

winget install vswhere 

It queries the well-known registry entries and does some other magic to find out what Visual Studio installations your machine has.

By default it looks for the latest version of Visual Studio available and returns easily parseable key:value pairs with various infos about the installation. Most notably, the installation path in installationPath.

It also has various querying capabilities, like showing only the VS installation that have the C++ workload installed.

For example, to get the installation path of the newest Visual Studio with C++ workload, you call:

vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath

Screenshot illustrating the output of calling vswhere to locate Visual Studio installation. Query: vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath results in the output of: C:\Program Files\Microsoft Visual Studio\2022\Professional

Caveats

I haven’t found an easy way to query vcvarsall for something along the lines “give me the latest available toolchain in a given VS product line (2019, 2022 etc.)”. So if you call an explicit version (like 14.29) and a newer one appears, you will still be looking for the older one. However:

  • When vcvarsall.bat is called without any toolchain parameter (vcvars_ver), it defaults to itself so you may assume that it’s the latest one in this installation folder.
  • Microsoft seems to stop bumping the relevant part of the version of the Visual Studio C++ compiler once the next version of VS is out. So for example it seems that 14.29 will be a proper target for VS2019 C++ compiler until the end of time.
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 Reducing Visual Studio Installations with Toolchains appeared first on KDAB.

Drag & Drop Widgets with PySide6 — Sort widgets visually with drag and drop in a container

I had an interesting question from a reader of my PySide6 book, about how to handle dragging and dropping of widgets in a container showing the dragged widget as it is moved.

I'm interested in managing movement of a QWidget with mouse in a container. I've implemented the application with drag & drop, exchanging the position of buttons, but I want to show the motion of QPushButton, like what you see in Qt Designer. Dragging a widget should show the widget itself, not just the mouse pointer.

First, we'll implement the simple case which drags widgets without showing anything extra. Then we can extend it to answer the question. By the end of this quick tutorial we'll have a generic drag drop implementation which looks like the following.

Drag & Drop Widgets

We'll start with a simple application which creates a window using QWidget and places a series of QPushButton widgets into it.

You can substitute QPushButton for any other widget you like, e.g. QLabel. Any widget can have drag behavior implemented on it, although some input widgets will not work well as we capture the mouse events for the drag.

python
from PySide6.QtWidgets import QApplication, QHBoxLayout, QPushButton, QWidget


class Window(QWidget):
    def __init__(self):
        super().__init__()

        self.blayout = QHBoxLayout()
        for l in ["A", "B", "C", "D"]:
            btn = QPushButton(l)
            self.blayout.addWidget(btn)

        self.setLayout(self.blayout)


app = QApplication([])
w = Window()
w.show()

app.exec()


If you run this you should see something like this.

Widgets in a layout The series of QPushButton widgets in a horizontal layout.

Here we're creating a window, but the Window widget is subclassed from QWidget, meaning you can add this widget to any other layout. See later for an example of a generic object sorting widget.

QPushButton objects aren't usually draggable, so to handle the mouse movements and initiate a drag we need to implement a subclass. We can add the following to the top of the file.

python
from PySide6.QtCore import QMimeData, Qt
from PySide6.QtGui import QDrag
from PySide6.QtWidgets import QApplication, QHBoxLayout, QPushButton, QWidget


class DragButton(QPushButton):
    def mouseMoveEvent(self, e):
        if e.buttons() == Qt.MouseButton.LeftButton:
            drag = QDrag(self)
            mime = QMimeData()
            drag.setMimeData(mime)
            drag.exec(Qt.DropAction.MoveAction)


We implement a mouseMoveEvent which accepts the single e parameter of the event. We check to see if the left mouse button is pressed on this event -- as it would be when dragging -- and then initiate a drag. To start a drag, we create a QDrag object, passing in self to give us access later to the widget that was dragged. We also must pass in mime data. This is used for including information about what is dragged, particularly for passing data between applications. However, as here, it is fine to leave this empty.

Finally, we initiate a drag by calling drag.exec_(Qt.MoveAction). As with dialogs exec_() starts a new event loop, blocking the main loop until the drag is complete. The parameter Qt.MoveAction tells the drag handler what type of operation is happening, so it can show the appropriate icon tip to the user.

You can update the main window code to use our new DragButton class as follows.

python
class Window(QWidget):
    def __init__(self):
        super().__init__()
        self.setAcceptDrops(True)

        self.blayout = QHBoxLayout()
        for l in ["A", "B", "C", "D"]:
            btn = DragButton(l)
            self.blayout.addWidget(btn)

        self.setLayout(self.blayout)

    def dragEnterEvent(self, e):
        e.accept()

If you run the code now, you can drag the buttons, but you'll notice the drag is forbidden.

Drag forbidden Dragging of the widget starts but is forbidden.

What's happening? The mouse movement is being detected by our DragButton object and the drag started, but the main window does not accept drag & drop.

To fix this we need to enable drops on the window and implement dragEnterEvent to actually accept them.

python
class Window(QWidget):
    def __init__(self):
        super().__init__()
        self.setAcceptDrops(True)

        self.blayout = QHBoxLayout()
        for l in ["A", "B", "C", "D"]:
            btn = DragButton(l)
            self.blayout.addWidget(btn)

        self.setLayout(self.blayout)

    def dragEnterEvent(self, e):
        e.accept()


If you run this now, you'll see the drag is now accepted and you see the move icon. This indicates that the drag has started and been accepted by the window we're dragging onto. The icon shown is determined by the action we pass when calling drag.exec_().

Drag accepted Dragging of the widget starts and is accepted, showing a move icon.

Releasing the mouse button during a drag drop operation triggers a dropEvent on the widget you're currently hovering the mouse over (if it is configured to accept drops). In our case that's the window. To handle the move we need to implement the code to do this in our dropEvent method.

The drop event contains the position the mouse was at when the button was released & the drop triggered. We can use this to determine where to move the widget to.

To determine where to place the widget, we iterate over all the widgets in the layout, until we find one who's x position is greater than that of the mouse pointer. If so then when insert the widget directly to the left of this widget and exit the loop.

If we get to the end of the loop without finding a match, we must be dropping past the end of the existing items, so we increment n one further (in the else: block below).

python
    def dropEvent(self, e):
        pos = e.position()
        widget = e.source()
        self.blayout.removeWidget(widget)

        for n in range(self.blayout.count()):
            # Get the widget at each index in turn.
            w = self.blayout.itemAt(n).widget()
            if pos.x() < w.x():
                # We didn't drag past this widget.
                # insert to the left of it.
                break
        else:
            # We aren't on the left hand side of any widget,
            # so we're at the end. Increment 1 to insert after.
            n += 1

        self.blayout.insertWidget(n, widget)

        e.accept()

The effect of this is that if you drag 1 pixel past the start of another widget the drop will happen to the right of it, which is a bit confusing. To fix this we can adjust the cut off to use the middle of the widget using if pos.x() < w.x() + w.size().width() // 2: -- that is x + half of the width.

python
    def dropEvent(self, e):
        pos = e.position()
        widget = e.source()
        self.blayout.removeWidget(widget)

        for n in range(self.blayout.count()):
            # Get the widget at each index in turn.
            w = self.blayout.itemAt(n).widget()
            if pos.x() < w.x() + w.size().width() // 2:
                # We didn't drag past this widget.
                # insert to the left of it.
                break
        else:
            # We aren't on the left hand side of any widget,
            # so we're at the end. Increment 1 to insert after.
            n += 1

        self.blayout.insertWidget(n, widget)

        e.accept()


The complete working drag-drop code is shown below.

python
from PySide6.QtCore import QMimeData, Qt
from PySide6.QtGui import QDrag
from PySide6.QtWidgets import QApplication, QHBoxLayout, QPushButton, QWidget


class DragButton(QPushButton):
    def mouseMoveEvent(self, e):
        if e.buttons() == Qt.MouseButton.LeftButton:
            drag = QDrag(self)
            mime = QMimeData()
            drag.setMimeData(mime)
            drag.exec(Qt.DropAction.MoveAction)


class Window(QWidget):
    def __init__(self):
        super().__init__()
        self.setAcceptDrops(True)

        self.blayout = QHBoxLayout()
        for l in ["A", "B", "C", "D"]:
            btn = DragButton(l)
            self.blayout.addWidget(btn)

        self.setLayout(self.blayout)

    def dragEnterEvent(self, e):
        e.accept()

    def dropEvent(self, e):
        pos = e.position()
        widget = e.source()
        self.blayout.removeWidget(widget)

        for n in range(self.blayout.count()):
            # Get the widget at each index in turn.
            w = self.blayout.itemAt(n).widget()
            if pos.x() < w.x() + w.size().width() // 2:
                # We didn't drag past this widget.
                # insert to the left of it.
                break
        else:
            # We aren't on the left hand side of any widget,
            # so we're at the end. Increment 1 to insert after.
            n += 1

        self.blayout.insertWidget(n, widget)

        e.accept()


app = QApplication([])
w = Window()
w.show()

app.exec()


Visual Drag & Drop

We now have a working drag & drop implementation. Next we'll move onto improving the UX by showing the drag visually. First we'll add support for showing the button being dragged next to the mouse point as it is dragged. That way the user knows exactly what it is they are dragging.

Qt's QDrag handler natively provides a mechanism for showing dragged objects which we can use. We can update our DragButton class to pass a pixmap image to QDrag and this will be displayed under the mouse pointer as the drag occurs. To show the widget, we just need to get a QPixmap of the widget we're dragging.

python
from PySide6.QtCore import QMimeData, Qt
from PySide6.QtGui import QDrag
from PySide6.QtWidgets import QApplication, QHBoxLayout, QPushButton, QWidget


class DragButton(QPushButton):
    def mouseMoveEvent(self, e):
        if e.buttons() == Qt.MouseButton.LeftButton:
            drag = QDrag(self)
            mime = QMimeData()
            drag.setMimeData(mime)
            drag.exec(Qt.DropAction.MoveAction)


To create the pixmap we create a QPixmap object passing in the size of the widget this event is fired on with self.size(). This creates an empty pixmap which we can then pass into self.render to render -- or draw -- the current widget onto it. That's it. Then we set the resulting pixmap on the drag object.

If you run the code with this modification you'll see something like the following --

Drag visual Dragging of the widget showing the dragged widget.

Generic Drag & Drop Container

We now have a working drag and drop behavior implemented on our window. We can take this a step further and implement a generic drag drop widget which allows us to sort arbitrary objects. In the code below we've created a new widget DragWidget which can be added to any window.

You can add items -- instances of DragItem -- which you want to be sorted, as well as setting data on them. When items are re-ordered the new order is emitted as a signal orderChanged.

python
from PySide6.QtCore import QMimeData, Qt, Signal
from PySide6.QtGui import QDrag, QPixmap
from PySide6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QLabel,
    QMainWindow,
    QVBoxLayout,
    QWidget,
)


class DragItem(QLabel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setContentsMargins(25, 5, 25, 5)
        self.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.setStyleSheet("border: 1px solid black;")
        # Store data separately from display label, but use label for default.
        self.data = self.text()

    def set_data(self, data):
        self.data = data

    def mouseMoveEvent(self, e):
        if e.buttons() == Qt.MouseButton.LeftButton:
            drag = QDrag(self)
            mime = QMimeData()
            drag.setMimeData(mime)

            pixmap = QPixmap(self.size())
            self.render(pixmap)
            drag.setPixmap(pixmap)

            drag.exec(Qt.DropAction.MoveAction)


class DragWidget(QWidget):
    """
    Generic list sorting handler.
    """

    orderChanged = Signal(list)

    def __init__(self, *args, orientation=Qt.Orientation.Vertical, **kwargs):
        super().__init__()
        self.setAcceptDrops(True)

        # Store the orientation for drag checks later.
        self.orientation = orientation

        if self.orientation == Qt.Orientation.Vertical:
            self.blayout = QVBoxLayout()
        else:
            self.blayout = QHBoxLayout()

        self.setLayout(self.blayout)

    def dragEnterEvent(self, e):
        e.accept()

    def dropEvent(self, e):
        pos = e.position()
        widget = e.source()
        self.blayout.removeWidget(widget)

        for n in range(self.blayout.count()):
            # Get the widget at each index in turn.
            w = self.blayout.itemAt(n).widget()
            if self.orientation == Qt.Orientation.Vertical:
                # Drag drop vertically.
                drop_here = pos.y() < w.y() + w.size().height() // 2
            else:
                # Drag drop horizontally.
                drop_here = pos.x() < w.x() + w.size().width() // 2

            if drop_here:
                break

        else:
            # We aren't on the left hand/upper side of any widget,
            # so we're at the end. Increment 1 to insert after.
            n += 1

        self.blayout.insertWidget(n, widget)
        self.orderChanged.emit(self.get_item_data())

        e.accept()

    def add_item(self, item):
        self.blayout.addWidget(item)

    def get_item_data(self):
        data = []
        for n in range(self.blayout.count()):
            # Get the widget at each index in turn.
            w = self.blayout.itemAt(n).widget()
            data.append(w.data)
        return data


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.drag = DragWidget(orientation=Qt.Orientation.Vertical)
        for n, l in enumerate(["A", "B", "C", "D"]):
            item = DragItem(l)
            item.set_data(n)  # Store the data.
            self.drag.add_item(item)

        # Print out the changed order.
        self.drag.orderChanged.connect(print)

        container = QWidget()
        layout = QVBoxLayout()
        layout.addStretch(1)
        layout.addWidget(self.drag)
        layout.addStretch(1)
        container.setLayout(layout)

        self.setCentralWidget(container)


app = QApplication([])
w = MainWindow()
w.show()

app.exec()


Generic drag drop horizontal Generic drag-drop sorting in horizontal orientation.

You'll notice that when creating the item, you can set the label by passing it in as a parameter (just like for a normal QLabel which we've subclassed from). But you can also set a data value, which is the internal value of this item -- this is what will be emitted when the order changes, or if you call get_item_data yourself. This separates the visual representation from what is actually being sorted, meaning you can use this to sort anything not just strings.

In the example above we're passing in the enumerated index as the data, so dragging will output (via the print connected to orderChanged) something like:

python
[1, 0, 2, 3]
[1, 2, 0, 3]
[1, 0, 2, 3]
[1, 2, 0, 3]

If you remove the item.set_data(n) you'll see the labels emitted on changes.

python
['B', 'A', 'C', 'D']
['B', 'C', 'A', 'D']

We've also implemented orientation onto the DragWidget using the Qt built in flags Qt.Orientation.Vertical or Qt.Orientation.Horizontal. This setting this allows you sort items either vertically or horizontally -- the calculations are handled for both directions.

Generic drag drop vertical Generic drag-drop sorting in vertical orientation.

Adding a Visual Drop Target

If you experiment with the drag-drop tool above you'll notice that it doesn't feel completely intuitive. When dragging you don't know where an item will be inserted until you drop it. If it ends up in the wrong place, you'll then need to pick it up and re-drop it again, using guesswork to get it right.

With a bit of practice you can get the hang of it, but it would be nicer to make the behavior immediately obvious for users. Many drag-drop interfaces solve this problem by showing a preview of where the item will be dropped while dragging -- either by showing the item in the place where it will be dropped, or showing some kind of placeholder.

In this final section we'll implement this type of drag and drop preview indicator.

The first step is to define our target indicator. This is just another label, which in our example is empty, with custom styles applied to make it have a solid "shadow" like background. This makes it obviously different to the items in the list, so it stands out as something distinct.

python
from PySide6.QtCore import QMimeData, Qt, Signal
from PySide6.QtGui import QDrag, QPixmap
from PySide6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QLabel,
    QMainWindow,
    QVBoxLayout,
    QWidget,
)


class DragTargetIndicator(QLabel):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setContentsMargins(25, 5, 25, 5)
        self.setStyleSheet(
            "QLabel { background-color: #ccc; border: 1px solid black; }"
        )



We've copied the contents margins from the items in the list. If you change your list items, remember to also update the indicator dimensions to match.

The drag item is unchanged, but we need to implement some additional behavior on our DragWidget to add the target, control showing and moving it.

First we'll add the drag target indicator to the layout on our DragWidget. This is hidden to begin with, but will be shown during the drag.

python
class DragWidget(QWidget):
    """
    Generic list sorting handler.
    """

    orderChanged = Signal(list)

    def __init__(self, *args, orientation=Qt.Orientation.Vertical, **kwargs):
        super().__init__()
        self.setAcceptDrops(True)

        # Store the orientation for drag checks later.
        self.orientation = orientation

        if self.orientation == Qt.Orientation.Vertical:
            self.blayout = QVBoxLayout()
        else:
            self.blayout = QHBoxLayout()

        # Add the drag target indicator. This is invisible by default,
        # we show it and move it around while the drag is active.
        self._drag_target_indicator = DragTargetIndicator()
        self.blayout.addWidget(self._drag_target_indicator)
        self._drag_target_indicator.hide()

        self.setLayout(self.blayout)

Next we modify the DragWidget.dragMoveEvent to show the drag target indicator. We show it by inserting it into the layout and then calling .show -- inserting a widget which is already in a layout will move it. We also hide the original item which is being dragged.

In the earlier examples we determined the position on drop by removing the widget being dragged, and then iterating over what is left. Because we now need to calculate the drop location before the drop, we take a different approach.

If we wanted to do it the same way, we'd need to remove the item on drag start, hold onto it and implement re-inserting at it's old position on drag fail. That's a lot of work.

Instead, the dragged item is left in place and hidden during move.

python
    def dragMoveEvent(self, e):
        # Find the correct location of the drop target, so we can move it there.
        index = self._find_drop_location(e)
        if index is not None:
            # Inserting moves the item if its alreaady in the layout.
            self.blayout.insertWidget(index, self._drag_target_indicator)
            # Hide the item being dragged.
            e.source().hide()
            # Show the target.
            self._drag_target_indicator.show()
        e.accept()


The method self._find_drop_location finds the index where the drag target will be shown (or the item dropped when the mouse released). We'll implement that next.

The calculation of the drop location follows the same pattern as before. We iterate over the items in the layout and calculate whether our mouse drop location is to the left of each widget. If it isn't to the left of any widget, we drop on the far right.

python
    def _find_drop_location(self, e):
        pos = e.position()
        spacing = self.blayout.spacing() / 2

        for n in range(self.blayout.count()):
            # Get the widget at each index in turn.
            w = self.blayout.itemAt(n).widget()

            if self.orientation == Qt.Orientation.Vertical:
                # Drag drop vertically.
                drop_here = (
                    pos.y() >= w.y() - spacing
                    and pos.y() <= w.y() + w.size().height() + spacing
                )
            else:
                # Drag drop horizontally.
                drop_here = (
                    pos.x() >= w.x() - spacing
                    and pos.x() <= w.x() + w.size().width() + spacing
                )

            if drop_here:
                # Drop over this target.
                break

        return n

The drop location n is returned for use in the dragMoveEvent to place the drop target indicator.

Next wee need to update the get_item_data handler to ignore the drop target indicator. To do this we check w against self._drag_target_indicator and skip if it is the same. With this change the method will work as expected.

python
    def get_item_data(self):
        data = []
        for n in range(self.blayout.count()):
            # Get the widget at each index in turn.
            w = self.blayout.itemAt(n).widget()
            if w != self._drag_target_indicator:
                # The target indicator has no data.
                data.append(w.data)
        return data


If you run the code a this point the drag behavior will work as expected. But if you drag the widget outside of the window and drop you'll notice a problem: the target indicator will stay in place, but dropping the item won't drop the item in that position (the drop will be cancelled).

To fix that we need to implement a dragLeaveEvent which hides the indicator.

python
    def dragLeaveEvent(self, e):
        self._drag_target_indicator.hide()
        e.accept()

With those changes, the drag-drop behavior should be working as intended. The complete code is shown below.

python
from PySide6.QtCore import QMimeData, Qt, Signal
from PySide6.QtGui import QDrag, QPixmap
from PySide6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QLabel,
    QMainWindow,
    QVBoxLayout,
    QWidget,
)


class DragTargetIndicator(QLabel):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setContentsMargins(25, 5, 25, 5)
        self.setStyleSheet(
            "QLabel { background-color: #ccc; border: 1px solid black; }"
        )


class DragItem(QLabel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setContentsMargins(25, 5, 25, 5)
        self.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.setStyleSheet("border: 1px solid black;")
        # Store data separately from display label, but use label for default.
        self.data = self.text()

    def set_data(self, data):
        self.data = data

    def mouseMoveEvent(self, e):
        if e.buttons() == Qt.MouseButton.LeftButton:
            drag = QDrag(self)
            mime = QMimeData()
            drag.setMimeData(mime)

            pixmap = QPixmap(self.size())
            self.render(pixmap)
            drag.setPixmap(pixmap)

            drag.exec(Qt.DropAction.MoveAction)


class DragWidget(QWidget):
    """
    Generic list sorting handler.
    """

    orderChanged = Signal(list)

    def __init__(self, *args, orientation=Qt.Orientation.Vertical, **kwargs):
        super().__init__()
        self.setAcceptDrops(True)

        # Store the orientation for drag checks later.
        self.orientation = orientation

        if self.orientation == Qt.Orientation.Vertical:
            self.blayout = QVBoxLayout()
        else:
            self.blayout = QHBoxLayout()

        # Add the drag target indicator. This is invisible by default,
        # we show it and move it around while the drag is active.
        self._drag_target_indicator = DragTargetIndicator()
        self.blayout.addWidget(self._drag_target_indicator)
        self._drag_target_indicator.hide()

        self.setLayout(self.blayout)

    def dragEnterEvent(self, e):
        e.accept()

    def dragLeaveEvent(self, e):
        self._drag_target_indicator.hide()
        e.accept()

    def dragMoveEvent(self, e):
        # Find the correct location of the drop target, so we can move it there.
        index = self._find_drop_location(e)
        if index is not None:
            # Inserting moves the item if its alreaady in the layout.
            self.blayout.insertWidget(index, self._drag_target_indicator)
            # Hide the item being dragged.
            e.source().hide()
            # Show the target.
            self._drag_target_indicator.show()
        e.accept()

    def dropEvent(self, e):
        widget = e.source()
        # Use drop target location for destination, then remove it.
        self._drag_target_indicator.hide()
        index = self.blayout.indexOf(self._drag_target_indicator)
        if index is not None:
            self.blayout.insertWidget(index, widget)
            self.orderChanged.emit(self.get_item_data())
            widget.show()
            self.blayout.activate()
        e.accept()

    def _find_drop_location(self, e):
        pos = e.position()
        spacing = self.blayout.spacing() / 2

        for n in range(self.blayout.count()):
            # Get the widget at each index in turn.
            w = self.blayout.itemAt(n).widget()

            if self.orientation == Qt.Orientation.Vertical:
                # Drag drop vertically.
                drop_here = (
                    pos.y() >= w.y() - spacing
                    and pos.y() <= w.y() + w.size().height() + spacing
                )
            else:
                # Drag drop horizontally.
                drop_here = (
                    pos.x() >= w.x() - spacing
                    and pos.x() <= w.x() + w.size().width() + spacing
                )

            if drop_here:
                # Drop over this target.
                break

        return n

    def add_item(self, item):
        self.blayout.addWidget(item)

    def get_item_data(self):
        data = []
        for n in range(self.blayout.count()):
            # Get the widget at each index in turn.
            w = self.blayout.itemAt(n).widget()
            if w != self._drag_target_indicator:
                # The target indicator has no data.
                data.append(w.data)
        return data


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.drag = DragWidget(orientation=Qt.Orientation.Vertical)
        for n, l in enumerate(["A", "B", "C", "D"]):
            item = DragItem(l)
            item.set_data(n)  # Store the data.
            self.drag.add_item(item)

        # Print out the changed order.
        self.drag.orderChanged.connect(print)

        container = QWidget()
        layout = QVBoxLayout()
        layout.addStretch(1)
        layout.addWidget(self.drag)
        layout.addStretch(1)
        container.setLayout(layout)

        self.setCentralWidget(container)


app = QApplication([])
w = MainWindow()
w.show()

app.exec()


If you run this example on macOS you may notice that the widget drag preview (the QPixmap created on DragItem) is a bit blurry. On high-resolution screens you need to set the device pixel ratio and scale up the pixmap when you create it. Below is a modified DragItem class which does this.

Update DragItem to support high resolution screens.

python
class DragItem(QLabel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setContentsMargins(25, 5, 25, 5)
        self.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.setStyleSheet("border: 1px solid black;")
        # Store data separately from display label, but use label for default.
        self.data = self.text()

    def set_data(self, data):
        self.data = data

    def mouseMoveEvent(self, e):
        if e.buttons() == Qt.MouseButton.LeftButton:
            drag = QDrag(self)
            mime = QMimeData()
            drag.setMimeData(mime)

            # Render at x2 pixel ratio to avoid blur on Retina screens.
            pixmap = QPixmap(self.size().width() * 2, self.size().height() * 2)
            pixmap.setDevicePixelRatio(2)
            self.render(pixmap)
            drag.setPixmap(pixmap)

            drag.exec(Qt.DropAction.MoveAction)

That's it! We've created a generic drag-drop handled which can be added to any projects where you need to be able to reposition items within a list. You should feel free to experiment with the styling of the drag items and targets as this won't affect the behavior.

Introduction to QML Course Available in Chinese on Qt Academy

by Emilia Valkonen-Damjanovic (Qt Blog)

We are excited to announce that we have published the first course, Introduction to QML, in Chinese (Simplified) on Qt Academy. The course is available for those who choose Chinese simplified as their preferred language for course materials after logging into the Academy. Thanks to Rita Qian, Qt’s solutions engineer, for helping with the translation. 

Incredibly Simple QR Generation in QML

The Need for Simple & Modular QR Generation in QML

Recently, our designer Nuno Pinheiro needed to generate QR codes for an Android app in QML and started asking around about a simple way to do this. The best existing QML solution was QZXing, a Qt/QML wrapper for the 1D/2D barcode image processing library ZXing. He felt this was too much.

QZXing is quite a large and feature-rich library and is a great choice for something that requires a lot more rigorous work with encoding and decoding barcodes and QR. However, this application wasn’t focused around barcode processing. It just needed to display a few QR codes below its other content; it didn’t need something so heavy-duty. It seemed like too much work to build and link this library and register QML types in C++ if there was something simpler available.

Finding A JavaScript Library to Wrap in QML

There are plenty of minimal QR Code libraries in JS, and JS files can be imported natively in QML. Why not just slap a minified JS file into our Qt resources and expose its functionality through a QML object? No compiling libraries, no CMake, simple setup for a simple task.

My colleague, the one and only Javier O. Cordero Pérez attempted to tackle this first using QRCode.js. He found a few issues, which I’ll let him explain. This is what Javier contributed:

Why Most Browser Libraries Don’t Work With QML

Not all ECMAScript or JavaScript environments are created equal. QML, for example, doesn’t have a DOM (Document Object Model) that represents the contents on screen. That feature comes from HTML, so when a JS library designed for use in the browser attempts to access the DOM from QML, it can’t find these APIs. This limits the use of JS libraries in QML to business logic. Frontend JS libaries would have to be ported to QML in order to work.

Note to those concerned with performance: At the time of writing, JS data structures, and many JS and C++ design patterns don’t optimize well in QML code when using QML compilers. You should use C++ for backend code if you work in embedded or performance is a concern for you. Even JavaScript libraries have started a trend of moving away from pure JS in favor of Rust and WASM for backend code. Having said that, we cannot understate the convenience of having JS or QML modules or libraries you can simply plug and play. This is why we did this in the first place.

In my first approach to using qrcodejs, I tried using the library from within a QML slot (Component.onCompleted) and found that QRCode.js calls document.documentElement, document.getElementById, document.documentElement, and document.createElement, which are undefined, because document is typically an HTMLDocument, part of the HTML DOM API.

I then began attempting to refactor the code, but quickly realized there was no easy way to get the library to use QtQuick’s Canvas element. I knew from past experiences that Canvas performs very poorly on Android, so, being pressed for time and Android being our target platform, I came up with a different solution.

Embedding and Communicating With A Browser View

My second approach was to give QRCode.js a browser to work with. I chose to use QtWebView, because on mobile, it displays web content using the operating system’s web view.

To keep things simple, I sent the QRCode’s data to the web page by encoding it to a safe character space using Base64 encoding and passing the result as a URL attribute. This attribute is then decoded inside the page and then sent to the library to generate a QR code on the Canvas. The WebView dimensions are also passed as attributes so the image can be produced at the width of the shortest side.

This is what my solution looked like at this point:

import QtQuick 2.15
import QtQuick.Window 2.15

Window {
    id: document
    QRCode {
        id: qr
        text: "https://kdab.com/"
        anchors.centerIn: parent
        // The smallest dimension determines and fixes QR code size
        width: 400
        height: 600
    }
    width: 640
    height: 480
    visible: true
    title: qsTr("Web based embedded QR Code")
}


// QRCode.qml
import QtQuick 2.15
import QtWebView 1.15

Item {
    required property string text
    // Due to platform limitations, overlapping the WebView with other QML components is not supported.
    // Doing this will have unpredictable results which may differ from platform to platform.
    WebView {
        id: document
        // String is encoded using base64 and transfered through page URL
        url: "qrc:///qr-loader.html?w=" + width + "&t=" + Qt.btoa(text)
        // Keep view dimensions to a minimum
        width: parent.width < parent.height ? parent.width : parent.height
        height: parent.height < parent.width ? parent.height : parent.width
        anchors.centerIn: parent
        // Note: To update the contents after the page has loaded, we could expand upon this
        // by calling runJavaScript(script: string, callback: var) from the WebView component.
        // Any method attributes, such as dimensions or the QR Code’s contents would have to
        // be concatenated inside the script parameter.
    }
}


// qr-loader.html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<style>
body {
  margin: 0;
  padding: 0;
}
#qr {
  width: 100%;
  height: 100%;
  margin: auto;
}
</style>
</head>
<body>
<h1>u</h1>
<div id="q"></div>
<script src="jquery.min.js"></script>
<script src="qrcode.min.js"></script>
<script>
function generateQRCode() {
  const s = new URLSearchParams(document.location.search);
  const w = Number(s.get("w"));
  const t = atob(s.get("t"));
  new QRCode(document.getElementById("q"), {
    text: t,
    width: w,
    height: w
  });
}
generateQRCode();
</script>
</body>


Why You Should Avoid QtWebView On Mobile

If you read the comments in the code, you’ll notice that “due to platform limitations, overlapping the WebView with other QML components is not supported. Doing this will have unpredictable results which may differ from platform to platform.” Additionally, there is so much overhead in loading an embedded site that you can see the exact moment the QR code appears on screen.

Unfortunately for Nuno, QtWebView is unable to load pages embedded as Qt resources on mobile systems. This is because by default, Qt resources become a part of the app’s binary, which can’t be read by the embedded browser. If the site was stored online or the app hosted its own web server, we could load our resources from there. Since this app didn’t do either of those things, all resources had to be copied into a temporary folder and accessed via the file:// protocol. Even then, the embedded browser would fail to locate or load the resources, making it necessary to inline all of our resources into the HTML for this to work.

As you can see, what started as a simple way to use a JS library on the desktop, quickly became cumbersome and difficult to maintain for mobile devices. Given more time, I would’ve chosen to instead re-implement QRCode.js‘s algorithm using QtQuick’s Shapes API. The Shapes API would allow the QR code to be rendered in a single pass of the scene graph.

Fortunately, there’s a better, simpler and more practical solution. I will defer back to Matt here, who figured it out:

Proper JS in QML Solution

I decided to expand on Javier’s idea and try qrcode-svg. This library uses a modified version of QRCode.js and enables creation of an SVG string from the QR Code data.

Here’s an example snipped from the project’s README:

var qrcode = new QRCode({
  content: "Hello World!",
  container: "svg-viewbox", // Responsive use
  join: true // Crisp rendering and 4-5x reduced file size
});
var svg = qrcode.svg();


Since the data is SVG, it can be used with QML’s Image item natively by transforming it into a data URI and using that as the source for the image. There’s no need to write or read anything to disk, just append the string to "data:image/svg+xml;utf8," and use that as the source file.

Starting Our Wrapper

We can just wrap the function call up in a QML type, called QR, and use that wherever we need a QR code. Let’s make a ridiculously basic QtObject that takes a content string and uses the library to produce an SVG:

// QR.qml

import QtQuick
import "qrcode.min.js" as QrSvg

QtObject {
    id: root

    required property string content
    property string svgString: ""

    Component.onCompleted: {
        root.svgString = new QrSvg.QRCode({
            content: root.content
        }).svg()
    }
}



So, whenever we make a QR object, the string bound to content is used to make the SVG and store it in svgString. Then we can render it in an Image item:

// example.qml

import QtQuick
import QtQuick.Window

Window {
    visible: true

    QR {
        id: qrObj
        content: "hello QR!"
    }

    Image {
        source: "data:image/svg+xml;utf8," + qrObj.svgString
    }
}


This is basically effortless and works like a charm.

Finishing Up The Wrapper

Now let’s completely wrap the QRCode constructor, so all the options from qrcode-svg are exposed by our QML object. We just need to set all options in the constructor through QML properties and give all the unrequired properties default values.

While we’re at it, let’s go ahead and connect to onContentChanged, so we can refresh the SVG automatically when the content changes.

// QR.qml

import QtQuick

import "qrcode.min.js" as QrSvg

QtObject {
    id: root

    required property string content
    property int padding: 4
    property int width: 256
    property int height: 256
    property string color: "black"
    property string background: "white"
    property string ecl: "M"
    property bool join: false
    property bool predefined: false
    property bool pretty: true
    property bool swap: false
    property bool xmlDeclaration: true
    property string container: "svg"

    property string svgString: ""

    function createSvgString() {
        root.svgString = new QrSvg.QRCode({
            content: root.content,
            padding: root.padding,
            width: root.width,
            height: root.height,
            color: root.color,
            background: root.background,
            ecl: root.ecl,
            join: root.join,
            predefined: root.predefined,
            pretty: root.pretty,
            swap: root.swap,
            xmlDeclaration: root.xmlDeclaration,
            container: root.container
        }).svg()
    }

    onContentChanged: createSvgString()
    Component.onCompleted: createSvgString()
}



Nice and Easy

With these 45 lines of QML and the minified JS file, we have a QML wrapper for the library. Now any arbitrary QML project can include these two files and generate any QR Code that qrcode-svg can make.

Here I use it to re-generate a QR code as you type the content into a TextInput:

// example.qml

import QtQuick
import QtQuick.Window
import QtQuick.Controls

Window {
    id: root

    visible: true

    QR {
        id: qrObj
        content: txtField.text
        join: true
    }

    TextField {
        id: txtField
        width: parent.width
    }

    Image {
        anchors.top: txtField.bottom
        source: (qrObj.svgString === "") 
                    ? ""
                    : ("data:image/svg+xml;utf8," + qrObj.svgString)
    }
}



This runs well when deployed on Android, and the image re-renders on content change in under 30 milliseconds, sometimes as low as 7.

Hopefully this code will be useful to those looking for the simplest no-frills method to generate a QR code in QML, and maybe the post can inspire other QML developers who feel like they’re overcomplicating something really simple.

The solution associated with this post is available in a GitHub repo linked here, so it can be used for your projects and tweaked if needed. There is also a branch that contains the code for Javier’s alternate solution, available here.

Note: Nuno settled on QZXing before we got a chance to show him this solution, and was so frustrated about not having it earlier that he made us write this blog post 😅

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 Incredibly Simple QR Generation in QML appeared first on KDAB.

Drag & Drop Widgets with PyQt6 — Sort widgets visually with drag and drop in a container

I had an interesting question from a reader of my PyQt6 book, about how to handle dragging and dropping of widgets in a container showing the dragged widget as it is moved.

I'm interested in managing movement of a QWidget with mouse in a container. I've implemented the application with drag & drop, exchanging the position of buttons, but I want to show the motion of QPushButton, like what you see in Qt Designer. Dragging a widget should show the widget itself, not just the mouse pointer.

First, we'll implement the simple case which drags widgets without showing anything extra. Then we can extend it to answer the question. By the end of this quick tutorial we'll have a generic drag drop implementation which looks like the following.

Drag & Drop Widgets

We'll start with a simple application which creates a window using QWidget and places a series of QPushButton widgets into it.

You can substitute QPushButton for any other widget you like, e.g. QLabel. Any widget can have drag behavior implemented on it, although some input widgets will not work well as we capture the mouse events for the drag.

python
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QPushButton, QWidget


class Window(QWidget):
    def __init__(self):
        super().__init__()

        self.blayout = QHBoxLayout()
        for l in ["A", "B", "C", "D"]:
            btn = QPushButton(l)
            self.blayout.addWidget(btn)

        self.setLayout(self.blayout)


app = QApplication([])
w = Window()
w.show()

app.exec()



If you run this you should see something like this.

Widgets in a layout The series of QPushButton widgets in a horizontal layout.

Here we're creating a window, but the Window widget is subclassed from QWidget, meaning you can add this widget to any other layout. See later for an example of a generic object sorting widget.

QPushButton objects aren't usually draggable, so to handle the mouse movements and initiate a drag we need to implement a subclass. We can add the following to the top of the file.

python
from PyQt6.QtCore import QMimeData, Qt
from PyQt6.QtGui import QDrag
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QPushButton, QWidget


class DragButton(QPushButton):
    def mouseMoveEvent(self, e):
        if e.buttons() == Qt.MouseButton.LeftButton:
            drag = QDrag(self)
            mime = QMimeData()
            drag.setMimeData(mime)
            drag.exec(Qt.DropAction.MoveAction)

We implement a mouseMoveEvent which accepts the single e parameter of the event. We check to see if the left mouse button is pressed on this event -- as it would be when dragging -- and then initiate a drag. To start a drag, we create a QDrag object, passing in self to give us access later to the widget that was dragged. We also must pass in mime data. This is used for including information about what is dragged, particularly for passing data between applications. However, as here, it is fine to leave this empty.

Finally, we initiate a drag by calling drag.exec_(Qt.MoveAction). As with dialogs exec_() starts a new event loop, blocking the main loop until the drag is complete. The parameter Qt.MoveAction tells the drag handler what type of operation is happening, so it can show the appropriate icon tip to the user.

You can update the main window code to use our new DragButton class as follows.

python
class Window(QWidget):
    def __init__(self):
        super().__init__()

        self.blayout = QHBoxLayout()
        for l in ["A", "B", "C", "D"]:
            btn = DragButton(l)
            self.blayout.addWidget(btn)

        self.setLayout(self.blayout)


If you run the code now, you can drag the buttons, but you'll notice the drag is forbidden.

Drag forbidden Dragging of the widget starts but is forbidden.

What's happening? The mouse movement is being detected by our DragButton object and the drag started, but the main window does not accept drag & drop.

To fix this we need to enable drops on the window and implement dragEnterEvent to actually accept them.

python
class Window(QWidget):
    def __init__(self):
        super().__init__()
        self.setAcceptDrops(True)

        self.blayout = QHBoxLayout()
        for l in ["A", "B", "C", "D"]:
            btn = DragButton(l)
            self.blayout.addWidget(btn)

        self.setLayout(self.blayout)

    def dragEnterEvent(self, e):
        e.accept()


If you run this now, you'll see the drag is now accepted and you see the move icon. This indicates that the drag has started and been accepted by the window we're dragging onto. The icon shown is determined by the action we pass when calling drag.exec_().

Drag accepted Dragging of the widget starts and is accepted, showing a move icon.

Releasing the mouse button during a drag drop operation triggers a dropEvent on the widget you're currently hovering the mouse over (if it is configured to accept drops). In our case that's the window. To handle the move we need to implement the code to do this in our dropEvent method.

The drop event contains the position the mouse was at when the button was released & the drop triggered. We can use this to determine where to move the widget to.

To determine where to place the widget, we iterate over all the widgets in the layout, until we find one who's x position is greater than that of the mouse pointer. If so then when insert the widget directly to the left of this widget and exit the loop.

If we get to the end of the loop without finding a match, we must be dropping past the end of the existing items, so we increment n one further (in the else: block below).

python
    def dropEvent(self, e):
        pos = e.position()
        widget = e.source()
        self.blayout.removeWidget(widget)

        for n in range(self.blayout.count()):
            # Get the widget at each index in turn.
            w = self.blayout.itemAt(n).widget()
            if pos.x() < w.x():
                # We didn't drag past this widget.
                # insert to the left of it.
                break
        else:
            # We aren't on the left hand side of any widget,
            # so we're at the end. Increment 1 to insert after.
            n += 1
        self.blayout.insertWidget(n, widget)

        e.accept()


The effect of this is that if you drag 1 pixel past the start of another widget the drop will happen to the right of it, which is a bit confusing. To fix this we can adjust the cut off to use the middle of the widget using if pos.x() < w.x() + w.size().width() // 2: -- that is x + half of the width.

python
    def dropEvent(self, e):
        pos = e.position()
        widget = e.source()
        self.blayout.removeWidget(widget)

        for n in range(self.blayout.count()):
            # Get the widget at each index in turn.
            w = self.blayout.itemAt(n).widget()
            if pos.x() < w.x() + w.size().width() // 2:
                # We didn't drag past this widget.
                # insert to the left of it.
                break
        else:
            # We aren't on the left hand side of any widget,
            # so we're at the end. Increment 1 to insert after.
            n += 1
        self.blayout.insertWidget(n, widget)

        e.accept()


The complete working drag-drop code is shown below.

python
from PyQt6.QtCore import QMimeData, Qt
from PyQt6.QtGui import QDrag
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QPushButton, QWidget


class DragButton(QPushButton):
    def mouseMoveEvent(self, e):
        if e.buttons() == Qt.MouseButton.LeftButton:
            drag = QDrag(self)
            mime = QMimeData()
            drag.setMimeData(mime)
            drag.exec(Qt.DropAction.MoveAction)


class Window(QWidget):
    def __init__(self):
        super().__init__()
        self.setAcceptDrops(True)

        self.blayout = QHBoxLayout()
        for l in ["A", "B", "C", "D"]:
            btn = DragButton(l)
            self.blayout.addWidget(btn)

        self.setLayout(self.blayout)

    def dragEnterEvent(self, e):
        e.accept()

    def dropEvent(self, e):
        pos = e.position()
        widget = e.source()
        self.blayout.removeWidget(widget)

        for n in range(self.blayout.count()):
            # Get the widget at each index in turn.
            w = self.blayout.itemAt(n).widget()
            if pos.x() < w.x() + w.size().width() // 2:
                # We didn't drag past this widget.
                # insert to the left of it.
                break
        else:
            # We aren't on the left hand side of any widget,
            # so we're at the end. Increment 1 to insert after.
            n += 1

        self.blayout.insertWidget(n, widget)

        e.accept()


app = QApplication([])
w = Window()
w.show()

app.exec()


Visual Drag & Drop

We now have a working drag & drop implementation. Next we'll move onto improving the UX by showing the drag visually. First we'll add support for showing the button being dragged next to the mouse point as it is dragged. That way the user knows exactly what it is they are dragging.

Qt's QDrag handler natively provides a mechanism for showing dragged objects which we can use. We can update our DragButton class to pass a pixmap image to QDrag and this will be displayed under the mouse pointer as the drag occurs. To show the widget, we just need to get a QPixmap of the widget we're dragging.

python
from PyQt6.QtCore import QMimeData, Qt
from PyQt6.QtGui import QDrag, QPixmap
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QPushButton, QWidget


class DragButton(QPushButton):
    def mouseMoveEvent(self, e):
        if e.buttons() == Qt.MouseButton.LeftButton:
            drag = QDrag(self)
            mime = QMimeData()
            drag.setMimeData(mime)

            pixmap = QPixmap(self.size())
            self.render(pixmap)
            drag.setPixmap(pixmap)

            drag.exec(Qt.DropAction.MoveAction)


To create the pixmap we create a QPixmap object passing in the size of the widget this event is fired on with self.size(). This creates an empty pixmap which we can then pass into self.render to render -- or draw -- the current widget onto it. That's it. Then we set the resulting pixmap on the drag object.

If you run the code with this modification you'll see something like the following --

Drag visual Dragging of the widget showing the dragged widget.

Generic Drag & Drop Container

We now have a working drag and drop behavior implemented on our window. We can take this a step further and implement a generic drag drop widget which allows us to sort arbitrary objects. In the code below we've created a new widget DragWidget which can be added to any window.

You can add items -- instances of DragItem -- which you want to be sorted, as well as setting data on them. When items are re-ordered the new order is emitted as a signal orderChanged.

python
from PyQt6.QtCore import QMimeData, Qt, pyqtSignal
from PyQt6.QtGui import QDrag, QPixmap
from PyQt6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QLabel,
    QMainWindow,
    QVBoxLayout,
    QWidget,
)


class DragItem(QLabel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setContentsMargins(25, 5, 25, 5)
        self.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.setStyleSheet("border: 1px solid black;")
        # Store data separately from display label, but use label for default.
        self.data = self.text()

    def set_data(self, data):
        self.data = data

    def mouseMoveEvent(self, e):
        if e.buttons() == Qt.MouseButton.LeftButton:
            drag = QDrag(self)
            mime = QMimeData()
            drag.setMimeData(mime)

            pixmap = QPixmap(self.size())
            self.render(pixmap)
            drag.setPixmap(pixmap)

            drag.exec(Qt.DropAction.MoveAction)


class DragWidget(QWidget):
    """
    Generic list sorting handler.
    """

    orderChanged = pyqtSignal(list)

    def __init__(self, *args, orientation=Qt.Orientation.Vertical, **kwargs):
        super().__init__()
        self.setAcceptDrops(True)

        # Store the orientation for drag checks later.
        self.orientation = orientation

        if self.orientation == Qt.Orientation.Vertical:
            self.blayout = QVBoxLayout()
        else:
            self.blayout = QHBoxLayout()

        self.setLayout(self.blayout)

    def dragEnterEvent(self, e):
        e.accept()

    def dropEvent(self, e):
        pos = e.position()
        widget = e.source()
        self.blayout.removeWidget(widget)

        for n in range(self.blayout.count()):
            # Get the widget at each index in turn.
            w = self.blayout.itemAt(n).widget()
            if self.orientation == Qt.Orientation.Vertical:
                # Drag drop vertically.
                drop_here = pos.y() < w.y() + w.size().height() // 2
            else:
                # Drag drop horizontally.
                drop_here = pos.x() < w.x() + w.size().width() // 2

            if drop_here:
                break

        else:
            # We aren't on the left hand/upper side of any widget,
            # so we're at the end. Increment 1 to insert after.
            n += 1

        self.blayout.insertWidget(n, widget)
        self.orderChanged.emit(self.get_item_data())

        e.accept()

    def add_item(self, item):
        self.blayout.addWidget(item)

    def get_item_data(self):
        data = []
        for n in range(self.blayout.count()):
            # Get the widget at each index in turn.
            w = self.blayout.itemAt(n).widget()
            data.append(w.data)
        return data


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.drag = DragWidget(orientation=Qt.Orientation.Vertical)
        for n, l in enumerate(["A", "B", "C", "D"]):
            item = DragItem(l)
            item.set_data(n)  # Store the data.
            self.drag.add_item(item)

        # Print out the changed order.
        self.drag.orderChanged.connect(print)

        container = QWidget()
        layout = QVBoxLayout()
        layout.addStretch(1)
        layout.addWidget(self.drag)
        layout.addStretch(1)
        container.setLayout(layout)

        self.setCentralWidget(container)


app = QApplication([])
w = MainWindow()
w.show()

app.exec()


Generic drag drop horizontal Generic drag-drop sorting in horizontal orientation.

You'll notice that when creating the item, you can set the label by passing it in as a parameter (just like for a normal QLabel which we've subclassed from). But you can also set a data value, which is the internal value of this item -- this is what will be emitted when the order changes, or if you call get_item_data yourself. This separates the visual representation from what is actually being sorted, meaning you can use this to sort anything not just strings.

In the example above we're passing in the enumerated index as the data, so dragging will output (via the print connected to orderChanged) something like:

python
[1, 0, 2, 3]
[1, 2, 0, 3]
[1, 0, 2, 3]
[1, 2, 0, 3]

If you remove the item.set_data(n) you'll see the labels emitted on changes.

python
['B', 'A', 'C', 'D']
['B', 'C', 'A', 'D']

We've also implemented orientation onto the DragWidget using the Qt built in flags Qt.Orientation.Vertical or Qt.Orientation.Horizontal. This setting this allows you sort items either vertically or horizontally -- the calculations are handled for both directions.

Generic drag drop vertical Generic drag-drop sorting in vertical orientation.

Adding a Visual Drop Target

If you experiment with the drag-drop tool above you'll notice that it doesn't feel completely intuitive. When dragging you don't know where an item will be inserted until you drop it. If it ends up in the wrong place, you'll then need to pick it up and re-drop it again, using guesswork to get it right.

With a bit of practice you can get the hang of it, but it would be nicer to make the behavior immediately obvious for users. Many drag-drop interfaces solve this problem by showing a preview of where the item will be dropped while dragging -- either by showing the item in the place where it will be dropped, or showing some kind of placeholder.

In this final section we'll implement this type of drag and drop preview indicator.

The first step is to define our target indicator. This is just another label, which in our example is empty, with custom styles applied to make it have a solid "shadow" like background. This makes it obviously different to the items in the list, so it stands out as something distinct.

python
from PyQt6.QtCore import QMimeData, Qt, pyqtSignal
from PyQt6.QtGui import QDrag, QPixmap
from PyQt6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QLabel,
    QMainWindow,
    QVBoxLayout,
    QWidget,
)


class DragTargetIndicator(QLabel):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setContentsMargins(25, 5, 25, 5)
        self.setStyleSheet(
            "QLabel { background-color: #ccc; border: 1px solid black; }"
        )



We've copied the contents margins from the items in the list. If you change your list items, remember to also update the indicator dimensions to match.

The drag item is unchanged, but we need to implement some additional behavior on our DragWidget to add the target, control showing and moving it.

First we'll add the drag target indicator to the layout on our DragWidget. This is hidden to begin with, but will be shown during the drag.

python
class DragWidget(QWidget):
    """
    Generic list sorting handler.
    """

    orderChanged = pyqtSignal(list)

    def __init__(self, *args, orientation=Qt.Orientation.Vertical, **kwargs):
        super().__init__()
        self.setAcceptDrops(True)

        # Store the orientation for drag checks later.
        self.orientation = orientation

        if self.orientation == Qt.Orientation.Vertical:
            self.blayout = QVBoxLayout()
        else:
            self.blayout = QHBoxLayout()

        # Add the drag target indicator. This is invisible by default,
        # we show it and move it around while the drag is active.
        self._drag_target_indicator = DragTargetIndicator()
        self.blayout.addWidget(self._drag_target_indicator)
        self._drag_target_indicator.hide()

        self.setLayout(self.blayout)

Next we modify the DragWidget.dragMoveEvent to show the drag target indicator. We show it by inserting it into the layout and then calling .show -- inserting a widget which is already in a layout will move it. We also hide the original item which is being dragged.

In the earlier examples we determined the position on drop by removing the widget being dragged, and then iterating over what is left. Because we now need to calculate the drop location before the drop, we take a different approach.

If we wanted to do it the same way, we'd need to remove the item on drag start, hold onto it and implement re-inserting at it's old position on drag fail. That's a lot of work.

Instead, the dragged item is left in place and hidden during move.

python
    def dragMoveEvent(self, e):
        # Find the correct location of the drop target, so we can move it there.
        index = self._find_drop_location(e)
        if index is not None:
            # Inserting moves the item if its alreaady in the layout.
            self.blayout.insertWidget(index, self._drag_target_indicator)
            # Hide the item being dragged.
            e.source().hide()
            # Show the target.
            self._drag_target_indicator.show()
        e.accept()


The method self._find_drop_location finds the index where the drag target will be shown (or the item dropped when the mouse released). We'll implement that next.

The calculation of the drop location follows the same pattern as before. We iterate over the items in the layout and calculate whether our mouse drop location is to the left of each widget. If it isn't to the left of any widget, we drop on the far right.

python
    def _find_drop_location(self, e):
        pos = e.position()
        spacing = self.blayout.spacing() / 2

        for n in range(self.blayout.count()):
            # Get the widget at each index in turn.
            w = self.blayout.itemAt(n).widget()

            if self.orientation == Qt.Orientation.Vertical:
                # Drag drop vertically.
                drop_here = (
                    pos.y() >= w.y() - spacing
                    and pos.y() <= w.y() + w.size().height() + spacing
                )
            else:
                # Drag drop horizontally.
                drop_here = (
                    pos.x() >= w.x() - spacing
                    and pos.x() <= w.x() + w.size().width() + spacing
                )

            if drop_here:
                # Drop over this target.
                break

        return n

The drop location n is returned for use in the dragMoveEvent to place the drop target indicator.

Next wee need to update the get_item_data handler to ignore the drop target indicator. To do this we check w against self._drag_target_indicator and skip if it is the same. With this change the method will work as expected.

python
    def get_item_data(self):
        data = []
        for n in range(self.blayout.count()):
            # Get the widget at each index in turn.
            w = self.blayout.itemAt(n).widget()
            if w != self._drag_target_indicator:
                # The target indicator has no data.
                data.append(w.data)
        return data


If you run the code a this point the drag behavior will work as expected. But if you drag the widget outside of the window and drop you'll notice a problem: the target indicator will stay in place, but dropping the item won't drop the item in that position (the drop will be cancelled).

To fix that we need to implement a dragLeaveEvent which hides the indicator.

python
    def dragLeaveEvent(self, e):
        self._drag_target_indicator.hide()
        e.accept()

With those changes, the drag-drop behavior should be working as intended. The complete code is shown below.

python
from PyQt6.QtCore import QMimeData, Qt, pyqtSignal
from PyQt6.QtGui import QDrag, QPixmap
from PyQt6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QLabel,
    QMainWindow,
    QVBoxLayout,
    QWidget,
)


class DragTargetIndicator(QLabel):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setContentsMargins(25, 5, 25, 5)
        self.setStyleSheet(
            "QLabel { background-color: #ccc; border: 1px solid black; }"
        )


class DragItem(QLabel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setContentsMargins(25, 5, 25, 5)
        self.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.setStyleSheet("border: 1px solid black;")
        # Store data separately from display label, but use label for default.
        self.data = self.text()

    def set_data(self, data):
        self.data = data

    def mouseMoveEvent(self, e):
        if e.buttons() == Qt.MouseButton.LeftButton:
            drag = QDrag(self)
            mime = QMimeData()
            drag.setMimeData(mime)

            pixmap = QPixmap(self.size())
            self.render(pixmap)
            drag.setPixmap(pixmap)

            drag.exec(Qt.DropAction.MoveAction)


class DragWidget(QWidget):
    """
    Generic list sorting handler.
    """

    orderChanged = pyqtSignal(list)

    def __init__(self, *args, orientation=Qt.Orientation.Vertical, **kwargs):
        super().__init__()
        self.setAcceptDrops(True)

        # Store the orientation for drag checks later.
        self.orientation = orientation

        if self.orientation == Qt.Orientation.Vertical:
            self.blayout = QVBoxLayout()
        else:
            self.blayout = QHBoxLayout()

        # Add the drag target indicator. This is invisible by default,
        # we show it and move it around while the drag is active.
        self._drag_target_indicator = DragTargetIndicator()
        self.blayout.addWidget(self._drag_target_indicator)
        self._drag_target_indicator.hide()

        self.setLayout(self.blayout)

    def dragEnterEvent(self, e):
        e.accept()

    def dragLeaveEvent(self, e):
        self._drag_target_indicator.hide()
        e.accept()

    def dragMoveEvent(self, e):
        # Find the correct location of the drop target, so we can move it there.
        index = self._find_drop_location(e)
        if index is not None:
            # Inserting moves the item if its alreaady in the layout.
            self.blayout.insertWidget(index, self._drag_target_indicator)
            # Hide the item being dragged.
            e.source().hide()
            # Show the target.
            self._drag_target_indicator.show()
        e.accept()

    def dropEvent(self, e):
        widget = e.source()
        # Use drop target location for destination, then remove it.
        self._drag_target_indicator.hide()
        index = self.blayout.indexOf(self._drag_target_indicator)
        if index is not None:
            self.blayout.insertWidget(index, widget)
            self.orderChanged.emit(self.get_item_data())
            widget.show()
            self.blayout.activate()
        e.accept()

    def _find_drop_location(self, e):
        pos = e.position()
        spacing = self.blayout.spacing() / 2

        for n in range(self.blayout.count()):
            # Get the widget at each index in turn.
            w = self.blayout.itemAt(n).widget()

            if self.orientation == Qt.Orientation.Vertical:
                # Drag drop vertically.
                drop_here = (
                    pos.y() >= w.y() - spacing
                    and pos.y() <= w.y() + w.size().height() + spacing
                )
            else:
                # Drag drop horizontally.
                drop_here = (
                    pos.x() >= w.x() - spacing
                    and pos.x() <= w.x() + w.size().width() + spacing
                )

            if drop_here:
                # Drop over this target.
                break

        return n

    def add_item(self, item):
        self.blayout.addWidget(item)

    def get_item_data(self):
        data = []
        for n in range(self.blayout.count()):
            # Get the widget at each index in turn.
            w = self.blayout.itemAt(n).widget()
            if w != self._drag_target_indicator:
                # The target indicator has no data.
                data.append(w.data)
        return data


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.drag = DragWidget(orientation=Qt.Orientation.Vertical)
        for n, l in enumerate(["A", "B", "C", "D"]):
            item = DragItem(l)
            item.set_data(n)  # Store the data.
            self.drag.add_item(item)

        # Print out the changed order.
        self.drag.orderChanged.connect(print)

        container = QWidget()
        layout = QVBoxLayout()
        layout.addStretch(1)
        layout.addWidget(self.drag)
        layout.addStretch(1)
        container.setLayout(layout)

        self.setCentralWidget(container)


app = QApplication([])
w = MainWindow()
w.show()

app.exec()


If you run this example on macOS you may notice that the widget drag preview (the QPixmap created on DragItem) is a bit blurry. On high-resolution screens you need to set the device pixel ratio and scale up the pixmap when you create it. Below is a modified DragItem class which does this.

Update DragItem to support high resolution screens.

python
class DragItem(QLabel):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setContentsMargins(25, 5, 25, 5)
        self.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.setStyleSheet("border: 1px solid black;")
        # Store data separately from display label, but use label for default.
        self.data = self.text()

    def set_data(self, data):
        self.data = data

    def mouseMoveEvent(self, e):
        if e.buttons() == Qt.MouseButton.LeftButton:
            drag = QDrag(self)
            mime = QMimeData()
            drag.setMimeData(mime)

            # Render at x2 pixel ratio to avoid blur on Retina screens.
            pixmap = QPixmap(self.size().width() * 2, self.size().height() * 2)
            pixmap.setDevicePixelRatio(2)
            self.render(pixmap)
            drag.setPixmap(pixmap)

            drag.exec(Qt.DropAction.MoveAction)

That's it! We've created a generic drag-drop handled which can be added to any projects where you need to be able to reposition items within a list. You should feel free to experiment with the styling of the drag items and targets as this won't affect the behavior.

QLineEdit — A Simple Text Input Widget

The QLineEdit class is a versatile tool for single-line text input. The widget facilitates text manipulation by supporting insertion, deletion, selection, and cut-copy-paste operations natively. You can use line edits when you need to accept text input from your users in a PyQt/PySide application.

The widget is highly customizable. You can set it up to include placeholder text, input masks, input validators, and more. It also supports many keyboard shortcuts out of the box and is able to process custom ones. This feature opens an opportunity to enhance user input speed and efficiency.

In this article, you will learn the basics of using QLineEdit by going through its most commonly used features and capabilities.

Creating Line Edit Widgets With QLineEdit

A line edit allows the user to enter and edit a single line of plain text with a useful collection of editing functionalities, such as insertion, deletion, selection, cut-copy-paste, and drag-and-drop operations.

If you've used PyQt or PySide to create GUI applications in Python, then it'd be likely that you already know about the QLineEdit class. This class allows you to create line edits in your graphical interfaces (GUI) quickly.

The QLineEidt class provides two different constructors that you can use to create line edits according to your needs:

Constructor Description
QLineEdit(parent: QWidget = None) Constructs a line edit with a parent widget but without text
QLineEdit(text: str, parent: QWidget = None) Constructs a line edit with default text and a parent widget

The parent widget would be the window or dialog where you need to place the line edit. The text can be a default text that will appear in the line edit when you run the application.

To illustrate how to use the above constructors, we can code a demo example:

python
from PyQt6.QtWidgets import QApplication, QLineEdit, QVBoxLayout, QWidget

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("QLineEdit Constructors")
        self.resize(300, 100)
        # Line edit with a parent widget
        top_line_edit = QLineEdit(parent=self)
        # Line edit with a parent widget and a default text
        bottom_line_edit = QLineEdit(
            "Hello! This is a line edit.", parent=self
        )

        layout = QVBoxLayout()
        layout.addWidget(top_line_edit)
        layout.addWidget(bottom_line_edit)
        self.setLayout(layout)

app = QApplication([])
window = Window()
window.show()
app.exec()

In this example, we first do the required imports and then define the Window class inheriting from QWidget. Inside Window, we create two QLineEdit widgets.

To create the first line edit, we use the first constructor of QLineEdit, passing only a parent widget. For the second line editor, we use the second constructor, which requires the parent widget and a default text. Note that the text is a regular Python string.

Save the code to a file called constructors.py file and run it from your command line. You'll get a window that looks something like this:

QLineEdit constructors example Standard window showing our two line edits.

The first line edit has no text. In most cases, this is how you would create your line edits because they're designed for accepting input. If you'd like to just display some text, then you can use a QLabel widget instead. The second line edit displays the text that you passed to the constructor.

Both line edits are ready for accepting input text. Note that you can use all the following keyboard shortcuts to optimize your text input process:

Keys Action
Left Arrow Moves the cursor one character to the left
Shift+Left Arrow Moves and selects text one character to the left
Right Arrow Moves the cursor one character to the right
Shift+Right Arrow Moves and selects text one character to the right
Home Moves the cursor to the beginning of the line
End Moves the cursor to the end of the line
Backspace Deletes the character to the left of the cursor
Ctrl+Backspace Deletes the word to the left of the cursor
Delete Deletes the character to the right of the cursor
Ctrl+Delete Deletes the word to the right of the cursor
Ctrl+A Select all
Ctrl+C Copies the selected text to the clipboard
Ctrl+Insert Copies the selected text to the clipboard
Ctrl+K Deletes to the end of the line
Ctrl+V Pastes the clipboard text into the line edit
Shift+Insert Pastes the clipboard text into the line edit
Ctrl+X Deletes the selected text and copies it to the clipboard
Shift+Delete Deletes the selected text and copies it to the clipboard
Ctrl+Z Undoes the last operation
Ctrl+Y Redoes the last undone operation

That's a lot of shortcuts! This table is just a sample of all the features that the QLineEdit class provides out of the box.

In addition to these keyboard shortcuts, the QLineEdit class provides a context menu that you can trigger by clicking on a line edit using the right button of your mouse:

QLineEdit context menu QLineEdit with a context menu visible.

The built-in context menu provides basic edition options, such as cut, copy, paste, and delete. It also has options for undoing and redoing edits, and for selecting all the content of a given line edit.

Creating Non-Editable Line Edits

Sometimes, we need to make a line edit non-editable. By default, all line edits are editable because their main use case is to provide text input for the user. However, in some situations, a non-editable line edit can be useful.

For example, say that you're creating a GUI application that generates some recovery keys for the users. The user must be able to copy the key to a safe place so that they can use it to recover access if they forget their password. In this situation, creating a non-editable line edit can provide a suitable solution.

To make a line edit read-only, we can use the readOnly property and its setter method setReadOnly() as in the following example:

python
import secrets

from PyQt6.QtWidgets import (
    QApplication,
    QLabel,
    QLineEdit,
    QVBoxLayout,
    QWidget,
)

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Non-editable QLineEdit")
        self.resize(300, 100)
        non_editable_line_edit = QLineEdit(parent=self)
        non_editable_line_edit.setReadOnly(True)
        non_editable_line_edit.setText(secrets.token_hex(16))

        layout = QVBoxLayout()
        layout.addWidget(QLabel(parent=self, text="Your secret key:"))
        layout.addWidget(non_editable_line_edit)
        self.setLayout(layout)

app = QApplication([])
window = Window()
window.show()
app.exec()

In this example, we create a non-editable line edit by using the setReadOnly() method. When we set the readOnly property to True, our line edit won't accept editions. It'll only allow us to select and copy its content.

Go ahead and run the application from your command line to explore how this line edit works. You'll get a window like the following:

Non-editable line edit A read-only line edit with editing disabled.

If you play a bit with this line edit, you'll soon discover that you can't change its text. You'll also note that the options in the context menu are now limited to Copy and Select All.

Creating Line Edits for Passwords

Another cool feature of the QLineEdit class is that it allows you to create text input for passwords. This can be pretty convenient for those applications that manage several users, and each user needs to have access credentials.

You can create line edits for passwords by using the echoMode() method. This method takes one of the following constants as an argument:

Constant Value Description
QLineEdit.EchoMode.Normal 0 Display characters as you enter them.
QLineEdit.EchoMode.NoEcho 1 Display no characters when you enter them.
QLineEdit.EchoMode.Password 2 Display platform-dependent password mask characters instead of the characters you enter.
QLineEdit.EchoMode.PasswordEchoOnEdit 3 Display characters as you enter them while editing. Display characters as the Password mode does when reading.

The Normal mode is the default. The NoEcho mode may be appropriate for critical passwords where even the length of the password should be kept secret. The Password mode is appropriate for most password use cases, however PasswordEchoOnEdit can be used instead if you need to give users some confirmation of what they are typing.

Here's a sample app that shows a user and password form:

python
from PyQt6.QtWidgets import (
    QApplication,
    QFormLayout,
    QLineEdit,
    QWidget,
)

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Password QLineEdit")
        self.resize(300, 100)
        username_line_edit = QLineEdit(parent=self)
        password_line_edit = QLineEdit(parent=self)
        password_line_edit.setEchoMode(QLineEdit.EchoMode.Password)

        layout = QFormLayout()
        layout.addRow("Username:", username_line_edit)
        layout.addRow("Password:", password_line_edit)
        self.setLayout(layout)

app = QApplication([])
window = Window()
window.show()
app.exec()

In this example, you call setEchoMode() on the password_line_edit widget using the Password mode as an argument. When you run this code from your command line, you get the following window on your screen:

Password line edit Window with a username and password line edit.

The username_line_edit line edit is in Normal mode, so we can see the characters as we type them in. In contrast, the Password line edit is in Password mode. In this case, when we enter a character, the line edit shows the platform's character for passwords.

Manipulating the Input in a Line Edit

You can change the text of a line edit using the setText() or insert() methods. You can retrieve the text with the text() method. However, these are not the only operations that you can perform with the text of a line edit.

The following table shows a summary of some of the most commonly used methods for text manipulation in line edits:

Method Description
setText(text) Sets the text of a line edit to text, clears the selection, clears the undo/redo history, moves the cursor to the end of the line, and resets the modified property to false.
insert(text) Deletes any selected text, inserts text, and validates the result. If it is valid, it sets it as the new contents of the line edit.
clear() Clears the contents of the line edit.
copy() Copies the selected text to the clipboard.
cut() Copies the selected text to the clipboard and deletes it from the line edit.
paste() Inserts the clipboard's text at the cursor position, deleting any selected text.
redo() Redoes the last operation if redo is available. Redo becomes available once the user has performed one or more undo operations on text in the line edit.
undo() Undoes the last operation if undo is available. Undo becomes available once the user has modified the text in the line edit.
selectAll() Selects all the text and moves the cursor to the end.

You can use any of these methods to manipulate the text of a line edit from your code. Consider the following example where you have two line edits and two buttons that take advantage of some of the above methods to copy some text from one line edit to the other:

python
from PyQt6.QtWidgets import (
    QApplication,
    QGridLayout,
    QLabel,
    QLineEdit,
    QPushButton,
    QWidget,
)

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Copy and Paste")
        self.resize(300, 100)

        self.source_line_edit = QLineEdit(parent=self)
        self.source_line_edit.setText("Hello, World!")
        self.dest_line_edit = QLineEdit(parent=self)

        copy_button = QPushButton(parent=self, text="Copy")
        paste_button = QPushButton(parent=self, text="Paste")

        copy_button.clicked.connect(self.copy)
        paste_button.clicked.connect(self.paste)

        layout = QGridLayout()
        layout.addWidget(self.source_line_edit, 0, 0)
        layout.addWidget(copy_button, 0, 1)
        layout.addWidget(self.dest_line_edit, 1, 0)
        layout.addWidget(paste_button, 1, 1)
        self.setLayout(layout)

    def copy(self):
        self.source_line_edit.selectAll()
        self.source_line_edit.copy()

    def paste(self):
        self.dest_line_edit.clear()
        self.dest_line_edit.paste()

app = QApplication([])
window = Window()
window.show()
app.exec()

In this example, we create two line edits. The first one will hold some sample text. The second line edit will receive the text. Then, we create two buttons and connect their clicked signals to the copy() and paste() slots.

Inside the copy() method we first select all the text from the source line edit. Then we use the copy() method to copy the selected text to the clipboard. In paste(), we call clear() on the destination line edit to remove any previous text. Then, we use the paste() method to copy the clipboard's content.

Go ahead and run the application. You'll get the following window on your screen:

QLineEdit Copy and Paste QLineEdit with Copy & Paste buttons attached to handlers.

Once you've run the app, then you can click the Copy button to copy the text in the first line edit. Next, you can click the Paste button to paste the copied text to the second line edit. Go ahead and give it a try!

Aligning and Formatting the Text in a Line Edit

You can also align and format the text in a line edit. For example, for text alignment, you can use the setAlignment() method with one or more alignment flags. Some of the most useful flags that you can find in the Qt.AlignmentFlag namespace includes the following:

Flag Description
AlignLeft Aligns with the left edge.
AlignRight Aligns with the right edge.
AlignHCenter Centers horizontally in the available space.
AlignJustify Justifies the text in the available space.
AlignTop Aligns with the top.
AlignBottom Aligns with the bottom.
AlignVCenter Centers vertically in the available space.
AlignCenter Centers in both dimensions.

If you want to apply multiple alignment flags to a given line edit, you don't have to call setAlignment() multiple times. You can just use the bitwise OR operator (|) to combine them. Consider the following example:

python
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QApplication, QLineEdit

class Window(QLineEdit):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Aligning Text")
        self.resize(300, 100)
        self.setText("Hello, World!")
        self.setAlignment(Qt.AlignmentFlag.AlignCenter)

app = QApplication([])
window = Window()
window.show()
app.exec()

In this example, we use a QLineEdit as the only component of our app's GUI. Using the setAlignment() method, we center the "Hello, World!" message in both directions, horizontally and vertically. To do this, we use the AlignCenter flag. Here's what the app looks like:

QLineEdit text alignment QLineEdit with text alignment set.

Go ahead and play with other flags to see their effect on the text alignment. Use the | bitwise operator to combine several alignment flags.

Line edits also have a textMargins property that you can tweak to customize the text alignment using specific values. To set margin values for your text, you can use the setTextMargins() method, which has the following signatures:

Method Description
setTextMargins(left, top, right, bottom) Sets the margins around the text to have the sizes left, top, right, and bottom.
setTextMargins(margins) Sets the margins around the text using a QMargins object.

The first signature allows you to provide four integer values as the left, top, right, and bottom margins for the text. The second signature accepts a QMargins object as an argument. To build this object, you can use four integer values with the same meaning as the left, top, right, and bottom arguments in the first signature.

Here's an example of how to set custom margins for the text in a line edit:

python
from PyQt6.QtWidgets import QApplication, QLineEdit

class Window(QLineEdit):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Aligning Text")
        self.resize(300, 100)
        self.setText("Hello, World!")
        self.setTextMargins(30, 30, 0, 0)

app = QApplication([])
window = Window()
window.show()
app.exec()

In this example, you set the left and top margins to custom values. Here's how this app looks when you run it:

QLineEdit text margins QLineEdit with text margins added.

Using the setTextMargins() method, we can place the text in the desired place in a line edit, which may be a requirement in some situations.

Connecting Signals and Slots

When you're creating a GUI application and you need to use line edits, you may need to perform actions when the user enters or modifies the content of the line edit. To do this, you need to connect some of the signals of the line edit to specific slots or functions.

Depending on specific user events, the QLineEdit class can emit several different signals. Here's is a summary of these signals and their corresponding meaning:

Signal Emitted
textChanged(text) Whenever the user changes the text either manually or programmatically. The text argument is the new text.
textEdited(text) Whenever the user edits the text manually. The text argument is the new text.
editingFinished When the user presses the Return or Enter key, or when the line edit loses focus, and its contents have changed since the last time this signal was emitted.
inputRejected When the user presses a key that is an unacceptable input.
returnPressed When the user presses the Return or Enter key.
selectionChanged When the selection changes.

We can connect either of these signals with any slot. A slot is a method or function that performs a concrete action in your application. We can connect a signal and a slot so that the slot gets called when the signal gets emitted. Here's the required syntax to do this:

python
line_edit.<signal>.connect(<method>)

In this construct, line_edit is the QLineEdit object that we need to connect with a given slot. The <signal> placeholder can be any of the abovementioned signals. Finally, <method> represents the target slot or method.

Let's write an example that puts this syntax into action. For this example, we'll connect the textEdited signal with a method that updates the text of a QLabel to match the text of our line edit:

python
from PyQt6.QtWidgets import (
    QApplication,
    QLabel,
    QLineEdit,
    QVBoxLayout,
    QWidget,
)

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Signal and Slot")
        self.resize(300, 100)

        self.line_edit = QLineEdit(parent=self)
        self.label = QLabel(parent=self)
        self.line_edit.textEdited.connect(self.update_label)

        layout = QVBoxLayout()
        layout.addWidget(self.line_edit)
        layout.addWidget(self.label)
        self.setLayout(layout)

    def update_label(self):
        self.label.setText(self.line_edit.text())

app = QApplication([])
window = Window()
window.show()
app.exec()

In this example, we connect the line edit's textEdited signal with the update_label() method, which sets the label's text to match the text we enter in our line edit. Go ahead and run the app. Then, enter some text in the line edit and see what happens with the label at the bottom of the app's window.

Validating Input in Line Edits

We can provide input validators to our line edits using the setValidator() method. This method takes a QValidator object as an argument. PyQt has a few built-in validators that you can use directly on a line edit:

Validator objects process the input to check whether it's valid. In general, validator object has three possible states:

Constant Value Description
QValidator.State.Invalid 0 The input is invalid.
QValidator.State.Intermediate 1 The input is a valid intermediate value.
QValidator.State.Acceptable 2 The input is acceptable as a final result.

When you set a validator to a given line edit, the user may change the content to any Intermediate value during editing. However, they can't edit the text to a value that is Invalid. The line edit will emit the returnPressed and editingFinished signals only when the validator validates input as Acceptable.

Here's an example where we use a QIntValidator and a QRegularExpressionValidator

python
from PyQt6.QtCore import QRegularExpression
from PyQt6.QtGui import QIntValidator, QRegularExpressionValidator
from PyQt6.QtWidgets import (
    QApplication,
    QFormLayout,
    QLabel,
    QLineEdit,
    QWidget,
)

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Input Validators")
        self.resize(300, 100)

        self.int_line_edit = QLineEdit(parent=self)
        self.int_line_edit.setValidator(QIntValidator())

        self.uppercase_line_edit = QLineEdit(parent=self)
        input_validator = QRegularExpressionValidator(
            QRegularExpression("[A-Z]+"), self.uppercase_line_edit
        )
        self.uppercase_line_edit.setValidator(input_validator)
        self.uppercase_line_edit.returnPressed.connect(self.update_label)

        self.signal_label = QLabel(parent=self)

        layout = QFormLayout()
        layout.addRow("Integers:", self.int_line_edit)
        layout.addRow("Uppercase letters:", self.uppercase_line_edit)
        layout.addRow("Signal:", self.signal_label)
        self.setLayout(layout)

    def update_label(self):
        self.signal_label.setText("Return pressed!")

if __name__ == "__main__":
    app = QApplication([])
    window = Window()
    window.show()
    app.exec()

In this example, we have two line edits. In the first line edit, we've used a QIntValidator object. This way, the line edit will only accept valid integer numbers. If you try to type in something different, then the line edit won't accept your input.

In the second line edit, we've used a QRegularExpressionValidator. In this specific case, the regular expression accepts one or more uppercase letters. Again if you try to enter something else, then the line edit won't accept the input.

The QLabel will show the text Return pressed! if the input in the second line edit is valid and you press the Enter key. Here's what the app looks like:

QLineEdit with input validator QLineEdit with input validator.

Validators provide a quick way to restrict the user input and set our own validation rules. This way, we can ensure valid user input in our applications.

Conclusion

Line edits are pretty useful widgets in any GUI application. They allow text input through a single-line editor that has many cool features.

In this tutorial, you've learned how to create, use, and customize your line edits while building a GUI application with PyQt/PySide.

Felgo Hot Reload: Enhance your Qt Project Development with QML Code Reload

Felgo specializes in unique Qt products and tools to develop fast and efficiently. QML Hot Reload is one of them, and it transforms the way you develop Qt Quick applications.

The Felgo Hot Reload release now makes the tool available as a standalone product - independent from the Felgo SDK. This means that you can integrate code reloading to any Qt/QML project to enhance your development workflow. To do so, simply add the Felgo QML Hot Reload library to your project. No change to your production code is required.

Get Felgo Hot Reload

Read this post to learn why you should not miss QML Hot Reload in your development setup, and what benefits it brings to your project:

KDSoap 2.2.0 Released

We’re pleased to announce the release of KDSoap version 2.2.0, an update that brings new enhancements to improve both the general build system and client-side functionality.

What is KDSoap?

KDSoap, a SOAP (“Simple Object Access Protocol“) component rooted in Qt, serves as an essential tool for both client-side and server-side operations. Tailored for C++ programmers using Qt, it not only facilitates the creation of client applications for web services but also empowers developers to seamlessly build web services without requiring additional components like dedicated web servers. For further details on KDSoap, visit here.

What’s New in KDSoap Version 2.2.0?

Build System Co-installability: The buildsystem now supports the co-installability of Qt 5 and Qt 6 headers. Qt 6 headers are installed into their dedicated subdirectory. This ensures compatibility with client code and allows co-installation with Qt 5.

Client-Side:

  • WS-Addressing Support: The new release adds KDSoapClientInterface::setMessageAddressingProperties(). This addition enables the use of WS-Addressing support specifically with WSDL-generated services.
  • SOAP Action Requirement Removal: KDSoap no longer requires a SOAP action for writing addressing properties.

WSDL Parser / Code Generator Changes:

Enhanced -import-path Support: Notable changes have been made to the WSDL parser and code generator, impacting both client and server sides. The update improves -import-path support by incorporating the import path in more areas within the code. This refinement enhances the overall functionality of the parser and code generator.

These updates collectively contribute to a more streamlined and efficient experience for KDSoap users, addressing specific issues and introducing valuable features to facilitate seamless integration with Qt-based applications. For detailed information and to explore these enhancements, we refer to the KDSoap documentation accompanying version 2.2.0 on GitHub.

How to Get Started with KDSoap Version 2.2.0?

For existing users, upgrading to the latest version is as simple as downloading the new release from the GitHub page. If you are new to KDSoap, we invite you to explore its capabilities and discover how it can streamline your web service development process.

As always, we appreciate your feedback and contributions to make KDSoap even better. Feel free to reach out to us with any questions, suggestions or bug reports on our GitHub repository.

Thank you for choosing KDSoap, and happy coding!

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 KDSoap 2.2.0 Released appeared first on KDAB.

QML Extension for Visual Studio Code: Develop Qt Quick with VS Code and QML Hot Reload

Visual Studio Code ranks #1 among the most popular development tools in the annual developers survey of Stack Overflow. More than 70% of all participants use this code editor for development. And there’s a reason why: It is fast, reliable, supports a wide range of languages and runs on Windows, macOS and Linux.

With the right extensions and tools, you can set up and use Visual Studio Code to develop your Qt and QML projects. Watch the tutorial or read this guide to learn how:

 

Qt 6 WASM: Uploading & Playing Back Audio Files

This article walks through an implementation using C++11 or later, Qt 6.5 or later for WebAssembly (multithreaded), and CMake. The browser environment used was Mozilla Firefox 119.0.1 (64-bit) provided by the Mozilla Firefox snap package for Ubuntu.

Overview & Motivation

Lately, I’ve been working on a small Qt Widgets project to help manage some weekly poker games with my friends, and realized it would be much nicer to distribute copies by web instead of building binaries. This gave me the perfect excuse to test out Qt for WebAssembly, and see how much tinkering I’d have to do with the original source code to make it work in Firefox!

Everything looked and felt great on the web browser when just compiling the desktop code with a Qt WASM CMake kit, but one feature caused some issues. The program plays a notification sound with QMediaPlayer, and allows the user to select any audio file on their computer to use as the sound. The original implementation for both getting a native file dialog and playing back audio files did not work in-browser. Using goofy audio is a must for our poker nights, so I quickly started rewriting.

Fixing the implementation to get the file dialog was simple and required me to replace a very small amount of code, but QMediaPlayer was unusable with file URLs. Firstly, browser code is sandboxed, so the QMediaPlayer can’t set its source to a file URL on the user’s file system. The data would have to be stored in the browser’s internal file system or on a server. Secondly, Qt Multimedia is pretty broken for WASM in general. It’s listed as not working in Qt 5, and untested in Qt 6, with some Qt 6 limitations detailed here.

However, I noticed that the Qt WASM method for opening files provided the file contents as a QByteArray.

I thought, why not just try to play back the audio from the buffer of raw bytes?

Please note that there are other (and usually better) ways to do this. The program I was working on was simple enough that storing a buffer as a DOM variable was sufficient enough. In many cases, it would be preferable to store audio persistently on a server and fetch the stream to play it back, or even in some cases write the file to the browser’s IndexedDB. Either way, knowing how to use C++ to play audio in-browser from a buffer could be useful in these cases, so this simple example should cover a lot of the common ground without making it too complex. Additionally, note that cross-browser implementation may require a little bit more code, and production code should perform more checks than you’ll see here.

Getting File Contents

I’ll start off this basic example by creating a regular Qt Widgets application, with a class that inherits from QMainWindow.

#pragma once

#include 

class MainWindow : public QMainWindow {
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = nullptr) noexcept;
    ~MainWindow() override;
};

In the constructor, I’ll create a QPushButton to open a file dialog (dialogButton) and one to play the audio back (playButton).

Let’s connect dialogButton‘s clicked signal to a lambda and get our file dialog.

To do this in a Qt WASM project, we use QFileDialog::getOpenFileContent.

connect(dialogButton, &QPushButton::clicked, this, [this]() {
    QFileDialog::getOpenFileContent(
        tr("Audio files (*.mp3 *.flac *.wav *.aiff *.ogg)"),
        [this](const QString &fileName, const QByteArray &fileContent) {
            if (fileName.isEmpty()) {
                // no file selected
            } else {
                // do stuff with file data
            }
        });
});

Notice that, as I mentioned earlier, the file’s contents are provided as a QByteArray. To play the audio, we need our buffer to contain PCM data, while these bytes are the file’s binary contents in a specific file format. We need to decode the bytes and transform them to PCM.

How do we do that?

Finding the Needed Decoding Method

Let’s stick with the QMediaPlayer idea for now to illustrate some basic concepts. We want to use a buffer of bytes rather than a file URL, so we can set the QMediaPlayer‘s source device to a QBuffer containing PCM data.

To obtain this buffer, we need to create an initial QBuffer from the QByteArray, then give it to a QAudioDecoder, read the decoded bytes as a QAudioBuffer, then create another QBuffer from the QAudioBuffer‘s raw byte data and byte count. Now this buffer can be given to a QMediaPlayer by calling setSourceDevice.

Here’s the problem: QAudioDecoder needs a QAudioFormat to know how to decode the data. This is not just file format – we need to know about channels and bitrate as well. We want to be able to upload files in different formats, with different bitrates, and in both mono and stereo, so we would have to extract all this information manually somehow to decode correctly.

On top of that, this limitation is mentioned in the Qt 6 WASM Multimedia page:

Playing streaming bytes instead of a url. e.g. setSource(QIOStream) is also not currently supported. Some advanced features may or may not work at this time.

Even if we do all the decoding properly, we can’t play from our buffer (by setSource(QIOStream) they mean setSourceDevice(QIODevice *))! We need to find another way.

Luckily, JavaScript’s Web Audio API has a great solution. BaseAudioContext has a nice decodeAudioData method that takes an ArrayBuffer of data in any kind of supported audio format and decodes it as an AudioBuffer. This buffer can be played with an AudioSourceNode connected to an AudioContext‘s destination node.

But wait, how do we get our QByteArray data into a JavaScript ArrayBuffer?

Using the Emscripten API

We need to use emscripten’s C++ API for this. On Qt for WASM, it’s already linked and you can include its headers out of the box.

The idea is that we want to not only invoke JS functions and use JS vars, but also have C++ objects representing JS objects. Then we can create them, perform other operations in our C++ context, and then invoke methods on the JS objects later, even passing data from C++ objects to JavaScript and vice-versa.

Our best option for this is to use emscripten’s embind, instead of writing inline JavaScript with something like EM_JS or EM_ASM. This way, our JS objects are accessible in whatever scope their corresponding C++ objects are defined in.

To start, we need to #include <emscripten/bind.h>.

We’ll be working with the C++ type emscripten::val, essentially emscripten’s wrapper for a JS var, which additionally has a static member function to access global objects and functions.

Since Qt 6.5, in Qt WASM, QByteArray conveniently has a member function toEcmaUint8Array() that represents its data in the layout of a JS Uint8Array and returns it as an emscripten::val. Since we receive fileContent as a const reference to a QByteArray and toEcmaUint8Array is not a const function, we’ll copy fileContent to a new QByteArray:

QFileDialog::getOpenFileContent(
    tr("Audio files (*.mp3 *.flac *.wav *.aiff *.ogg)"),
    [this](const QString &fileName, const QByteArray &fileContent) {
        if (fileName.isEmpty()) {
            // no file selected
        } else {
            auto contentCopy = fileContent;
            auto buf = contentCopy.toEcmaUint8Array();
            // ...
            // ...
            // ...
        }
    });

Nice. Now we have to get the buffer property from our buf object and call decodeAudioData. For that, we need to initialize an AudioContext.

An easy way is to just give our MainWindow class a private data member emscripten::val mAudioContext and initialize it in the constructor. To get the embind equivalent of the JS expression mAudioContext = new AudioContext() we do this:

mAudioContext = emscripten::val::global("AudioContext").new_();

We can go ahead and initialize it in our constructor’s initializer list:

MainWindow::MainWindow(QWidget *parent) noexcept
    : QMainWindow(parent)
    , mAudioContext(emscripten::val::global("AudioContext").new_())
{
    // ...
    // ...
    // ...
}

So why this syntax?

In JavaScript, functions are first-class objects. For a browser that complies with the Web Audio API, the constructor AudioContext is a property of globalThis, so it can be accessed as a global object with emscripten::val::global("AudioContext"). Then we can call new_(Args&&... args) on it, which calls the constructor with args using the new keyword.

Now let’s get back to setting up our AudioBuffer.

Finally Decoding the Audio

Recall that we have this:

auto contentCopy = fileContent;
auto buf = contentCopy.toEcmaUint8Array();

We need to access the property buffer of buf to get an ArrayBuffer object that we can pass to decodeAudioData. To get the property, we just use operator[] like so: buf["buffer"]. Easy.

To call decodeAudioData, we just invoke the template function ReturnValue call(const char *name, Args&&... args) const like so:

auto decodedBuffer = mAudioContext
                         .call<emscripten::val>(
                             "decodeAudioData", /* arguments */);

But wait a second, decodeAudioData is overloaded. There are two versions of this function with different arguments and return types. One involves a promise-await syntax where you just pass the buffer, which returns the decoded buffer. The other is void and involves passing both the buffer and a callback function that takes the decoded buffer as an argument. How can we do JS promise-await or pass JS callback functions as arguments in C++?

Well, to do promise-await syntax, we can just call await() on our return value.

auto decodedBuffer = mAudioContext
                         .call<emscripten::val>(
                             "decodeAudioData", buf["buffer"])
                         .await();

Please keep in mind this await() is only available when Asyncify is enabled. If you’re using the multithreaded version of Qt WASM, you can enable this. Just add the following to your CMakeLists.txt:

target_link_options(<target name> PUBLIC -sASYNCIFY -O3)

In C++20, you can also use coroutines to do promise-await syntax by placing a co_await operator to the left of the function call.

For the callback function, well, Function is a global object. Thus, you can call the Function constructor with new_(Args&&... args), passing the function body as a string, and store the returned function as an emscripten::val.

The problem is that the callback function takes the decoded buffer as an argument, so we get it in JS rather than C++. It is possible to call a C++ function from the callback and forward the decoded buffer to it, which involves exposing the function to emscripten’s Module object using EMSCRIPTEN_BINDINGS. However, if we need it to be a member function, we get type errors on the function pointer.

I am still looking at how the callback syntax would work, but I believe it involves exposing the entire class to Module.

So, let’s just go with the promise-await syntax for now.

Since we will want to play from the buffer on a button press or some other event, let’s make a data member emscripten::val mDecodedBuffer so it’s in the class scope.

We now have this:

QFileDialog::getOpenFileContent(
    tr("Audio files (*.mp3 *.flac *.wav *.aiff *.ogg)"),
    [this](const QString &fileName, const QByteArray &fileContent) {
        if (fileName.isEmpty()) {
            // no file selected
        } else {
            auto contentCopy = fileContent;
            auto buf = contentCopy.toEcmaUint8Array();

            mDecodedBuffer = mAudioContext
                                 .call<emscripten::val>(
                                     "decodeAudioData", buf["buffer"])
                                 .await();
        }
    });
    

Cool, we can store the data from an audio file in a buffer in PCM format!

Let’s move on to playing the audio from that buffer.

Playing Back the Audio

When we want to play, we first need to create an AudioBufferSourceNode from the AudioContext, and set its buffer to mDecodedBuffer. We then connect the source node to our audio context’s destination node (our audio device). After this, we can call start on the source node to play the sound!

Here’s what that looks like with embind:

auto source = mAudioContext.call<emscripten::val>("createBufferSource");
source.set("buffer", mDecodedBuffer);
source.call<void>("connect", mAudioContext["destination"]);
source.call<void>("start");

A new AudioBufferSourceNode needs to be created every time you want to play the sound, but they are inexpensive to construct.

I have this code to play on the QPushButton click:

connect(playButton, &QPushButton::clicked, this, [this]() {
    auto source = mAudioContext.call<emscripten::val>("createBufferSource");
    source.set("buffer", mDecodedBuffer);
    source.call<void>("connect", mAudioContext["destination"]);
    source.call<void>("start");
});

To make sure the buffer will be populated successfully before this code executes, I make the button initially disabled and use Qt signals readyToPlay and notReady to enable and disable the button respectively.

Complete Code & Final Thoughts

Here is my full header for this basic example:

#pragma once

#include <QMainWindow>
#include <emscripten/bind.h>

class MainWindow : public QMainWindow {
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = nullptr) noexcept;
    ~MainWindow() override;

signals:
    void notReady();
    void readyToPlay();

private:
    emscripten::val mAudioContext;
    emscripten::val mDecodedBuffer;
};


and my entire implementation file (it’s a very basic example so I just left everything in the constructor):

#include "mainwindow.h"

#include <QFileDialog>
#include <QHBoxLayout>
#include <QPushButton>

MainWindow::MainWindow(QWidget *parent) noexcept
    : QMainWindow(parent)
    , mAudioContext(emscripten::val::global("AudioContext").new_())
{
    auto *centerWidget = new QWidget(this);
    auto *layout = new QHBoxLayout(centerWidget);

    auto *dialogButton = new QPushButton(centerWidget);
    dialogButton->setText(tr("Choose file"));

    auto *playButton = new QPushButton(centerWidget);
    playButton->setText(tr("Play"));
    playButton->setEnabled(false);

    layout->addWidget(dialogButton);
    layout->addWidget(playButton);
    centerWidget->setLayout(layout);
    setCentralWidget(centerWidget);

    connect(dialogButton, &QPushButton::clicked, this, [this]() {
        QFileDialog::getOpenFileContent(
            tr("Audio files (*.mp3 *.flac *.wav *.aiff *.ogg)"),
            [this](const QString &fileName, const QByteArray &fileContent) {
                if (fileName.isEmpty()) {
                    emscripten::val::global().call<void>(
                        "alert", std::string("no file selected"));
                } else {
                    emit notReady();

                    auto contentCopy = fileContent;
                    auto buf = contentCopy.toEcmaUint8Array();

                    mDecodedBuffer = mAudioContext
                                         .call<emscripten::val>(
                                             "decodeAudioData", buf["buffer"])
                                         .await();

                    emit readyToPlay();
                }
            });
    });

    connect(playButton, &QPushButton::clicked, this, [this]() {
        auto source = mAudioContext.call<emscripten::val>("createBufferSource");
        source.set("buffer", mDecodedBuffer);
        source.call<void>("connect", mAudioContext["destination"]);
        source.call<void>("start");
    });

    connect(this, &MainWindow::notReady, this, [playButton]() {
        if (playButton->isEnabled())
            playButton->setEnabled(false);
    });

    connect(this, &MainWindow::readyToPlay, this, [playButton]() {
        if (!playButton->isEnabled())
            playButton->setEnabled(true);
    });
}

MainWindow::~MainWindow() = default;


This example was simple, but I hope readers can use these building blocks in more complete codebases to work with audio in Qt WASM!

Note: if you want to stream your audio, look into the Media Capture and Streams API and use a MediaStreamAudioSourceNode instead of an AudioBufferSourceNode.

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 Qt 6 WASM: Uploading & Playing Back Audio Files appeared first on KDAB.

Cutelyst v4 - 10 years 🎉

Cutelyst the Qt web framework is now at v4.0.0 just a bit later for it's 10th anniversary.

With 2.5k commits it has been steadly improving, and in production for many high traffic applications. With this release we say good bye to our old Qt5 friend, also dropped uWSGI support, clearsilver and Grantlee were also removed, many methods now take a QStringView and Cutelyst::Header class was heavly refactored to allow usage of QByteArrayView, and stopped storing QStrings internally in a QHash, they are QByteArray inside a vector.

Before, all headers were uppercased and dashes replaced with underscores, this was quite some work, so that when searching the string had to be converted to this format to be searcheable, this had the advantage of allowing the use of QHash and in templates you could c.request.header.CONTENT_TYPE. Turns out both cases aren't so important, speed is more important for the wider use cases.

With these changes Cutelyst managed to get 10 - 15% faster on TechEmpower benchmarks, which is great as we are still well positioned as a full stack framework there.

https://github.com/cutelyst/cutelyst/releases/tag/v4.0.0

Have fun, Merry Christmas and Happy New Year!

Qt World Summit 2023: Together We Shape the Future of Qt

The Qt World Summit 2023 in Berlin was a huge success. After several years of virtual events, we once again had the chance to meet with industry experts and Qt enthusiasts from all around the world. We thus want to share our experiences, discuss the latest advancements, and strengthen our bonds with the Qt community! 

Introducing KDDockWidgets 2.0

We’re happy to announce KDDockWidgets version 2.0, a major milestone that brings many improvements and architectural enhancements to our powerful docking library.

KDDockWidgets is a versatile framework for custom-tailored docking systems in Qt written by KDAB’s Sérgio Martins. For more information about its rich set of features, take a gander at its GitHub repository.

What motivated the rewrite?

In the 1.x series, the integration of QtQuick/QML support in KDDockWidgets was a bit of a hack. Maintaining and adding new features for QtQuick proved challenging and risked introducing regressions in the QtWidgets code. The 2.0 release marks a comprehensive architecture rewrite, addressing these issues. Now, fixing bugs in the QtQuick frontend is more straightforward, without impacting the stability of the QtWidgets implementation. This rewrite not only enhances the overall robustness of the library but also paves the way for non-Qt frontends, such as Slint and Flutter.

If you’ve been using QtQuick in the 1.x series, we strongly recommend updating to KDDockWidgets 2.0 The 2.0 release ensures a smoother experience and better compatibility with QtQuick. However, if you’re content with QtWidgets in version 1.x, you’re welcome to stick with version 1.7. While version 1.7 will continue to receive necessary build and crash fixes, the focus will shift towards version 2.0 for future developments.

About Version 2.0

The process of porting from KDDockWidgets version 1 to version 2 is generally straightforward, involving minor changes in namespaces and includes. However, if you’ve been using private APIs, the process might be more involved. For detailed guidance on porting, we refer to our porting guide.

The cornerstone of this release is a comprehensive architecture rewrite, laying the foundation for broader frontend compatibility. Developers can now effortlessly extend KDDockWidgets to support a myriad of GUI frameworks beyond QtWidgets and QtQuick, opening up new possibilities for cross-platform development.

Exciting times lie ahead as we work towards integrating KDDockWidgets with Slint as well as Flutter. While these frameworks are still in progress, we eagerly anticipate their support for multi-window functionality before finalizing the implementation.

Whether you’re a seasoned developer or just starting your coding journey, KDDockWidgets 2.0 offers a world of possibilities. Dive into the new architecture, explore enhanced integration options, and leverage the flexibility to tailor your user interface to perfection.

Visit our GitHub repository to download the latest release and check out the documentation.

More information about KDDockWidgets

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 Introducing KDDockWidgets 2.0 appeared first on KDAB.

How to Create a Custom Title Bar for a PyQt Window — Customize Your Python App's Title Bars

PyQt provides plenty of tools for creating unique and visually appealing graphical user interfaces (GUIs). One aspect of your applications that you may not have considered customizing is the title bar. The title bar is the topmost part of the window, where your users find the app's name, window controls & other elements.

This part of the window is usually drawn by the operating system or desktop environment and it's default look & feel may not gel well with the rest of your application. However, you may want to customize it to add additional functionality. For example, in web browsers the document tabs are now typically collapsed into the title bar to maximize available space for viewing pages.

In this tutorial, you will learn how to create custom title bars in PyQt. By the end of this tutorial, you will have the necessary knowledge to enhance your PyQt applications with personalized and (hopefully!) stylish title bars.

Creating Frameless Windows in PyQt

The first step to providing a PyQt application with a custom title bar is to remove the default title bar and window decoration provided by the operating system. If we don't take this step, we'll end up with multiple title bars at the top of our windows.

In PyQt, we can create a frameless window using the setWindowFlags() method available on all QWidget subclasses, including QMainWindow. We call this method, passing in the FramelessWindowHint flag, which lives in the Qt namespace under the WindowType enumeration.

Here's the code of a minimal PyQt app whose main window is frameless:

python
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QApplication, QMainWindow

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Custom Title Bar")
        self.resize(400, 200)
        self.setWindowFlags(Qt.WindowType.FramelessWindowHint)

if __name__ == "__main__":
    app = QApplication([])
    window = MainWindow()
    window.show()
    app.exec()

After importing the required classes, we create a window by subclassing QMainWindow. In the class initializer method, we set the window's title and resize the window using the resize() method. Then we use the setWindowFlags() to make the window frameless. The rest is the usual boilerplate code for creating PyQt applications.

If you run this app from your command line, you'll get the following window on your screen:

A frameless window in PyQt A frameless window in PyQt

As you can see, the app's main window doesn't have a title bar or any other decoration. It's only a gray rectangle on your screen.

Because the window has no buttons, you need to press Alt-F4 on Windows and Linux or Cmd+Q on macOS to close the app.

This isn't very helpful, of course, but we'll be adding back in our custom title bar shortly.

Setting Up the Main Window

Before creating our custom title bar, we'll finish the initialization of our app's main window, import some additional classes and create the window's central widget and layouts.

Here's the code update:

python
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
    QApplication,
    QLabel,
    QMainWindow,
    QVBoxLayout,
    QWidget,
)

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Custom Title Bar")
        self.resize(400, 200)
        self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
        central_widget = QWidget()
        self.title_bar = CustomTitleBar(self)

        work_space_layout = QVBoxLayout()
        work_space_layout.setContentsMargins(11, 11, 11, 11)
        work_space_layout.addWidget(QLabel("Hello, World!", self))

        centra_widget_layout = QVBoxLayout()
        centra_widget_layout.setContentsMargins(0, 0, 0, 0)
        centra_widget_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
        centra_widget_layout.addWidget(self.title_bar)
        centra_widget_layout.addLayout(work_space_layout)

        central_widget.setLayout(centra_widget_layout)
        self.setCentralWidget(central_widget)

# ...

First, we import the QLabel, QVBoxLayout, and QWidget classes. In our window's initializer, we create a central widget by instantiating QWidget(). Next, we create an instance attribute called title_bar by instantiating a class called CustomTitleBar. We still need to implement this class -- we'll do this in a moment.

The next step is to create a layout for our window's workspace. In this example, we're using a QVBoxLayout, but you can use the layout that better fits your needs. We also set some margins for the layout content and added a label containing the phrase "Hello, World!".

Next, we create a global layout for our central widget. Again, we use a QVBoxLayout. We set the layout's margins to 0 and aligned it on the top of our frameless window. In this layout, we need to add the title bar at the top and the workspace at the bottom. Finally, we set the central widget's layout and the app's central widget.

That's it! We have all the boilerplate code we need for our window to work correctly. Now we're ready to write our custom title bar.

Creating a Custom Title Bar for a PyQt Window

In this section, we will create a custom title bar for our main window. To do this, we will create a new class by inheriting from QWidget. First, go ahead and update your imports like in the code below:

python
from PyQt6.QtCore import QSize, Qt
from PyQt6.QtGui import QPalette
from PyQt6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QLabel,
    QMainWindow,
    QStyle,
    QToolButton,
    QVBoxLayout,
    QWidget,
)

# ...

Here, we've imported a few new classes. We will use these classes as building blocks for our title bar. Without further ado, let's get into the title bar code. We'll introduce the code in small consecutive chunks to facilitate the explanation. Here's the first piece:

python
# ...

class CustomTitleBar(QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        self.setAutoFillBackground(True)
        self.setBackgroundRole(QPalette.ColorRole.Highlight)
        self.initial_pos = None
        title_bar_layout = QHBoxLayout(self)
        title_bar_layout.setContentsMargins(1, 1, 1, 1)
        title_bar_layout.setSpacing(2)

In this code snippet, we create a new class by inheriting from QWidget. This way, our title bar will have all the standard features and functionalities of any PyQt widgets. In the class initializer, we set autoFillBackground to true because we want to give a custom color to the bar. The next line of code sets the title bar's background color to QPalette.ColorRole.Highlight, which is a blueish color.

The next line of code creates and initializes an instance attribute called initial_pos. We'll use this attribute later on when we deal with moving the window around our screen.

The final three lines of code allow us to create a layout for our title bar. Because the title bar should be horizontally oriented, we use a QHBoxLayout class to structure it.

The piece of code below deals with our window's title:

python
# ...

class CustomTitleBar(QWidget):
    def __init__(self, parent):
        # ...
        self.title = QLabel(f"{self.__class__.__name__}", self)
        self.title.setStyleSheet(
            """font-weight: bold;
               border: 2px solid black;
               border-radius: 12px;
               margin: 2px;
            """
        )
        self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
        if title := parent.windowTitle():
            self.title.setText(title)
        title_bar_layout.addWidget(self.title)

The first line of new code creates a title attribute. It's a QLable object that will hold the window's title. Because we want to build a cool title bar, we'd like to add some custom styling to the title. To do this, we use the setStyleSheet() method with a string representing a CSS style sheet as an argument. The style sheet tweaks the font, borders, and margins of our title label.

Next, we center the title using the setAlignment() method with the Qt.AlignmentFlag.AlignCenter flag as an argument.

In the conditional statement, we check whether our window has a title. If that's the case, we set the text of our title label to the current window's title. Finally, we added the title label to the title bar layout.

The next step in our journey to build a custom title bar is to provide standard window controls. In other words, we need to add the minimize, maximize, close, and normal buttons. These buttons will allow our users to interact with our window. To create the buttons, we'll use the QToolButton class.

Here's the required code:

python
# ...

class CustomTitleBar(QWidget):
    def __init__(self, parent):
        # ...
        # Min button
        self.min_button = QToolButton(self)
        min_icon = self.style().standardIcon(
            QStyle.StandardPixmap.SP_TitleBarMinButton
        )
        self.min_button.setIcon(min_icon)
        self.min_button.clicked.connect(self.window().showMinimized)

        # Max button
        self.max_button = QToolButton(self)
        max_icon = self.style().standardIcon(
            QStyle.StandardPixmap.SP_TitleBarMaxButton
        )
        self.max_button.setIcon(max_icon)
        self.max_button.clicked.connect(self.window().showMaximized)

        # Close button
        self.close_button = QToolButton(self)
        close_icon = self.style().standardIcon(
            QStyle.StandardPixmap.SP_TitleBarCloseButton
        )
        self.close_button.setIcon(close_icon)
        self.close_button.clicked.connect(self.window().close)

        # Normal button
        self.normal_button = QToolButton(self)
        normal_icon = self.style().standardIcon(
            QStyle.StandardPixmap.SP_TitleBarNormalButton
        )
        self.normal_button.setIcon(normal_icon)
        self.normal_button.clicked.connect(self.window().showNormal)
        self.normal_button.setVisible(False)

In this code snippet, we define all the required buttons by instantiating the QToolButton class. The minimize, maximize, and close buttons follow the same pattern. We create the button, define an icon for the buttons at hand, and set the icon using the setIcon() method.

Note that we use the standard icons that PyQt provides. For example, the minimize button uses the SP_TitleBarMinButton icon. Similarly, the maximize and close buttons use the SP_TitleBarMaxButton and SP_TitleBarCloseButton icons. We find all these icons in the QStyle.StandardPixmap namespace.

Finally, we connect the button's clicked() signal with the appropriate slot. For the minimize buttons, the proper slot is .showMinimized(). For the maximize and close buttons, the right slots are .showMaximized() and close(), respectively. All these slots are part of the main window's class.

The normal button at the end of the above code uses the SP_TitleBarNormalButton icon and showNormal() slot. This button has an extra setting. We've set its visibility to False, meaning that the button will be hidden by default. It'll only appear when we maximize the window to allow us to return to the normal state.

Now that we've created and tweaked the buttons, we must add them to our title bar. To do this, we can use the following for loop:

python
# ...

class CustomTitleBar(QWidget):
    def __init__(self, parent):
        # ...
        buttons = [
            self.min_button,
            self.normal_button,
            self.max_button,
            self.close_button,
        ]
        for button in buttons:
            button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
            button.setFixedSize(QSize(28, 28))
            button.setStyleSheet(
                """QToolButton { border: 2px solid white;
                                 border-radius: 12px;
                                }
                """
            )
            title_bar_layout.addWidget(button)

This loop iterates over our four buttons in a predefined order. The first thing to do inside the loop is to define the focus policy of each button. We don't want these buttons to steal focus from buttons in the window's working space , therefore we set their focus policy to NoFocus.

Next, we set a fixed size of 28 by 28 pixels for the three buttons using the setFixedSize() method with a QSize object as an argument.

Our main goal in this section is to create a custom title bar. A handy way to customize the look and feel of PyQt widgets is to use CSS style sheets. In the above piece of code, we use the setStyleSheet() method to apply a custom CSS style sheet to our four buttons. The sheet defines a white and round border for each button.

The final line in the above code calls the addWidget() method to add each custom button to our title bar's layout. That's it! We're now ready to give our title bar a try. Go ahead and run the application from your command line. You'll see a window like the following:

A PyQt window with a custom title bar A PyQt window with a custom title bar

This is pretty simple styling, but you get the idea. You can tweak the title bar further, depending on your needs. For example, you can change the colors and borders, customize the title's font, add other widgets to the bar, and more.

We'll apply some nicer styles later, once we have the functionality in place! Keep reading.

Even though the title bar looks different, it has limited functionality. For example, if you click the maximize button, then the window will change to its maximized state. However, the normal button won't show up to allow you to return the window to its previous state.

In addition to this, if you try to move the window around your screen, you'll quickly notice a problem: it's impossible to move the window!

In the following sections, we'll write the necessary code to fix these issues and make our custom title bar fully functional. To kick things off, let's start by fixing the state issues.

Updating the Window's State

To fix the issue related to the window's state, we'll write two new methods. We need to override one method and write another. In the MainWindow class, we'll override the changeEvent() method. The changeEvent() method is called directly by Qt whenever the window state changes: for example if the window is maximized or hidden. By overriding this event we can add our own custom behavior.

Here's the code that overrides the changeEvent() method:

python
from PyQt6.QtCore import QSize, Qt, QEvent
# ...

class MainWindow(QMainWindow):
    # ...

    def changeEvent(self, event):
        if event.type() == QEvent.Type.WindowStateChange:
            self.title_bar.window_state_changed(self.windowState())
        super().changeEvent(event)
        event.accept()

This method is fairly straightforward. We check the event type to see if it is a WindowStateChange. If that's the case, we call the window_state_changed() method of our custom title bar, passing the current window's state as an argument. In the final two lines, we call the parent class's changeEvent() method and accept the event to signal that we've correctly processed it.

Here's the implementation of our window_state_changed() method:

python
# ...

class CustomTitleBar(QWidget):
    # ...

    def window_state_changed(self, state):
        if state == Qt.WindowState.WindowMaximized:
            self.normal_button.setVisible(True)
            self.max_button.setVisible(False)
        else:
            self.normal_button.setVisible(False)
            self.max_button.setVisible(True)

This method takes a window's state as an argument. Depending on the value of the state parameter we will optionally show and hide the maximize & restore buttons.

First, if the window is currently maximized we will show the normal button and hide the maximize button. Alternatively, if the window is currently not maximized we will hide the normal button and show the maximize button.

The effect of this, together with the order we added the buttons above, is that when you maximize the window the maximize button will appear to be replaced with the normal button. When you restore the window to it's normal size, the normal button will be replaced with the maximize button.

Go ahead and run the app again. Click the maximize button. You'll note that when the window gets maximized, the middle button changes its icon. Now you have access to the normal button. If you click it, then the window will recover its previous state.

Handling Window's Moves

Now it's time to write the code that enables us to move the window around the screen while holding your mouse's left-click button on the title bar. To fix this issue, we only need to add code to the CustomTitleBar class.

In particular, we need to override three mouse events:

  • mousePressEvent() will let us know when the user clicks on our custom title bar using the mouse's left-click button. This may indicate that the window movement should start.
  • mouseMoveEvent() will let us process the window movements.
  • mouseReleaseEvent() will let us know when the user has released the mouse's left-click button so that we can stop moving the window.

Here's the code that overrides the mousePressEvent() method:

python
# ...

class CustomTitleBar(QWidget):
    # ...

    def mousePressEvent(self, event):
        if event.button() == Qt.MouseButton.LeftButton:
            self.initial_pos = event.position().toPoint()
        super().mousePressEvent(event)
        event.accept()

In this method, we first check if the user clicks on the title bar using the mouse's left-click button. If that's the case, then we update our initial_pos attribute to the clicked point. Remember that we defined initial_pos and initialized it to None back in the __init__() method of CustomTitleBar.

Next, we need to override the mousePressEvent() method. Here's the required code:

python
# ...

class CustomTitleBar(QWidget):
    # ...

    def mouseMoveEvent(self, event):
        if self.initial_pos is not None:
            delta = event.position().toPoint() - self.initial_pos
            self.window().move(
                self.window().x() + delta.x(),
                self.window().y() + delta.y(),
            )
        super().mouseMoveEvent(event)
        event.accept()

This if statement in mouseMoveEvent() checks if the initial_pos attribute is not None. If this condition is true, then the if code block executes because we have a valid initial position.

The first line in the if code block calculates the difference, ordelta, between the current and initial mouse positions. To get the current position, we call the position() method on the event object and convert that position into a QPoint object using the toPoint() method.

The following four lines update the position of our application's main window by adding the delta values to the current window position. The move() method does the hard work of moving the window.

In summary, this code updates the window position based on the movement of our mouse. It tracks the initial position of the mouse, calculates the difference between the initial position and the current position, and applies that difference to the window's position.

Finally, we can complete the mouseReleaseEvent() method:

python
# ...

class CustomTitleBar(QWidget):
    # ...

    def mouseReleaseEvent(self, event):
        self.initial_pos = None
        super().mouseReleaseEvent(event)
        event.accept()

This method's implementation is pretty straightforward. Its purpose is to reset the initial position by setting it back to None when the mouse is released, indicating that the drag is complete.

That's it! Go ahead and run your app again. Click on your custom title bar and move the window around while holding the mouse's left-click button. Can you move the window? Great! Your custom title bar is now fully functional.

The completed code for the custom title bar is shown below.

python
from PyQt6.QtCore import QSize, Qt, QEvent
from PyQt6.QtGui import QPalette
from PyQt6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QLabel,
    QMainWindow,
    QStyle,
    QToolButton,
    QVBoxLayout,
    QWidget,
)


class CustomTitleBar(QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        self.setAutoFillBackground(True)
        self.setBackgroundRole(QPalette.ColorRole.Highlight)
        self.initial_pos = None
        title_bar_layout = QHBoxLayout(self)
        title_bar_layout.setContentsMargins(1, 1, 1, 1)
        title_bar_layout.setSpacing(2)

        self.title = QLabel(f"{self.__class__.__name__}", self)
        self.title.setStyleSheet(
            """font-weight: bold;
               border: 2px solid black;
               border-radius: 12px;
               margin: 2px;
            """
        )
        self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
        if title := parent.windowTitle():
            self.title.setText(title)
        title_bar_layout.addWidget(self.title)
        # Min button
        self.min_button = QToolButton(self)
        min_icon = self.style().standardIcon(
            QStyle.StandardPixmap.SP_TitleBarMinButton
        )
        self.min_button.setIcon(min_icon)
        self.min_button.clicked.connect(self.window().showMinimized)

        # Max button
        self.max_button = QToolButton(self)
        max_icon = self.style().standardIcon(
            QStyle.StandardPixmap.SP_TitleBarMaxButton
        )
        self.max_button.setIcon(max_icon)
        self.max_button.clicked.connect(self.window().showMaximized)

        # Close button
        self.close_button = QToolButton(self)
        close_icon = self.style().standardIcon(
            QStyle.StandardPixmap.SP_TitleBarCloseButton
        )
        self.close_button.setIcon(close_icon)
        self.close_button.clicked.connect(self.window().close)

        # Normal button
        self.normal_button = QToolButton(self)
        normal_icon = self.style().standardIcon(
            QStyle.StandardPixmap.SP_TitleBarNormalButton
        )
        self.normal_button.setIcon(normal_icon)
        self.normal_button.clicked.connect(self.window().showNormal)
        self.normal_button.setVisible(False)
        # Add buttons
        buttons = [
            self.min_button,
            self.normal_button,
            self.max_button,
            self.close_button,
        ]
        for button in buttons:
            button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
            button.setFixedSize(QSize(28, 28))
            button.setStyleSheet(
                """QToolButton { border: 2px solid white;
                                 border-radius: 12px;
                                }
                """
            )
            title_bar_layout.addWidget(button)

    def window_state_changed(self, state):
        if state == Qt.WindowState.WindowMaximized:
            self.normal_button.setVisible(True)
            self.max_button.setVisible(False)
        else:
            self.normal_button.setVisible(False)
            self.max_button.setVisible(True)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Custom Title Bar")
        self.resize(400, 200)
        self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
        central_widget = QWidget()
        self.title_bar = CustomTitleBar(self)

        work_space_layout = QVBoxLayout()
        work_space_layout.setContentsMargins(11, 11, 11, 11)
        work_space_layout.addWidget(QLabel("Hello, World!", self))

        centra_widget_layout = QVBoxLayout()
        centra_widget_layout.setContentsMargins(0, 0, 0, 0)
        centra_widget_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
        centra_widget_layout.addWidget(self.title_bar)
        centra_widget_layout.addLayout(work_space_layout)

        central_widget.setLayout(centra_widget_layout)
        self.setCentralWidget(central_widget)

    def changeEvent(self, event):
        if event.type() == QEvent.Type.WindowStateChange:
            self.title_bar.window_state_changed(self.windowState())
        super().changeEvent(event)
        event.accept()

    def window_state_changed(self, state):
        self.normal_button.setVisible(state == Qt.WindowState.WindowMaximized)
        self.max_button.setVisible(state != Qt.WindowState.WindowMaximized)

    def mousePressEvent(self, event):
        if event.button() == Qt.MouseButton.LeftButton:
            self.initial_pos = event.position().toPoint()
        super().mousePressEvent(event)
        event.accept()

    def mouseMoveEvent(self, event):
        if self.initial_pos is not None:
            delta = event.position().toPoint() - self.initial_pos
            self.window().move(
                self.window().x() + delta.x(),
                self.window().y() + delta.y(),
            )
        super().mouseMoveEvent(event)
        event.accept()

    def mouseReleaseEvent(self, event):
        self.initial_pos = None
        super().mouseReleaseEvent(event)
        event.accept()

if __name__ == "__main__":
    app = QApplication([])
    window = MainWindow()
    window.show()
    app.exec()

Making it a little more beautiful

So far we've covered the technical aspects of styling our window with a custom title bar, and added the code to make it function as expected. But it doesn't look great. In this section we'll take our existing code & tweak the styling and buttons to produce something that's a little more professional looking.

One common reason for wanting to apply custom title bars to a window is to integrate the title bar with the rest of the application. This technique is called a unified title bar and can be seen in some popular applications such as web browsers, or Spotify.

Unified title bar in Spotify

In this section we'll look at how we can reproduce the same effect in PyQt using a combination of stylesheets & icons. Below is a screenshot of the final result which we'll be building.

Style custom title bar in PyQt6

As you can see the window & the toolbar blend nicely together and the window has rounded corners. There are a few different ways to do this, but we'll cover a simple approach using Qt stylesheets to apply styling over the entire window.

In order to customize the shape of the window, we need to first tell the OS to stop drawing the default window outline and background for us. We do that by setting a window attribute on the window. This is similar to the flags we already discussed, in that it turns on & off different window manager behaviors.

python
# ...
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Custom Title Bar")
        self.resize(400, 200)
        self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
        self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
        # ...

We've added a call to self.setAttribute which sets the attribute Qt.WidgetAttribute.WA_TranslucentBackground on the window. If you run the code now you will see the window has become transparent, with only the widget text & toolbar visible.

Next we'll tell Qt to draw a new custom background for us. If you've worked with QSS before, the most obvious way to apply curved edges to the window using QSS stylesheets would be to set border-radius: styles on the main window directly, e.g.

python
#...
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        # ...
        self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
        self.setStyleSheet("background-color: gray; border-radius: 10px;")
#...

However, if you try this you'll notice that it doesn't work. If you enable a translucent background, the background of the window is not drawn (including your styles). If you don't set translucent background, the window is filled to the edges with a solid color ignoring the border radius.

Stylesheets can't alter window shape.

The good news is that, with a bit of lateral thinking, there is a simple solution. We already know that we can construct interfaces by nesting widgets in layouts. Since we can't style the border-radius of a window, but we can style any other widget, the solution is to simply add a container widget into our window & apply the curved-edge and background styles to that.

On our MainWindow object we already have a central widget which contains our layout, so we can apply the styles there.

python
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        # ...
        central_widget = QWidget()
        # This container holds the window contents, so we can style it.
        central_widget.setObjectName("Container")
        central_widget.setStyleSheet("""#Container {
            background: qlineargradient(x1:0 y1:0, x2:1 y2:1, stop:0 #051c2a stop:1 #44315f);
            border-radius: 5px;
        }""")
        self.title_bar = CustomTitleBar(self)
        # ...

We've taken the existing central_widget object and assigned an object name to it. This is a ID which we can use to refer to the widget from QSS, to apply our styles specifically to that widget.

If you're familiar with CSS you might expect that IDs like #Container must be unique. However, they are not: you can give multiple widgets the same object name if you like. So you can re-use this technique and QSS on multiple windows in your application without problems.

With this style applied on our window, we have a nice gradient background with curved corners.

Unfortunately, the title bar we created is drawn filled, and so the background and curved corners of our window are over-written. To make things look coherent we need to make our title bar also transparent by removing the background color & auto-fill behavior we set earlier.

We don't need to set any flags or attributes this widget because it is not a window. A QWidget object is transparent by default.

We can also make some tweaks to the style of the title label, such as adjusting the font size and making the title capitalized using text-transform: uppercase -- feel free to customize this yourself.

python
class CustomTitleBar(QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        # self.setAutoFillBackground(True) # <-- remove
        # self.setBackgroundRole(QPalette.ColorRole.Highlight) # <-- remove
        self.initial_pos = None
        title_bar_layout = QHBoxLayout(self)
        title_bar_layout.setContentsMargins(1, 1, 1, 1)
        title_bar_layout.setSpacing(2)
        self.title = QLabel(f"{self.__class__.__name__}", self)
        self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.title.setStyleSheet("""
        QLabel { text-transform: uppercase; font-size: 10pt; margin-left: 48px; }
        """)

QSS is very similar to CSS, especially for text styling.

The margin-left: 48px is to compensate for the 3 * 16px window icons on the right hand side so the text align centrally.

The icons are currently using built-in Qt icons which are a little bit plain & ugly. Next let's update the icons, using custom SVG icons of simple colored circles, for the minimize, maximize, close & restore buttons.

python
from PyQt6.QtGui import QIcon
# ...
class CustomTitleBar(QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        # ...
        # Min button
        self.min_button = QToolButton(self)
        min_icon = QIcon()
        min_icon.addFile('min.svg')
        self.min_button.setIcon(min_icon)
        self.min_button.clicked.connect(self.window().showMinimized)

        # Max button
        self.max_button = QToolButton(self)
        max_icon = QIcon()
        max_icon.addFile('max.svg')
        self.max_button.setIcon(max_icon)
        self.max_button.clicked.connect(self.window().showMaximized)

        # Close button
        self.close_button = QToolButton(self)
        close_icon = QIcon()
        close_icon.addFile('close.svg') # Close has only a single state.
        self.close_button.setIcon(close_icon)
        self.close_button.clicked.connect(self.window().close)

        # Normal button
        self.normal_button = QToolButton(self)
        normal_icon = QIcon()
        normal_icon.addFile('normal.svg')
        self.normal_button.setIcon(normal_icon)
        self.normal_button.clicked.connect(self.window().showNormal)
        self.normal_button.setVisible(False)
        # ...

This code follows the same basic structure as before, but instead of using the built-in icons here we're loading our icons from SVG images. These images are very simple, consisting of a single circle in green, red or yellow for the different states mimicking macOS.

The normal.svg file for returning a maximized window to normal size shows a semi-transparent green circle for simplicity's sake, but you can include iconography and hover behaviors on the buttons if you prefer.

You can download these icons & all source code for this tutorial here: https://downloads.pythonguis.com/custom-title-bar-pyqt6.zip

The final step is to iterate through the created buttons, adding them to title bar layout. This is slightly tweaked from before to remove the border styling replacing it with simple padding & setting the icon sizes to 16px. Because we are using SVG files the icons will automatically scale to the available space.

python
class CustomTitleBar(QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        # ...
        for button in buttons:
            button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
            button.setFixedSize(QSize(16, 16))
            button.setStyleSheet(
                """QToolButton {
                    border: none;
                    padding: 2px;
                }
                """
            )
            title_bar_layout.addWidget(button)

And that's it! With these changes, you can now run your application and you'll see a nice sleek modern-looking UI with unified title bar and custom controls.

Style custom titlebar in PyQt6 The final result, showing our unified title bar and window design.

The complete code is shown below:

python
from PyQt6.QtCore import QEvent, QSize, Qt
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QLabel,
    QMainWindow,
    QStyle,
    QToolButton,
    QVBoxLayout,
    QWidget,
)


class CustomTitleBar(QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        self.initial_pos = None
        title_bar_layout = QHBoxLayout(self)
        title_bar_layout.setContentsMargins(1, 1, 1, 1)
        title_bar_layout.setSpacing(2)
        self.title = QLabel(f"{self.__class__.__name__}", self)
        self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.title.setStyleSheet(
            """
        QLabel { text-transform: uppercase; font-size: 10pt; margin-left: 48px; }
        """
        )

        if title := parent.windowTitle():
            self.title.setText(title)
        title_bar_layout.addWidget(self.title)
        # Min button
        self.min_button = QToolButton(self)
        min_icon = QIcon()
        min_icon.addFile("min.svg")
        self.min_button.setIcon(min_icon)
        self.min_button.clicked.connect(self.window().showMinimized)

        # Max button
        self.max_button = QToolButton(self)
        max_icon = QIcon()
        max_icon.addFile("max.svg")
        self.max_button.setIcon(max_icon)
        self.max_button.clicked.connect(self.window().showMaximized)

        # Close button
        self.close_button = QToolButton(self)
        close_icon = QIcon()
        close_icon.addFile("close.svg")  # Close has only a single state.
        self.close_button.setIcon(close_icon)
        self.close_button.clicked.connect(self.window().close)

        # Normal button
        self.normal_button = QToolButton(self)
        normal_icon = QIcon()
        normal_icon.addFile("normal.svg")
        self.normal_button.setIcon(normal_icon)
        self.normal_button.clicked.connect(self.window().showNormal)
        self.normal_button.setVisible(False)
        # Add buttons
        buttons = [
            self.min_button,
            self.normal_button,
            self.max_button,
            self.close_button,
        ]
        for button in buttons:
            button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
            button.setFixedSize(QSize(16, 16))
            button.setStyleSheet(
                """QToolButton {
                    border: none;
                    padding: 2px;
                }
                """
            )
            title_bar_layout.addWidget(button)

    def window_state_changed(self, state):
        if state == Qt.WindowState.WindowMaximized:
            self.normal_button.setVisible(True)
            self.max_button.setVisible(False)
        else:
            self.normal_button.setVisible(False)
            self.max_button.setVisible(True)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Custom Title Bar")
        self.resize(400, 200)
        self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
        self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
        central_widget = QWidget()
        # This container holds the window contents, so we can style it.
        central_widget.setObjectName("Container")
        central_widget.setStyleSheet(
            """#Container {
            background: qlineargradient(x1:0 y1:0, x2:1 y2:1, stop:0 #051c2a stop:1 #44315f);
            border-radius: 5px;
        }"""
        )
        self.title_bar = CustomTitleBar(self)

        work_space_layout = QVBoxLayout()
        work_space_layout.setContentsMargins(11, 11, 11, 11)
        work_space_layout.addWidget(QLabel("Hello, World!", self))

        centra_widget_layout = QVBoxLayout()
        centra_widget_layout.setContentsMargins(0, 0, 0, 0)
        centra_widget_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
        centra_widget_layout.addWidget(self.title_bar)
        centra_widget_layout.addLayout(work_space_layout)

        central_widget.setLayout(centra_widget_layout)
        self.setCentralWidget(central_widget)

    def changeEvent(self, event):
        if event.type() == QEvent.Type.WindowStateChange:
            self.title_bar.window_state_changed(self.windowState())
        super().changeEvent(event)
        event.accept()

    def window_state_changed(self, state):
        self.normal_button.setVisible(state == Qt.WindowState.WindowMaximized)
        self.max_button.setVisible(state != Qt.WindowState.WindowMaximized)

    def mousePressEvent(self, event):
        if event.button() == Qt.MouseButton.LeftButton:
            self.initial_pos = event.position().toPoint()
        super().mousePressEvent(event)
        event.accept()

    def mouseMoveEvent(self, event):
        if self.initial_pos is not None:
            delta = event.position().toPoint() - self.initial_pos
            self.window().move(
                self.window().x() + delta.x(),
                self.window().y() + delta.y(),
            )
        super().mouseMoveEvent(event)
        event.accept()

    def mouseReleaseEvent(self, event):
        self.initial_pos = None
        super().mouseReleaseEvent(event)
        event.accept()


if __name__ == "__main__":
    app = QApplication([])
    window = MainWindow()
    window.show()
    app.exec()


Conclusion

In this tutorial, we have learned the fundamentals of creating custom title bars in PyQt. To do this, we have combined PyQt's widgets, layouts, and styling capabilities to create a visually appealing title bar for a PyQt app.

With this skill under your belt, you're now ready to create title bars that align perfectly with your application's unique style and branding. This will allow you to break away from the standard window decoration provided by your operating system and add a personal touch to your user interface.

Now let your imagination run and transform your PyQt application's UX.

CXX-Qt 0.6 Release

We just released CXX-Qt version 0.6!

CXX-Qt is a set of Rust crates for creating bidirectional Rust ⇄ C++ bindings with Qt. It can be used to integrate Rust into C++ applications using CMake or build Rust applications with Cargo. CXX-Qt provides tools for implementing QObject subclasses in Rust that can be used from C++, QML, and JavaScript.

For 0.6, we’re excited to announce that we’re on the road to stabilization! 🥳 We’ve done a (hopefully final) iteration of our API and are now happier than ever to release it.

The new API is now truly in the spirit of CXX. All implementation has moved out of the #[cxx_qt::bridge] and is now simply outside the bridge, like it is for CXX as well. To get an overview of the new API, check out our the planned 1.0 API, which is available starting with this release.

Check out the new release through the usual channels:

Some of the more extensive developer-facing changes are listed below.

New CXX-style API

extern “C++Qt”

Like extern "C++" in CXX, the extern "C++Qt" block now only uses a declaration-like syntax to expose functionality to C++/Qt. This is an extension to CXX’s extern "C++" blocks, which allow defining methods as #[qsignal]. These signals can then be emitted from Rust, as well as connected to.

extern “RustQt”

Like extern "Rust" in CXX, the extern "RustQt" block now only uses a declaration-like syntax to expose functionality to C++/Qt. Most existing features have moved to an attribute-syntax, which should look very familiar to existing Qt developers.

The new API includes:

  • #[qobject] is now on a type alias to distinguish between QObject and Rust struct clearer
  • #[qproperty(...)] is now just an attribute on the #[qobject]
  • #[inherit],#[qsignal] & #[qinvokable] have now moved to attributes of external function declarations. They can also now be freely mixed
  • #[cxx_override], #[cxx_final], #[cxx_virtual] are now independent attributes rather than imbedded in #[qinvokable]

Support for Qt enums as shared types

Like shared types in CXX, enums for Qt can be declared using a shared enum in the bridge. These can be defined using the #[qenum] attribute and qnamespace!(...) macro.

Implementation is outside the bridge

Like CXX the implementations on types are now defined outside of the bridge rather than inside. This allows you to choose the prefix for QObjects, as the hard-coded qobject:: prefix is now simply the name of the #[cxx_qt::bridge] module.

Another great new feature in this release: Connecting to Signals via Rust is now possible! Simply use the on_[SIGNAL-NAME]/connect_[SIGNAL-NAME] methods generated for each signal. Closures are supported when connecting to signals as well.

QML modules can now be built

The build system has been improved so that QML modules can now be output by CXX-Qt. This allows for using the declarative syntax of #[qml_element] and #[qml_singleton] on a #[qobject], whereas before Rust QObject types needed to be registered in C++ with the QML engine.

use cxx_qt_build::{CxxQtBuilder, QmlModule};
fn main() {
    CxxQtBuilder::new()
        .qml_module(QmlModule {
            uri: "com.kdab.cxx_qt.demo",
            rust_files: &["src/cxx_qt_bridge.rs"],
            qml_files: &["qml/main.qml"],
            ..Default::default()
        })
        .build();
}

This should also allow for tooling to inspect QML modules that were generated from Rust in the future.

New traits for shaping code generation and interacting with features

Most features of the CXX-Qt code generation are now expressed through Rust traits and can be enabled/disabled or otherwise configured using a syntax similar to CXX shim trait impls.

New features

  • Custom constructors through cxx_qt::Constructor – this drops the requirement for Default on the Rust struct.
  • Easy default-constructor implementation through cxx_qt::Initialize.
  • cxx_qt::CxxQtType trait for reflection and less “magic” methods.

Existing features that can now be enabled/disabled:

  • Threading through the cxx_qt::Threading trait.
  • Locking through the cxx_qt::Locking trait.

Thanks to all of our contributors that helped us with this release

  • Be Wilson
  • Laurent Montel
  • Olivier Le Doeuff
  • Jimmy van Hest
  • Michael (aka @mmMike)
  • Cyril Jacquet
  • Magnus Groß
  • jimmyvanhest
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 CXX-Qt 0.6 Release appeared first on KDAB.

Hello, RHI – How to get started with Qt RHI

Hello, RHI – How to get started with Qt RHI

For some time now, Qt has been internally utilizing RHI (Rendering Hardware Interface), a new cross-platform technology for graphic rendering. Since Qt 6.6, this API has been semi-public, meaning that the API is mature for practical use but may still be subject to potential changes between major Qt versions.

In this blog post, we demonstrate how to to get started with RHI.

Continue reading Hello, RHI – How to get started with Qt RHI at basysKom GmbH.

How to Restore the Window's Geometry in a PyQt App — Make Your Windows Remember Their Last Geometry

In GUI applications the window's position & size are known as the window geometry. Saving and restoring the geometry of a window between executions is a useful feature in many applications. With persistent geometry users can arrange applications on their desktop for an optimal workflow and have the applications return to those positions every time they are launched.

In this tutorial, we will explore how to save and restore the geometry and state of a PyQt window using the QSettings class. With this functionality, you will be able to give your applications a usability boost.

To follow along with this tutorial, you should have prior knowledge of creating GUI apps with Python and PyQt. Additionally, having a basic understanding of using the QSettings class to manage an application's settings will be beneficial.

Understanding a Window's Geometry

PyQt defines the geometry of a window using a few properties. These properties represent a window's position on the screen and size. Here's a summary of PyQt's geometry-related properties:

Property Description Access Method
x Holds the x coordinate of a widget relative to its parent. If the widget is a window, x includes any window frame and is relative to the desktop. This property defaults to 0. x()
y Holds the y coordinate of a widget relative to its parent. If the widget is a window, y includes any window frame and is relative to the desktop. This property defaults to 0. y()
pos Holds the position of the widget within its parent widget. If the widget is a window, the position is relative to the desktop and includes any frame. pos()
geometry Holds the widget's geometry relative to its parent and excludes the window frame. geometry()
width Holds the width of the widget, excluding any window frame. width()
height Holds the height of the widget, excluding any window frame. height()
size Holds the size of the widget, excluding any window frame. size()

In PyQt, the QWidget class provides the access methods in the table above. Note that when your widget is a window or form, the first three methods operate on the window and its frame, while the last four methods operate on the client area, which is the window's workspace without the external frame.

Additionally, the x and y coordinates are relative to the screen of your computer. The origin of coordinates is the upper left corner of the screen, at which point both x and y are 0.

Let's create a small demo app to inspect all these properties in real time. To do this, go ahead and fire up your code editor or IDE and create a new Python file called geometry_properties.py. Then add the following code to the file and save it in your favorite working directory:

python
from PyQt6.QtWidgets import (
    QApplication,
    QLabel,
    QMainWindow,
    QPushButton,
    QVBoxLayout,
    QWidget,
)

class Window(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Window's Geometry")
        self.resize(400, 200)
        self.central_widget = QWidget()
        self.global_layout = QVBoxLayout()
        self.geometry_properties = [
            "x",
            "y",
            "pos",
            "width",
            "height",
            "size",
            "geometry",
        ]
        for prop in self.geometry_properties:
            self.__dict__[f"{prop}_label"] = QLabel(f"{prop}:")
            self.global_layout.addWidget(self.__dict__[f"{prop}_label"])
        button = QPushButton("Update Geometry Properties")
        button.clicked.connect(self.update_labels)
        self.global_layout.addWidget(button)
        self.central_widget.setLayout(self.global_layout)
        self.setCentralWidget(self.central_widget)

    def update_labels(self):
        for prop in self.geometry_properties:
            self.__dict__[f"{prop}_label"].setText(
                f"{prop}: {getattr(self, prop)()}"
            )

if __name__ == "__main__":
    app = QApplication([])
    window = Window()
    window.show()
    app.exec()

Wow! There's a lot of code in this file. First, we import the required classes from PyQt6.QtWidgets. Then, we create our app's main window by inheriting from QMainWindow.

In the initializer method, we set the window's title and size using setWindowTitle() and resize(), respectively. Next, we define a central widget and a layout for our main window.

We also define a list of properties. We'll use that list to add some QLabel objects. Each label will show a geometry property and its current values. The Update Geometry Properties button allows us to update the value of the window's geometry properties.

Finally, we define the update_labels() method to update the values of all the geometry properties using their corresponding access methods. That's it! Go ahead and run the app. You'll get the following window on your screen:

A Window Showing Labels for Every Geometry Property A Window Showing Labels for Every Geometry Property

Looking good! Now go ahead and click the Update Geometry Properties button. You'll see how all the properties get updated. Your app's window will look something like this:

A Window Showing the Current Value of Every Geometry Property A Window Showing the Current Value of Every Geometry Property

As you can see, x and y are numeric values, while pos is a QPoint object with x and y as its coordinates. These properties define the position of this window on your computer screen.

The width and height properties are also numeric values, while the size property is a QSize object defined after the current width and height.

Finally, the geometry property is a QRect object. In this case, the rectangle comprises x, y, width, and height.

Great! With this first approach to how PyQt defines a window's geometry, we're ready to continue digging into this tutorial's main topic: restoring the geometry of a window in PyQt.

Keeping an App's Geometry Settings: The QSetting Class

Users of GUI apps will generally expect the apps to remember their settings across sessions. This information is often referred to as settings or preferences. In PyQt applications, you'll manage settings and preferences using the QSettings class. This class allows you to have persistent platform-independent settings in your GUI app.

A commonly expected feature is that the app remembers the geometry of its windows, particularly the main window.

In this section, you'll learn how to save and restore the window's geometry in a PyQt application. Let's start by creating a skeleton PyQt application to kick things off. Go ahead and create a new Python file called geometry.py. Once you have the file opened in your favorite code editor or IDE, then add the following code:

python
from PyQt6.QtWidgets import QApplication, QMainWindow

class Window(QMainWindow):
    def __init__(self):
        super(Window, self).__init__()
        self.setWindowTitle("Window's Geometry")
        self.move(50, 50)
        self.resize(400, 200)

if __name__ == "__main__":
    app = QApplication([])
    window = Window()
    window.show()
    app.exec()

This code creates a minimal PyQt app with an empty main window. The window will appear at 50 pixels from the upper left corner of your computer screen and have a size of 400 by 200 pixels.

We'll use the above code as a starting point to make the app remember and restore the main window's geometry across sessions.

First, we need to have a QSettings instance in our app. Therefore, you have to import QSettings from PyQt6.QtCore and instantiate it as in the code below:

python
from PyQt6.QtCore import QSettings
from PyQt6.QtWidgets import QApplication, QMainWindow

class Window(QMainWindow):
    def __init__(self):
        super(Window, self).__init__()
        self.setWindowTitle("Window's Geometry")
        self.move(50, 50)
        self.resize(400, 200)
        self.settings = QSettings("PyhonGUIs", "GeometryApp")

When instantiating QSettings, we must provide the name of our company or organization and the name of our application. We use "PyhonGUIs" as the organization and "GeometryApp" as the application name.

Now that we have a QSettings instance, we should implement two methods. The first method should allow you to save the app's settings and preferences. The second method should help you read and load the settings. In this tutorial, we'll call these methods write_settings() and read_settings(), respectively:

python
class Window(QMainWindow):
    # ...

    def write_settings(self):
        # Write settings here...

    def read_settings(self):
        # Read settings here...

Note that our methods don't do anything yet. You'll write them in a moment. For now, they're just placeholders.

The write_settings() method must be called when the user closes or terminates the application. This way, you guarantee that all the modified settings get saved for the next session. So, the appropriate place to call write_settings() is from the main window's close event handler.

Let's override the closeEvent() method as in the code below:

python
class Window(QMainWindow):
    # ...

    def closeEvent(self, event):
        self.write_settings()
        super().closeEvent(event)
        event.accept()

In this code, we override the closeEvent() handler method. The first line calls write_settings() to ensure that we save the current state of our app's settings. Then, we call the closeEvent() of our superclass QMainWindow to ensure the app's window closes correctly. Finally, we accept the current event to signal that it's been processed.

Now, where should we call read_settings() from? In this example, the best place for calling the read_settings() method is .__init__(). Go ahead and add the following line of code to the end of your __init__() method:

python
class Window(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Window's Geometry")
        self.move(50, 50)
        self.resize(400, 200)
        self.settings = QSettings("PythonGUIs", "GeometryApp")
        self.read_settings()

By calling the read_settings() method from __init__(), we ensure that our app will read and load its settings every time the main window gets created and initialized.

Great! We're on the way to getting our application to remember and restore its window's geometry. First, you need to know that you have at least two ways to restore the geometry of a window in PyQt:

  • Using the pos and size properties
  • Using the geometry property

In both cases, you need to save the current value of the selected property and load the saved value when the application starts. To kick things off, let's start with the first approach.

Restoring the Window's Geometry With pos and size

In this section, we'll first write the required code to save the current value of pos and size by taking advantage of our QSettings object. The code snippet below shows the changes that you need to make on your write_settings() method to get this done:

python
class Window(QMainWindow):
    # ...

    def write_settings(self):
        self.settings.setValue("pos", self.pos())
        self.settings.setValue("size", self.size())

This code is straightforward. We call the setValue() method on our setting object to set the "pos" and "size" configuration parameters. Note that we get the current value of each property using the corresponding access method.

With the write_settings() method updated, we're now ready to read and load the geometry properties from our app's settings. Go ahead and update the read_settings() method as in the code below:

python
class Window(QMainWindow):
    # ...

    def read_settings(self):
        self.move(self.settings.value("pos", defaultValue=QPoint(50, 50)))
        self.resize(self.settings.value("size", defaultValue=QSize(400, 200)))

The first line inside read_settings() retrieves the value of the "pos" setting parameter. If there's no saved value for this parameter, then we use QPoint(50, 50) as the default value. Next, the move() method moves the app's window to the resulting position on your screen.

The second line in read_settings() does something similar to the first one. It retrieves the current value of the "size" parameter and resizes the window accordingly.

Great! It's time for a test! Go ahead and run your application. Then, move the app's window to another position on your screen and resize the window as desired. Finally, close the app's window to terminate the current session. When you run the app again, the window will appear in the same position. It will also have the same size.

If you have any issues completing and running the example app, then you can grab the entire code below:

python
from PyQt6.QtCore import QPoint, QSettings, QSize
from PyQt6.QtWidgets import QApplication, QMainWindow

class Window(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Window's Geometry")
        self.move(50, 50)
        self.resize(400, 200)
        self.settings = QSettings("PyhonGUIs", "GeometryApp")
        self.read_settings()

    def write_settings(self):
        self.settings.setValue("pos", self.pos())
        self.settings.setValue("size", self.size())

    def read_settings(self):
        self.move(self.settings.value("pos", defaultValue=QPoint(50, 50)))
        self.resize(self.settings.value("size", defaultValue=QSize(400, 200)))

    def closeEvent(self, event):
        self.write_settings()
        super().closeEvent(event)
        event.accept()

if __name__ == "__main__":
    app = QApplication([])
    window = Window()
    window.show()
    app.exec()

Now you know how to restore the geometry of a window in a PyQt app using the pos and size properties. It's time to change gears and learn how to do this using the geometry property.

Restoring the Window's Geometry With geometry

We can also restore the geometry of a PyQt window using its geometry property and the restoreGeometry() method. To do that, we first need to save the current geometry using our QSettings object.

Go ahead and create a new Python file in your working directory. Once you have the file in place, add the following code to it:

python
from PyQt6.QtCore import QByteArray, QSettings
from PyQt6.QtWidgets import QApplication, QMainWindow

class Window(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Window's Geometry")
        self.move(50, 50)
        self.resize(400, 200)
        self.settings = QSettings("PythonGUIs", "GeometryApp")
        self.read_settings()

    def write_settings(self):
        self.settings.setValue("geometry", self.saveGeometry())

    def read_settings(self):
        self.restoreGeometry(self.settings.value("geometry", QByteArray()))

    def closeEvent(self, event):
        self.write_settings()
        super().closeEvent(event)
        event.accept()

if __name__ == "__main__":
    app = QApplication([])
    window = Window()
    window.show()
    app.exec()

There are only two changes in this code compared to the code from the previous section. We've modified the implementation of the write_settings() and read_settings() methods.

In write_settings(), we use the setValue() to save the current geometry of our app's window. The saveGeometry() allows us to access and save the current window's geometry. In read_settings(), we call the value() method to retrieve the saved geometry value. Then, we use restoreGeometry() to restore the geometry of our window.

Again, you can run the application consecutive times and change the position and size of its main window to ensure your code works correctly.

Restoring the Window's Geometry and State

If your app's window has toolbars and dock widgets, then you want to restore their state on the parent window. To do that, you can use the restoreState() method. To illustrate this, let's reuse the code from the previous section.

Update the content of write_settings() and read_settings() as follows:

python
class Window(QMainWindow):
    # ...

    def write_settings(self):
        self.settings.setValue("geometry", self.saveGeometry())
        self.settings.setValue("windowState", self.saveState())

    def read_settings(self):
        self.restoreGeometry(self.settings.value("geometry", QByteArray()))
        self.restoreState(self.settings.value("windowState", QByteArray()))

In write_settings(), we add a new setting value called "windowState". To keep this setting, we use the saveState() method, which saves the current state of this window's toolbars and dock widgets. Meanwhile, in read_settings(), we restore the window's state by calling the value() method, as usual, to get the state value back from our QSettings object. Finally, we use restoreState() to restore the state of toolbars and dock widgets.

Now, to make sure that this new code works as expected, let's add a sample toolbar and a dock window to our app's main window. Go ahead and add the following methods right after the __init__() method:

python
from PyQt6.QtCore import QByteArray, QSettings, Qt
from PyQt6.QtWidgets import QApplication, QDockWidget, QMainWindow

class Window(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Window's State")
        self.resize(400, 200)
        self.settings = QSettings("PythonGUIs", "GeometryApp")
        self.create_toolbar()
        self.create_dock()
        self.read_settings()

    def create_toolbar(self):
        toolbar = self.addToolBar("Toolbar")
        toolbar.addAction("One")
        toolbar.addAction("Two")
        toolbar.addAction("Three")

    def create_dock(self):
        dock = QDockWidget("Dock", self)
        dock.setAllowedAreas(
            Qt.DockWidgetArea.LeftDockWidgetArea
            | Qt.DockWidgetArea.RightDockWidgetArea
        )
        self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, dock)

    # ...

In this new update, we first import the Qt namespace from PyQt6.QtCore and QDockWidget from PyQt6.QtWidgets. Then we call the two new methods from __init__() to create the toolbar and dock widget at initialization time.

In the create_toolbar() method, we create a sample toolbar with three sample buttons. This toolbar will show at the top of our app's window by default.

Next, we create a dock widget in create_dock(). This widget will occupy the rest of our window's working area.

That's it! You're now ready to give your app a try. You'll see a window like the following:

A Window Showing a Sample Toolbar and a Dock Widget A Window Showing a Sample Toolbar and a Dock Widget

Play with the toolbar and the dock widget. Move them around. Then close the app's window and run the app again. Your toolbar and dock widget will show in the last position you left them.

Conclusion

Through this tutorial, you have learned how to restore the geometry and state of a window in PyQt applications using the QSettings class. By utilizing the pos, size, geometry, and state properties, you can give your users the convenience of persistent position and size on your app's windows.

With this knowledge, you can enhance the usability of your PyQt applications, making your app more intuitive and user-friendly.

Supercharging VS Code with C++ Extensions

In a previous blog we demonstrated the most straightforward method to optimize Visual Studio Code for a Qt / C++ environment: simply let the tools do all the work! The example GitHub project we discussed automatically installs both the Microsoft C/C++ and clangd extensions into VS Code. You might wonder why you need both C++ extensions. The short answer is that having both will maximize your productivity in every situation… but read on for more detail.

The power of extensions

The thing that makes VS Code an amazing productivity tool is that you can add all kinds of extensions to it – tools to help you write, document, and debug code. While the C/C++ extensions from Microsoft and clangd do many of the same things, they each have individual strong suits.

A rather simple block diagram of our dual C++ extensions looks something like this.

VS Code with both the C/C++ and clangd extensions installed.

Extensions that use the language server protocol (LSP) can add feature support tailored to the programming language in use. LSP allows a “mere” editor application like VS Code to provide features like auto completion, identifier location, grammar-sensitive tips, parameter name hints, and refactoring operations, and both the Microsoft C/C++ extension and clangd use LSP.

However, you can also see that the Microsoft C/C++ extension uses a debug adaptor to connect to different compiler toolchains. This provides debugging functionality for all the debuggers that the C/C++ extension supports. That means that if you want to debug C++ code from within VS Code, you need the Microsoft C/C++ extension installed (or an equivalent C++ extension that has a debug adapter – something that clangd notably does not have).

Why not just Microsoft?

But if the Microsoft C/C++ extension provides both language support and debugging support, why do you need clangd at all?

There are a few small differences between the two in the refactoring or code actions that they offer, but the biggest difference is in speed.

For finding identifiers and taking you to their definition or their references, clangd is significantly faster than Microsoft. What is almost instantaneous with clangd takes a second or two with the Microsoft C/C++ extension, requiring a progress bar. Most developers spend more time reading code than writing it, and I almost always prefer enabling clangd because of this. Especially for developers who are frequently navigating big source trees, those little delays can be a minor annoyance that adds up.

Thankfully, you can have both extensions enabled, but disable the Microsoft C/C++ Intellisense features. That lets you use the faster clangd source navigation and still use the Microsoft extension when it comes to debugging your code.

Configuring clangd

When installing the clangd extension, it will automatically disable the Microsoft C/C++ Intellisense features for you. If it’s not the case, you can change this setting:

"C_Cpp.intelliSenseEngine": "disabled",

You do need a couple settings to make clangd work properly. These are automatically configured in our VS Code C++/Qt template but to do it by hand you’ll need the following:

    • Insert the line set(CMAKE_EXPORT_COMPILE_COMMANDS ON) near the top of your CMakeLists.txt file or set the CMAKE_EXPORT_COMPILE_COMMANDS environment variable to 1 in the environment where you will run CMake.
    • Add this setting
"cmake.copyCompileCommands": "${workspaceFolder}/compile_commands.json",

 

to the .vscode/settings.json file in your project directory.

These two commands ensure cmake will capture the compiler commands that it needs to build the project during the configure stage. The file containing those commands is then copied to the root of the workspace where the clangd extension expects them. Without these two pieces, clangd won’t work properly.

You don’t need to do a full build to create and install your compile_commands.json file, you can just select the appropriate cmake preset on the bottom status bar. Alternatively, you can type Ctrl+Shift+P (Command+Shift+P on a Mac) to get the command palette and type “CMake: Configure”.

Using clangd configuration files

Everything said up to now is more than enough for most cases, but there are still some open issues we may need to take care of:

  • Using clangd works out of the box when using a compilation database using the MSVC compiler, because clangd is able to pick correctly the right driver for clang from the compilation database. However, warning/error flags for MSVC are not always correctly mapped to clang ones. How can we get a more accurate code model in that case?
  • Only one instance of clangd is running for each vscode window, so by default we can use only one compile_commands.json file. What happens if we are working in a multi-folder workspace, where each folder is an independently buildable application or library?

We can get to a solution for both cases by using clang configuration files.

Clangd provides a configuration mechanism starting from version 11, which allows among other things loading multiple compile_commands.json files and customizing compilation flags for the code model. A complete documentation for this mechanism can be found here

To set up a clangd configuration file, you just have to create a .clangd file in your project folder or one of its ancestors (e.g. if you’re working on two sibling folders, you should have the .clangd file on their parent folder).

Then, to have VSCode’s clangd extension to pick up you configuration file, you should add --enable-config among your clangd settings, like this:

"clangd.arguments": [
    ... // 
    "--enable-config"
]

Customize compilation flags through clangd configuration file

To customize compilation flags we need to use a CompileFlags section in our configuration files. The section supports the following options:

  • Add: to append compilation flags to the compile commands
  • Remove: to remove compilation flags
  • CompilationDatabase: to specify a custom folder where to fetch the compilation database (compile_commands.json)
  • Compiler: allows to specify the compiler executable who needs to run (clang, clang++, clang-cl, …).

Here’s an example to add custom flags:

CompileFlags:
    Add: [-Wall, -Wextras, -Wno-c++98-compat, -Wno-pre-c++17-compat]

And if you want to add specific flags for a given file or a given folder you can wrap the CompileFlags section into an If section:

If:
    PathMatch: .*/sourcesubdir/.*\.cpp
CompilerFlags:
    Add: [...]

Working with a multi-folder workspace with clangd

Using the conditional If section you can get one step ahead and configure clangd to correctly process all our source files in a multi-folder workspace.

First, you just set up the cmake configuration to generate a compiler database using the guide shown in the previous sections.

Then you set up the clangd configuration file to correctly map different compilation database with the correct subfolder in you workspace, as shown in the following example:

If:
    PathMatch: Project1/.*
CompileFlags:
    CompilationDatabase: Project1 // the parent folder for your compile_commands.json
    // You can optionally customize specific compiler flags for Project1 here
---

If:
    PathMatch: Project2/.*
CompileFlags:
    CompilationDatabase: Project2
    // You can optionally customize specific compiler flags for Project2 here

Having trouble?

If your clangd extension doesn’t seem to be working properly, here’s a few troubleshooting tips:

  • Confirm that compile_commands.json is in the root of the workspace directory. If it’s not, either use our GitHub example and instructions per this blog, or double-check the configuration settings in the above section.
  • Make sure clangd indexing has been finished. For small projects, this happens nearly instantly. But for big projects, you might see a progress indicator in the bottom status bar that says, “indexing X/Y”. Wait until that goes away and try again.
  • Restart clangd. This isn’t necessary too often, but if nothing else works, it’s worth a try: type Ctrl+Shift+P (or Command+Shift+P) and type “Restart Language Server”.

That’s it!

VS Code is an amazing tool and we’re always learning new ways to optimize our development environment. If you’ve got any related tips to share, leave a comment!

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 Supercharging VS Code with C++ Extensions appeared first on KDAB.