Qt for MCUs 2.5.3 LTS Released

Qt for MCUs 2.5.3 LTS (Long-Term Support) has been released and is available for download. As a patch release, Qt for MCUs 2.5.3 LTS provides bug fixes and other improvements, and maintains source compatibility with Qt for MCUs 2.5.x. It does not add any new functionality.

Recursive Instantiation with Qt Quick and JSON

Recently I was tasked to come up with an architecture for remote real time instantiation and updating of arbitrary QML components.

This entry shows how you can use a simple variation of the factory method pattern in QML for instantiating arbitrary components. I’ve split my findings into 3 blog entries, each one covering a slightly different topic. Part 1 focuses on the software design pattern used to dynamically instantiate components. Part 2 shows how to layout these dynamic components by incorporating QML’ s positioning and layout APIs. The last entry, consisting of Parts 3 and 4, addresses the anchors API and important safety aspects.

This is Part 1: Recursive Instantiation with Qt Quick and JSON.

The original factory method pattern made use of static methods to programmatically instantiate objects of different classes, instead of having to call their constructors. It achieved that by having the classes share a common ancestor. Our variation of the popular pattern uses a Loader to choose which component to load, and a Repeater to dynamically instantiate arbitrary instances of this loader using a model.

Here we specify which components with a JSON array and use a Repeater to load them.

    id: root
    // A JSON representation of a QML layout:
    property var factoryModel: [
        {
            "component": "Button",
        },
        {
            "component": "Button",
        }
    ]
    // Root of our component factory
    Repeater {
        model: root.factoryModel
        delegate: loaderComp
    }

To be able to instantiate any kind of item, you can use a Component with a Loader inside, as the Repeater’s delegate. This allows you to load a different component based on the Repeater’s model data.

    // Root component of the factory and nodes
    Component {
        id: loaderComp
        Loader {
            id: instantiator
            required property var modelData
            sourceComponent: switch (modelData.component) {
                case "Button":
                return buttonComp;
                case "RowLayout":
                return rowLayoutComp;
                case "Item":
                default: return itemComp;
            }
        }
    }

To assign values from the model to the component, add a method that gets called when the Loader’s onItemChanged event is triggered. I use this method to take care of anything that involves the component’s properties:

    // Root component of the factory and nodes
    Component {
        id: loaderComp
        Loader {
            id: instantiator
            required property var modelData
            sourceComponent: switch (modelData.component) {
                case "Button":
                return buttonComp;
                case "RowLayout":
                return rowLayoutComp;
                case "Item":
                default: return itemComp;
            }
            onItemChanged: {
                // Pass children (see explanation below)
                if (typeof(modelData.children) === "object")
                    item.model = modelData.children;

                // Button properties
                switch (modelData.component) {
                    case "Button":
                    // If the model contains certain value, we may assign it:
                    if (typeof(modelData.text) !== "undefined")
                        item.text = modelData.text;
                    break;
                }

                // Item properties
                // Since Item is the parent of all repeatable, we don't need to check
                // if the component supports Item properties before we assign them:
                if (typeof(modelData.x) !== "undefined")
                    loaderComp.x = Number(modelData.x);
                if (typeof(modelData.y) !== "undefined")
                    loaderComp.y = Number(modelData.y);
                // ...
            }
        }
    }

Examples of components that loaderComp could load are defined below. To enable recursion, these components must contain a Repeater that instantiates children components, with loaderComp set as the delegate:

    Component {
        id: itemComp
        Item {
            property alias children: itemRepeater.model
            children: Repeater {
                id: itemRepeater
                delegate: loaderComp
            }
        }
    }
    Component {
        id: buttonComp
        Button {
            property alias children: itemRepeater.model
            children: Repeater {
                id: itemRepeater
                delegate: loaderComp
            }
        }
    }
    Component {
        id: rowLayoutComp
        RowLayout {
            property alias children: itemRepeater.model
            children: Repeater {
                id: itemRepeater
                delegate: loaderComp
            }
        }
    }

The Repeater inside of the components allows us to instantiate components recursively, by having a branch or more of children components in the model, like so:

    // This model lays out buttons vertically
    property var factoryModel: [
        {
            "component": "RowLayout",
            "children": [
                {
                    "component": "Button",
                    "text": "Button 1"
                },
                {
                    "component": "Button",
                    "text": "Button 2"
                }
            ]
        }
    ]

Here we’ve seen how we can use a Repeater, a JSON model, a Loader delegate, and simple recursive definition to instantiate arbitrary QML objects from a JSON description. In my next entry I will focus on how you can lay out these arbitrarily instantiated objects on your screen.

Thanks to Kevin Krammer and Jan Marker whose insights helped improve the code you’ve seen here.

I hope you’ve found this useful! Part 2 may be found already or later by following this link.

Reference
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 Recursive Instantiation with Qt Quick and JSON appeared first on KDAB.

Insights from Industry Experts: Improving Software Dev Productivity

 

Software development is a dynamic and constantly evolving field that requires professionals to stay up-to-date with the latest technologies and tools to improve their productivity and efficiency. For developers, the ultimate goal is to complete tasks effectively, leading to better products and faster time-to-market. These topics were explored in our World Summit QtWS23 last November, where experts Kate Gregory, Kevlin Henney, and Volker Hilsheimer shared valuable insights. 

Embedding the Servo Web Engine in Qt

With the Qt WebEngine module, Qt makes it possible to embed a webview component inside an otherwise native application. Under the hood, Qt WebEngine uses the Chromium browser engine, currently the de facto standard engine for such use cases.

While the task of writing a brand new standard-compliant browser engine is infamous as being almost unachievable nowadays (and certainly so with Chromium coming in at 31 million lines of code), the Rust ecosystem has been brewing up a new web rendering engine called Servo. Initially created by Mozilla in 2012, Servo is still being developed today, now under the stewardship of the Linux Foundation.

With the browser inherently being exposed to the internet, it is usually the biggest attack vector on a system. Naturally this makes Servo very attractive as an alternative browser engine, given that it is written in a memory-safe language.

A Servo WebView

At KDAB we managed to embed the Servo web engine inside Qt, by using our CXX-Qt library as a bridge between Rust and C++. This means that we can now use Servo as an alternative to Chromium for webviews in Qt applications.

From a QML perspective this component is similar to the Chromium WebView, such as providing canGoBack, canGoForward, loading, title, url properties and goBack, goForward methods. The QML item itself acts in the same way with the contents being rendered to match its size.

import QtQuick
import QtQuick.Window

import com.kdab.servo

Window {
  height: 720
  width: 1280
  title: webView.title
  visible: true

  ServoWebView {
    id: webView
    anchors.fill: parent
    url: "https://servo.org/"
  }
}

The screenshot below shows a basic QML application with a toolbar containing back, forward, go buttons and an address bar. We use CXX-Qt to define Qt properties, invokables, and event handlers (e.g. touch events) in Rust and trigger events in the Servo engine. Then any update requests from Servo can trigger an update of the Qt side via the Qt event loop.

As we move towards stabilising CXX-Qt at KDAB, investigating real world use cases, such as exposing Servo to Qt, allows us to identify potential missing functionality and explore what is possible when joining the Rust and Qt ecosystems together.

Technical details

Under the hood most of the heavy lifting is done by our CXX-Qt bindings, which already bridges the obvious gap between the Rust and Qt/C++ worlds. However, some further glue is needed to connect the rendering contexts of Servo to being able to render the surfaces into the actual Qt application. Internally, Servo uses surfman, a Rust library to manage rendering surfaces. At the time of writing, surfman supports OpenGL and Metal, with support for Vulkan being planned.

We use surfman to create a new OpenGL context, that Servo then uses for rendering. To render the result into the QtQuick scene, we borrow the surface from Servo, create a new framebuffer object and blit the framebuffer into a QQuickFrameBufferObject on the Qt side.

Future possibilities

Servo development is active again after a period of less activity, therefore the API is evolving and there is work to improve the API for embedders. This could result in a simpler and documented process for integrating Servo into apps. Also as part of the Tauri and Servo collaboration, a backend for WRY could become available. All of these result in many possible changes for the bridge to Qt, as currently this demo directly constructs Servo components (similar to servoshell) but could instead use a shared library or WRY instead.

On the Qt side, there are areas that could be improved or investigated further. For example, currently we are using a framebuffer object which forces use of the OpenGL backend, but with RHI, developers might want to use other backends. A way to solve this for QML would be to change the implementation to instead use a custom Qt Scene Graph node, which can then have implementations for Vulkan, OpenGL etc and read from the Servo engine.

Alternatively Qt 6.7 has introduced a new QQuickRhiItem element, which is currently a technical preview, but can be used as a rendering API-agnostic alternative to QQuickFrameBufferObject.

If this sounds interesting to your use case or you would like to collaborate with us, the code for this tech demo is available on GitHub under KDABLabs/cxx-qt-servo-webview or contact KDAB directly. We also have a Zulip chat if you want to discuss any parts of bridging Servo or CXX-Qt with us.

Come and see us at Embedded World 2024 where we will have the Servo demo and others on display!

 

The post Embedding the Servo Web Engine in Qt appeared first on KDAB.

Use Compute Shader in Qt Quick

Use Compute Shader in Qt Quick

With this blog post, we introduce the QtQuickComputeItem - a Qt Quick item that allows you to easily integrate compute shader into your Qt Quick Code.
Compute
Shader are used to perform arbitrary computations on the GPU. For
example, the screenshot below shows a Qt Quick application that
generates Gray Scott Reaction Diffusion patterns.  The simulation is executed by a compute shader that is configured directly in QML.

Continue reading Use Compute Shader in Qt Quick at basysKom GmbH.

Q&A: How Do I Display Images in PySide6? — Using QLabel to easily add images to your applications

Adding images to your application is a common requirement, whether you're building an image/photo viewer, or just want to add some decoration to your GUI. Unfortunately, because of how this is done in Qt, it can be a little bit tricky to work out at first.

In this short tutorial, we will look at how you can insert an external image into your PySide6 application layout, using both code and Qt Designer.

Which widget to use?

Since you're wanting to insert an image you might be expecting to use a widget named QImage or similar, but that would make a bit too much sense! QImage is actually Qt's image object type, which is used to store the actual image data for use within your application. The widget you use to display an image is QLabel.

The primary use of QLabel is of course to add labels to a UI, but it also has the ability to display an image — or pixmap — instead, covering the entire area of the widget. Below we'll look at how to use QLabel to display a widget in your applications.

Using Qt Designer

First, create a MainWindow object in Qt Designer and add a "Label" to it. You can find Label at in Display Widgets in the bottom of the left hand panel. Drag this onto the QMainWindow to add it.

MainWindow with a single QLabel added MainWindow with a single QLabel added

Next, with the Label selected, look in the right hand QLabel properties panel for the pixmap property (scroll down to the blue region). From the property editor dropdown select "Choose File…" and select an image file to insert.

As you can see, the image is inserted, but the image is kept at its original size, cropped to the boundaries of theQLabel box. You need to resize the QLabel to be able to see the entire image.

In the same controls panel, click to enable scaledContents.

When scaledContents is enabled the image is resized to the fit the bounding box of the QLabel widget. This shows the entire image at all times, although it does not respect the aspect ratio of the image if you resize the widget.

You can now save your UI to file (e.g. as mainwindow.ui).

To view the resulting UI, we can use the standard application template below. This loads the .ui file we've created (mainwindow.ui) creates the window and starts up the application.

PySide6
import sys
from PySide6 import QtWidgets
from PySide6.QtUiTools import QUiLoader

loader = QUiLoader()
app = QtWidgets.QApplication(sys.argv)
window = loader.load("mainwindow.ui", None)
window.show()
app.exec()

Running the above code will create a window, with the image displayed in the middle.

QtDesigner application showing a Cat QtDesigner application showing a Cat

Using Code

Instead of using Qt Designer, you might also want to show an image in your application through code. As before we use a QLabel widget and add a pixmap image to it. This is done using the QLabel method .setPixmap(). The full code is shown below.

PySide6
import sys
from PySide6.QtGui import QPixmap
from PySide6.QtWidgets import QMainWindow, QApplication, QLabel

class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()
        self.title = "Image Viewer"
        self.setWindowTitle(self.title)

        label = QLabel(self)
        pixmap = QPixmap('cat.jpg')
        label.setPixmap(pixmap)
        self.setCentralWidget(label)
        self.resize(pixmap.width(), pixmap.height())


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

The block of code below shows the process of creating the QLabel, creating a QPixmap object from our file cat.jpg (passed as a file path), setting this QPixmap onto the QLabel with .setPixmap() and then finally resizing the window to fit the image.

python
label = QLabel(self)
pixmap = QPixmap('cat.jpg')
label.setPixmap(pixmap)
self.setCentralWidget(label)
self.resize(pixmap.width(), pixmap.height())

Launching this code will show a window with the cat photo displayed and the window sized to the size of the image.

QMainWindow with Cat image displayed QMainWindow with Cat image displayed

Just as in Qt designer, you can call .setScaledContents(True) on your QLabel image to enable scaled mode, which resizes the image to fit the available space.

python
label = QLabel(self)
pixmap = QPixmap('cat.jpg')
label.setPixmap(pixmap)
label.setScaledContents(True)
self.setCentralWidget(label)
self.resize(pixmap.width(), pixmap.height())

Notice that you set the scaled state on the QLabel widget and not the image pixmap itself.

Conclusion

In this quick tutorial we've covered how to insert images into your Qt UIs using QLabel both from Qt Designer and directly from PySide6 code.

Introducing the ConnectionEvaluator in KDBindings

Managing the timing and context of signals and slots in multithreaded applications, especially those with a GUI, can be a complex task. The concept of deferred connection evaluation offers a nice and easy API, allowing for controlled and efficient signal-slot connections. This approach is particularly useful when dealing with worker threads and GUI threads.

A classic example where cross-thread signal-slot connections are useful is when we have a worker thread performing some computations or data collection and periodically emits signals. The GUI thread can connect to these signals and then display the data however it wishes. Graphical display usually must happen on the main thread and at specific times. Therefore, controlling exactly when the receiving slots get executed is of critical importance for correctness and performance.

A Quick Recap on Signals & Slots and KDBindings

Signals and slots are integral to event handling in C++ applications. Signals, emitted upon certain events or conditions, trigger connected slots (functions or methods) to respond accordingly. This framework is highly effective, but there are cases where immediate slot invocation is not ideal, such as in multithreaded applications where you might want to emit a signal in one thread and handle it in another, like a GUI event loop.

KDBindings is a headeronly C++17 library that implements the signals and slots design pattern. In addition, KDBindings also provides an easy to use property and bindings system to enable reactive programming in C++. For more information, read this introduction blog.

Understanding Deferred Connection Evaluation

In Qt, a signal that should be evaluated on a different thread will be executed by the event loop of that thread. As KDBindings doesn’t have its own event loop and needs to be able to integrate into any framework, we’re introducing the ConnectionEvaluator as our solution to those nuanced requirements. It allows for a deferred evaluation of connections, providing a much-needed flexibility in handling signal emissions.

In multithreaded environments, the immediate execution of slots in response to signals can lead to complications. Deferred connection evaluation addresses this by delaying the execution of a slot until a developer-determined point. This is achieved through a ConnectionEvaluator, akin to a BindingEvaluator, which governs when a slot is called.

Implementing and Utilizing Deferred Connections

We have introduced connectDeferred. This function, mirroring the standard connect method, takes a ConnectionEvaluator as its first argument, followed by the standard signal and slot parameters.

Signals emitted are queued in the ConnectionEvaluator instead of immediately triggering the slot. Developers can then execute these queued slots at an appropriate time, akin to the evaluateAll method in the BindingEvaluator.

How It Works

1. Create a ConnectionEvaluator:

auto evaluator = std::make_shared<ConnectionEvaluator>(); // Shared ownership for proper lifetime management

2. Connect with Deferred Evaluation:

Signal<int> signal;
int value = 0;

auto connection = signal.connectDeferred(evaluator, [&value](int increment) {
value += increment;
});

3. Queue Deferred Connections

When the signal emits, connected slots aren’t executed immediately but are queued:

signal.emit(5);  // The slot is queued, not executed.

4. Evaluate Deferred Connections

Execute the queued connections at the desired time:

evaluator.evaluateDeferredConnections();  // Executes the queued slot.

Usage Example

To illustrate how this enhanced signal/slot system works in a multithreaded scenario, let’s have a look at a usage example. Consider a scenario where a worker thread emits signals, but we want the connected slots to be executed in an event loop on the GUI thread:

Signal<int> workerSignal;
int guiValue = 0;
auto evaluator = std::make_shared<ConnectionEvaluator>();

// Connect a slot to the workerSignal with deferred evaluation
workerSignal.connectDeferred(evaluator, [&guiValue](int value) {
// This slot will be executed later in the GUI thread's event loop
guiValue += value;
});

// ... Worker thread emits signals ...

// In the GUI thread's event loop or at the right time
evaluator->evaluateDeferredConnections();

// The connected slot is now executed, and guiValue is updated

In this example, the connectDeferred function allows us to queue the connection for deferred execution, and the ConnectionEvaluator determines when it should be invoked. This level of control enables more sophisticated and responsive applications.

Conclusion

In simple terms, the ConnectionEvaluator is a valuable tool we’ve added to KDBindings. It lets you decide when exactly your connections should ‘wake up’ and do their jobs. This is especially helpful in complex applications where you want to make sure that certain tasks are performed at the right time and in the right order (preferably in multithreading applications). Think of it like having a remote control for your connections, allowing you to press ‘play’ whenever you’re ready.

It may also be worthwhile mentioning that we will be integrating this feature into the KDFoundation library and event loop in the KDUtils GitHub repo. Additionally, it’s important to note that it could easily be integrated into any application or framework, regardless of whether it utilizes an event loop.

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 the ConnectionEvaluator in KDBindings appeared first on KDAB.

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)
            self.show() # Show this widget again, if it's dropped outside.


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)
            self.show() # Show this widget again, if it's dropped outside.

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.

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)
            self.show() # Show this widget again, if it's dropped outside.


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)
            self.show() # Show this widget again, if it's dropped outside.

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.